@memberjunction/ng-composer 6.1.0-edge.1 → 6.1.0-edge.2
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/dist/lib/components/mention/mention-dropdown.component.d.ts +4 -2
- package/dist/lib/components/mention/mention-dropdown.component.d.ts.map +1 -1
- package/dist/lib/components/mention/mention-dropdown.component.js +21 -7
- package/dist/lib/components/mention/mention-dropdown.component.js.map +1 -1
- package/dist/lib/components/mention/mention-editor.component.d.ts +73 -1
- package/dist/lib/components/mention/mention-editor.component.d.ts.map +1 -1
- package/dist/lib/components/mention/mention-editor.component.js +267 -21
- package/dist/lib/components/mention/mention-editor.component.js.map +1 -1
- package/dist/lib/components/message/message-input-box.component.d.ts +103 -42
- package/dist/lib/components/message/message-input-box.component.d.ts.map +1 -1
- package/dist/lib/components/message/message-input-box.component.js +221 -119
- package/dist/lib/components/message/message-input-box.component.js.map +1 -1
- package/dist/lib/events/composer-events.d.ts +40 -0
- package/dist/lib/events/composer-events.d.ts.map +1 -0
- package/dist/lib/events/composer-events.js +40 -0
- package/dist/lib/events/composer-events.js.map +1 -0
- package/dist/public-api.d.ts +1 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +2 -0
- package/dist/public-api.js.map +1 -1
- package/package.json +5 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Component, Input, Output, EventEmitter, ViewChild, forwardRef } from '@angular/core';
|
|
1
|
+
import { Component, Input, Output, EventEmitter, ViewChild, forwardRef, HostListener } from '@angular/core';
|
|
2
2
|
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
3
3
|
import { DiscoverComposerTriggerProviders } from '../../composer-trigger-provider';
|
|
4
4
|
import { UUIDsEqual } from '@memberjunction/global';
|
|
@@ -62,7 +62,7 @@ function MentionEditorComponent_Conditional_5_Template(rf, ctx) { if (rf & 1) {
|
|
|
62
62
|
i0.ɵɵelementEnd();
|
|
63
63
|
} if (rf & 2) {
|
|
64
64
|
const ctx_r3 = i0.ɵɵnextContext();
|
|
65
|
-
i0.ɵɵproperty("suggestions", ctx_r3.mentionSuggestions)("position", ctx_r3.mentionDropdownPosition)("showAbove", ctx_r3.mentionDropdownShowAbove)("useFixedPositioning", true)("visible", true);
|
|
65
|
+
i0.ɵɵproperty("suggestions", ctx_r3.mentionSuggestions)("position", ctx_r3.mentionDropdownPosition)("showAbove", ctx_r3.mentionDropdownShowAbove)("alignRight", ctx_r3.mentionDropdownAlignRight)("useFixedPositioning", true)("visible", true);
|
|
66
66
|
} }
|
|
67
67
|
/**
|
|
68
68
|
* ContentEditable-based mention editor with visual chips/pills
|
|
@@ -70,6 +70,7 @@ function MentionEditorComponent_Conditional_5_Template(rf, ctx) { if (rf & 1) {
|
|
|
70
70
|
*/
|
|
71
71
|
export class MentionEditorComponent {
|
|
72
72
|
cdr;
|
|
73
|
+
hostRef;
|
|
73
74
|
editorRef;
|
|
74
75
|
placeholder = 'Type @ to mention agents or users, # for entities...';
|
|
75
76
|
disabled = false;
|
|
@@ -112,10 +113,32 @@ export class MentionEditorComponent {
|
|
|
112
113
|
mentionSuggestions = [];
|
|
113
114
|
mentionDropdownPosition = { top: 0, left: 0 };
|
|
114
115
|
mentionDropdownShowAbove = false;
|
|
116
|
+
/** Right-align the dropdown against its anchor. Only the button path sets this. */
|
|
117
|
+
mentionDropdownAlignRight = false;
|
|
115
118
|
mentionStartIndex = -1;
|
|
116
119
|
mentionQuery = '';
|
|
117
120
|
/** The trigger char that opened the current mention dropdown */
|
|
118
121
|
activeTrigger = '@';
|
|
122
|
+
/**
|
|
123
|
+
* Set while a dropdown was opened by {@link OpenTrigger} rather than by typing.
|
|
124
|
+
*
|
|
125
|
+
* In this mode the trigger character is NOT in the editor — there is nothing to strip on
|
|
126
|
+
* dismissal, and nothing for the user to see until they pick something. It also changes how the
|
|
127
|
+
* query and the chip insertion are measured, since neither can anchor on a character that is not
|
|
128
|
+
* there; {@link virtualTriggerBaseline} stands in for it.
|
|
129
|
+
*/
|
|
130
|
+
virtualTriggerOpen = false;
|
|
131
|
+
/** Text length at the moment a virtual trigger opened — the origin for its query and deletion. */
|
|
132
|
+
virtualTriggerBaseline = 0;
|
|
133
|
+
/**
|
|
134
|
+
* The control that opened a virtual trigger, used to anchor the dropdown to it.
|
|
135
|
+
*
|
|
136
|
+
* STICKY for the whole session on purpose. `positionMentionDropdown()` re-runs on every keystroke
|
|
137
|
+
* while the user filters, and once there is text the caret measures real coordinates — so
|
|
138
|
+
* anchoring only on first paint would open at the button and then jump to the far-left caret on
|
|
139
|
+
* the first keypress, which is worse than not anchoring at all.
|
|
140
|
+
*/
|
|
141
|
+
virtualTriggerAnchor = null;
|
|
119
142
|
/**
|
|
120
143
|
* Monotonic sequence guarding the async suggestion fetch — a response only applies
|
|
121
144
|
* when no newer keystroke (or dropdown close) has superseded it.
|
|
@@ -149,8 +172,39 @@ export class MentionEditorComponent {
|
|
|
149
172
|
onTouched = () => { };
|
|
150
173
|
/** Value written via writeValue() before the view (editorRef) exists — applied in ngAfterViewInit. */
|
|
151
174
|
pendingWriteValue = null;
|
|
152
|
-
constructor(cdr) {
|
|
175
|
+
constructor(cdr, hostRef) {
|
|
153
176
|
this.cdr = cdr;
|
|
177
|
+
this.hostRef = hostRef;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Close the dropdown when the pointer goes down anywhere outside it.
|
|
181
|
+
*
|
|
182
|
+
* Dismissal previously relied entirely on the editor's blur, which is not enough: clicking a
|
|
183
|
+
* non-focusable area (the message list, page background) does not blur a contenteditable, so the
|
|
184
|
+
* dropdown stayed open with nothing to close it. That was always true for a typed trigger; the
|
|
185
|
+
* Skills button makes it easy to hit, because the button deliberately keeps focus in the editor
|
|
186
|
+
* so the user's next click is the FIRST thing that could dismiss it.
|
|
187
|
+
*
|
|
188
|
+
* `mousedown` rather than `click`: it fires before the browser moves focus, so this decides
|
|
189
|
+
* before blur's own 200ms timer gets involved and the two cannot race.
|
|
190
|
+
*
|
|
191
|
+
* The dropdown renders with fixed positioning but stays inside this component's host, so the
|
|
192
|
+
* containment check covers a click on a suggestion row — which must select, not dismiss.
|
|
193
|
+
*/
|
|
194
|
+
onDocumentMouseDown(event) {
|
|
195
|
+
// Also fires for a press still awaiting suggestions: on a cold session the provider load takes
|
|
196
|
+
// hundreds of ms, and without this the user's dismissing click is ignored and the menu pops
|
|
197
|
+
// open afterwards, unbidden. closeMentionDropdown bumps the request sequence, which drops the
|
|
198
|
+
// in-flight response.
|
|
199
|
+
if (!this.showMentionDropdown && !this.virtualTriggerOpen) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const target = event.target;
|
|
203
|
+
if (target && this.hostRef.nativeElement.contains(target)) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
this.closeMentionDropdown();
|
|
207
|
+
this.cdr.markForCheck();
|
|
154
208
|
}
|
|
155
209
|
async ngOnInit() {
|
|
156
210
|
// Warm up the active providers (best-effort — a provider that fails to warm
|
|
@@ -220,7 +274,7 @@ export class MentionEditorComponent {
|
|
|
220
274
|
// Close dropdown when editor loses focus
|
|
221
275
|
// Use setTimeout to allow mousedown events on dropdown to fire first
|
|
222
276
|
setTimeout(() => {
|
|
223
|
-
if (this.showMentionDropdown) {
|
|
277
|
+
if (this.showMentionDropdown || this.virtualTriggerOpen) {
|
|
224
278
|
this.closeMentionDropdown();
|
|
225
279
|
}
|
|
226
280
|
}, 200);
|
|
@@ -272,9 +326,16 @@ export class MentionEditorComponent {
|
|
|
272
326
|
* Handle keydown events
|
|
273
327
|
*/
|
|
274
328
|
onKeyDown(event) {
|
|
275
|
-
// Enter alone: Send message
|
|
276
|
-
|
|
329
|
+
// Enter alone: Send message. The dropdown only earns Enter when it has something to pick. A
|
|
330
|
+
// button-opened menu stays open on an empty result set (so the press is not a silent no-op),
|
|
331
|
+
// which made "showing but empty" reachable for the first time — and there Enter was neither
|
|
332
|
+
// handled nor prevented, so it fell through to the contenteditable and inserted a newline
|
|
333
|
+
// instead of sending.
|
|
334
|
+
if (event.key === 'Enter' && !event.shiftKey && (!this.showMentionDropdown || this.mentionSuggestions.length === 0)) {
|
|
277
335
|
event.preventDefault();
|
|
336
|
+
if (this.showMentionDropdown) {
|
|
337
|
+
this.closeMentionDropdown();
|
|
338
|
+
}
|
|
278
339
|
const plainText = this.getPlainText();
|
|
279
340
|
this.enterPressed.emit(plainText);
|
|
280
341
|
return;
|
|
@@ -305,16 +366,33 @@ export class MentionEditorComponent {
|
|
|
305
366
|
const range = selection.getRangeAt(0);
|
|
306
367
|
const textBeforeCursor = this.getTextBeforeCursor(range);
|
|
307
368
|
// Find the nearest active trigger char before the cursor — whichever is closest wins
|
|
369
|
+
// A button-opened session only recognises trigger characters typed AFTER it opened. Without
|
|
370
|
+
// this floor the scan runs over the whole message, so a URL ('.../docs'), an email ('a@b.io')
|
|
371
|
+
// or a date ('9/11') already in the box is found instead, and the first keystroke either closes
|
|
372
|
+
// the menu the button just opened or filters it by the wrong query. searchFrom is 0 on the
|
|
373
|
+
// typed path, which leaves that path's behaviour byte-for-byte unchanged.
|
|
374
|
+
const searchFrom = this.virtualTriggerOpen ? this.virtualTriggerBaseline : 0;
|
|
308
375
|
let triggerIndex = -1;
|
|
309
376
|
let triggerChar = '@';
|
|
310
377
|
for (const t of activeTriggers) {
|
|
311
378
|
const idx = textBeforeCursor.lastIndexOf(t);
|
|
312
|
-
if (idx > triggerIndex) {
|
|
379
|
+
if (idx >= searchFrom && idx > triggerIndex) {
|
|
313
380
|
triggerIndex = idx;
|
|
314
381
|
triggerChar = t;
|
|
315
382
|
}
|
|
316
383
|
}
|
|
317
384
|
if (triggerIndex === -1) {
|
|
385
|
+
// A virtual trigger has no character in the text, so the search above finds nothing. Keep it
|
|
386
|
+
// open and measure the query from where it opened, otherwise the first keystroke after
|
|
387
|
+
// clicking the button would dismiss the list the button just produced.
|
|
388
|
+
if (this.virtualTriggerOpen && textBeforeCursor.length >= this.virtualTriggerBaseline) {
|
|
389
|
+
const query = textBeforeCursor.substring(this.virtualTriggerBaseline);
|
|
390
|
+
if (!query.includes(' ')) {
|
|
391
|
+
this.mentionQuery = query;
|
|
392
|
+
void this.fetchSuggestions(this.activeTrigger, query);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
318
396
|
this.closeMentionDropdown();
|
|
319
397
|
return;
|
|
320
398
|
}
|
|
@@ -324,6 +402,15 @@ export class MentionEditorComponent {
|
|
|
324
402
|
this.closeMentionDropdown();
|
|
325
403
|
return;
|
|
326
404
|
}
|
|
405
|
+
// A real trigger character now owns the session, so the virtual one must let go: otherwise
|
|
406
|
+
// chip insertion still takes the virtual branch and deletes only the typed tail, orphaning the
|
|
407
|
+
// trigger text, and the menu stays pinned to the button instead of the caret.
|
|
408
|
+
if (this.virtualTriggerOpen) {
|
|
409
|
+
this.virtualTriggerOpen = false;
|
|
410
|
+
this.virtualTriggerBaseline = 0;
|
|
411
|
+
this.virtualTriggerAnchor = null;
|
|
412
|
+
this.mentionDropdownAlignRight = false;
|
|
413
|
+
}
|
|
327
414
|
// Extract query
|
|
328
415
|
this.mentionQuery = textAfterTrigger;
|
|
329
416
|
this.mentionStartIndex = triggerIndex;
|
|
@@ -355,6 +442,14 @@ export class MentionEditorComponent {
|
|
|
355
442
|
this.showMentionDropdown = true;
|
|
356
443
|
this.positionMentionDropdown();
|
|
357
444
|
}
|
|
445
|
+
else if (this.virtualTriggerOpen) {
|
|
446
|
+
// A button press must never be a silent no-op. Closing on empty results is right for a TYPED
|
|
447
|
+
// trigger (the user is mid-word and nothing matched yet), but a host with zero skills clicking
|
|
448
|
+
// the button would get no feedback at all and read it as broken. Staying open surfaces the
|
|
449
|
+
// dropdown's own empty state, which was otherwise unreachable.
|
|
450
|
+
this.showMentionDropdown = true;
|
|
451
|
+
this.positionMentionDropdown();
|
|
452
|
+
}
|
|
358
453
|
else {
|
|
359
454
|
this.closeMentionDropdown();
|
|
360
455
|
}
|
|
@@ -382,17 +477,37 @@ export class MentionEditorComponent {
|
|
|
382
477
|
const selection = window.getSelection();
|
|
383
478
|
if (!selection || selection.rangeCount === 0)
|
|
384
479
|
return;
|
|
480
|
+
// A button-opened trigger anchors to the BUTTON, not the caret. On the typed path the user's
|
|
481
|
+
// eyes and query are at the caret so the caret is right; on the button path nobody is looking at
|
|
482
|
+
// the caret, they are looking at the control they just pressed. Sticky for the session (see
|
|
483
|
+
// virtualTriggerAnchor) so filtering does not yank the menu to the caret on the first keystroke.
|
|
484
|
+
if (this.virtualTriggerOpen && this.virtualTriggerAnchor) {
|
|
485
|
+
this.positionToAnchor(this.virtualTriggerAnchor, containerRect);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
385
488
|
const range = selection.getRangeAt(0);
|
|
386
|
-
|
|
489
|
+
let cursorRect = range.getBoundingClientRect();
|
|
490
|
+
// A collapsed range in an EMPTY editor measures 0x0 at 0,0 in every browser, which pinned the
|
|
491
|
+
// dropdown to the bottom-left corner of the viewport and ran it off screen. Rare while typing
|
|
492
|
+
// (there is text to measure) but the normal case when a BUTTON opens the trigger on an empty
|
|
493
|
+
// composer. Fall back to the editor's own box so the menu lands over the composer either way.
|
|
494
|
+
if (cursorRect.width === 0 && cursorRect.height === 0 && cursorRect.top === 0 && cursorRect.left === 0) {
|
|
495
|
+
const editorRect = editor.getBoundingClientRect();
|
|
496
|
+
cursorRect = editorRect;
|
|
497
|
+
}
|
|
387
498
|
// Check space below vs above
|
|
388
499
|
const spaceBelow = window.innerHeight - cursorRect.bottom;
|
|
389
500
|
const spaceAbove = cursorRect.top;
|
|
390
501
|
const dropdownHeight = Math.min(this.mentionSuggestions.length * 56, 300);
|
|
391
|
-
|
|
502
|
+
// Prefer ABOVE. The composer sits at the bottom of the chat, so a dropdown that grows downward
|
|
503
|
+
// lands on top of the very text being typed. Below is the fallback for the rare host that
|
|
504
|
+
// mounts the composer high in a tall viewport.
|
|
505
|
+
this.mentionDropdownAlignRight = false;
|
|
506
|
+
this.mentionDropdownShowAbove = spaceAbove >= dropdownHeight || spaceAbove > spaceBelow;
|
|
392
507
|
if (this.mentionDropdownShowAbove) {
|
|
393
508
|
// Position above, aligning with container top if possible
|
|
394
509
|
this.mentionDropdownPosition = {
|
|
395
|
-
top: containerRect ? containerRect.top
|
|
510
|
+
top: (containerRect ? containerRect.top : cursorRect.top) + window.scrollY - 8,
|
|
396
511
|
left: cursorRect.left + window.scrollX
|
|
397
512
|
};
|
|
398
513
|
}
|
|
@@ -404,6 +519,66 @@ export class MentionEditorComponent {
|
|
|
404
519
|
};
|
|
405
520
|
}
|
|
406
521
|
}
|
|
522
|
+
/**
|
|
523
|
+
* Right-align the dropdown above a trigger control, matching where the voice-options picker puts
|
|
524
|
+
* its popover so two buttons in the same strip do not open menus on opposite corners.
|
|
525
|
+
*
|
|
526
|
+
* Right alignment is applied as a transform in CSS (`align-right`) rather than computed here,
|
|
527
|
+
* because the dropdown's width is content-based; `left` is still clamped so no anchor position can
|
|
528
|
+
* push it off the left edge on a narrow viewport.
|
|
529
|
+
*/
|
|
530
|
+
positionToAnchor(anchor, containerRect) {
|
|
531
|
+
const GUTTER = 16;
|
|
532
|
+
const rect = anchor.getBoundingClientRect();
|
|
533
|
+
// The dropdown is rendered with `useFixedPositioning`, so `top`/`left` are VIEWPORT coordinates.
|
|
534
|
+
// Never add window.scrollX/scrollY here — getBoundingClientRect is already viewport-relative, and
|
|
535
|
+
// adding scroll converts to document coordinates, which under `position: fixed` drags the menu
|
|
536
|
+
// off by exactly the scroll offset.
|
|
537
|
+
// HORIZONTAL. Align the box's LEFT edge to the button's left edge and let it grow rightward.
|
|
538
|
+
// Right-alignment is wrong for this control: the Skills button sits in `.attach-buttons` on the
|
|
539
|
+
// LEFT of the composer, so pinning the box's right edge to the button's right edge sends a
|
|
540
|
+
// 280-400px menu off the left of the screen, and clamping it back parks it over the middle of
|
|
541
|
+
// the composer. Flip to right-aligned only when growing right would overflow the viewport,
|
|
542
|
+
// which is what a menu anchored to a right-hand control needs.
|
|
543
|
+
const assumedWidth = Math.min(400, window.innerWidth - GUTTER * 2);
|
|
544
|
+
const overflowsRight = rect.left + assumedWidth > window.innerWidth - GUTTER;
|
|
545
|
+
this.mentionDropdownAlignRight = overflowsRight;
|
|
546
|
+
// align-right shifts the box back by its own width, so `left` is then its RIGHT edge; otherwise
|
|
547
|
+
// `left` is its left edge. Clamp both forms inside the gutter.
|
|
548
|
+
//
|
|
549
|
+
// When flipping, align to the CONTAINER's right edge, not the button's. `.attach-buttons` is
|
|
550
|
+
// pinned to the composer's bottom-right and Skills is the leftmost of five icons, so pinning the
|
|
551
|
+
// box to that one button's right edge hangs its whole 400px body out to the left of the icon the
|
|
552
|
+
// user pressed. Aligning to the composer instead puts the menu above the button cluster, which is
|
|
553
|
+
// what "above the button" means when the menu is ten times the button's width.
|
|
554
|
+
const flippedRightEdge = containerRect
|
|
555
|
+
? Math.min(containerRect.right, window.innerWidth - GUTTER)
|
|
556
|
+
: Math.min(rect.right, window.innerWidth - GUTTER);
|
|
557
|
+
const left = overflowsRight
|
|
558
|
+
? Math.max(assumedWidth + GUTTER, flippedRightEdge)
|
|
559
|
+
: Math.min(Math.max(GUTTER, rect.left), window.innerWidth - GUTTER - assumedWidth);
|
|
560
|
+
// VERTICAL. Prefer the composer's top edge so the menu clears the whole input box rather than
|
|
561
|
+
// overlapping the text the user is composing; fall back to the button when there is no container.
|
|
562
|
+
// Not unconditional: `show-above` lifts the box by its own height, so forcing it where there is
|
|
563
|
+
// no room renders it off the top of the viewport where it cannot be reached.
|
|
564
|
+
const anchorTop = containerRect ? containerRect.top : rect.top;
|
|
565
|
+
const dropdownHeight = Math.min(Math.max(this.mentionSuggestions.length, 1) * 56, 300);
|
|
566
|
+
// Prefer above, but when it does not fit, take whichever side has MORE room rather than
|
|
567
|
+
// dropping below unconditionally — the composer is bottom-docked, so below is usually the worse
|
|
568
|
+
// side. In a short viewport (a popped-out chat, or mobile with the keyboard up) that difference
|
|
569
|
+
// is the whole menu versus a clipped sliver.
|
|
570
|
+
const spaceBelow = window.innerHeight - rect.bottom;
|
|
571
|
+
const showAbove = anchorTop >= dropdownHeight + GUTTER || anchorTop > spaceBelow;
|
|
572
|
+
this.mentionDropdownShowAbove = showAbove;
|
|
573
|
+
// Above: `top` is the box's BOTTOM edge (the transform lifts it). Below: it is the top edge.
|
|
574
|
+
// Clamped either way, because dropdownHeight is an estimate (rows with descriptions run taller
|
|
575
|
+
// than 56px) and an underestimate would otherwise clip the first row off the top of the screen.
|
|
576
|
+
const rawTop = showAbove ? anchorTop - 8 : rect.bottom + 8;
|
|
577
|
+
const top = showAbove
|
|
578
|
+
? Math.max(GUTTER + dropdownHeight, Math.min(rawTop, window.innerHeight - GUTTER))
|
|
579
|
+
: Math.max(GUTTER, Math.min(rawTop, window.innerHeight - GUTTER - dropdownHeight));
|
|
580
|
+
this.mentionDropdownPosition = { top, left };
|
|
581
|
+
}
|
|
407
582
|
/**
|
|
408
583
|
* Handle mention selection from dropdown
|
|
409
584
|
*/
|
|
@@ -446,6 +621,61 @@ export class MentionEditorComponent {
|
|
|
446
621
|
}
|
|
447
622
|
return true;
|
|
448
623
|
}
|
|
624
|
+
/**
|
|
625
|
+
* Open the suggestion dropdown for a trigger character, exactly as if the user had typed it.
|
|
626
|
+
*
|
|
627
|
+
* Exists so a trigger can have a BUTTON as well as a keystroke. `/` commands were previously
|
|
628
|
+
* reachable only by knowing to type `/`, which is not an affordance — nothing on screen says the
|
|
629
|
+
* feature is there.
|
|
630
|
+
*
|
|
631
|
+
* Deliberately inserts the character and re-runs the normal input path rather than opening the
|
|
632
|
+
* dropdown directly. Everything downstream (which providers own the char, permission filtering,
|
|
633
|
+
* the query the user then types, replacing the char with the chosen chip) is already driven by
|
|
634
|
+
* that path; short-circuiting it would fork the flow and leave `mentionStartIndex` /
|
|
635
|
+
* `activeTrigger` unset, so the first selection would insert in the wrong place.
|
|
636
|
+
*
|
|
637
|
+
* @returns false when the editor is disabled, unavailable, or no active provider owns the char.
|
|
638
|
+
*/
|
|
639
|
+
OpenTrigger(triggerChar, anchorEl) {
|
|
640
|
+
const editor = this.editorRef?.nativeElement;
|
|
641
|
+
if (!editor || this.disabled) {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
if (!this.mentionTriggers.includes(triggerChar)) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
if (!this.FocusCaretAtEnd()) {
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
const selection = window.getSelection();
|
|
651
|
+
if (!selection || selection.rangeCount === 0) {
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
// NOTHING is written into the editor. An earlier version inserted the trigger character to
|
|
655
|
+
// reuse the typed path, and cleaned it up on dismissal — but a character the user never typed,
|
|
656
|
+
// appearing and then vanishing, is visible churn in their own message. The dropdown is opened
|
|
657
|
+
// directly instead, and the character only ever exists as the chip the user chooses.
|
|
658
|
+
this.virtualTriggerOpen = true;
|
|
659
|
+
this.virtualTriggerAnchor = anchorEl ?? null;
|
|
660
|
+
this.virtualTriggerBaseline = this.getTextBeforeCursor(selection.getRangeAt(0)).length;
|
|
661
|
+
this.activeTrigger = triggerChar;
|
|
662
|
+
this.mentionQuery = '';
|
|
663
|
+
void this.fetchSuggestions(triggerChar, '');
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* True when the suggestion dropdown is open FOR THIS trigger character.
|
|
668
|
+
*
|
|
669
|
+
* Lets a control that opens a trigger render its own pressed state without duplicating the
|
|
670
|
+
* editor's state. Both halves matter: `showMentionDropdown` alone would light the Skills button
|
|
671
|
+
* while an unrelated `@` mention dropdown is open.
|
|
672
|
+
*/
|
|
673
|
+
IsTriggerOpen(triggerChar) {
|
|
674
|
+
// virtualTriggerOpen covers the window between the press and the provider's response. Reporting
|
|
675
|
+
// "closed" there makes the second press of a quick double-click re-run the open path instead of
|
|
676
|
+
// toggling, re-emitting the Before/After pair.
|
|
677
|
+
return (this.showMentionDropdown || this.virtualTriggerOpen) && this.activeTrigger === triggerChar;
|
|
678
|
+
}
|
|
449
679
|
/** True when keyboard focus currently sits inside this editor. */
|
|
450
680
|
get HasFocus() {
|
|
451
681
|
const editor = this.editorRef?.nativeElement;
|
|
@@ -486,12 +716,17 @@ export class MentionEditorComponent {
|
|
|
486
716
|
if (!selection || selection.rangeCount === 0)
|
|
487
717
|
return;
|
|
488
718
|
const range = selection.getRangeAt(0);
|
|
489
|
-
// Delete the trigger + query text (use the active trigger char)
|
|
719
|
+
// Delete the trigger + query text (use the active trigger char). A VIRTUAL trigger has no
|
|
720
|
+
// character in the editor, so the span to remove is only what the user typed since it opened —
|
|
721
|
+
// anchoring on the trigger char here would delete an unrelated earlier one, or nothing.
|
|
490
722
|
const textBeforeCursor = this.getTextBeforeCursor(range);
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
723
|
+
const deleteLength = this.virtualTriggerOpen
|
|
724
|
+
? Math.max(0, textBeforeCursor.length - this.virtualTriggerBaseline)
|
|
725
|
+
: textBeforeCursor.length - textBeforeCursor.lastIndexOf(this.activeTrigger);
|
|
726
|
+
if (deleteLength > 0) {
|
|
727
|
+
range.setStart(range.startContainer, range.startOffset - deleteLength);
|
|
728
|
+
range.deleteContents();
|
|
729
|
+
}
|
|
495
730
|
// Create mention chip element
|
|
496
731
|
const chip = this.createMentionChip(suggestion);
|
|
497
732
|
// Insert chip
|
|
@@ -855,6 +1090,12 @@ export class MentionEditorComponent {
|
|
|
855
1090
|
* Close mention dropdown
|
|
856
1091
|
*/
|
|
857
1092
|
closeMentionDropdown() {
|
|
1093
|
+
// No text cleanup: a virtual trigger never wrote anything into the editor, which is the whole
|
|
1094
|
+
// point of it. Dismissing leaves the message exactly as the user left it.
|
|
1095
|
+
this.virtualTriggerOpen = false;
|
|
1096
|
+
this.virtualTriggerBaseline = 0;
|
|
1097
|
+
this.virtualTriggerAnchor = null;
|
|
1098
|
+
this.mentionDropdownAlignRight = false;
|
|
858
1099
|
// Invalidate any in-flight suggestion fetch so a late response can't reopen the dropdown
|
|
859
1100
|
this.suggestionRequestSeq++;
|
|
860
1101
|
this.showMentionDropdown = false;
|
|
@@ -1427,19 +1668,21 @@ export class MentionEditorComponent {
|
|
|
1427
1668
|
input.onchange = (e) => this.onFileSelected(e);
|
|
1428
1669
|
input.click();
|
|
1429
1670
|
}
|
|
1430
|
-
static ɵfac = function MentionEditorComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MentionEditorComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
|
|
1671
|
+
static ɵfac = function MentionEditorComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || MentionEditorComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef), i0.ɵɵdirectiveInject(i0.ElementRef)); };
|
|
1431
1672
|
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: MentionEditorComponent, selectors: [["mj-mention-editor"]], viewQuery: function MentionEditorComponent_Query(rf, ctx) { if (rf & 1) {
|
|
1432
1673
|
i0.ɵɵviewQuery(_c0, 5);
|
|
1433
1674
|
} if (rf & 2) {
|
|
1434
1675
|
let _t;
|
|
1435
1676
|
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.editorRef = _t.first);
|
|
1677
|
+
} }, hostBindings: function MentionEditorComponent_HostBindings(rf, ctx) { if (rf & 1) {
|
|
1678
|
+
i0.ɵɵlistener("mousedown", function MentionEditorComponent_mousedown_HostBindingHandler($event) { return ctx.onDocumentMouseDown($event); }, i0.ɵɵresolveDocument);
|
|
1436
1679
|
} }, inputs: { placeholder: "placeholder", disabled: "disabled", currentUser: "currentUser", enableMentions: "enableMentions", TriggerProviders: "TriggerProviders", ExcludedTriggerKeys: "ExcludedTriggerKeys", Provider: "Provider", autoFocus: "autoFocus", enableAttachments: "enableAttachments", maxAttachments: "maxAttachments", maxAttachmentSizeBytes: "maxAttachmentSizeBytes", acceptedFileTypes: "acceptedFileTypes" }, outputs: { valueChange: "valueChange", mentionSelected: "mentionSelected", enterPressed: "enterPressed", attachmentsChanged: "attachmentsChanged", attachmentError: "attachmentError", attachmentClicked: "attachmentClicked", editorBlurred: "editorBlurred" }, standalone: false, features: [i0.ɵɵProvidersFeature([
|
|
1437
1680
|
{
|
|
1438
1681
|
provide: NG_VALUE_ACCESSOR,
|
|
1439
1682
|
useExisting: forwardRef(() => MentionEditorComponent),
|
|
1440
1683
|
multi: true
|
|
1441
1684
|
}
|
|
1442
|
-
])], decls: 6, vars: 7, consts: [["editor", ""], [1, "mention-editor-container", 3, "click", "dragover", "dragleave", "drop"], [1, "pending-attachments"], ["contenteditable", "true", 1, "mention-editor", 3, "input", "keydown", "blur", "paste"], [1, "drop-overlay"], [3, "suggestions", "position", "showAbove", "useFixedPositioning", "visible"], [1, "attachment-thumbnail"], [1, "attachment-thumbnail", 3, "click"], [1, "thumbnail-image", 3, "src", "alt"], [1, "thumbnail-placeholder"], ["title", "Remove attachment", 1, "remove-attachment", 3, "click"], [1, "fa-solid", "fa-times"], [1, "fa-solid", "fa-file"], [1, "file-name"], [1, "drop-message"], [1, "fa-solid", "fa-cloud-arrow-up"], [3, "suggestionSelected", "closed", "suggestions", "position", "showAbove", "useFixedPositioning", "visible"]], template: function MentionEditorComponent_Template(rf, ctx) { if (rf & 1) {
|
|
1685
|
+
])], decls: 6, vars: 7, consts: [["editor", ""], [1, "mention-editor-container", 3, "click", "dragover", "dragleave", "drop"], [1, "pending-attachments"], ["contenteditable", "true", 1, "mention-editor", 3, "input", "keydown", "blur", "paste"], [1, "drop-overlay"], [3, "suggestions", "position", "showAbove", "alignRight", "useFixedPositioning", "visible"], [1, "attachment-thumbnail"], [1, "attachment-thumbnail", 3, "click"], [1, "thumbnail-image", 3, "src", "alt"], [1, "thumbnail-placeholder"], ["title", "Remove attachment", 1, "remove-attachment", 3, "click"], [1, "fa-solid", "fa-times"], [1, "fa-solid", "fa-file"], [1, "file-name"], [1, "drop-message"], [1, "fa-solid", "fa-cloud-arrow-up"], [3, "suggestionSelected", "closed", "suggestions", "position", "showAbove", "alignRight", "useFixedPositioning", "visible"]], template: function MentionEditorComponent_Template(rf, ctx) { if (rf & 1) {
|
|
1443
1686
|
const _r1 = i0.ɵɵgetCurrentView();
|
|
1444
1687
|
i0.ɵɵelementStart(0, "div", 1);
|
|
1445
1688
|
i0.ɵɵlistener("click", function MentionEditorComponent_Template_div_click_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onContainerClick($event)); })("dragover", function MentionEditorComponent_Template_div_dragover_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onDragOver($event)); })("dragleave", function MentionEditorComponent_Template_div_dragleave_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onDragLeave($event)); })("drop", function MentionEditorComponent_Template_div_drop_0_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onDrop($event)); });
|
|
@@ -1448,7 +1691,7 @@ export class MentionEditorComponent {
|
|
|
1448
1691
|
i0.ɵɵlistener("input", function MentionEditorComponent_Template_div_input_2_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onInput()); })("keydown", function MentionEditorComponent_Template_div_keydown_2_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onKeyDown($event)); })("blur", function MentionEditorComponent_Template_div_blur_2_listener() { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onBlur()); })("paste", function MentionEditorComponent_Template_div_paste_2_listener($event) { i0.ɵɵrestoreView(_r1); return i0.ɵɵresetView(ctx.onPaste($event)); });
|
|
1449
1692
|
i0.ɵɵelementEnd();
|
|
1450
1693
|
i0.ɵɵconditionalCreate(4, MentionEditorComponent_Conditional_4_Template, 5, 0, "div", 4);
|
|
1451
|
-
i0.ɵɵconditionalCreate(5, MentionEditorComponent_Conditional_5_Template, 1,
|
|
1694
|
+
i0.ɵɵconditionalCreate(5, MentionEditorComponent_Conditional_5_Template, 1, 6, "mj-mention-dropdown", 5);
|
|
1452
1695
|
i0.ɵɵelementEnd();
|
|
1453
1696
|
} if (rf & 2) {
|
|
1454
1697
|
i0.ɵɵclassProp("drag-over", ctx.isDragOver);
|
|
@@ -1470,8 +1713,8 @@ export class MentionEditorComponent {
|
|
|
1470
1713
|
useExisting: forwardRef(() => MentionEditorComponent),
|
|
1471
1714
|
multi: true
|
|
1472
1715
|
}
|
|
1473
|
-
], template: "<div class=\"mention-editor-container\"\n [class.drag-over]=\"isDragOver\"\n (click)=\"onContainerClick($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n\n <!-- Pending Attachments Preview -->\n @if (pendingAttachments.length > 0) {\n <div class=\"pending-attachments\">\n @for (attachment of pendingAttachments; track attachment) {\n <div class=\"attachment-thumbnail\"\n (click)=\"onAttachmentClick(attachment)\">\n @if (attachment.thumbnailUrl) {\n <img\n [src]=\"attachment.thumbnailUrl\"\n [alt]=\"attachment.fileName\"\n class=\"thumbnail-image\">\n }\n @if (!attachment.thumbnailUrl) {\n <div class=\"thumbnail-placeholder\">\n <i class=\"fa-solid fa-file\"></i>\n <span class=\"file-name\">{{ attachment.fileName }}</span>\n </div>\n }\n <button class=\"remove-attachment\"\n (click)=\"removeAttachment(attachment.id); $event.stopPropagation()\"\n title=\"Remove attachment\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n }\n </div>\n }\n\n <div\n #editor\n class=\"mention-editor\"\n contenteditable=\"true\"\n [attr.contenteditable]=\"!disabled\"\n [attr.data-placeholder]=\"placeholder\"\n (input)=\"onInput()\"\n (keydown)=\"onKeyDown($event)\"\n (blur)=\"onBlur()\"\n (paste)=\"onPaste($event)\"\n ></div>\n\n <!-- Drag & Drop Overlay -->\n @if (isDragOver) {\n <div class=\"drop-overlay\">\n <div class=\"drop-message\">\n <i class=\"fa-solid fa-cloud-arrow-up\"></i>\n <span>Drop files here</span>\n </div>\n </div>\n }\n\n <!-- Mention Dropdown -->\n @if (showMentionDropdown && enableMentions) {\n <mj-mention-dropdown\n [suggestions]=\"mentionSuggestions\"\n [position]=\"mentionDropdownPosition\"\n [showAbove]=\"mentionDropdownShowAbove\"\n [useFixedPositioning]=\"true\"\n [visible]=\"true\"\n (suggestionSelected)=\"onMentionSelected($event)\"\n (closed)=\"closeMentionDropdown()\">\n </mj-mention-dropdown>\n }\n</div>\n", styles: [".mention-editor-container {\n position: relative;\n width: 100%;\n display: block;\n}\n\n.mention-editor-container.drag-over {\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, transparent);\n border-color: var(--mj-brand-primary);\n}\n\n.mention-editor {\n display: block;\n width: 100%;\n min-height: 100px;\n max-height: 300px;\n padding: 7px 3.5rem 7px 12px;\n outline: none;\n border: none;\n background: transparent;\n font-family: inherit;\n font-size: 1rem;\n line-height: 1.5;\n overflow-y: auto;\n overflow-x: hidden;\n word-wrap: break-word;\n white-space: pre-wrap;\n cursor: text;\n user-select: text;\n box-sizing: border-box;\n}\n.mention-editor:empty:before {\n content: attr(data-placeholder);\n color: var(--mj-text-muted);\n pointer-events: none;\n position: absolute;\n}\n.mention-editor:focus {\n outline: none;\n}\n.mention-editor[contenteditable=false] {\n cursor: not-allowed;\n opacity: 0.6;\n background: var(--mj-bg-surface-sunken);\n}\n@media (max-width: 768px) {\n .mention-editor {\n font-size: 16px;\n padding: 5px;\n padding-right: 3rem;\n }\n}\n\n::ng-deep .mention-chip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 4px 12px;\n margin: 0 3px;\n border-radius: 16px;\n font-size: 13px;\n font-weight: 600;\n cursor: default;\n user-select: none;\n vertical-align: middle;\n white-space: nowrap;\n pointer-events: all;\n -webkit-user-modify: read-only;\n -moz-user-modify: read-only;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n border: 2px solid color-mix(in srgb, var(--mj-brand-primary) 40%, transparent);\n box-shadow: var(--mj-shadow-md);\n animation: chipInsert 0.15s ease-out;\n}\n::ng-deep .mention-chip i {\n font-size: 12px;\n opacity: 0.95;\n}\n::ng-deep .mention-chip[data-mention-type=user] {\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n border: 2px solid color-mix(in srgb, var(--mj-brand-primary) 40%, transparent);\n}\n::ng-deep .mention-chip:hover {\n transform: translateY(-1px);\n box-shadow: var(--mj-shadow-lg);\n transition: all 0.2s ease;\n}\n::ng-deep .mention-chip:focus, ::ng-deep .mention-chip.selected {\n outline: 2px solid var(--mj-brand-primary);\n outline-offset: 2px;\n}\n@keyframes chipInsert {\n 0% {\n transform: scale(0.8);\n opacity: 0;\n }\n 100% {\n transform: scale(1);\n opacity: 1;\n }\n}\n\n.mention-editor .mention-chip + .mention-chip {\n margin-left: 4px;\n}\n.mention-editor br {\n display: block;\n content: \"\";\n margin: 0;\n}\n\n.mention-editor::-webkit-scrollbar {\n width: 6px;\n}\n.mention-editor::-webkit-scrollbar-track {\n background: transparent;\n}\n.mention-editor::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--mj-text-primary) 20%, transparent);\n border-radius: 3px;\n}\n.mention-editor::-webkit-scrollbar-thumb:hover {\n background: color-mix(in srgb, var(--mj-text-primary) 30%, transparent);\n}\n\n/* ==================== Attachment Styles ==================== */\n\n.pending-attachments {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n padding: 8px 12px;\n border-bottom: 1px solid color-mix(in srgb, var(--mj-text-primary) 8%, transparent);\n background: color-mix(in srgb, var(--mj-text-primary) 2%, transparent);\n}\n\n.attachment-thumbnail {\n position: relative;\n width: 80px;\n height: 80px;\n border-radius: 8px;\n overflow: hidden;\n cursor: pointer;\n transition: transform 0.2s, box-shadow 0.2s;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid color-mix(in srgb, var(--mj-text-primary) 10%, transparent);\n}\n\n.attachment-thumbnail:hover {\n transform: scale(1.05);\n box-shadow: var(--mj-shadow-lg);\n}\n\n.thumbnail-image {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n.thumbnail-placeholder {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 8px;\n color: var(--mj-text-secondary);\n}\n\n.thumbnail-placeholder i {\n font-size: 24px;\n margin-bottom: 4px;\n color: var(--mj-text-muted);\n}\n\n.thumbnail-placeholder .file-name {\n font-size: 10px;\n text-align: center;\n word-break: break-all;\n line-height: 1.2;\n max-height: 36px;\n overflow: hidden;\n}\n\n.remove-attachment {\n position: absolute;\n top: 4px;\n right: 4px;\n width: 20px;\n height: 20px;\n border-radius: 50%;\n background: color-mix(in srgb, var(--mj-text-primary) 60%, transparent);\n color: var(--mj-text-inverse);\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n opacity: 0;\n transition: opacity 0.2s, background 0.2s;\n}\n\n.attachment-thumbnail:hover .remove-attachment {\n opacity: 1;\n}\n\n.remove-attachment:hover {\n background: color-mix(in srgb, var(--mj-status-error) 90%, transparent);\n}\n\n.remove-attachment i {\n font-size: 10px;\n}\n\n/* Drop Overlay */\n.drop-overlay {\n position: absolute;\n inset: 0;\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n backdrop-filter: blur(2px);\n border: 2px dashed var(--mj-brand-primary);\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100;\n pointer-events: none;\n}\n\n.drop-message {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n color: var(--mj-brand-primary);\n font-weight: 600;\n}\n\n.drop-message i {\n font-size: 32px;\n}\n\n.drop-message span {\n font-size: 14px;\n}\n"] }]
|
|
1474
|
-
}], () => [{ type: i0.ChangeDetectorRef }], { editorRef: [{
|
|
1716
|
+
], template: "<div class=\"mention-editor-container\"\n [class.drag-over]=\"isDragOver\"\n (click)=\"onContainerClick($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n\n <!-- Pending Attachments Preview -->\n @if (pendingAttachments.length > 0) {\n <div class=\"pending-attachments\">\n @for (attachment of pendingAttachments; track attachment) {\n <div class=\"attachment-thumbnail\"\n (click)=\"onAttachmentClick(attachment)\">\n @if (attachment.thumbnailUrl) {\n <img\n [src]=\"attachment.thumbnailUrl\"\n [alt]=\"attachment.fileName\"\n class=\"thumbnail-image\">\n }\n @if (!attachment.thumbnailUrl) {\n <div class=\"thumbnail-placeholder\">\n <i class=\"fa-solid fa-file\"></i>\n <span class=\"file-name\">{{ attachment.fileName }}</span>\n </div>\n }\n <button class=\"remove-attachment\"\n (click)=\"removeAttachment(attachment.id); $event.stopPropagation()\"\n title=\"Remove attachment\">\n <i class=\"fa-solid fa-times\"></i>\n </button>\n </div>\n }\n </div>\n }\n\n <div\n #editor\n class=\"mention-editor\"\n contenteditable=\"true\"\n [attr.contenteditable]=\"!disabled\"\n [attr.data-placeholder]=\"placeholder\"\n (input)=\"onInput()\"\n (keydown)=\"onKeyDown($event)\"\n (blur)=\"onBlur()\"\n (paste)=\"onPaste($event)\"\n ></div>\n\n <!-- Drag & Drop Overlay -->\n @if (isDragOver) {\n <div class=\"drop-overlay\">\n <div class=\"drop-message\">\n <i class=\"fa-solid fa-cloud-arrow-up\"></i>\n <span>Drop files here</span>\n </div>\n </div>\n }\n\n <!-- Mention Dropdown -->\n @if (showMentionDropdown && enableMentions) {\n <mj-mention-dropdown\n [suggestions]=\"mentionSuggestions\"\n [position]=\"mentionDropdownPosition\"\n [showAbove]=\"mentionDropdownShowAbove\"\n [alignRight]=\"mentionDropdownAlignRight\"\n [useFixedPositioning]=\"true\"\n [visible]=\"true\"\n (suggestionSelected)=\"onMentionSelected($event)\"\n (closed)=\"closeMentionDropdown()\">\n </mj-mention-dropdown>\n }\n</div>\n", styles: [".mention-editor-container {\n position: relative;\n width: 100%;\n display: block;\n}\n\n.mention-editor-container.drag-over {\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, transparent);\n border-color: var(--mj-brand-primary);\n}\n\n.mention-editor {\n display: block;\n width: 100%;\n min-height: 100px;\n max-height: 300px;\n padding: 7px 3.5rem 7px 12px;\n outline: none;\n border: none;\n background: transparent;\n font-family: inherit;\n font-size: 1rem;\n line-height: 1.5;\n overflow-y: auto;\n overflow-x: hidden;\n word-wrap: break-word;\n white-space: pre-wrap;\n cursor: text;\n user-select: text;\n box-sizing: border-box;\n}\n.mention-editor:empty:before {\n content: attr(data-placeholder);\n color: var(--mj-text-muted);\n pointer-events: none;\n position: absolute;\n}\n.mention-editor:focus {\n outline: none;\n}\n.mention-editor[contenteditable=false] {\n cursor: not-allowed;\n opacity: 0.6;\n background: var(--mj-bg-surface-sunken);\n}\n@media (max-width: 768px) {\n .mention-editor {\n font-size: 16px;\n padding: 5px;\n padding-right: 3rem;\n }\n}\n\n::ng-deep .mention-chip {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 4px 12px;\n margin: 0 3px;\n border-radius: 16px;\n font-size: 13px;\n font-weight: 600;\n cursor: default;\n user-select: none;\n vertical-align: middle;\n white-space: nowrap;\n pointer-events: all;\n -webkit-user-modify: read-only;\n -moz-user-modify: read-only;\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n border: 2px solid color-mix(in srgb, var(--mj-brand-primary) 40%, transparent);\n box-shadow: var(--mj-shadow-md);\n animation: chipInsert 0.15s ease-out;\n}\n::ng-deep .mention-chip i {\n font-size: 12px;\n opacity: 0.95;\n}\n::ng-deep .mention-chip[data-mention-type=user] {\n background: var(--mj-brand-primary);\n color: var(--mj-text-inverse);\n border: 2px solid color-mix(in srgb, var(--mj-brand-primary) 40%, transparent);\n}\n::ng-deep .mention-chip:hover {\n transform: translateY(-1px);\n box-shadow: var(--mj-shadow-lg);\n transition: all 0.2s ease;\n}\n::ng-deep .mention-chip:focus, ::ng-deep .mention-chip.selected {\n outline: 2px solid var(--mj-brand-primary);\n outline-offset: 2px;\n}\n@keyframes chipInsert {\n 0% {\n transform: scale(0.8);\n opacity: 0;\n }\n 100% {\n transform: scale(1);\n opacity: 1;\n }\n}\n\n.mention-editor .mention-chip + .mention-chip {\n margin-left: 4px;\n}\n.mention-editor br {\n display: block;\n content: \"\";\n margin: 0;\n}\n\n.mention-editor::-webkit-scrollbar {\n width: 6px;\n}\n.mention-editor::-webkit-scrollbar-track {\n background: transparent;\n}\n.mention-editor::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--mj-text-primary) 20%, transparent);\n border-radius: 3px;\n}\n.mention-editor::-webkit-scrollbar-thumb:hover {\n background: color-mix(in srgb, var(--mj-text-primary) 30%, transparent);\n}\n\n/* ==================== Attachment Styles ==================== */\n\n.pending-attachments {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n padding: 8px 12px;\n border-bottom: 1px solid color-mix(in srgb, var(--mj-text-primary) 8%, transparent);\n background: color-mix(in srgb, var(--mj-text-primary) 2%, transparent);\n}\n\n.attachment-thumbnail {\n position: relative;\n width: 80px;\n height: 80px;\n border-radius: 8px;\n overflow: hidden;\n cursor: pointer;\n transition: transform 0.2s, box-shadow 0.2s;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid color-mix(in srgb, var(--mj-text-primary) 10%, transparent);\n}\n\n.attachment-thumbnail:hover {\n transform: scale(1.05);\n box-shadow: var(--mj-shadow-lg);\n}\n\n.thumbnail-image {\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n.thumbnail-placeholder {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n padding: 8px;\n color: var(--mj-text-secondary);\n}\n\n.thumbnail-placeholder i {\n font-size: 24px;\n margin-bottom: 4px;\n color: var(--mj-text-muted);\n}\n\n.thumbnail-placeholder .file-name {\n font-size: 10px;\n text-align: center;\n word-break: break-all;\n line-height: 1.2;\n max-height: 36px;\n overflow: hidden;\n}\n\n.remove-attachment {\n position: absolute;\n top: 4px;\n right: 4px;\n width: 20px;\n height: 20px;\n border-radius: 50%;\n background: color-mix(in srgb, var(--mj-text-primary) 60%, transparent);\n color: var(--mj-text-inverse);\n border: none;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n opacity: 0;\n transition: opacity 0.2s, background 0.2s;\n}\n\n.attachment-thumbnail:hover .remove-attachment {\n opacity: 1;\n}\n\n.remove-attachment:hover {\n background: color-mix(in srgb, var(--mj-status-error) 90%, transparent);\n}\n\n.remove-attachment i {\n font-size: 10px;\n}\n\n/* Drop Overlay */\n.drop-overlay {\n position: absolute;\n inset: 0;\n background: color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n backdrop-filter: blur(2px);\n border: 2px dashed var(--mj-brand-primary);\n border-radius: 8px;\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100;\n pointer-events: none;\n}\n\n.drop-message {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n color: var(--mj-brand-primary);\n font-weight: 600;\n}\n\n.drop-message i {\n font-size: 32px;\n}\n\n.drop-message span {\n font-size: 14px;\n}\n"] }]
|
|
1717
|
+
}], () => [{ type: i0.ChangeDetectorRef }, { type: i0.ElementRef }], { editorRef: [{
|
|
1475
1718
|
type: ViewChild,
|
|
1476
1719
|
args: ['editor', { static: false }]
|
|
1477
1720
|
}], placeholder: [{
|
|
@@ -1512,6 +1755,9 @@ export class MentionEditorComponent {
|
|
|
1512
1755
|
type: Output
|
|
1513
1756
|
}], editorBlurred: [{
|
|
1514
1757
|
type: Output
|
|
1758
|
+
}], onDocumentMouseDown: [{
|
|
1759
|
+
type: HostListener,
|
|
1760
|
+
args: ['document:mousedown', ['$event']]
|
|
1515
1761
|
}] }); })();
|
|
1516
|
-
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MentionEditorComponent, { className: "MentionEditorComponent", filePath: "src/lib/components/mention/mention-editor.component.ts", lineNumber:
|
|
1762
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(MentionEditorComponent, { className: "MentionEditorComponent", filePath: "src/lib/components/mention/mention-editor.component.ts", lineNumber: 73 }); })();
|
|
1517
1763
|
//# sourceMappingURL=mention-editor.component.js.map
|