@libs-ui/components-inputs-mention 0.2.78
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/README.md +3 -0
- package/defines/caret-coords.define.d.ts +5 -0
- package/defines/keyboard.define.d.ts +9 -0
- package/defines/template.define.d.ts +4 -0
- package/defines/utils.define.d.ts +18 -0
- package/esm2022/defines/caret-coords.define.mjs +115 -0
- package/esm2022/defines/keyboard.define.mjs +10 -0
- package/esm2022/defines/template.define.mjs +25 -0
- package/esm2022/defines/utils.define.mjs +280 -0
- package/esm2022/index.mjs +3 -0
- package/esm2022/interfaces/mention.interface.mjs +2 -0
- package/esm2022/libs-ui-components-inputs-mention.mjs +5 -0
- package/esm2022/list/list.component.mjs +166 -0
- package/esm2022/mention.directive.mjs +504 -0
- package/fesm2022/libs-ui-components-inputs-mention.mjs +1092 -0
- package/fesm2022/libs-ui-components-inputs-mention.mjs.map +1 -0
- package/index.d.ts +2 -0
- package/interfaces/mention.interface.d.ts +40 -0
- package/list/list.component.d.ts +36 -0
- package/mention.directive.d.ts +48 -0
- package/package.json +25 -0
|
@@ -0,0 +1,1092 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { signal, model, viewChild, viewChildren, Component, ChangeDetectionStrategy, inject, ElementRef, input, output, effect, untracked, Directive } from '@angular/core';
|
|
3
|
+
import { LibsUiDynamicComponentService } from '@libs-ui/services-dynamic-component';
|
|
4
|
+
import { get, deleteUnicode, set, isNil, uuid } from '@libs-ui/utils';
|
|
5
|
+
import { Subject, fromEvent, takeUntil } from 'rxjs';
|
|
6
|
+
import { tap, takeUntil as takeUntil$1 } from 'rxjs/operators';
|
|
7
|
+
import { LibsUiComponentsAvatarComponent } from '@libs-ui/components-avatar';
|
|
8
|
+
import { LibsUiComponentsPopoverComponent } from '@libs-ui/components-popover';
|
|
9
|
+
|
|
10
|
+
const KEY_BACKSPACE = 8;
|
|
11
|
+
const KEY_TAB = 9;
|
|
12
|
+
const KEY_ENTER = 13;
|
|
13
|
+
const KEY_SHIFT = 16;
|
|
14
|
+
const KEY_ESCAPE = 27;
|
|
15
|
+
const KEY_SPACE = 32;
|
|
16
|
+
const KEY_UP = 38;
|
|
17
|
+
const KEY_DOWN = 40;
|
|
18
|
+
const KEY_BUFFERED = 229;
|
|
19
|
+
|
|
20
|
+
const PATTERN_INSERT = '\ufeff';
|
|
21
|
+
const buildTemplate = (value, id, feId) => {
|
|
22
|
+
return `<span class="mo-lib-shared-mention" style="font-weight: 600;-moz-osx-font-smoothing: grayscale;-webkit-font-smoothing: antialiased; color: #7239EA" contenteditable="false" id="${id}" feId="${feId}" value="${value}">${value}</span> `;
|
|
23
|
+
};
|
|
24
|
+
const DEFAULT_CONFIG = {
|
|
25
|
+
items: [],
|
|
26
|
+
triggerChar: '@',
|
|
27
|
+
labelKey: 'email',
|
|
28
|
+
allowSpace: true,
|
|
29
|
+
returnTrigger: false,
|
|
30
|
+
limitSpaceSearchQuery: 3,
|
|
31
|
+
mentionEventName: 'click',
|
|
32
|
+
mentionActionByEvent: (item, trigger) => console.log(item, trigger),
|
|
33
|
+
mentionSelect: (item) => `@${get(item, 'email')}`,
|
|
34
|
+
mentionFilter: (search, items) => {
|
|
35
|
+
const searchString = deleteUnicode(search.toLowerCase());
|
|
36
|
+
return items?.filter(item => {
|
|
37
|
+
const name = deleteUnicode(get(item, 'name').toLowerCase());
|
|
38
|
+
const userName = deleteUnicode(get(item, 'username').toLowerCase());
|
|
39
|
+
return name.includes(searchString) || userName.includes(searchString);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const setValue = (el, value) => {
|
|
45
|
+
if (isInputOrTextAreaElement(el)) {
|
|
46
|
+
el.value = value;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
el.textContent = value;
|
|
50
|
+
};
|
|
51
|
+
const getValue = (el) => {
|
|
52
|
+
return isInputOrTextAreaElement(el) ? el.value : el.textContent;
|
|
53
|
+
};
|
|
54
|
+
const insertValue = (el, start, end, text, iframe, noRecursion = false) => {
|
|
55
|
+
if (isTextElement(el)) {
|
|
56
|
+
const val = getValue(el);
|
|
57
|
+
text = noRecursion ? PATTERN_INSERT : text;
|
|
58
|
+
setValue(el, val?.substring(0, start) + text + val?.substring(end, val.length));
|
|
59
|
+
setCaretPosition(el, start + text.length, iframe);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (noRecursion) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
return insertValueToContentEditable(start, end, text);
|
|
66
|
+
};
|
|
67
|
+
const insertValueToContentEditable = (start, end, text) => {
|
|
68
|
+
if (!window.getSelection) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const sel = window.getSelection();
|
|
72
|
+
if (!sel || !sel.anchorNode || !sel.getRangeAt || !sel.rangeCount) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
let range = sel.getRangeAt(0);
|
|
76
|
+
const anchorNode = sel.anchorNode;
|
|
77
|
+
const numberTextBeforeCursor = (getTextBeforeCursor() || '').length;
|
|
78
|
+
range.setStart(anchorNode, numberTextBeforeCursor - (end - start));
|
|
79
|
+
range.setEnd(anchorNode, numberTextBeforeCursor);
|
|
80
|
+
range.deleteContents();
|
|
81
|
+
// Range.createContextualFragment() would be useful here but is
|
|
82
|
+
// only relatively recently standardized and is not supported in
|
|
83
|
+
// some browsers (IE9, for one)
|
|
84
|
+
const el = document.createElement("SPAN");
|
|
85
|
+
el.innerHTML = text;
|
|
86
|
+
const frag = document.createDocumentFragment();
|
|
87
|
+
let node, lastNode, nodeHandlerEvent;
|
|
88
|
+
while ((node = el.firstChild)) {
|
|
89
|
+
if (get(node, 'getAttribute')) {
|
|
90
|
+
nodeHandlerEvent = node;
|
|
91
|
+
}
|
|
92
|
+
lastNode = frag.appendChild(node);
|
|
93
|
+
}
|
|
94
|
+
range.insertNode(frag);
|
|
95
|
+
// Preserve the selection
|
|
96
|
+
if (lastNode) {
|
|
97
|
+
range = range.cloneRange();
|
|
98
|
+
range.setStartAfter(lastNode);
|
|
99
|
+
range.collapse(true);
|
|
100
|
+
sel.removeAllRanges();
|
|
101
|
+
sel.addRange(range);
|
|
102
|
+
}
|
|
103
|
+
return nodeHandlerEvent;
|
|
104
|
+
};
|
|
105
|
+
const getTextBeforeCursor = () => {
|
|
106
|
+
const sel = window.getSelection();
|
|
107
|
+
if (!sel || !sel.anchorNode) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const range = sel.getRangeAt(0);
|
|
111
|
+
const anchorNode = sel.anchorNode;
|
|
112
|
+
const rangeNew = document.createRange();
|
|
113
|
+
rangeNew.selectNodeContents(anchorNode);
|
|
114
|
+
rangeNew.setEnd(range.startContainer, range.startOffset);
|
|
115
|
+
const fragNew = rangeNew.cloneContents();
|
|
116
|
+
const elmPrev = document.createElement("SPAN");
|
|
117
|
+
elmPrev.appendChild(fragNew);
|
|
118
|
+
return elmPrev.innerText;
|
|
119
|
+
};
|
|
120
|
+
const isInputOrTextAreaElement = (el) => {
|
|
121
|
+
return el !== null && (el.nodeName === 'INPUT' || el.nodeName === 'TEXTAREA');
|
|
122
|
+
};
|
|
123
|
+
const isTextElement = (el) => {
|
|
124
|
+
return el !== null && (el.nodeName === 'INPUT' || el.nodeName === 'TEXTAREA' || el.nodeName === '#text');
|
|
125
|
+
};
|
|
126
|
+
const setCaretPosition = (el, pos, iframe) => {
|
|
127
|
+
if (isInputOrTextAreaElement(el) && el.selectionStart) {
|
|
128
|
+
el.focus();
|
|
129
|
+
el.setSelectionRange(pos, pos);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const range = getDocument(iframe)?.createRange();
|
|
133
|
+
range?.setStart(el, pos);
|
|
134
|
+
range?.collapse(true);
|
|
135
|
+
const sel = getWindowSelection(iframe);
|
|
136
|
+
if (!sel) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
sel.removeAllRanges();
|
|
140
|
+
if (range) {
|
|
141
|
+
sel.addRange(range);
|
|
142
|
+
}
|
|
143
|
+
;
|
|
144
|
+
};
|
|
145
|
+
const getCaretPosition = (el, iframe) => {
|
|
146
|
+
if (isInputOrTextAreaElement(el)) {
|
|
147
|
+
const val = el.value;
|
|
148
|
+
return val.slice(0, el.selectionStart || undefined).length;
|
|
149
|
+
}
|
|
150
|
+
const selObj = getWindowSelection(iframe); //window.getSelection();
|
|
151
|
+
if (selObj && selObj.rangeCount > 0) {
|
|
152
|
+
const selRange = selObj.getRangeAt(0);
|
|
153
|
+
const preCaretRange = selRange.cloneRange();
|
|
154
|
+
preCaretRange.selectNodeContents(el);
|
|
155
|
+
preCaretRange.setEnd(selRange.endContainer, selRange.endOffset);
|
|
156
|
+
const position = preCaretRange.toString().length;
|
|
157
|
+
return position;
|
|
158
|
+
}
|
|
159
|
+
return 0;
|
|
160
|
+
};
|
|
161
|
+
// Based on ment.io functions...
|
|
162
|
+
//
|
|
163
|
+
const getDocument = (iframe) => {
|
|
164
|
+
if (!iframe) {
|
|
165
|
+
return document;
|
|
166
|
+
}
|
|
167
|
+
return iframe.contentWindow?.document;
|
|
168
|
+
};
|
|
169
|
+
const getWindowSelection = (iframe) => {
|
|
170
|
+
if (!iframe) {
|
|
171
|
+
return window.getSelection();
|
|
172
|
+
}
|
|
173
|
+
return iframe.contentWindow?.getSelection() || null;
|
|
174
|
+
};
|
|
175
|
+
const getContentEditableCaretCoords = (ctx) => {
|
|
176
|
+
const markerTextChar = '\ufeff';
|
|
177
|
+
const markerId = 'sel_' + new Date().getTime() + '_' + Math.random().toString().substring(2);
|
|
178
|
+
const doc = getDocument(ctx.iframe);
|
|
179
|
+
const sel = getWindowSelection(ctx.iframe);
|
|
180
|
+
if (!sel) {
|
|
181
|
+
return {
|
|
182
|
+
left: 0,
|
|
183
|
+
top: 0,
|
|
184
|
+
bottom: 0
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
const prevRange = sel.getRangeAt(0);
|
|
188
|
+
// create new range and set postion using prevRange
|
|
189
|
+
const range = doc.createRange();
|
|
190
|
+
const anchorNode = sel.anchorNode;
|
|
191
|
+
range.setStart(anchorNode, prevRange.startOffset);
|
|
192
|
+
range.setEnd(anchorNode, prevRange.startOffset);
|
|
193
|
+
range.collapse(false);
|
|
194
|
+
// Create the marker element containing a single invisible character
|
|
195
|
+
// using DOM methods and insert it at the position in the range
|
|
196
|
+
const markerEl = doc.createElement('span');
|
|
197
|
+
markerEl.id = markerId;
|
|
198
|
+
markerEl.appendChild(doc.createTextNode(markerTextChar));
|
|
199
|
+
range.insertNode(markerEl);
|
|
200
|
+
sel.removeAllRanges();
|
|
201
|
+
sel.addRange(prevRange);
|
|
202
|
+
const coordinates = {
|
|
203
|
+
left: 0,
|
|
204
|
+
top: markerEl.offsetHeight,
|
|
205
|
+
bottom: -1
|
|
206
|
+
};
|
|
207
|
+
localToRelativeCoordinates(ctx, markerEl, coordinates);
|
|
208
|
+
markerEl.parentNode?.removeChild(markerEl);
|
|
209
|
+
const widthScreen = (ctx.windowParent ? window.parent.innerWidth : window.innerWidth) || document.documentElement.clientWidth || document.body.clientWidth;
|
|
210
|
+
if (coordinates.left + 315 > widthScreen) {
|
|
211
|
+
coordinates.left = coordinates.left - 300;
|
|
212
|
+
}
|
|
213
|
+
const heightScreen = (ctx.windowParent ? window.parent.innerHeight : window.innerHeight) || document.documentElement.clientHeight || document.body.clientHeight;
|
|
214
|
+
if (coordinates.top + 190 > heightScreen) {
|
|
215
|
+
coordinates.bottom = heightScreen - coordinates.top + 15;
|
|
216
|
+
coordinates.top = -1;
|
|
217
|
+
}
|
|
218
|
+
return coordinates;
|
|
219
|
+
};
|
|
220
|
+
const localToRelativeCoordinates = (ctx, element, coordinates) => {
|
|
221
|
+
let obj = element;
|
|
222
|
+
let iframe = ctx ? ctx.iframe : null;
|
|
223
|
+
while (obj) {
|
|
224
|
+
if (ctx.parent !== null && ctx.parent === obj) {
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
coordinates.left += obj.offsetLeft + obj.clientLeft;
|
|
228
|
+
coordinates.top += obj.offsetTop + obj.clientTop;
|
|
229
|
+
obj = obj.offsetParent;
|
|
230
|
+
if (!obj && iframe) {
|
|
231
|
+
obj = iframe;
|
|
232
|
+
iframe = null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
obj = element;
|
|
236
|
+
iframe = ctx ? ctx.iframe : null;
|
|
237
|
+
while ((obj !== getDocument(undefined)?.body || ctx.windowParent) && obj !== null) {
|
|
238
|
+
if (ctx.parent !== null && ctx.parent === obj) {
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
if (obj.scrollTop && obj.scrollTop > 0) {
|
|
242
|
+
coordinates.top -= obj.scrollTop;
|
|
243
|
+
}
|
|
244
|
+
if (obj.scrollLeft && obj.scrollLeft > 0) {
|
|
245
|
+
coordinates.left -= obj.scrollLeft;
|
|
246
|
+
}
|
|
247
|
+
obj = ctx.windowParent ? obj.offsetParent : obj.parentNode;
|
|
248
|
+
if (!obj && iframe) {
|
|
249
|
+
obj = iframe;
|
|
250
|
+
iframe = null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
const setEndOfContentEditable = (contentEditableElement) => {
|
|
255
|
+
if (document.createRange) {
|
|
256
|
+
const range = document.createRange(); //Create a range (a range is a like the selection but invisible)
|
|
257
|
+
const selection = window.getSelection(); //get the selection object (allows you to change selection)
|
|
258
|
+
range.selectNodeContents(contentEditableElement); //Select the entire contents of the element with the range
|
|
259
|
+
range.collapse(false); //collapse the range to the end point. false means collapse to end rather than the start
|
|
260
|
+
selection?.removeAllRanges(); //remove any selections already made
|
|
261
|
+
selection?.addRange(range); //make the range you have just created the visible selection
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
};
|
|
265
|
+
const insertTextAtCaret = (element, text) => {
|
|
266
|
+
if (window.getSelection) {
|
|
267
|
+
if (!elementContainsSelection(element)) {
|
|
268
|
+
setEndOfContentEditable(element);
|
|
269
|
+
insertTextAtCaret(element, text);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const sel = window.getSelection();
|
|
273
|
+
if (sel?.getRangeAt && sel.rangeCount) {
|
|
274
|
+
let range = sel.getRangeAt(0);
|
|
275
|
+
const el = document.createElement("SPAN");
|
|
276
|
+
range.deleteContents();
|
|
277
|
+
el.innerHTML = text;
|
|
278
|
+
const frag = document.createDocumentFragment();
|
|
279
|
+
let node;
|
|
280
|
+
let lastNode;
|
|
281
|
+
while ((node = el.firstChild)) {
|
|
282
|
+
lastNode = frag.appendChild(node);
|
|
283
|
+
}
|
|
284
|
+
range.insertNode(frag);
|
|
285
|
+
if (lastNode) {
|
|
286
|
+
range = range.cloneRange();
|
|
287
|
+
range.setStartAfter(lastNode);
|
|
288
|
+
range.collapse(true);
|
|
289
|
+
sel.removeAllRanges();
|
|
290
|
+
sel.addRange(range);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
const elementContainsSelection = (el) => {
|
|
296
|
+
if (!window.getSelection) {
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
const sel = window.getSelection();
|
|
300
|
+
if (sel && sel.rangeCount > 0) {
|
|
301
|
+
for (let i = 0; i < sel.rangeCount; ++i) {
|
|
302
|
+
if (!isOrContains(sel.getRangeAt(i).commonAncestorContainer, el)) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
return false;
|
|
309
|
+
};
|
|
310
|
+
const isOrContains = (node, container) => {
|
|
311
|
+
while (node) {
|
|
312
|
+
if (node === container) {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
if (node.parentNode) {
|
|
316
|
+
node = node.parentNode;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
/* From: https://github.com/component/textarea-caret-position */
|
|
323
|
+
// We'll copy the properties below into the mirror div.
|
|
324
|
+
// Note that some browsers, such as Firefox, do not concatenate properties
|
|
325
|
+
// into their shorthand (e.g. padding-top, padding-bottom etc. -> padding),
|
|
326
|
+
// so we have to list every single property explicitly.
|
|
327
|
+
const properties = [
|
|
328
|
+
'direction', // RTL support
|
|
329
|
+
'boxSizing',
|
|
330
|
+
'width', // on Chrome and IE, exclude the scrollbar, so the mirror div wraps exactly as the textarea does
|
|
331
|
+
'height',
|
|
332
|
+
'overflowX',
|
|
333
|
+
'overflowY', // copy the scrollbar for IE
|
|
334
|
+
'borderTopWidth',
|
|
335
|
+
'borderRightWidth',
|
|
336
|
+
'borderBottomWidth',
|
|
337
|
+
'borderLeftWidth',
|
|
338
|
+
'borderStyle',
|
|
339
|
+
'paddingTop',
|
|
340
|
+
'paddingRight',
|
|
341
|
+
'paddingBottom',
|
|
342
|
+
'paddingLeft',
|
|
343
|
+
// https://developer.mozilla.org/en-US/docs/Web/CSS/font
|
|
344
|
+
'fontStyle',
|
|
345
|
+
'fontVariant',
|
|
346
|
+
'fontWeight',
|
|
347
|
+
'fontStretch',
|
|
348
|
+
'fontSize',
|
|
349
|
+
'fontSizeAdjust',
|
|
350
|
+
'lineHeight',
|
|
351
|
+
'fontFamily',
|
|
352
|
+
'textAlign',
|
|
353
|
+
'textTransform',
|
|
354
|
+
'textIndent',
|
|
355
|
+
'textDecoration', // might not make a difference, but better be safe
|
|
356
|
+
'letterSpacing',
|
|
357
|
+
'wordSpacing',
|
|
358
|
+
'tabSize',
|
|
359
|
+
'MozTabSize'
|
|
360
|
+
];
|
|
361
|
+
const isBrowser = (typeof window !== 'undefined');
|
|
362
|
+
const isFirefox = (isBrowser && get(window, 'mozInnerScreenX') !== null);
|
|
363
|
+
const getCaretCoordinates = (element, position) => {
|
|
364
|
+
if (!isBrowser) {
|
|
365
|
+
throw new Error('textarea-caret-position#getCaretCoordinates should only be called in a browser');
|
|
366
|
+
}
|
|
367
|
+
// The mirror div will replicate the textarea's style
|
|
368
|
+
const div = document.createElement('div');
|
|
369
|
+
div.id = 'input-textarea-caret-position-mirror-div';
|
|
370
|
+
document.body.appendChild(div);
|
|
371
|
+
const style = div.style;
|
|
372
|
+
const computed = window.getComputedStyle ? window.getComputedStyle(element) : get(element, 'currentStyle'); // currentStyle for IE < 9
|
|
373
|
+
const isInput = element.nodeName === 'INPUT';
|
|
374
|
+
// Default textarea styles
|
|
375
|
+
style.whiteSpace = 'pre-wrap';
|
|
376
|
+
if (!isInput) {
|
|
377
|
+
style.wordWrap = 'break-word';
|
|
378
|
+
} // only for textarea-s
|
|
379
|
+
// Position off-screen
|
|
380
|
+
style.position = 'absolute'; // required to return coordinates properly
|
|
381
|
+
// Transfer the element's properties to the div
|
|
382
|
+
properties.forEach(prop => {
|
|
383
|
+
if (isInput && prop === 'lineHeight') {
|
|
384
|
+
// Special case for <input>s because text is rendered centered and line height may be != height
|
|
385
|
+
if (computed.boxSizing !== "border-box") {
|
|
386
|
+
style.lineHeight = computed.height;
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const height = parseInt(computed.height);
|
|
390
|
+
const outerHeight = parseInt(computed.paddingTop) + parseInt(computed.paddingBottom) + parseInt(computed.borderTopWidth) + parseInt(computed.borderBottomWidth);
|
|
391
|
+
const targetHeight = outerHeight + parseInt(computed.lineHeight);
|
|
392
|
+
if (height > targetHeight) {
|
|
393
|
+
style.lineHeight = height - outerHeight + "px";
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (height === targetHeight) {
|
|
397
|
+
style.lineHeight = computed.lineHeight;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
style.lineHeight = '0';
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
set(style, prop, computed[prop]);
|
|
404
|
+
});
|
|
405
|
+
if (!isFirefox) {
|
|
406
|
+
style.overflow = 'hidden';
|
|
407
|
+
// Firefox lies about the overflow property for textareas: https://bugzilla.mozilla.org/show_bug.cgi?id=984275
|
|
408
|
+
}
|
|
409
|
+
const value = get(element, 'value');
|
|
410
|
+
if (isFirefox && element.scrollHeight > parseInt(computed.height)) {
|
|
411
|
+
style.overflowY = 'scroll';
|
|
412
|
+
}
|
|
413
|
+
div.textContent = value.substring(0, position);
|
|
414
|
+
// The second special handling for input type="text" vs textarea:
|
|
415
|
+
// spaces need to be replaced with non-breaking spaces - http://stackoverflow.com/a/13402035/1269037
|
|
416
|
+
if (isInput && !isNil(div.textContent)) {
|
|
417
|
+
div.textContent = div.textContent.replace(/\s/g, '\u00a0');
|
|
418
|
+
}
|
|
419
|
+
const span = document.createElement('span');
|
|
420
|
+
// Wrapping must be replicated *exactly*, including when a long word gets
|
|
421
|
+
// onto the next line, with whitespace at the end of the line before (#7).
|
|
422
|
+
// The *only* reliable way to do that is to copy the *entire* rest of the
|
|
423
|
+
// textarea's content into the <span> created at the caret position.
|
|
424
|
+
// For inputs, just '.' would be enough, but no need to bother.
|
|
425
|
+
span.textContent = value.substring(position) || '.'; // || because a completely empty faux span doesn't render at all
|
|
426
|
+
div.appendChild(span);
|
|
427
|
+
const coordinates = {
|
|
428
|
+
top: span.offsetTop + parseInt(computed['borderTopWidth']),
|
|
429
|
+
left: span.offsetLeft + parseInt(computed['borderLeftWidth']),
|
|
430
|
+
height: parseInt(computed['lineHeight'])
|
|
431
|
+
};
|
|
432
|
+
document.body.removeChild(div);
|
|
433
|
+
return coordinates;
|
|
434
|
+
};
|
|
435
|
+
|
|
436
|
+
class LibsUiComponentsInputsMentionListComponent {
|
|
437
|
+
/* PROPERTY */
|
|
438
|
+
items = signal([]);
|
|
439
|
+
activeIndex = signal(0);
|
|
440
|
+
hidden = signal(false);
|
|
441
|
+
styleOff = signal(false);
|
|
442
|
+
parentHandlerKeyDown = signal(undefined);
|
|
443
|
+
dropUp = signal(false);
|
|
444
|
+
coords = signal({ top: 0, left: 0, bottom: 0 });
|
|
445
|
+
offset = signal(0);
|
|
446
|
+
nativeParentElement = signal(undefined);
|
|
447
|
+
onDestroy = new Subject();
|
|
448
|
+
/* INPUT */
|
|
449
|
+
labelKey = model('label');
|
|
450
|
+
zIndex = model(1000);
|
|
451
|
+
isIframe = model.required();
|
|
452
|
+
defaultAvatar = model();
|
|
453
|
+
/* VIEW CHILD */
|
|
454
|
+
elementEl = viewChild('element');
|
|
455
|
+
itemsEl = viewChildren('item');
|
|
456
|
+
ngOnInit() {
|
|
457
|
+
if (this.isIframe()) {
|
|
458
|
+
this.initEvent(window.parent, 'wheel');
|
|
459
|
+
this.initEvent(window.parent, 'resize');
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
this.initEvent(window, 'wheel');
|
|
463
|
+
this.initEvent(window, 'resize');
|
|
464
|
+
}
|
|
465
|
+
/* FUNCTIONS */
|
|
466
|
+
initEvent(element, eventName) {
|
|
467
|
+
fromEvent(element, eventName).pipe(takeUntil(this.onDestroy)).subscribe((event) => {
|
|
468
|
+
if (eventName === 'resize' || !this.elementEl()?.nativeElement.contains(event.target)) {
|
|
469
|
+
this.handlerListenClose(event);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
handlerListenClose(event) {
|
|
474
|
+
if (event.target !== this.elementEl()?.nativeElement) {
|
|
475
|
+
this.hidden.set(true);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
// lots of confusion here between relative coordinates and containers
|
|
479
|
+
position(nativeParentElement, iframe, leftDiv) {
|
|
480
|
+
this.nativeParentElement.set(nativeParentElement);
|
|
481
|
+
if (isInputOrTextAreaElement(nativeParentElement)) {
|
|
482
|
+
// parent elements need to have postition:relative for this to work correctly?
|
|
483
|
+
this.coords.set(getCaretCoordinates(nativeParentElement, nativeParentElement.selectionStart));
|
|
484
|
+
this.coords.update(item => ({
|
|
485
|
+
...item,
|
|
486
|
+
top: nativeParentElement.offsetTop + item.top - nativeParentElement.scrollTop,
|
|
487
|
+
left: nativeParentElement.offsetLeft + item.left - nativeParentElement.scrollLeft + nativeParentElement.getBoundingClientRect().left
|
|
488
|
+
}));
|
|
489
|
+
// getCretCoordinates() for text/input elements needs an additional offset to position the list correctly
|
|
490
|
+
this.offset.set(this.getBlockCursorDimensions(nativeParentElement).height);
|
|
491
|
+
this.positionElement();
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (iframe) {
|
|
495
|
+
const context = { iframe: iframe, parent: window.parent.document.body, windowParent: true };
|
|
496
|
+
// const rect = iframe.getBoundingClientRect();
|
|
497
|
+
const caretRelativeToView = getContentEditableCaretCoords(context);
|
|
498
|
+
const doc = document.documentElement;
|
|
499
|
+
const scrollLeft = (window.scrollX || doc.scrollLeft) - (doc.clientLeft || 0);
|
|
500
|
+
this.coords.update(item => ({
|
|
501
|
+
...item,
|
|
502
|
+
left: caretRelativeToView.left - scrollLeft - (leftDiv || 0),
|
|
503
|
+
bottom: window.parent.innerHeight - caretRelativeToView.top + 18
|
|
504
|
+
}));
|
|
505
|
+
if (caretRelativeToView.bottom && caretRelativeToView.bottom !== -1) {
|
|
506
|
+
this.coords.update(item => ({
|
|
507
|
+
...item,
|
|
508
|
+
bottom: caretRelativeToView.bottom
|
|
509
|
+
}));
|
|
510
|
+
}
|
|
511
|
+
this.positionElement();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const doc = document.documentElement;
|
|
515
|
+
const scrollLeft = (window.scrollX || doc.scrollLeft) - (doc.clientLeft || 0);
|
|
516
|
+
const scrollTop = (window.scrollX || doc.scrollTop) - (doc.clientTop || 0);
|
|
517
|
+
// bounding rectangles are relative to view, offsets are relative to container?
|
|
518
|
+
const caretRelativeToView = getContentEditableCaretCoords({ iframe, parent: null });
|
|
519
|
+
this.coords.update(item => ({
|
|
520
|
+
...item,
|
|
521
|
+
top: caretRelativeToView.top - scrollTop + 10,
|
|
522
|
+
left: caretRelativeToView.left - scrollLeft - (leftDiv || 0),
|
|
523
|
+
bottom: caretRelativeToView.bottom
|
|
524
|
+
}));
|
|
525
|
+
this.positionElement();
|
|
526
|
+
}
|
|
527
|
+
get ActiveItem() {
|
|
528
|
+
return this.items()[this.activeIndex()];
|
|
529
|
+
}
|
|
530
|
+
handlerClick(event, index) {
|
|
531
|
+
event.stopPropagation();
|
|
532
|
+
this.activeIndex.set(index);
|
|
533
|
+
set(event, 'keyCode', KEY_ENTER);
|
|
534
|
+
this.parentHandlerKeyDown()?.(event, this.nativeParentElement());
|
|
535
|
+
}
|
|
536
|
+
activateNextItem() {
|
|
537
|
+
this.activeIndex.set(this.items().length - 1 > this.activeIndex() ? this.activeIndex() + 1 : this.activeIndex());
|
|
538
|
+
this.scrollToActive();
|
|
539
|
+
}
|
|
540
|
+
activatePreviousItem() {
|
|
541
|
+
this.activeIndex.set(this.activeIndex() > 0 ? this.activeIndex() - 1 : this.activeIndex());
|
|
542
|
+
this.scrollToActive();
|
|
543
|
+
}
|
|
544
|
+
positionElement(left = this.coords().left, top = this.coords().top, dropUp = this.dropUp(), bottom = this.coords().bottom || 0) {
|
|
545
|
+
const el = this.elementEl()?.nativeElement;
|
|
546
|
+
top += dropUp ? 0 : this.offset(); // top of list is next line
|
|
547
|
+
el.style.position = "absolute";
|
|
548
|
+
el.style.left = left + 'px';
|
|
549
|
+
el.style.top = bottom !== -1 ? 'auto' : top + 'px';
|
|
550
|
+
el.style.bottom = bottom === -1 ? 'auto' : bottom + 'px';
|
|
551
|
+
this.nativeParentElement()?.normalize();
|
|
552
|
+
}
|
|
553
|
+
getBlockCursorDimensions(nativeParentElement) {
|
|
554
|
+
const parentStyles = window.getComputedStyle(nativeParentElement);
|
|
555
|
+
return {
|
|
556
|
+
height: parseFloat(parentStyles.lineHeight),
|
|
557
|
+
width: parseFloat(parentStyles.fontSize)
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
handleActiveItem(event, index) {
|
|
561
|
+
event.stopPropagation();
|
|
562
|
+
this.activeIndex.set(index);
|
|
563
|
+
}
|
|
564
|
+
scrollToActive() {
|
|
565
|
+
if (!this.itemsEl() || (this.itemsEl().length <= this.activeIndex())) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
this.itemsEl()[this.activeIndex()].nativeElement.scrollIntoView();
|
|
569
|
+
}
|
|
570
|
+
scrollContent() {
|
|
571
|
+
setTimeout(() => {
|
|
572
|
+
const element = this.elementEl();
|
|
573
|
+
if (element && element.nativeElement) {
|
|
574
|
+
element.nativeElement.scrollTop = 0;
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
ngOnDestroy() {
|
|
579
|
+
this.onDestroy.next();
|
|
580
|
+
this.onDestroy.complete();
|
|
581
|
+
}
|
|
582
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: LibsUiComponentsInputsMentionListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
583
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: LibsUiComponentsInputsMentionListComponent, isStandalone: true, selector: "libs_ui-components-inputs-mention-list", inputs: { labelKey: { classPropertyName: "labelKey", publicName: "labelKey", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null }, isIframe: { classPropertyName: "isIframe", publicName: "isIframe", isSignal: true, isRequired: true, transformFunction: null }, defaultAvatar: { classPropertyName: "defaultAvatar", publicName: "defaultAvatar", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { labelKey: "labelKeyChange", zIndex: "zIndexChange", isIframe: "isIframeChange", defaultAvatar: "defaultAvatarChange" }, viewQueries: [{ propertyName: "elementEl", first: true, predicate: ["element"], descendants: true, isSignal: true }, { propertyName: "itemsEl", predicate: ["item"], descendants: true, isSignal: true }], ngImport: i0, template: "<div #element\n class=\"libs-ui-mention-list\"\n [style.zIndex]=\"zIndex()\"\n [class.hidden]=\"hidden()\">\n @for (item of items(); track item) {\n <div #item\n class=\"libs-ui-mention-list-item\"\n [class.libs-ui-mention-list-item-active]=\"$index === activeIndex()\"\n (mousedown)=\"handlerClick($event, $index)\"\n (mouseenter)=\"handleActiveItem($event, $index)\">\n <div class=\"px-[12px] py-[6px] flex items-center\">\n <libs_ui-components-avatar [size]=\"24\"\n [linkAvatar]=\"item.avatar\"\n [linkAvatarError]=\"defaultAvatar()\"\n [textAvatar]=\"item.name\"\n [idGenColor]=\"item.id\"\n [getLastTextAfterSpace]=\"true\" />\n <libs_ui-components-popover [classInclude]=\"'libs-ui-font-h5r'\"\n [type]=\"'text'\"\n [ignoreShowPopover]=\"true\"\n [config]=\"{width: 250}\">\n <!-- // ta.m \u1EA9n show tooltip v\u00EC ch\u01B0a x\u1EED l\u00FD \u0111c v\u1ECB tr\u00ED -->\n {{ item.name + ' (' + item.username + ')' }}\n </libs_ui-components-popover>\n </div>\n </div>\n }\n</div>\n", styles: [".libs-ui-mention-list{position:absolute;top:-999px;left:-9999px;z-index:1202;float:left;width:315px;padding:4px 0;background-color:#fff;border:solid 1px #e6e8ed;border-radius:5px;max-height:190px;overflow:auto;box-shadow:0 2px 10px 1px #3333331a}.libs-ui-mention-list .libs-ui-mention-list-item{background:#fff;cursor:pointer}.libs-ui-mention-list .libs-ui-mention-list-item:hover,.libs-ui-mention-list .libs-ui-mention-list-item-active{background-color:var(--libs-ui-color-light-3, #f4f8ff)}\n"], dependencies: [{ kind: "component", type: LibsUiComponentsAvatarComponent, selector: "libs_ui-components-avatar", inputs: ["getLastTextAfterSpace", "typeShape", "classInclude", "size", "linkAvatar", "linkAvatarError", "idGenColor", "textAvatar", "classImageInclude"], outputs: ["outAvatarError"] }, { kind: "component", type: LibsUiComponentsPopoverComponent, selector: "libs_ui-components-popover,[LibsUiComponentsPopoverDirective]", inputs: ["debugId", "flagMouse", "type", "mode", "config", "ignoreShowPopover", "elementRefCustom", "classInclude", "ignoreHiddenPopoverContentWhenMouseLeave", "ignoreStopPropagationEvent", "ignoreCursorPointerModeLikeClick", "isAddContentToParentDocument", "ignoreClickOutside"], outputs: ["outEvent", "outChangStageFlagMouse", "outEventPopoverContent", "outFunctionsControl"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
584
|
+
}
|
|
585
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: LibsUiComponentsInputsMentionListComponent, decorators: [{
|
|
586
|
+
type: Component,
|
|
587
|
+
args: [{ selector: 'libs_ui-components-inputs-mention-list', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
588
|
+
LibsUiComponentsAvatarComponent,
|
|
589
|
+
LibsUiComponentsPopoverComponent
|
|
590
|
+
], template: "<div #element\n class=\"libs-ui-mention-list\"\n [style.zIndex]=\"zIndex()\"\n [class.hidden]=\"hidden()\">\n @for (item of items(); track item) {\n <div #item\n class=\"libs-ui-mention-list-item\"\n [class.libs-ui-mention-list-item-active]=\"$index === activeIndex()\"\n (mousedown)=\"handlerClick($event, $index)\"\n (mouseenter)=\"handleActiveItem($event, $index)\">\n <div class=\"px-[12px] py-[6px] flex items-center\">\n <libs_ui-components-avatar [size]=\"24\"\n [linkAvatar]=\"item.avatar\"\n [linkAvatarError]=\"defaultAvatar()\"\n [textAvatar]=\"item.name\"\n [idGenColor]=\"item.id\"\n [getLastTextAfterSpace]=\"true\" />\n <libs_ui-components-popover [classInclude]=\"'libs-ui-font-h5r'\"\n [type]=\"'text'\"\n [ignoreShowPopover]=\"true\"\n [config]=\"{width: 250}\">\n <!-- // ta.m \u1EA9n show tooltip v\u00EC ch\u01B0a x\u1EED l\u00FD \u0111c v\u1ECB tr\u00ED -->\n {{ item.name + ' (' + item.username + ')' }}\n </libs_ui-components-popover>\n </div>\n </div>\n }\n</div>\n", styles: [".libs-ui-mention-list{position:absolute;top:-999px;left:-9999px;z-index:1202;float:left;width:315px;padding:4px 0;background-color:#fff;border:solid 1px #e6e8ed;border-radius:5px;max-height:190px;overflow:auto;box-shadow:0 2px 10px 1px #3333331a}.libs-ui-mention-list .libs-ui-mention-list-item{background:#fff;cursor:pointer}.libs-ui-mention-list .libs-ui-mention-list-item:hover,.libs-ui-mention-list .libs-ui-mention-list-item-active{background-color:var(--libs-ui-color-light-3, #f4f8ff)}\n"] }]
|
|
591
|
+
}] });
|
|
592
|
+
|
|
593
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
594
|
+
class LibsUiComponentsInputsMentionDirective {
|
|
595
|
+
/* PROPERTY */
|
|
596
|
+
listComponentRef = signal(undefined);
|
|
597
|
+
nodesInsert = signal(new Set());
|
|
598
|
+
activeConfig = signal(undefined);
|
|
599
|
+
triggerChars = signal({});
|
|
600
|
+
searchString = signal('');
|
|
601
|
+
startPos = signal(undefined);
|
|
602
|
+
startNode = signal(undefined);
|
|
603
|
+
searchList = signal(undefined);
|
|
604
|
+
searching = signal(false);
|
|
605
|
+
iframe = signal(undefined);
|
|
606
|
+
lastKeyCode = signal(undefined);
|
|
607
|
+
isEditor = signal(false);
|
|
608
|
+
iframeId = signal(undefined);
|
|
609
|
+
element = inject(ElementRef);
|
|
610
|
+
dynamicComponentService = inject(LibsUiDynamicComponentService);
|
|
611
|
+
onDestroy = new Subject();
|
|
612
|
+
/* INPUT */
|
|
613
|
+
mentionConfig = input();
|
|
614
|
+
mentionListTemplate = input();
|
|
615
|
+
/* OUTPUT */
|
|
616
|
+
outSearchTerm = output();
|
|
617
|
+
outItemSelected = output();
|
|
618
|
+
outToggle = output();
|
|
619
|
+
outInsertMention = output();
|
|
620
|
+
outFunctionControl = output();
|
|
621
|
+
constructor() {
|
|
622
|
+
effect(() => {
|
|
623
|
+
this.mentionConfig();
|
|
624
|
+
untracked(() => this.updateConfig());
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
ngAfterViewInit() {
|
|
628
|
+
setTimeout(() => {
|
|
629
|
+
if (!this.mentionConfig()) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
const isContentEditable = this.element.nativeElement.getAttribute('contenteditable');
|
|
633
|
+
this.isEditor.set(!isContentEditable);
|
|
634
|
+
const element = !this.isEditor() ? this.element.nativeElement : this.element.nativeElement.querySelector('.ql-editor[contenteditable="true"]');
|
|
635
|
+
element.addEventListener('keydown', (event) => {
|
|
636
|
+
this.handlerKeydown.bind(this)(event, element);
|
|
637
|
+
});
|
|
638
|
+
element.addEventListener('input', (event) => {
|
|
639
|
+
this.handlerInput.bind(this)(event, element);
|
|
640
|
+
});
|
|
641
|
+
element.addEventListener('blur', (event) => {
|
|
642
|
+
this.handlerBlur.bind(this)(event);
|
|
643
|
+
});
|
|
644
|
+
element.addEventListener('click', (event) => {
|
|
645
|
+
this.handlerFocus.bind(this)(event, element);
|
|
646
|
+
});
|
|
647
|
+
this.outFunctionControl.emit({
|
|
648
|
+
addMention: () => {
|
|
649
|
+
insertTextAtCaret(element, `${this.mentionConfig()?.triggerChar}`);
|
|
650
|
+
setTimeout(() => {
|
|
651
|
+
this.handlerKeydown({ key: this.mentionConfig()?.triggerChar, keyCode: KEY_BACKSPACE, shiftKey: true, inputEvent: true }, element);
|
|
652
|
+
}, 500);
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
/* FUNCTIONS */
|
|
658
|
+
updateConfig() {
|
|
659
|
+
const mentionConfig = this.mentionConfig();
|
|
660
|
+
if (!mentionConfig) {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
this.triggerChars.set({});
|
|
664
|
+
this.addConfig(mentionConfig);
|
|
665
|
+
if (mentionConfig.mention) {
|
|
666
|
+
mentionConfig.mention.forEach(config => this.addConfig(config));
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
addConfig(config) {
|
|
670
|
+
const defaults = Object.assign({}, DEFAULT_CONFIG);
|
|
671
|
+
const labelKey = config.labelKey || 'label';
|
|
672
|
+
config = Object.assign(defaults, config);
|
|
673
|
+
if (config.items && config.items.length > 0) {
|
|
674
|
+
if (typeof config.items[0] === 'string') {
|
|
675
|
+
config.items.forEach((label) => {
|
|
676
|
+
const object = {};
|
|
677
|
+
object[labelKey] = label;
|
|
678
|
+
return object;
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
if (labelKey) {
|
|
682
|
+
// remove items without an labelKey (as it's required to filter the list)
|
|
683
|
+
config.items = config.items.filter(e => e[labelKey]);
|
|
684
|
+
if (!config.disableSort) {
|
|
685
|
+
config.items.sort((a, b) => (a[labelKey] || '').localeCompare((b[labelKey] || '')));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
this.triggerChars.update(item => ({
|
|
690
|
+
...item,
|
|
691
|
+
[config.triggerChar || '@']: config
|
|
692
|
+
}));
|
|
693
|
+
if (this.activeConfig() && this.activeConfig()?.triggerChar === config.triggerChar) {
|
|
694
|
+
this.activeConfig.set(config);
|
|
695
|
+
this.updateSearchList();
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
setIframe(iframe) {
|
|
699
|
+
this.iframe.set(iframe);
|
|
700
|
+
}
|
|
701
|
+
stopEvent(event) {
|
|
702
|
+
if (get(event, 'wasClick')) {
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
event.preventDefault();
|
|
706
|
+
event.stopPropagation();
|
|
707
|
+
event.stopImmediatePropagation();
|
|
708
|
+
}
|
|
709
|
+
handlerBlur(event) {
|
|
710
|
+
setTimeout(() => {
|
|
711
|
+
this.stopEvent(event);
|
|
712
|
+
this.stopSearch();
|
|
713
|
+
}, 100);
|
|
714
|
+
}
|
|
715
|
+
handlerInput(event, nativeElement = this.element.nativeElement) {
|
|
716
|
+
const data = get(event, 'data');
|
|
717
|
+
if (this.lastKeyCode() === KEY_BUFFERED && data) {
|
|
718
|
+
const keyCode = data.charCodeAt(0);
|
|
719
|
+
this.handlerKeydown({ keyCode, inputEvent: true }, nativeElement);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
handlerKeydown(event, nativeElement = this.element.nativeElement) {
|
|
723
|
+
this.lastKeyCode.set(event.keyCode);
|
|
724
|
+
if (event.isComposing || event.keyCode === KEY_BUFFERED) {
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
const val = getValue(nativeElement);
|
|
728
|
+
let pos = getCaretPosition(nativeElement, this.iframe());
|
|
729
|
+
let charPressed = event.key;
|
|
730
|
+
if (!this.isEditor()) {
|
|
731
|
+
if (event.keyCode === KEY_BACKSPACE) {
|
|
732
|
+
this.appendEmptyToLastNode(pos, val.length, nativeElement);
|
|
733
|
+
}
|
|
734
|
+
setTimeout(() => {
|
|
735
|
+
this.addNodeZeroWidthSpace(nativeElement);
|
|
736
|
+
}, 0);
|
|
737
|
+
}
|
|
738
|
+
if (!charPressed) {
|
|
739
|
+
const charCode = event.which || event.keyCode;
|
|
740
|
+
charPressed = String.fromCharCode(event.which || event.keyCode);
|
|
741
|
+
if (!event.shiftKey && (charCode >= 65 && charCode <= 90)) {
|
|
742
|
+
charPressed = String.fromCharCode(charCode + 32);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
if (event.keyCode === KEY_ENTER && event.wasClick && pos < (this.startPos() || 0)) {
|
|
746
|
+
pos = this.startNode().length;
|
|
747
|
+
setCaretPosition(this.startNode(), pos, this.iframe());
|
|
748
|
+
}
|
|
749
|
+
const config = this.triggerChars()[charPressed];
|
|
750
|
+
if (config) {
|
|
751
|
+
this.activeConfig.set(config);
|
|
752
|
+
this.startPos.set(event.inputEvent ? pos - 1 : pos);
|
|
753
|
+
this.startNode.set(getWindowSelection(this.iframe())?.anchorNode);
|
|
754
|
+
this.searching.set(true);
|
|
755
|
+
this.searchString.set('');
|
|
756
|
+
this.showSearchList(nativeElement);
|
|
757
|
+
this.updateSearchList();
|
|
758
|
+
if (config.returnTrigger) {
|
|
759
|
+
this.outSearchTerm.emit(config.triggerChar);
|
|
760
|
+
}
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
setTimeout(() => {
|
|
764
|
+
for (const itemSet of this.nodesInsert()) {
|
|
765
|
+
if (!nativeElement.contains(itemSet.elementSpan)) {
|
|
766
|
+
itemSet.subscription?.unsubscribe();
|
|
767
|
+
this.nodesInsert().delete(itemSet);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
const startPos = this.startPos();
|
|
772
|
+
if (isNil(startPos) || startPos < 0 || !this.searching()) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const searchList = this.searchList();
|
|
776
|
+
if (pos <= startPos) {
|
|
777
|
+
if (searchList) {
|
|
778
|
+
searchList.hidden.set(true);
|
|
779
|
+
}
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
if (event.keyCode === KEY_SHIFT || event.metaKey || event.altKey || event.ctrlKey || pos <= startPos) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
if (!this.activeConfig()?.allowSpace && event.keyCode === KEY_SPACE) {
|
|
786
|
+
this.startPos.set(-1);
|
|
787
|
+
}
|
|
788
|
+
else if (event.keyCode === KEY_BACKSPACE && pos > 0) {
|
|
789
|
+
pos--;
|
|
790
|
+
if (pos === this.startPos()) {
|
|
791
|
+
this.stopSearch();
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
else if (searchList?.hidden()) {
|
|
795
|
+
if (event.keyCode === KEY_TAB || event.keyCode === KEY_ENTER) {
|
|
796
|
+
this.stopSearch();
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
else if (!searchList?.hidden() && this.activeConfig()) {
|
|
801
|
+
if (event.keyCode === KEY_TAB || event.keyCode === KEY_ENTER) {
|
|
802
|
+
this.stopEvent(event);
|
|
803
|
+
// emit the selected list item
|
|
804
|
+
this.outItemSelected.emit(searchList?.ActiveItem);
|
|
805
|
+
// optional function to format the selected item before inserting the text
|
|
806
|
+
this.insertMention(nativeElement, pos);
|
|
807
|
+
// fire input event so angular bindings are updated
|
|
808
|
+
if ("Event" in window) {
|
|
809
|
+
// this seems backwards, but fire the event from this elements nativeElement (not the
|
|
810
|
+
// one provided that may be in an iframe, as it won't be propogate)
|
|
811
|
+
// Create the event using the modern Event constructor
|
|
812
|
+
const evt = new Event(this.iframe() ? "change" : "input", {
|
|
813
|
+
bubbles: true,
|
|
814
|
+
cancelable: false
|
|
815
|
+
});
|
|
816
|
+
// Dispatch the event on the native element
|
|
817
|
+
nativeElement.dispatchEvent(evt);
|
|
818
|
+
}
|
|
819
|
+
this.startPos.set(-1);
|
|
820
|
+
this.stopSearch();
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
if (event.keyCode === KEY_ESCAPE) {
|
|
824
|
+
this.stopEvent(event);
|
|
825
|
+
this.stopSearch();
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
if (event.keyCode === KEY_DOWN) {
|
|
829
|
+
this.stopEvent(event);
|
|
830
|
+
searchList?.activateNextItem();
|
|
831
|
+
return false;
|
|
832
|
+
}
|
|
833
|
+
if (event.keyCode === KEY_UP) {
|
|
834
|
+
this.stopEvent(event);
|
|
835
|
+
searchList?.activatePreviousItem();
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (charPressed.length !== 1 && event.keyCode !== KEY_BACKSPACE) {
|
|
840
|
+
this.stopEvent(event);
|
|
841
|
+
return false;
|
|
842
|
+
}
|
|
843
|
+
if (!this.searching()) {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
let mention = val.substring((this.startPos() || 0) + 1, pos);
|
|
847
|
+
if (event.keyCode !== KEY_BACKSPACE && !event.inputEvent) {
|
|
848
|
+
mention += charPressed;
|
|
849
|
+
}
|
|
850
|
+
const searchString = mention.replace(String.fromCharCode(160), ' ').replace(String.fromCharCode(0), '');
|
|
851
|
+
this.searchString.set(searchString.trim());
|
|
852
|
+
const limitSpaceSearchQuery = this.activeConfig()?.limitSpaceSearchQuery;
|
|
853
|
+
if (limitSpaceSearchQuery && (searchString.split(' ').length - 1) > limitSpaceSearchQuery) {
|
|
854
|
+
this.searchString.set('');
|
|
855
|
+
}
|
|
856
|
+
if (this.activeConfig()?.returnTrigger) {
|
|
857
|
+
const triggerChar = (this.searchString() || event.keyCode === KEY_BACKSPACE) ? val.substring(this.startPos(), (this.startPos() || 0) + 1) : '';
|
|
858
|
+
this.outSearchTerm.emit(triggerChar + this.searchString());
|
|
859
|
+
this.updateSearchList();
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
this.outSearchTerm.emit(this.searchString());
|
|
863
|
+
this.updateSearchList();
|
|
864
|
+
}
|
|
865
|
+
handlerFocus(e, nativeElement = this.element.nativeElement) {
|
|
866
|
+
e?.stopPropagation();
|
|
867
|
+
setTimeout(() => {
|
|
868
|
+
const node = getWindowSelection(this.iframe())?.anchorNode;
|
|
869
|
+
if (node?.nodeType !== 3) {
|
|
870
|
+
this.stopSearch();
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const textBeforeCursor = getTextBeforeCursor() || '';
|
|
874
|
+
const triggerChars = Object.keys(this.triggerChars()).map((key) => {
|
|
875
|
+
return {
|
|
876
|
+
key: key,
|
|
877
|
+
config: this.triggerChars()[key],
|
|
878
|
+
index: textBeforeCursor.lastIndexOf(key)
|
|
879
|
+
};
|
|
880
|
+
});
|
|
881
|
+
const matchingCharacters = triggerChars.reduce((pre, current) => pre && current && current.index > pre.index ? current : pre);
|
|
882
|
+
if (matchingCharacters.index < 0) {
|
|
883
|
+
this.stopSearch();
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const search = textBeforeCursor?.substring(matchingCharacters.index, textBeforeCursor.length);
|
|
887
|
+
if (!search) {
|
|
888
|
+
this.stopSearch();
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const pos = getCaretPosition(nativeElement, this.iframe());
|
|
892
|
+
this.activeConfig.set(matchingCharacters.config);
|
|
893
|
+
this.startPos.set(pos - (textBeforeCursor.length - matchingCharacters.index));
|
|
894
|
+
this.searchString.set(search.replace(matchingCharacters.key, ''));
|
|
895
|
+
this.searching.set(true);
|
|
896
|
+
this.handlerKeydown({ keyCode: ''.charCodeAt(0), inputEvent: false }, nativeElement);
|
|
897
|
+
const divElement = document.createElement("div");
|
|
898
|
+
divElement.style.width = 'max-content';
|
|
899
|
+
divElement.style.position = 'absolute';
|
|
900
|
+
divElement.innerText = search;
|
|
901
|
+
document.body.appendChild(divElement);
|
|
902
|
+
let iframe = undefined;
|
|
903
|
+
try {
|
|
904
|
+
iframe = this.mentionConfig()?.iframe ? window.parent.document.getElementById(this.mentionConfig()?.iframe) : undefined;
|
|
905
|
+
}
|
|
906
|
+
catch (error) {
|
|
907
|
+
console.log(error);
|
|
908
|
+
}
|
|
909
|
+
const searchList = this.searchList();
|
|
910
|
+
if (searchList) {
|
|
911
|
+
searchList.position(nativeElement, iframe, divElement.getBoundingClientRect().width);
|
|
912
|
+
}
|
|
913
|
+
divElement.remove();
|
|
914
|
+
nativeElement.normalize();
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
stopSearch() {
|
|
918
|
+
const searchList = this.searchList();
|
|
919
|
+
if (searchList && !searchList.hidden()) {
|
|
920
|
+
searchList.hidden.set(true);
|
|
921
|
+
this.outToggle.emit(false);
|
|
922
|
+
}
|
|
923
|
+
this.activeConfig.set(undefined);
|
|
924
|
+
this.searching.set(false);
|
|
925
|
+
}
|
|
926
|
+
updateSearchList() {
|
|
927
|
+
const matches = [];
|
|
928
|
+
const activeConfig = this.activeConfig();
|
|
929
|
+
const searchString = this.searchString();
|
|
930
|
+
const searchList = this.searchList();
|
|
931
|
+
// disabling the search relies on the async operation to do the filtering
|
|
932
|
+
if (activeConfig && activeConfig.items && !activeConfig.disableSearch && !isNil(searchString) && activeConfig.labelKey && activeConfig.mentionFilter) {
|
|
933
|
+
matches.push(...activeConfig.mentionFilter(searchString, activeConfig.items));
|
|
934
|
+
}
|
|
935
|
+
// update the search list
|
|
936
|
+
if (!searchList) {
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
searchList.items.set(matches);
|
|
940
|
+
searchList.hidden.set(!matches.length);
|
|
941
|
+
searchList.activeIndex.set(0);
|
|
942
|
+
searchList.scrollContent();
|
|
943
|
+
this.outToggle.emit(matches.length ? true : false);
|
|
944
|
+
}
|
|
945
|
+
showSearchList(nativeElement) {
|
|
946
|
+
let iframe = undefined;
|
|
947
|
+
const mentionConfig = this.mentionConfig();
|
|
948
|
+
try {
|
|
949
|
+
iframe = mentionConfig?.iframe ? window.parent.document.getElementById(mentionConfig?.iframe) : undefined;
|
|
950
|
+
}
|
|
951
|
+
catch (error) {
|
|
952
|
+
console.log(error);
|
|
953
|
+
}
|
|
954
|
+
if (!mentionConfig) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
this.outToggle.emit(true);
|
|
958
|
+
let firstTime = false;
|
|
959
|
+
if (!this.listComponentRef()) {
|
|
960
|
+
firstTime = true;
|
|
961
|
+
this.listComponentRef.set(this.dynamicComponentService.resolveComponentFactory(LibsUiComponentsInputsMentionListComponent));
|
|
962
|
+
this.searchList.set(this.listComponentRef()?.instance);
|
|
963
|
+
}
|
|
964
|
+
this.searchList()?.labelKey.set(this.activeConfig()?.labelKey || '');
|
|
965
|
+
this.searchList()?.styleOff.set(this.mentionConfig()?.disableStyle || false);
|
|
966
|
+
this.searchList()?.activeIndex.set(0);
|
|
967
|
+
this.searchList()?.zIndex.set(this.mentionConfig()?.zIndex || 2500);
|
|
968
|
+
this.searchList()?.isIframe.set(iframe ? true : false);
|
|
969
|
+
this.searchList()?.parentHandlerKeyDown.set(this.handlerKeydown.bind(this));
|
|
970
|
+
this.searchList()?.position(nativeElement, iframe);
|
|
971
|
+
if (firstTime && this.listComponentRef()) {
|
|
972
|
+
this.iframeId.set(this.dynamicComponentService.addToBody(this.listComponentRef(), iframe ? true : false));
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
destroyListSearch() {
|
|
976
|
+
this.dynamicComponentService.remove(this.listComponentRef, this.iframeId());
|
|
977
|
+
this.listComponentRef.set(undefined);
|
|
978
|
+
}
|
|
979
|
+
appendEmptyToLastNode(position, length, nativeElement = this.element.nativeElement) {
|
|
980
|
+
const empty = document.createTextNode('\u00A0');
|
|
981
|
+
const selection = getWindowSelection(this.iframe());
|
|
982
|
+
const anchorNode = selection?.anchorNode;
|
|
983
|
+
if (this.isEditor()) {
|
|
984
|
+
const parentNode = anchorNode?.parentNode;
|
|
985
|
+
if (parentNode && parentNode.getAttribute && parentNode.getAttribute('id')) {
|
|
986
|
+
if (!parentNode.nextSibling || !parentNode.nextSibling.textContent?.length) {
|
|
987
|
+
parentNode?.parentNode?.insertBefore(empty, parentNode.nextSibling);
|
|
988
|
+
}
|
|
989
|
+
const range = document.createRange();
|
|
990
|
+
const sel = window.getSelection();
|
|
991
|
+
range.setStart(parentNode.nextSibling, 0);
|
|
992
|
+
range.collapse(true);
|
|
993
|
+
sel?.removeAllRanges();
|
|
994
|
+
sel?.addRange(range);
|
|
995
|
+
}
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
let preNode = null;
|
|
999
|
+
const anchorOffset = selection?.anchorOffset;
|
|
1000
|
+
if (!anchorNode || !anchorOffset) {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
if (anchorNode.nodeName.toLowerCase() !== '#text') {
|
|
1004
|
+
nativeElement.appendChild(empty);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
for (let i = 0; i < nativeElement.childNodes.length; i++) {
|
|
1008
|
+
const node = nativeElement.childNodes[i];
|
|
1009
|
+
if (anchorNode.nodeType === 3 && anchorNode === node && preNode && preNode.getAttribute && preNode.getAttribute('id') && (!anchorNode.textContent?.length || anchorNode.textContent.length === 1) && position === length) {
|
|
1010
|
+
nativeElement.appendChild(empty);
|
|
1011
|
+
break;
|
|
1012
|
+
}
|
|
1013
|
+
preNode = node;
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
addNodeZeroWidthSpace(nativeElement = this.element.nativeElement) {
|
|
1017
|
+
const empty = document.createTextNode('\u200b');
|
|
1018
|
+
let preNode = null;
|
|
1019
|
+
for (let i = 0; i < nativeElement.childNodes.length; i++) {
|
|
1020
|
+
const node = nativeElement.childNodes[i];
|
|
1021
|
+
if (preNode && preNode.getAttribute && preNode.getAttribute('id') && node && node.getAttribute && node.getAttribute('id')) {
|
|
1022
|
+
nativeElement.insertBefore(empty, preNode.nextSibling);
|
|
1023
|
+
}
|
|
1024
|
+
preNode = node;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
insertMention(nativeElement, pos) {
|
|
1028
|
+
const activeConfig = this.activeConfig();
|
|
1029
|
+
if (!activeConfig) {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const searchList = this.searchList();
|
|
1033
|
+
let text = '';
|
|
1034
|
+
const feId = uuid();
|
|
1035
|
+
const id = searchList?.ActiveItem.id || uuid();
|
|
1036
|
+
const itemActive = searchList?.ActiveItem;
|
|
1037
|
+
const { triggerChar, mentionEventName, mentionActionByEvent } = activeConfig;
|
|
1038
|
+
if (activeConfig.mentionSelect) {
|
|
1039
|
+
text = activeConfig.mentionSelect(searchList?.ActiveItem, activeConfig.triggerChar);
|
|
1040
|
+
}
|
|
1041
|
+
if (!this.isEditor()) {
|
|
1042
|
+
let span = document.createElement('TEXTAREA');
|
|
1043
|
+
span.innerText = text;
|
|
1044
|
+
text = span.innerHTML;
|
|
1045
|
+
span.remove();
|
|
1046
|
+
text = buildTemplate(text, id, feId);
|
|
1047
|
+
span = insertValue(nativeElement, this.startPos() || 0, pos, text, this.iframe());
|
|
1048
|
+
if (span) {
|
|
1049
|
+
const data = {
|
|
1050
|
+
elementSpan: span,
|
|
1051
|
+
subscription: undefined,
|
|
1052
|
+
item: itemActive,
|
|
1053
|
+
triggerChar: triggerChar,
|
|
1054
|
+
manualAdd: true
|
|
1055
|
+
};
|
|
1056
|
+
this.nodesInsert().add(data);
|
|
1057
|
+
if (mentionEventName) {
|
|
1058
|
+
data.subscription = fromEvent(span, mentionEventName).pipe(tap(e => e.stopPropagation()), takeUntil$1(this.onDestroy)).subscribe(() => {
|
|
1059
|
+
if (mentionActionByEvent) {
|
|
1060
|
+
mentionActionByEvent(itemActive, triggerChar);
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
else {
|
|
1067
|
+
this.outInsertMention.emit({ data: { id, value: text, feId }, lengthKey: pos - (this.startPos() || 0) });
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
ngOnDestroy() {
|
|
1071
|
+
this.destroyListSearch();
|
|
1072
|
+
this.onDestroy.next();
|
|
1073
|
+
this.onDestroy.complete();
|
|
1074
|
+
}
|
|
1075
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: LibsUiComponentsInputsMentionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1076
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.2.13", type: LibsUiComponentsInputsMentionDirective, isStandalone: true, selector: "[LibsUiComponentsInputsMentionDirective]", inputs: { mentionConfig: { classPropertyName: "mentionConfig", publicName: "mentionConfig", isSignal: true, isRequired: false, transformFunction: null }, mentionListTemplate: { classPropertyName: "mentionListTemplate", publicName: "mentionListTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { outSearchTerm: "outSearchTerm", outItemSelected: "outItemSelected", outToggle: "outToggle", outInsertMention: "outInsertMention", outFunctionControl: "outFunctionControl" }, ngImport: i0 });
|
|
1077
|
+
}
|
|
1078
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: LibsUiComponentsInputsMentionDirective, decorators: [{
|
|
1079
|
+
type: Directive,
|
|
1080
|
+
args: [{
|
|
1081
|
+
// eslint-disable-next-line @angular-eslint/directive-selector
|
|
1082
|
+
selector: '[LibsUiComponentsInputsMentionDirective]',
|
|
1083
|
+
standalone: true
|
|
1084
|
+
}]
|
|
1085
|
+
}], ctorParameters: () => [] });
|
|
1086
|
+
|
|
1087
|
+
/**
|
|
1088
|
+
* Generated bundle index. Do not edit.
|
|
1089
|
+
*/
|
|
1090
|
+
|
|
1091
|
+
export { LibsUiComponentsInputsMentionDirective };
|
|
1092
|
+
//# sourceMappingURL=libs-ui-components-inputs-mention.mjs.map
|