@h-k-dev/angular-email-editor 0.0.1 → 0.0.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/fesm2022/h-k-dev-angular-email-editor-address-chip.mjs +109 -11
- package/fesm2022/h-k-dev-angular-email-editor-address-chip.mjs.map +1 -1
- package/fesm2022/h-k-dev-angular-email-editor-address-input.mjs +445 -25
- package/fesm2022/h-k-dev-angular-email-editor-address-input.mjs.map +1 -1
- package/fesm2022/h-k-dev-angular-email-editor-focus.mjs +10 -0
- package/fesm2022/h-k-dev-angular-email-editor-focus.mjs.map +1 -1
- package/fesm2022/h-k-dev-angular-email-editor.mjs +5 -3
- package/fesm2022/h-k-dev-angular-email-editor.mjs.map +1 -1
- package/package.json +1 -1
- package/types/h-k-dev-angular-email-editor-address-chip.d.ts +47 -6
- package/types/h-k-dev-angular-email-editor-address-input.d.ts +126 -5
- package/types/h-k-dev-angular-email-editor-focus.d.ts +6 -0
- package/types/h-k-dev-angular-email-editor.d.ts +2 -2
|
@@ -1,10 +1,38 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, TemplateRef,
|
|
2
|
+
import { Directive, inject, TemplateRef, model, input, signal, computed, numberAttribute, booleanAttribute, output, contentChild, viewChild, ElementRef, Injector, viewChildren, afterNextRender, Component } from '@angular/core';
|
|
3
3
|
import { ADDRESS_RULES, isMailbox, isPending, addressKey, separatesAt, defaultAddressRules } from '@h-k-dev/angular-email-editor/address';
|
|
4
|
-
import { AddressChipRemove, AddressChip } from '@h-k-dev/angular-email-editor/address-chip';
|
|
4
|
+
import { AddressChipRemove, AddressChip, ADDRESS_DRAG_TYPE } from '@h-k-dev/angular-email-editor/address-chip';
|
|
5
5
|
import { NgTemplateOutlet } from '@angular/common';
|
|
6
6
|
import { validate } from '@angular/forms/signals';
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Scopes chip drag and drop to the address inputs inside it: they trade
|
|
10
|
+
* chips with each other and refuse anyone else's — a writer's From, To, Cc
|
|
11
|
+
* and Bcc, and not the rows of another writer on the same page. Put it on
|
|
12
|
+
* whatever holds the rows:
|
|
13
|
+
*
|
|
14
|
+
* <form emailAddressDragGroup [formRoot]="envelope">
|
|
15
|
+
* <div email-address-input [formField]="envelope.from" limit="1"></div>
|
|
16
|
+
* <div email-address-input [formField]="envelope.to"></div>
|
|
17
|
+
* …
|
|
18
|
+
* </form>
|
|
19
|
+
*
|
|
20
|
+
* The nearest group around an input is its group. Inputs in no group make
|
|
21
|
+
* up one group of their own, so a page with a single writer needs none.
|
|
22
|
+
* Behaviour only: it renders nothing and sets no attribute.
|
|
23
|
+
*
|
|
24
|
+
* A chip lands in an address input or nowhere — it carries no plain text,
|
|
25
|
+
* so a subject line, an editor or any other field leaves it alone.
|
|
26
|
+
*/
|
|
27
|
+
class AddressDragGroup {
|
|
28
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: AddressDragGroup, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
29
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.6", type: AddressDragGroup, isStandalone: true, selector: "[emailAddressDragGroup]", exportAs: ["emailAddressDragGroup"], ngImport: i0 });
|
|
30
|
+
}
|
|
31
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: AddressDragGroup, decorators: [{
|
|
32
|
+
type: Directive,
|
|
33
|
+
args: [{ selector: '[emailAddressDragGroup]', exportAs: 'emailAddressDragGroup' }]
|
|
34
|
+
}] });
|
|
35
|
+
|
|
8
36
|
/**
|
|
9
37
|
* The address input's label slot — opt in. By default the control owns no
|
|
10
38
|
* label: the template that holds the form draws the row and the label beside
|
|
@@ -32,6 +60,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
32
60
|
}] });
|
|
33
61
|
|
|
34
62
|
let nextId = 0;
|
|
63
|
+
/** The chip drag in flight from an address input on this page: where it
|
|
64
|
+
came from, and whether an address input took it as a move. The drag
|
|
65
|
+
data can't be read until the drop, and `dropEffect` at the drag's end is
|
|
66
|
+
not to be trusted (WebKit reports `all`): this is how a target refuses
|
|
67
|
+
a duplicate before the drop and how the source learns to let go. */
|
|
68
|
+
let drag = null;
|
|
69
|
+
/** The entry a chip on this page last put on the clipboard — by a hold, by
|
|
70
|
+
Ctrl+C or by Ctrl+X. A hold writes plain text only (the async clipboard
|
|
71
|
+
takes no type of ours), so a paste of exactly this text is known to be a
|
|
72
|
+
chip's, and comes back as one. */
|
|
73
|
+
let clipboardEntry = null;
|
|
35
74
|
/**
|
|
36
75
|
* An address field the way Gmail's works: committed addresses as chips,
|
|
37
76
|
* free typing after them. Enter, comma, semicolon or a paste commits what
|
|
@@ -68,7 +107,9 @@ let nextId = 0;
|
|
|
68
107
|
* the caret at the start of the typing, ← puts a highlight on the last chip
|
|
69
108
|
* and walks back along the list; → walks forward, and past the last chip
|
|
70
109
|
* returns to the caret. Enter or F2 — or a double click on
|
|
71
|
-
* any chip — edits it,
|
|
110
|
+
* any chip — edits it, Alt+← and Alt+→ move it a place along the list,
|
|
111
|
+
* Ctrl+C (Cmd+C) copies it, as a hold does, and Ctrl+X cuts it — unless
|
|
112
|
+
* text is selected in the input, which copies as ever —
|
|
72
113
|
* Backspace or Delete removes it, Escape drops the highlight, and so does
|
|
73
114
|
* typing. A click on a chip highlights it. Reaching a chip either way is
|
|
74
115
|
* being done with the typing: an address in the input becomes a chip first,
|
|
@@ -120,6 +161,32 @@ let nextId = 0;
|
|
|
120
161
|
* the input, and the selection with it — focus leaving the control clears
|
|
121
162
|
* the selection.
|
|
122
163
|
*
|
|
164
|
+
* **Chips move between inputs by drag and drop.** A chip of an editable
|
|
165
|
+
* input can be dragged to another — To into Cc, as Gmail and Outlook do —
|
|
166
|
+
* and leaves its own list when the other takes it. The other commits it
|
|
167
|
+
* as it was, flagged or pending alike. A chip whose address the other
|
|
168
|
+
* already holds is absorbed, as Gmail does: it leaves its list and the
|
|
169
|
+
* other keeps its own entry. A full list (From holds one) refuses the
|
|
170
|
+
* drop, and the chip stays. Dropped in its own list, a chip is reordered:
|
|
171
|
+
* it moves to the line; the line is not drawn where the chip already is. Alt (Option), or Ctrl, makes
|
|
172
|
+
* it a copy — onto a duplicate, a copy is nothing, so refused. Nothing
|
|
173
|
+
* moves while a drag is over a target: a thin line between its chips
|
|
174
|
+
* (`drop-marker`) shows where the chip will land, before or after the chip
|
|
175
|
+
* under the pointer, and the drop puts it there; the host carries
|
|
176
|
+
* `data-dropping` meanwhile. Only chips are places: a draft — typed in the
|
|
177
|
+
* input, or left at the end of the list when the row lost focus — is not,
|
|
178
|
+
* and a drop lands before it.
|
|
179
|
+
* A drop counts as working both fields: each is touched.
|
|
180
|
+
* A chip lands in an address input or nowhere: it carries no plain text,
|
|
181
|
+
* so a subject line or an editor can't take it. `emailAddressDragGroup` on
|
|
182
|
+
* the element around a writer's rows keeps its chips among them. Plain text
|
|
183
|
+
* dropped from elsewhere is a paste — the addresses become chips, the rest
|
|
184
|
+
* is left typed.
|
|
185
|
+
* `moveTo()` is the same move for the keyboard — a host's "Move to Cc" —
|
|
186
|
+
* and the moved chip says so (`movedLabel`). A touch is never a drag: on
|
|
187
|
+
* a phone the press is the chip's hold-to-copy. `allowDrag` turns it off;
|
|
188
|
+
* a disabled or read-only input is neither source nor target.
|
|
189
|
+
*
|
|
123
190
|
* **The chips' remove control is a template slot.** An
|
|
124
191
|
* `ng-template[emailAddressChipRemove]` in the input's content is handed to
|
|
125
192
|
* every chip it renders, in place of the default × button.
|
|
@@ -174,6 +241,14 @@ class AddressInput {
|
|
|
174
241
|
/** The chips' note on a placeholder a template fills in later. */
|
|
175
242
|
pendingLabel = input('filled in when sent', /* @ts-ignore */
|
|
176
243
|
...(ngDevMode ? [{ debugName: "pendingLabel" }] : /* istanbul ignore next */ []));
|
|
244
|
+
/** What a chip moved into this input says, before its address — a host
|
|
245
|
+
with labelled rows says where: "Moved to Cc:". */
|
|
246
|
+
movedLabel = input('Moved', /* @ts-ignore */
|
|
247
|
+
...(ngDevMode ? [{ debugName: "movedLabel" }] : /* istanbul ignore next */ []));
|
|
248
|
+
/** Chips may be dragged out of this input and dropped into it. Set false
|
|
249
|
+
for a row that only shows; a disabled or read-only input never takes
|
|
250
|
+
part either way. */
|
|
251
|
+
allowDrag = input(true, { ...(ngDevMode ? { debugName: "allowDrag" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
177
252
|
// ── the form contract: pushed in by the field directive ───────────────
|
|
178
253
|
/** The input's `name` attribute; the directive derives one from the path. */
|
|
179
254
|
name = input('', /* @ts-ignore */
|
|
@@ -202,7 +277,11 @@ class AddressInput {
|
|
|
202
277
|
...(ngDevMode ? [{ debugName: "removeSlot" }] : /* istanbul ignore next */ []));
|
|
203
278
|
field = viewChild.required('field', /* @ts-ignore */
|
|
204
279
|
...(ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []));
|
|
280
|
+
/** The list of chips, where a press on a draggable chip parks focus. */
|
|
281
|
+
chipList = viewChild.required('chipList', /* @ts-ignore */
|
|
282
|
+
...(ngDevMode ? [{ debugName: "chipList" }] : /* istanbul ignore next */ []));
|
|
205
283
|
#host = inject(ElementRef);
|
|
284
|
+
#injector = inject(Injector);
|
|
206
285
|
/** The address rule in force — ours, or the host's (`provideAddressRules`):
|
|
207
286
|
how typing splits, what is an address, what is one recipient. */
|
|
208
287
|
#rules = inject(ADDRESS_RULES);
|
|
@@ -210,6 +289,15 @@ class AddressInput {
|
|
|
210
289
|
template fills in later. Everything else is a typo to hand back. */
|
|
211
290
|
#accepts = (raw) => isMailbox(raw, this.#rules) || isPending(raw, this.#rules);
|
|
212
291
|
#key = (raw) => addressKey(raw, this.#rules);
|
|
292
|
+
/** The list has this address already — in any case, under any name. */
|
|
293
|
+
#holds = (raw) => this.#keys().has(this.#key(raw));
|
|
294
|
+
/** The list's addresses by key, parsed once a change — not on every
|
|
295
|
+
`dragover`. */
|
|
296
|
+
#keys = computed(() => new Set(this.value().map(this.#key)), /* @ts-ignore */
|
|
297
|
+
...(ngDevMode ? [{ debugName: "#keys" }] : /* istanbul ignore next */ []));
|
|
298
|
+
/** The chips this input trades with: those of the nearest
|
|
299
|
+
`emailAddressDragGroup`, or of every input in none. */
|
|
300
|
+
#group = inject(AddressDragGroup, { optional: true });
|
|
213
301
|
#focused = signal(false, /* @ts-ignore */
|
|
214
302
|
...(ngDevMode ? [{ debugName: "#focused" }] : /* istanbul ignore next */ []));
|
|
215
303
|
/** Focus is somewhere in the control — the input or a chip's remove
|
|
@@ -266,6 +354,12 @@ class AddressInput {
|
|
|
266
354
|
return value.slice(start);
|
|
267
355
|
}, /* @ts-ignore */
|
|
268
356
|
...(ngDevMode ? [{ debugName: "#trailingMalformed" }] : /* istanbul ignore next */ []));
|
|
357
|
+
/** Where the draft starts in the list: the trailing malformed entries are
|
|
358
|
+
the typing the row was left with — what focus hands back to the input
|
|
359
|
+
— and no chip is placed among or after them. A drop, or `moveTo()`,
|
|
360
|
+
lands before. */
|
|
361
|
+
#draftStart = computed(() => this.value().length - this.#trailingMalformed().length, /* @ts-ignore */
|
|
362
|
+
...(ngDevMode ? [{ debugName: "#draftStart" }] : /* istanbul ignore next */ []));
|
|
269
363
|
/** Full, and nothing in it to fix: the input hides — the chips say
|
|
270
364
|
everything. A full list ending in a typo keeps it, so focus can hand
|
|
271
365
|
the typo back. */
|
|
@@ -275,6 +369,24 @@ class AddressInput {
|
|
|
275
369
|
button, the input is out of play. */
|
|
276
370
|
locked = computed(() => this.disabled() || this.readonly(), /* @ts-ignore */
|
|
277
371
|
...(ngDevMode ? [{ debugName: "locked" }] : /* istanbul ignore next */ []));
|
|
372
|
+
/** Chips can be dragged out of the list, and dropped into it. */
|
|
373
|
+
canDrag = computed(() => this.allowDrag() && !this.locked(), /* @ts-ignore */
|
|
374
|
+
...(ngDevMode ? [{ debugName: "canDrag" }] : /* istanbul ignore next */ []));
|
|
375
|
+
/** Where a drag over the input would land: the index in the list, and
|
|
376
|
+
the line drawn there — in the chip list's own coordinates. Null while
|
|
377
|
+
no drag the input would take is over it. Drawn on top, out of the
|
|
378
|
+
flow, so nothing moves under the pointer. */
|
|
379
|
+
dropMarker = signal(null, { ...(ngDevMode ? { debugName: "dropMarker" } : /* istanbul ignore next */ {}),
|
|
380
|
+
// A drag sends `dragover` every few dozen milliseconds, moving or not:
|
|
381
|
+
// the same place again is no change, and renders nothing.
|
|
382
|
+
equal: (a, b) => a === b ||
|
|
383
|
+
(!!a && !!b && a.index === b.index && a.x === b.x && a.y === b.y && a.height === b.height) });
|
|
384
|
+
/** A drag the input would take is over it. */
|
|
385
|
+
dropping = computed(() => this.dropMarker() !== null, /* @ts-ignore */
|
|
386
|
+
...(ngDevMode ? [{ debugName: "dropping" }] : /* istanbul ignore next */ []));
|
|
387
|
+
/** The press under way is a mouse's (or a pen's) on a draggable chip:
|
|
388
|
+
its default has to stay, or the browser never starts the drag. */
|
|
389
|
+
#dragPress = false;
|
|
278
390
|
/** Invalid *and* touched — the moment the state becomes the user's business. */
|
|
279
391
|
flagged = computed(() => this.invalid() && this.touched(), /* @ts-ignore */
|
|
280
392
|
...(ngDevMode ? [{ debugName: "flagged" }] : /* istanbul ignore next */ []));
|
|
@@ -356,7 +468,13 @@ class AddressInput {
|
|
|
356
468
|
would move focus off again. */
|
|
357
469
|
onPointerdown(event) {
|
|
358
470
|
const input = this.field().nativeElement;
|
|
359
|
-
|
|
471
|
+
const target = event.target;
|
|
472
|
+
this.#dragPress =
|
|
473
|
+
event.pointerType !== 'touch' &&
|
|
474
|
+
this.canDrag() &&
|
|
475
|
+
!!target.closest?.('[email-address-chip]') &&
|
|
476
|
+
!target.closest('[data-slot=trailing]');
|
|
477
|
+
if (target === input || this.locked() || this.closed())
|
|
360
478
|
return;
|
|
361
479
|
input.focus();
|
|
362
480
|
}
|
|
@@ -364,10 +482,16 @@ class AddressInput {
|
|
|
364
482
|
its button, the gap between — leaves the caret where it is. The
|
|
365
483
|
browser would otherwise move focus to the body for the press and back
|
|
366
484
|
on the click, and in that instant the row would drop to text and
|
|
367
|
-
reflow: a flash, and a blur that commits and touches for nothing.
|
|
485
|
+
reflow: a flash, and a blur that commits and touches for nothing.
|
|
486
|
+
Except a mouse's press on a draggable chip: no engine starts a drag
|
|
487
|
+
from a press whose default was declined, so it keeps it, and the
|
|
488
|
+
press's focus lands on the chip list instead (`tabindex="-1"`) —
|
|
489
|
+
still inside the control, so nothing commits; the click or the drag's
|
|
490
|
+
end brings the caret back. */
|
|
368
491
|
keepCaret(event) {
|
|
369
|
-
if (event.target
|
|
370
|
-
|
|
492
|
+
if (event.target === this.field().nativeElement || this.#dragPress)
|
|
493
|
+
return;
|
|
494
|
+
event.preventDefault();
|
|
371
495
|
}
|
|
372
496
|
/** A click on a chip — not on its remove control — picks it, and only
|
|
373
497
|
that: a click on the picked chip picks it again. The caret is in the
|
|
@@ -392,6 +516,219 @@ class AddressInput {
|
|
|
392
516
|
const chip = this.chips().find((candidate) => candidate.address() === address);
|
|
393
517
|
return chip ? chip.copy() : false;
|
|
394
518
|
}
|
|
519
|
+
/** Moves a chip — the picked one by default — to another address input:
|
|
520
|
+
the keyboard's way to do what a drag does, for a host's own "Move to
|
|
521
|
+
Cc". The target takes it as it is, to the end of its list — before
|
|
522
|
+
a draft the row was left with — and it
|
|
523
|
+
leaves this one; both are touched, and the moved chip says so
|
|
524
|
+
(`movedLabel`). One the target already holds is absorbed: it leaves
|
|
525
|
+
here, and the target keeps its own entry. A full target, or either
|
|
526
|
+
input locked, moves nothing. Returns whether it moved. */
|
|
527
|
+
moveTo(target, address = this.current()) {
|
|
528
|
+
if (address === null || target === this || this.locked() || target.locked())
|
|
529
|
+
return false;
|
|
530
|
+
if (!this.value().includes(address))
|
|
531
|
+
return false;
|
|
532
|
+
if (!target.#holds(address) && !target.#insert([address], target.#draftStart()).length) {
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
this.remove(address);
|
|
536
|
+
this.touch.emit();
|
|
537
|
+
target.touch.emit();
|
|
538
|
+
target.#announceMoved(address);
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
/** The chip that arrived by a move — or the entry that absorbed it —
|
|
542
|
+
says so, once it is drawn. */
|
|
543
|
+
#announceMoved(address) {
|
|
544
|
+
const key = this.#key(address);
|
|
545
|
+
afterNextRender(() => this.chips()
|
|
546
|
+
.find((chip) => this.#key(chip.address()) === key)
|
|
547
|
+
?.announce(`${this.movedLabel()} ${address}`), { injector: this.#injector });
|
|
548
|
+
}
|
|
549
|
+
// ── drag and drop ─────────────────────────────────────────────────────
|
|
550
|
+
/** A chip of this list started a drag: remember it, so a target on the
|
|
551
|
+
page can judge the drop before it happens and tell this list to let
|
|
552
|
+
go after. */
|
|
553
|
+
onChipDragstart(address) {
|
|
554
|
+
drag = this.canDrag() ? { source: this, group: this.#group, address, moved: false } : null;
|
|
555
|
+
}
|
|
556
|
+
/** The drag ended: if an address input took the chip as a move, it
|
|
557
|
+
leaves — through `remove()`, as the × takes it — and the field is
|
|
558
|
+
touched. The press parked focus on the list; the caret comes back. */
|
|
559
|
+
onChipDragend() {
|
|
560
|
+
const ended = drag;
|
|
561
|
+
drag = null;
|
|
562
|
+
if (ended?.source === this && ended.moved) {
|
|
563
|
+
this.remove(ended.address);
|
|
564
|
+
this.touch.emit();
|
|
565
|
+
}
|
|
566
|
+
if (document.activeElement === this.chipList().nativeElement)
|
|
567
|
+
this.focus();
|
|
568
|
+
}
|
|
569
|
+
/** What a drop here would be: `move` or `copy` for a drag the input
|
|
570
|
+
takes, `none` for one it refuses — said, so the text input inside
|
|
571
|
+
can't take the text in its place — and null for a drag that isn't an
|
|
572
|
+
address (files: the page's drop zone's), left to the page. A chip is
|
|
573
|
+
taken only from another input of the same group, while this page's
|
|
574
|
+
drag is in flight — a chip from anywhere else is refused. */
|
|
575
|
+
#dropEffect(event) {
|
|
576
|
+
const types = event.dataTransfer?.types ?? [];
|
|
577
|
+
const chip = types.includes(ADDRESS_DRAG_TYPE);
|
|
578
|
+
if (!chip && !types.includes('text/plain'))
|
|
579
|
+
return null;
|
|
580
|
+
if (!this.canDrag())
|
|
581
|
+
return chip ? 'none' : null;
|
|
582
|
+
if (!chip)
|
|
583
|
+
return this.full() ? 'none' : 'copy';
|
|
584
|
+
const from = drag;
|
|
585
|
+
if (!from || from.group !== this.#group)
|
|
586
|
+
return 'none';
|
|
587
|
+
const copy = event.altKey || event.ctrlKey;
|
|
588
|
+
// Its own list: a move is a reorder; a copy would add nothing.
|
|
589
|
+
if (from.source === this)
|
|
590
|
+
return copy ? 'none' : 'move';
|
|
591
|
+
// Held already: a move is absorbed, even into a full list; a copy is
|
|
592
|
+
// nothing.
|
|
593
|
+
if (this.#holds(from.address))
|
|
594
|
+
return copy ? 'none' : 'move';
|
|
595
|
+
if (this.full())
|
|
596
|
+
return 'none';
|
|
597
|
+
return copy ? 'copy' : 'move';
|
|
598
|
+
}
|
|
599
|
+
onDragover(event) {
|
|
600
|
+
const effect = this.#dropEffect(event);
|
|
601
|
+
if (effect === null)
|
|
602
|
+
return;
|
|
603
|
+
event.preventDefault();
|
|
604
|
+
event.dataTransfer.dropEffect = effect;
|
|
605
|
+
const marker = effect === 'none' ? null : this.#dropAt(event);
|
|
606
|
+
this.dropMarker.set(marker && this.#inPlace(marker.index) ? null : marker);
|
|
607
|
+
}
|
|
608
|
+
/** A drop at this index would leave the dragged chip where it is: right
|
|
609
|
+
before or right after itself, in its own list. No line there. */
|
|
610
|
+
#inPlace(index) {
|
|
611
|
+
if (drag?.source !== this)
|
|
612
|
+
return false;
|
|
613
|
+
const at = this.value().indexOf(drag.address);
|
|
614
|
+
return index === at || index === at + 1;
|
|
615
|
+
}
|
|
616
|
+
/** Moves a chip of the list to stand before the entry at `at` (the end,
|
|
617
|
+
past the last). True when it moved — not when it would stay put. */
|
|
618
|
+
#reorder(address, at) {
|
|
619
|
+
const from = this.value().indexOf(address);
|
|
620
|
+
if (from < 0 || at < 0 || at > this.value().length || at === from || at === from + 1) {
|
|
621
|
+
return false;
|
|
622
|
+
}
|
|
623
|
+
const to = at > from ? at - 1 : at;
|
|
624
|
+
this.value.update((current) => {
|
|
625
|
+
const next = current.filter((held) => held !== address);
|
|
626
|
+
next.splice(to, 0, address);
|
|
627
|
+
return next;
|
|
628
|
+
});
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
/** Where a drop at the pointer lands, in reading order: before the first
|
|
632
|
+
entry that starts after the pointer — on a later line, or on its line
|
|
633
|
+
past the pointer's side of the entry's middle — else at the end. And
|
|
634
|
+
the line that says so: midway between two entries on one line, or
|
|
635
|
+
just outside the entry at a line's start or end. */
|
|
636
|
+
#dropAt(event) {
|
|
637
|
+
const list = this.chipList().nativeElement;
|
|
638
|
+
const box = list.getBoundingClientRect();
|
|
639
|
+
const rtl = getComputedStyle(list).direction === 'rtl';
|
|
640
|
+
const x = event.clientX ?? 0;
|
|
641
|
+
const y = event.clientY ?? 0;
|
|
642
|
+
const chips = [...list.querySelectorAll(':scope > [email-address-chip]')];
|
|
643
|
+
// Only the chips before the draft are places; the draft is not.
|
|
644
|
+
const rects = chips.slice(0, this.#draftStart()).map((chip) => chip.getBoundingClientRect());
|
|
645
|
+
let index = rects.findIndex((r) => y < r.top || (y <= r.bottom && (rtl ? x > r.left + r.width / 2 : x < r.left + r.width / 2)));
|
|
646
|
+
if (index < 0)
|
|
647
|
+
index = rects.length;
|
|
648
|
+
const start = (r) => (rtl ? r.right : r.left);
|
|
649
|
+
const end = (r) => (rtl ? r.left : r.right);
|
|
650
|
+
// At a line's start or end the line stands a gap off the entry, as Gmail's does.
|
|
651
|
+
const out = rtl ? 4 : -4;
|
|
652
|
+
const at = rects[index];
|
|
653
|
+
const before = rects[index - 1];
|
|
654
|
+
let line;
|
|
655
|
+
if (at && before && Math.abs(before.top - at.top) < 1) {
|
|
656
|
+
line = { x: (end(before) + start(at)) / 2, top: at.top, height: at.height };
|
|
657
|
+
}
|
|
658
|
+
else if (at) {
|
|
659
|
+
line = { x: start(at) + out, top: at.top, height: at.height };
|
|
660
|
+
}
|
|
661
|
+
else if (before) {
|
|
662
|
+
line = { x: end(before) - out, top: before.top, height: before.height };
|
|
663
|
+
}
|
|
664
|
+
else {
|
|
665
|
+
// No chip yet: at the start — of the draft, if the row was left with one.
|
|
666
|
+
const first = (chips[0] ?? list.querySelector('[data-slot=entry]')).getBoundingClientRect();
|
|
667
|
+
line = { x: start(first) + (chips[0] ? out : 0), top: first.top, height: first.height };
|
|
668
|
+
}
|
|
669
|
+
return { index, x: line.x - box.left, y: line.top - box.top, height: line.height };
|
|
670
|
+
}
|
|
671
|
+
/** Leaving for somewhere outside the control: the drag is no longer over
|
|
672
|
+
it. (A leave with nowhere named — some engines — ends it too; the
|
|
673
|
+
next `dragover` inside brings it back.) */
|
|
674
|
+
onDragleave(event) {
|
|
675
|
+
const to = event.relatedTarget;
|
|
676
|
+
if (!to || !this.#host.nativeElement.contains(to))
|
|
677
|
+
this.dropMarker.set(null);
|
|
678
|
+
}
|
|
679
|
+
/** The drop: a chip is committed as it was; plain text from elsewhere is
|
|
680
|
+
a paste — its addresses become chips, the rest joins the typing. What
|
|
681
|
+
was typed stays. A chip the list holds already is absorbed by a move:
|
|
682
|
+
nothing is added, and the source lets it go. Nothing added otherwise
|
|
683
|
+
(the limit) is no drop: the source keeps its chip. */
|
|
684
|
+
onDrop(event) {
|
|
685
|
+
const effect = this.#dropEffect(event);
|
|
686
|
+
this.dropMarker.set(null);
|
|
687
|
+
if (effect === null)
|
|
688
|
+
return;
|
|
689
|
+
event.preventDefault();
|
|
690
|
+
const transfer = event.dataTransfer;
|
|
691
|
+
if (effect === 'none') {
|
|
692
|
+
transfer.dropEffect = 'none';
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const entry = transfer.getData(ADDRESS_DRAG_TYPE);
|
|
696
|
+
const { index } = this.#dropAt(event);
|
|
697
|
+
let changed;
|
|
698
|
+
if (entry && drag?.source === this) {
|
|
699
|
+
// Its own chip: a reorder. The source is this list; nothing leaves.
|
|
700
|
+
changed = this.#reorder(entry, index);
|
|
701
|
+
if (changed)
|
|
702
|
+
this.active.set(null);
|
|
703
|
+
}
|
|
704
|
+
else if (entry) {
|
|
705
|
+
const absorbed = effect === 'move' && this.#holds(entry);
|
|
706
|
+
changed = absorbed || this.#insert([entry], index).length > 0;
|
|
707
|
+
if (changed && drag?.address === entry) {
|
|
708
|
+
drag.moved = effect === 'move';
|
|
709
|
+
if (drag.moved)
|
|
710
|
+
this.#announceMoved(entry);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
const tokens = this.#rules.split(transfer.getData('text/plain'));
|
|
715
|
+
changed =
|
|
716
|
+
this.#insert(tokens.filter((token) => this.#accepts(token)), index).length > 0;
|
|
717
|
+
const rest = tokens.filter((token) => !this.#accepts(token));
|
|
718
|
+
if (rest.length) {
|
|
719
|
+
const input = this.field().nativeElement;
|
|
720
|
+
this.#write(input, [input.value, ...rest].filter(Boolean).join(', '));
|
|
721
|
+
this.refused.set(true);
|
|
722
|
+
input.focus();
|
|
723
|
+
changed = true;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (!changed) {
|
|
727
|
+
transfer.dropEffect = 'none';
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
this.touch.emit();
|
|
731
|
+
}
|
|
395
732
|
/** Picking a chip is being done with the typing: the addresses in it
|
|
396
733
|
become chips first, as Enter would make them, and what is not an
|
|
397
734
|
address stays behind, flagged. */
|
|
@@ -477,6 +814,20 @@ class AddressInput {
|
|
|
477
814
|
to the caret. */
|
|
478
815
|
#onActiveKey(event, active, input) {
|
|
479
816
|
const chips = this.value();
|
|
817
|
+
if (event.altKey && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) {
|
|
818
|
+
// Alt+← / Alt+→ move the chip a place — the drag's reorder, by key.
|
|
819
|
+
// At either end there is nowhere to go, and the key is still the
|
|
820
|
+
// chip's: it never walks the highlight instead.
|
|
821
|
+
if (this.locked())
|
|
822
|
+
return false;
|
|
823
|
+
const address = chips[active];
|
|
824
|
+
if (this.#reorder(address, event.key === 'ArrowLeft' ? active - 1 : active + 2)) {
|
|
825
|
+
this.active.set(this.value().indexOf(address));
|
|
826
|
+
this.touch.emit();
|
|
827
|
+
}
|
|
828
|
+
event.preventDefault();
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
480
831
|
switch (event.key) {
|
|
481
832
|
case 'ArrowLeft':
|
|
482
833
|
this.active.set(Math.max(0, active - 1));
|
|
@@ -511,13 +862,68 @@ class AddressInput {
|
|
|
511
862
|
}
|
|
512
863
|
/** A paste of several addresses: the addresses become chips, the rest
|
|
513
864
|
joins what the input already holds. A single token is left to the
|
|
514
|
-
input, so a name with spaces can still be edited before it commits
|
|
865
|
+
input, so a name with spaces can still be edited before it commits —
|
|
866
|
+
but a chip's own copy (Ctrl+C, Ctrl+X, a hold) comes back as a chip,
|
|
867
|
+
as it was. With a chip picked, what is pasted goes right after it. */
|
|
515
868
|
onPaste(event, input) {
|
|
516
|
-
|
|
517
|
-
|
|
869
|
+
if (this.locked())
|
|
870
|
+
return;
|
|
871
|
+
const data = event.clipboardData;
|
|
872
|
+
const text = data?.getData('text') ?? '';
|
|
873
|
+
const chip = [...(data?.types ?? [])].includes(ADDRESS_DRAG_TYPE);
|
|
874
|
+
const entry = (chip && data.getData(ADDRESS_DRAG_TYPE)) || (text === clipboardEntry ? text : '');
|
|
875
|
+
const active = this.active();
|
|
876
|
+
const at = active === null ? this.#draftStart() : active + 1;
|
|
877
|
+
if (entry) {
|
|
878
|
+
event.preventDefault();
|
|
879
|
+
this.#insert([entry], at);
|
|
880
|
+
this.active.set(null);
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
const tokens = this.#rules.split(text);
|
|
884
|
+
if (tokens.length < 2 && active === null)
|
|
518
885
|
return;
|
|
519
886
|
event.preventDefault();
|
|
520
|
-
|
|
887
|
+
if (active === null) {
|
|
888
|
+
this.#settle(input, `${input.value}, ${text}`);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const addresses = tokens.filter((token) => this.#accepts(token));
|
|
892
|
+
this.#insert(addresses, at);
|
|
893
|
+
const rest = tokens.filter((token) => !this.#accepts(token));
|
|
894
|
+
if (rest.length) {
|
|
895
|
+
this.#write(input, [input.value, ...rest].filter(Boolean).join(', '));
|
|
896
|
+
this.refused.set(true);
|
|
897
|
+
}
|
|
898
|
+
this.active.set(null);
|
|
899
|
+
}
|
|
900
|
+
/** Ctrl+C or Ctrl+X (Cmd on a Mac, or the Edit menu) with a chip picked
|
|
901
|
+
and no text selected: the chip goes on the clipboard — its header
|
|
902
|
+
form, as text and as the library's own type, so a paste into an
|
|
903
|
+
address input brings it back as a chip — and shows and says so, as a
|
|
904
|
+
hold does. A cut then removes it, as the × would. */
|
|
905
|
+
onClipboard(event, input, cut) {
|
|
906
|
+
const address = this.current();
|
|
907
|
+
const data = event.clipboardData;
|
|
908
|
+
if (address === null || !data || input.selectionStart !== input.selectionEnd)
|
|
909
|
+
return;
|
|
910
|
+
if (cut && this.locked())
|
|
911
|
+
return;
|
|
912
|
+
event.preventDefault();
|
|
913
|
+
data.setData('text/plain', address);
|
|
914
|
+
data.setData(ADDRESS_DRAG_TYPE, address);
|
|
915
|
+
clipboardEntry = address;
|
|
916
|
+
if (cut) {
|
|
917
|
+
this.remove(address);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
this.chips()
|
|
921
|
+
.find((chip) => chip.address() === address)
|
|
922
|
+
?.showCopied();
|
|
923
|
+
}
|
|
924
|
+
/** A chip's hold copied it: remembered, so a paste of it is a chip. */
|
|
925
|
+
onChipCopied(address) {
|
|
926
|
+
clipboardEntry = address;
|
|
521
927
|
}
|
|
522
928
|
/** Focus arrived in the input: the typos at the end of the list come back
|
|
523
929
|
into it, to be fixed — unless the user is already typing there. They
|
|
@@ -539,6 +945,9 @@ class AddressInput {
|
|
|
539
945
|
onBlur(event, input) {
|
|
540
946
|
if (this.#within(event))
|
|
541
947
|
return;
|
|
948
|
+
this.#leave(input);
|
|
949
|
+
}
|
|
950
|
+
#leave(input) {
|
|
542
951
|
const tokens = this.#rules.split(input.value);
|
|
543
952
|
this.#insert(tokens.filter((token) => this.#accepts(token)));
|
|
544
953
|
this.#insert(tokens.filter((token) => !this.#accepts(token)));
|
|
@@ -549,6 +958,10 @@ class AddressInput {
|
|
|
549
958
|
onFocusout(event) {
|
|
550
959
|
if (this.#within(event))
|
|
551
960
|
return;
|
|
961
|
+
// Focus parked on the chip list by a press that came to nothing leaves
|
|
962
|
+
// from there: leaving is leaving, wherever the caret had got to.
|
|
963
|
+
if (event.target === this.chipList().nativeElement)
|
|
964
|
+
this.#leave(this.field().nativeElement);
|
|
552
965
|
this.#focused.set(false);
|
|
553
966
|
this.active.set(null);
|
|
554
967
|
}
|
|
@@ -559,25 +972,27 @@ class AddressInput {
|
|
|
559
972
|
this.#write(input, tokens.filter((token) => !this.#accepts(token)).join(', '));
|
|
560
973
|
this.refused.set(!!input.value);
|
|
561
974
|
}
|
|
562
|
-
/** Adds the new addresses to the
|
|
563
|
-
only if its address is not in
|
|
564
|
-
keeps its name.
|
|
565
|
-
|
|
975
|
+
/** Adds the new addresses to the list — at the end, or before the entry
|
|
976
|
+
at `at` (a drop) — up to `limit`, each only if its address is not in
|
|
977
|
+
the list yet; the entry already there keeps its name. Returns what
|
|
978
|
+
was added. */
|
|
979
|
+
#insert(incoming, at = Infinity) {
|
|
980
|
+
const added = [];
|
|
566
981
|
if (!incoming.length)
|
|
567
|
-
return;
|
|
982
|
+
return added;
|
|
568
983
|
this.value.update((current) => {
|
|
569
|
-
const
|
|
570
|
-
const seen = new Set(next.map(this.#key));
|
|
984
|
+
const seen = new Set(current.map(this.#key));
|
|
571
985
|
for (const address of incoming) {
|
|
572
|
-
if (
|
|
986
|
+
if (current.length + added.length >= this.limit())
|
|
573
987
|
break;
|
|
574
988
|
if (seen.has(this.#key(address)))
|
|
575
989
|
continue;
|
|
576
990
|
seen.add(this.#key(address));
|
|
577
|
-
|
|
991
|
+
added.push(address);
|
|
578
992
|
}
|
|
579
|
-
return
|
|
993
|
+
return [...current.slice(0, at), ...added, ...current.slice(at)];
|
|
580
994
|
});
|
|
995
|
+
return added;
|
|
581
996
|
}
|
|
582
997
|
/** Puts text in the input — the one way it is written, so `text` follows. */
|
|
583
998
|
#write(input, text) {
|
|
@@ -590,7 +1005,7 @@ class AddressInput {
|
|
|
590
1005
|
return !!destination && this.#host.nativeElement.contains(destination);
|
|
591
1006
|
}
|
|
592
1007
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: AddressInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
593
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: AddressInput, isStandalone: true, selector: "[email-address-input]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, limit: { classPropertyName: "limit", publicName: "limit", isSignal: true, isRequired: false, transformFunction: null }, removeLabel: { classPropertyName: "removeLabel", publicName: "removeLabel", isSignal: true, isRequired: false, transformFunction: null }, invalidLabel: { classPropertyName: "invalidLabel", publicName: "invalidLabel", isSignal: true, isRequired: false, transformFunction: null }, pendingLabel: { classPropertyName: "pendingLabel", publicName: "pendingLabel", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touch: "touch" }, host: { listeners: { "pointerdown": "onPointerdown($event)", "mousedown": "keepCaret($event)", "click": "focus()", "focusin": "onFocusin()", "focusout": "onFocusout($event)" }, properties: { "attr.aria-disabled": "disabled() || null", "attr.data-invalid": "flagged() || null", "attr.data-full": "full() || null", "attr.data-focused": "focused() || null" } }, queries: [{ propertyName: "labelSlot", first: true, predicate: AddressInputLabel, descendants: true, isSignal: true }, { propertyName: "removeSlot", first: true, predicate: AddressChipRemove, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "field", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "chips", predicate: AddressChip, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- No label here: the row around this control names it, through\n aria-labelledby or aria-label, both passed down to the text input. -->\n<ul data-slot=\"chips\" role=\"list\">\n <!-- The opt-in label (emailAddressLabel): first in the row, so the chips\n wrap under it. Hidden from assistive tech as text \u2014 it names the\n text input instead, through aria-labelledby. -->\n @if (labelSlot(); as slot) {\n <li data-slot=\"label\" aria-hidden=\"true\" [id]=\"labelId\">\n <ng-container [ngTemplateOutlet]=\"slot.template\" />\n </li>\n }\n @for (address of value(); track address) {\n <li\n email-address-chip\n [id]=\"chipId($index)\"\n [address]=\"address\"\n [removable]=\"!locked()\"\n [inline]=\"!focused()\"\n [aria-current]=\"active() === $index\"\n [removeTabbable]=\"closed()\"\n [removeLabel]=\"removeLabel()\"\n [removeTemplate]=\"removeSlot()?.template ?? null\"\n [invalidLabel]=\"invalidLabel()\"\n [pendingLabel]=\"pendingLabel()\"\n [aria-disabled]=\"disabled()\"\n (removed)=\"removeChip(address)\"\n (click)=\"onChipClick($event, $index, field)\"\n (dblclick)=\"onChipDblclick($event, address, field)\"\n ></li>\n }\n <!-- The entry is a list item too, so the chips and the typing sit in one\n wrapping row \u2014 always after the last chip. Hidden once the field is full (From takes one address)\n with nothing to fix: the chips say everything, and a second address\n would only be refused. -->\n <li data-slot=\"entry\" [hidden]=\"closed()\">\n <input\n #field\n data-slot=\"input\"\n type=\"text\"\n inputmode=\"email\"\n autocomplete=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n [id]=\"id\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [placeholder]=\"value().length ? '' : placeholder()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() || null\"\n [attr.aria-invalid]=\"flagged() || refused() || null\"\n [attr.aria-activedescendant]=\"activeId()\"\n [attr.data-refused]=\"refused() || null\"\n (keydown)=\"onKeydown($event, field)\"\n (paste)=\"onPaste($event, field)\"\n (input)=\"onInput(field)\"\n (mousedown)=\"active.set(null)\"\n (focus)=\"onFocus(field)\"\n (blur)=\"onBlur($event, field)\"\n />\n </li>\n</ul>\n", styles: [":host{display:flex;min-width:0;color:var(--email-address-input-color, var(--mat-sys-on-surface, light-dark(#1f1f1f, #e3e3e3)));font-family:var(--email-address-input-font-family, var(--mat-sys-body-medium-font, system-ui, sans-serif));font-size:var(--email-address-input-font-size, var(--mat-sys-body-medium-size, .875rem));font-weight:var(--email-address-input-font-weight, var(--mat-sys-body-medium-weight, 400));line-height:var(--email-address-input-line-height, var(--mat-sys-body-medium-line-height, 1.25rem));letter-spacing:var(--email-address-input-letter-spacing, var(--mat-sys-body-medium-tracking, .016rem));cursor:text}:host([aria-disabled=true]){cursor:default;opacity:.6}:host :where([data-slot=chips]){flex:1 1 0;display:flex;flex-wrap:wrap;align-items:center;gap:var(--email-address-input-row-gap, 8px) var(--email-address-input-column-gap, 4px);min-width:0;margin:0;padding:0;list-style:none}:host :where([data-slot=label]){flex:none;margin-inline-end:var(--email-address-input-label-gap, 4px);color:var(--email-address-input-label-color, var(--mat-sys-on-surface-variant, light-dark(#444746, #c4c7c5)));line-height:var(--email-address-input-label-line-height, 24px);white-space:nowrap}:host(:not([data-focused])) :where([data-slot=chips]){column-gap:var(--email-address-input-inline-gap, .3em)}:host(:not([data-focused])) :where([email-address-chip]:has(+[email-address-chip])):after{content:\",\"}:host :where([data-slot=entry]){flex:1 1 8ch;min-width:8ch;display:flex}:host :where([data-slot=entry][hidden]){display:none}:host(:not([data-focused])) :where([data-slot=entry]){flex-basis:0;min-width:0}:host :where([data-slot=input]){flex:1 1 auto;width:100%;min-width:0;height:var(--email-address-chip-height, 24px);margin:0;padding:0;box-sizing:border-box;border:none;outline:none;background:none;color:inherit;font:inherit;letter-spacing:inherit;line-height:var(--email-address-chip-height, 24px)}:host :where([data-slot=input][data-refused]){color:var(--email-address-chip-invalid-color, var(--mat-sys-error, light-dark(#b3261e, #f2b8b5)))}:host :where([data-slot=input])::placeholder{color:var(--email-address-input-placeholder-color, var(--mat-sys-on-surface-variant, light-dark(#5f6368, #9aa0a6)))}\n"], dependencies: [{ kind: "component", type: AddressChip, selector: "[email-address-chip]", inputs: ["address", "removable", "inline", "aria-current", "removeTabbable", "removeLabel", "invalidLabel", "pendingLabel", "aria-disabled", "holdDelay", "copiedLabel", "removeTemplate"], outputs: ["removed", "copied"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
1008
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.6", type: AddressInput, isStandalone: true, selector: "[email-address-input]", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledby: { classPropertyName: "ariaLabelledby", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedby: { classPropertyName: "ariaDescribedby", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, limit: { classPropertyName: "limit", publicName: "limit", isSignal: true, isRequired: false, transformFunction: null }, removeLabel: { classPropertyName: "removeLabel", publicName: "removeLabel", isSignal: true, isRequired: false, transformFunction: null }, invalidLabel: { classPropertyName: "invalidLabel", publicName: "invalidLabel", isSignal: true, isRequired: false, transformFunction: null }, pendingLabel: { classPropertyName: "pendingLabel", publicName: "pendingLabel", isSignal: true, isRequired: false, transformFunction: null }, movedLabel: { classPropertyName: "movedLabel", publicName: "movedLabel", isSignal: true, isRequired: false, transformFunction: null }, allowDrag: { classPropertyName: "allowDrag", publicName: "allowDrag", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, required: { classPropertyName: "required", publicName: "required", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange", touch: "touch" }, host: { listeners: { "pointerdown": "onPointerdown($event)", "mousedown": "keepCaret($event)", "click": "focus()", "focusin": "onFocusin()", "focusout": "onFocusout($event)", "dragenter": "onDragover($event)", "dragover": "onDragover($event)", "dragleave": "onDragleave($event)", "drop": "onDrop($event)" }, properties: { "attr.aria-disabled": "disabled() || null", "attr.data-invalid": "flagged() || null", "attr.data-full": "full() || null", "attr.data-focused": "focused() || null", "attr.data-dropping": "dropping() || null" } }, queries: [{ propertyName: "labelSlot", first: true, predicate: AddressInputLabel, descendants: true, isSignal: true }, { propertyName: "removeSlot", first: true, predicate: AddressChipRemove, descendants: true, isSignal: true }], viewQueries: [{ propertyName: "field", first: true, predicate: ["field"], descendants: true, isSignal: true }, { propertyName: "chipList", first: true, predicate: ["chipList"], descendants: true, isSignal: true }, { propertyName: "chips", predicate: AddressChip, descendants: true, isSignal: true }], ngImport: i0, template: "<!-- No label here: the row around this control names it, through\n aria-labelledby or aria-label, both passed down to the text input. -->\n<!-- Focusable by script and by a press only (tabindex -1, while chips can\n be dragged): a press on a draggable chip must keep its default for the\n drag to start, and its focus lands here \u2014 inside the control. -->\n<ul #chipList data-slot=\"chips\" role=\"list\" [attr.tabindex]=\"canDrag() ? -1 : null\">\n <!-- The opt-in label (emailAddressLabel): first in the row, so the chips\n wrap under it. Hidden from assistive tech as text \u2014 it names the\n text input instead, through aria-labelledby. -->\n @if (labelSlot(); as slot) {\n <li data-slot=\"label\" aria-hidden=\"true\" [id]=\"labelId\">\n <ng-container [ngTemplateOutlet]=\"slot.template\" />\n </li>\n }\n @for (address of value(); track address) {\n <li\n email-address-chip\n [id]=\"chipId($index)\"\n [address]=\"address\"\n [removable]=\"!locked()\"\n [inline]=\"!focused()\"\n [draggable]=\"canDrag()\"\n [aria-current]=\"active() === $index\"\n [removeTabbable]=\"closed()\"\n [removeLabel]=\"removeLabel()\"\n [removeTemplate]=\"removeSlot()?.template ?? null\"\n [invalidLabel]=\"invalidLabel()\"\n [pendingLabel]=\"pendingLabel()\"\n [aria-disabled]=\"disabled()\"\n (removed)=\"removeChip(address)\"\n (copied)=\"onChipCopied($event)\"\n (click)=\"onChipClick($event, $index, field)\"\n (dblclick)=\"onChipDblclick($event, address, field)\"\n (dragstart)=\"onChipDragstart(address)\"\n (dragend)=\"onChipDragend()\"\n ></li>\n }\n <!-- Where a drag over the control would land: a line between entries,\n drawn on top and out of the flow \u2014 nothing moves under the pointer. -->\n @if (dropMarker(); as marker) {\n <li\n data-slot=\"drop-marker\"\n aria-hidden=\"true\"\n [style.left.px]=\"marker.x\"\n [style.top.px]=\"marker.y\"\n [style.height.px]=\"marker.height\"\n ></li>\n }\n <!-- The entry is a list item too, so the chips and the typing sit in one\n wrapping row \u2014 always after the last chip. Hidden once the field is full (From takes one address)\n with nothing to fix: the chips say everything, and a second address\n would only be refused. -->\n <li data-slot=\"entry\" [hidden]=\"closed()\">\n <input\n #field\n data-slot=\"input\"\n type=\"text\"\n inputmode=\"email\"\n autocomplete=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n [id]=\"id\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [placeholder]=\"value().length ? '' : placeholder()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() || null\"\n [attr.aria-invalid]=\"flagged() || refused() || null\"\n [attr.aria-activedescendant]=\"activeId()\"\n [attr.data-refused]=\"refused() || null\"\n (keydown)=\"onKeydown($event, field)\"\n (paste)=\"onPaste($event, field)\"\n (copy)=\"onClipboard($event, field, false)\"\n (cut)=\"onClipboard($event, field, true)\"\n (input)=\"onInput(field)\"\n (mousedown)=\"active.set(null)\"\n (focus)=\"onFocus(field)\"\n (blur)=\"onBlur($event, field)\"\n />\n </li>\n</ul>\n", styles: [":host{display:flex;min-width:0;color:var(--email-address-input-color, var(--mat-sys-on-surface, light-dark(#1f1f1f, #e3e3e3)));font-family:var(--email-address-input-font-family, var(--mat-sys-body-medium-font, system-ui, sans-serif));font-size:var(--email-address-input-font-size, var(--mat-sys-body-medium-size, .875rem));font-weight:var(--email-address-input-font-weight, var(--mat-sys-body-medium-weight, 400));line-height:var(--email-address-input-line-height, var(--mat-sys-body-medium-line-height, 1.25rem));letter-spacing:var(--email-address-input-letter-spacing, var(--mat-sys-body-medium-tracking, .016rem));cursor:text}:host([aria-disabled=true]){cursor:default;opacity:.6}:host :where([data-slot=chips]){position:relative;flex:1 1 0;display:flex;flex-wrap:wrap;align-items:center;gap:var(--email-address-input-row-gap, 8px) var(--email-address-input-column-gap, 4px);min-width:0;margin:0;padding:0;list-style:none}:host :where([data-slot=chips]):focus{outline:none}:host :where([data-slot=drop-marker]){position:absolute;box-sizing:content-box;margin-top:calc(-1 * var(--email-address-input-drop-overhang, 4px));padding-block:var(--email-address-input-drop-overhang, 4px);width:var(--email-address-input-drop-width, 2px);margin-left:calc(var(--email-address-input-drop-width, 2px) / -2);border-radius:1px;background:var(--email-address-input-drop-color, var(--mat-sys-primary, light-dark(#0b57d0, #a8c7fa)));pointer-events:none}:host :where([data-slot=label]){flex:none;margin-inline-end:var(--email-address-input-label-gap, 4px);color:var(--email-address-input-label-color, var(--mat-sys-on-surface-variant, light-dark(#444746, #c4c7c5)));line-height:var(--email-address-input-label-line-height, 24px);white-space:nowrap}:host(:not([data-focused])) :where([data-slot=chips]){column-gap:var(--email-address-input-inline-gap, .3em)}:host(:not([data-focused])) :where([email-address-chip]:has(+[email-address-chip])):after{content:\",\"}:host :where([data-slot=entry]){flex:1 1 8ch;min-width:8ch;display:flex}:host :where([data-slot=entry][hidden]){display:none}:host(:not([data-focused])) :where([data-slot=entry]){flex-basis:0;min-width:0}:host :where([data-slot=input]){flex:1 1 auto;width:100%;min-width:0;height:var(--email-address-chip-height, 24px);margin:0;padding:0;box-sizing:border-box;border:none;outline:none;background:none;color:inherit;font:inherit;letter-spacing:inherit;line-height:var(--email-address-chip-height, 24px)}:host :where([data-slot=input][data-refused]){color:var(--email-address-chip-invalid-color, var(--mat-sys-error, light-dark(#b3261e, #f2b8b5)))}:host :where([data-slot=input])::placeholder{color:var(--email-address-input-placeholder-color, var(--mat-sys-on-surface-variant, light-dark(#5f6368, #9aa0a6)))}\n"], dependencies: [{ kind: "component", type: AddressChip, selector: "[email-address-chip]", inputs: ["address", "removable", "inline", "aria-current", "removeTabbable", "removeLabel", "invalidLabel", "pendingLabel", "aria-disabled", "holdDelay", "copiedLabel", "draggable", "removeTemplate"], outputs: ["removed", "copied"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
594
1009
|
}
|
|
595
1010
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImport: i0, type: AddressInput, decorators: [{
|
|
596
1011
|
type: Component,
|
|
@@ -599,13 +1014,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.6", ngImpor
|
|
|
599
1014
|
'[attr.data-invalid]': 'flagged() || null',
|
|
600
1015
|
'[attr.data-full]': 'full() || null',
|
|
601
1016
|
'[attr.data-focused]': 'focused() || null',
|
|
1017
|
+
'[attr.data-dropping]': 'dropping() || null',
|
|
602
1018
|
'(pointerdown)': 'onPointerdown($event)',
|
|
603
1019
|
'(mousedown)': 'keepCaret($event)',
|
|
604
1020
|
'(click)': 'focus()',
|
|
605
1021
|
'(focusin)': 'onFocusin()',
|
|
606
1022
|
'(focusout)': 'onFocusout($event)',
|
|
607
|
-
|
|
608
|
-
|
|
1023
|
+
'(dragenter)': 'onDragover($event)',
|
|
1024
|
+
'(dragover)': 'onDragover($event)',
|
|
1025
|
+
'(dragleave)': 'onDragleave($event)',
|
|
1026
|
+
'(drop)': 'onDrop($event)',
|
|
1027
|
+
}, template: "<!-- No label here: the row around this control names it, through\n aria-labelledby or aria-label, both passed down to the text input. -->\n<!-- Focusable by script and by a press only (tabindex -1, while chips can\n be dragged): a press on a draggable chip must keep its default for the\n drag to start, and its focus lands here \u2014 inside the control. -->\n<ul #chipList data-slot=\"chips\" role=\"list\" [attr.tabindex]=\"canDrag() ? -1 : null\">\n <!-- The opt-in label (emailAddressLabel): first in the row, so the chips\n wrap under it. Hidden from assistive tech as text \u2014 it names the\n text input instead, through aria-labelledby. -->\n @if (labelSlot(); as slot) {\n <li data-slot=\"label\" aria-hidden=\"true\" [id]=\"labelId\">\n <ng-container [ngTemplateOutlet]=\"slot.template\" />\n </li>\n }\n @for (address of value(); track address) {\n <li\n email-address-chip\n [id]=\"chipId($index)\"\n [address]=\"address\"\n [removable]=\"!locked()\"\n [inline]=\"!focused()\"\n [draggable]=\"canDrag()\"\n [aria-current]=\"active() === $index\"\n [removeTabbable]=\"closed()\"\n [removeLabel]=\"removeLabel()\"\n [removeTemplate]=\"removeSlot()?.template ?? null\"\n [invalidLabel]=\"invalidLabel()\"\n [pendingLabel]=\"pendingLabel()\"\n [aria-disabled]=\"disabled()\"\n (removed)=\"removeChip(address)\"\n (copied)=\"onChipCopied($event)\"\n (click)=\"onChipClick($event, $index, field)\"\n (dblclick)=\"onChipDblclick($event, address, field)\"\n (dragstart)=\"onChipDragstart(address)\"\n (dragend)=\"onChipDragend()\"\n ></li>\n }\n <!-- Where a drag over the control would land: a line between entries,\n drawn on top and out of the flow \u2014 nothing moves under the pointer. -->\n @if (dropMarker(); as marker) {\n <li\n data-slot=\"drop-marker\"\n aria-hidden=\"true\"\n [style.left.px]=\"marker.x\"\n [style.top.px]=\"marker.y\"\n [style.height.px]=\"marker.height\"\n ></li>\n }\n <!-- The entry is a list item too, so the chips and the typing sit in one\n wrapping row \u2014 always after the last chip. Hidden once the field is full (From takes one address)\n with nothing to fix: the chips say everything, and a second address\n would only be refused. -->\n <li data-slot=\"entry\" [hidden]=\"closed()\">\n <input\n #field\n data-slot=\"input\"\n type=\"text\"\n inputmode=\"email\"\n autocomplete=\"off\"\n autocapitalize=\"off\"\n spellcheck=\"false\"\n [id]=\"id\"\n [attr.name]=\"name() || null\"\n [disabled]=\"disabled()\"\n [readOnly]=\"readonly()\"\n [placeholder]=\"value().length ? '' : placeholder()\"\n [attr.aria-labelledby]=\"labelledBy()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-describedby]=\"describedBy()\"\n [attr.aria-required]=\"required() || null\"\n [attr.aria-invalid]=\"flagged() || refused() || null\"\n [attr.aria-activedescendant]=\"activeId()\"\n [attr.data-refused]=\"refused() || null\"\n (keydown)=\"onKeydown($event, field)\"\n (paste)=\"onPaste($event, field)\"\n (copy)=\"onClipboard($event, field, false)\"\n (cut)=\"onClipboard($event, field, true)\"\n (input)=\"onInput(field)\"\n (mousedown)=\"active.set(null)\"\n (focus)=\"onFocus(field)\"\n (blur)=\"onBlur($event, field)\"\n />\n </li>\n</ul>\n", styles: [":host{display:flex;min-width:0;color:var(--email-address-input-color, var(--mat-sys-on-surface, light-dark(#1f1f1f, #e3e3e3)));font-family:var(--email-address-input-font-family, var(--mat-sys-body-medium-font, system-ui, sans-serif));font-size:var(--email-address-input-font-size, var(--mat-sys-body-medium-size, .875rem));font-weight:var(--email-address-input-font-weight, var(--mat-sys-body-medium-weight, 400));line-height:var(--email-address-input-line-height, var(--mat-sys-body-medium-line-height, 1.25rem));letter-spacing:var(--email-address-input-letter-spacing, var(--mat-sys-body-medium-tracking, .016rem));cursor:text}:host([aria-disabled=true]){cursor:default;opacity:.6}:host :where([data-slot=chips]){position:relative;flex:1 1 0;display:flex;flex-wrap:wrap;align-items:center;gap:var(--email-address-input-row-gap, 8px) var(--email-address-input-column-gap, 4px);min-width:0;margin:0;padding:0;list-style:none}:host :where([data-slot=chips]):focus{outline:none}:host :where([data-slot=drop-marker]){position:absolute;box-sizing:content-box;margin-top:calc(-1 * var(--email-address-input-drop-overhang, 4px));padding-block:var(--email-address-input-drop-overhang, 4px);width:var(--email-address-input-drop-width, 2px);margin-left:calc(var(--email-address-input-drop-width, 2px) / -2);border-radius:1px;background:var(--email-address-input-drop-color, var(--mat-sys-primary, light-dark(#0b57d0, #a8c7fa)));pointer-events:none}:host :where([data-slot=label]){flex:none;margin-inline-end:var(--email-address-input-label-gap, 4px);color:var(--email-address-input-label-color, var(--mat-sys-on-surface-variant, light-dark(#444746, #c4c7c5)));line-height:var(--email-address-input-label-line-height, 24px);white-space:nowrap}:host(:not([data-focused])) :where([data-slot=chips]){column-gap:var(--email-address-input-inline-gap, .3em)}:host(:not([data-focused])) :where([email-address-chip]:has(+[email-address-chip])):after{content:\",\"}:host :where([data-slot=entry]){flex:1 1 8ch;min-width:8ch;display:flex}:host :where([data-slot=entry][hidden]){display:none}:host(:not([data-focused])) :where([data-slot=entry]){flex-basis:0;min-width:0}:host :where([data-slot=input]){flex:1 1 auto;width:100%;min-width:0;height:var(--email-address-chip-height, 24px);margin:0;padding:0;box-sizing:border-box;border:none;outline:none;background:none;color:inherit;font:inherit;letter-spacing:inherit;line-height:var(--email-address-chip-height, 24px)}:host :where([data-slot=input][data-refused]){color:var(--email-address-chip-invalid-color, var(--mat-sys-error, light-dark(#b3261e, #f2b8b5)))}:host :where([data-slot=input])::placeholder{color:var(--email-address-input-placeholder-color, var(--mat-sys-on-surface-variant, light-dark(#5f6368, #9aa0a6)))}\n"] }]
|
|
1028
|
+
}], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }], placeholder: [{ type: i0.Input, args: [{ isSignal: true, alias: "placeholder", required: false }] }], ariaLabelledby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaDescribedby: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], limit: [{ type: i0.Input, args: [{ isSignal: true, alias: "limit", required: false }] }], removeLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "removeLabel", required: false }] }], invalidLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalidLabel", required: false }] }], pendingLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "pendingLabel", required: false }] }], movedLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "movedLabel", required: false }] }], allowDrag: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowDrag", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], required: [{ type: i0.Input, args: [{ isSignal: true, alias: "required", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }], touch: [{ type: i0.Output, args: ["touch"] }], labelSlot: [{ type: i0.ContentChild, args: [i0.forwardRef(() => AddressInputLabel), { isSignal: true }] }], removeSlot: [{ type: i0.ContentChild, args: [i0.forwardRef(() => AddressChipRemove), { isSignal: true }] }], field: [{ type: i0.ViewChild, args: ['field', { isSignal: true }] }], chipList: [{ type: i0.ViewChild, args: ['chipList', { isSignal: true }] }], chips: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => AddressChip), { isSignal: true }] }] } });
|
|
609
1029
|
|
|
610
1030
|
/** The error kinds `addressList` reports, for a host that switches on them. */
|
|
611
1031
|
const ADDRESS_LIST_EMPTY = 'addressList.empty';
|
|
@@ -682,5 +1102,5 @@ function rulesInForce() {
|
|
|
682
1102
|
* Generated bundle index. Do not edit.
|
|
683
1103
|
*/
|
|
684
1104
|
|
|
685
|
-
export { ADDRESS_LIST_EMPTY, ADDRESS_LIST_INVALID, ADDRESS_LIST_TOO_MANY, AddressInput, AddressInputLabel, addressList };
|
|
1105
|
+
export { ADDRESS_LIST_EMPTY, ADDRESS_LIST_INVALID, ADDRESS_LIST_TOO_MANY, AddressDragGroup, AddressInput, AddressInputLabel, addressList };
|
|
686
1106
|
//# sourceMappingURL=h-k-dev-angular-email-editor-address-input.mjs.map
|