@nyaruka/temba-components 0.163.0 → 0.164.0
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/CHANGELOG.md +14 -0
- package/dist/temba-components.js +1842 -678
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/display/Chat.ts +3 -2
- package/src/display/Lightbox.ts +57 -109
- package/src/display/Thumbnail.ts +34 -7
- package/src/interfaces.ts +79 -2
- package/src/layout/Card.ts +311 -0
- package/src/layout/CardLayout.ts +425 -0
- package/src/layout/CardStack.ts +131 -0
- package/src/layout/Dialog.ts +33 -24
- package/src/layout/HeaderBar.ts +38 -0
- package/src/layout/PageHeader.ts +5 -6
- package/src/layout/Resizer.ts +20 -0
- package/src/list/CampaignList.ts +144 -0
- package/src/list/ContactList.ts +3 -1
- package/src/list/FlowList.ts +31 -7
- package/src/list/MsgList.ts +18 -12
- package/src/list/SortableList.ts +45 -6
- package/src/list/TriggerList.ts +538 -0
- package/src/live/CampaignEvents.ts +1094 -0
- package/src/live/ContactDetails.ts +3 -4
- package/src/live/ContactFields.ts +1 -1
- package/src/live/ContactNameFetch.ts +5 -2
- package/src/live/ContactNotepad.ts +88 -2
- package/src/live/ContactStoreElement.ts +12 -2
- package/src/utils.ts +19 -0
- package/temba-modules.ts +14 -0
- package/web-dev-server.config.mjs +54 -0
package/src/layout/PageHeader.ts
CHANGED
|
@@ -70,6 +70,8 @@ export class PageHeader extends RapidElement {
|
|
|
70
70
|
}
|
|
71
71
|
.title {
|
|
72
72
|
font-size: 15.5px;
|
|
73
|
+
/* slotted contact names render at the title's size */
|
|
74
|
+
--contact-name-font-size: 15.5px;
|
|
73
75
|
font-weight: var(--w-semibold);
|
|
74
76
|
color: var(--text-1);
|
|
75
77
|
line-height: 1.25;
|
|
@@ -139,10 +141,6 @@ export class PageHeader extends RapidElement {
|
|
|
139
141
|
.menu-button.primary:hover {
|
|
140
142
|
background: var(--accent-700);
|
|
141
143
|
}
|
|
142
|
-
.menu-button[disabled] {
|
|
143
|
-
opacity: 0.45;
|
|
144
|
-
cursor: not-allowed;
|
|
145
|
-
}
|
|
146
144
|
|
|
147
145
|
/* Overflow toggle — the trailing ⋮ that opens the rest of the
|
|
148
146
|
menu items. Plain icon button, matches the list's Search. */
|
|
@@ -282,7 +280,9 @@ export class PageHeader extends RapidElement {
|
|
|
282
280
|
* origin — same payload the standalone content menu emits, so the
|
|
283
281
|
* host's existing menu handling keeps working. */
|
|
284
282
|
private handleItemClicked(item: ContentMenuItem, event: MouseEvent): void {
|
|
285
|
-
|
|
283
|
+
// note: item.disabled does NOT make the item inert — in the content
|
|
284
|
+
// menu contract it means the opened modal starts with its submit
|
|
285
|
+
// disabled (hosts pass it through to showModax)
|
|
286
286
|
const el = event.currentTarget as Element;
|
|
287
287
|
const rect = el?.getBoundingClientRect();
|
|
288
288
|
this.fireCustomEvent(CustomEventType.Selection, {
|
|
@@ -300,7 +300,6 @@ export class PageHeader extends RapidElement {
|
|
|
300
300
|
(button) => html`
|
|
301
301
|
<div
|
|
302
302
|
class="menu-button ${button.primary ? 'primary' : ''}"
|
|
303
|
-
?disabled=${button.disabled}
|
|
304
303
|
@click=${(e: MouseEvent) => this.handleItemClicked(button, e)}
|
|
305
304
|
>
|
|
306
305
|
${button.label}
|
package/src/layout/Resizer.ts
CHANGED
|
@@ -10,6 +10,9 @@ export class Resizer extends ResizeElement {
|
|
|
10
10
|
display: block;
|
|
11
11
|
position: relative;
|
|
12
12
|
width: var(--box-width, 200px);
|
|
13
|
+
/* hold the drag-clamp floor against flex compression too, so
|
|
14
|
+
shrinking the browser can't squeeze us below minWidth */
|
|
15
|
+
min-width: var(--box-min-width, 200px);
|
|
13
16
|
--resizer-handle-size: 15px;
|
|
14
17
|
}
|
|
15
18
|
|
|
@@ -69,6 +72,20 @@ export class Resizer extends ResizeElement {
|
|
|
69
72
|
this.stopResize = this.stopResize.bind(this);
|
|
70
73
|
}
|
|
71
74
|
|
|
75
|
+
protected willUpdate(
|
|
76
|
+
changes: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
|
77
|
+
): void {
|
|
78
|
+
super.willUpdate(changes);
|
|
79
|
+
// clamp programmatic width sets (e.g. a stored width restored on
|
|
80
|
+
// load) the same way drags are clamped
|
|
81
|
+
if (changes.has('currentWidth') && this.currentWidth != null) {
|
|
82
|
+
this.currentWidth = Math.min(
|
|
83
|
+
Math.max(this.currentWidth, this.minWidth),
|
|
84
|
+
this.maxWidth
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
72
89
|
protected updated(
|
|
73
90
|
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
|
74
91
|
): void {
|
|
@@ -76,6 +93,9 @@ export class Resizer extends ResizeElement {
|
|
|
76
93
|
if (_changedProperties.has('currentWidth')) {
|
|
77
94
|
this.style.setProperty('--box-width', `${this.currentWidth}px`);
|
|
78
95
|
}
|
|
96
|
+
if (_changedProperties.has('minWidth')) {
|
|
97
|
+
this.style.setProperty('--box-min-width', `${this.minWidth}px`);
|
|
98
|
+
}
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
public setWidth(width: number) {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { css, html, TemplateResult } from 'lit';
|
|
2
|
+
import { ContentList, ContentListColumn } from './ContentList';
|
|
3
|
+
import { Icon } from '../Icons';
|
|
4
|
+
import { Campaign, CustomEventType, ObjectReference } from '../interfaces';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Campaign CRUDL list — drop-in replacement for the rapidpro
|
|
8
|
+
* `campaigns/campaign_list.html` table. Every row leads with the
|
|
9
|
+
* campaign clock icon (from the styleguide's campaign list), then
|
|
10
|
+
* name, the group pill the campaign schedules against, contact /
|
|
11
|
+
* event counts, and the last-updated date.
|
|
12
|
+
*/
|
|
13
|
+
export class CampaignList extends ContentList<Campaign> {
|
|
14
|
+
static get styles() {
|
|
15
|
+
return css`
|
|
16
|
+
${ContentList.styles}
|
|
17
|
+
.campaign-name {
|
|
18
|
+
color: inherit;
|
|
19
|
+
font-weight: var(--w-medium);
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
text-overflow: ellipsis;
|
|
22
|
+
white-space: nowrap;
|
|
23
|
+
}
|
|
24
|
+
.num {
|
|
25
|
+
font-variant-numeric: tabular-nums;
|
|
26
|
+
color: inherit;
|
|
27
|
+
}
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
super();
|
|
33
|
+
this.valueKey = 'uuid';
|
|
34
|
+
this.emptyMessage = 'No campaigns';
|
|
35
|
+
this.searchPlaceholder = 'Search campaigns';
|
|
36
|
+
this.columns = [
|
|
37
|
+
{
|
|
38
|
+
key: 'name',
|
|
39
|
+
label: 'Name',
|
|
40
|
+
sortable: true,
|
|
41
|
+
minWidth: '160px',
|
|
42
|
+
maxWidth: '280px',
|
|
43
|
+
pinned: true
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
key: 'group',
|
|
47
|
+
label: 'Group',
|
|
48
|
+
minWidth: '120px',
|
|
49
|
+
maxWidth: '220px'
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: 'contacts',
|
|
53
|
+
label: 'Contacts',
|
|
54
|
+
sortable: true,
|
|
55
|
+
minWidth: '72px',
|
|
56
|
+
align: 'right'
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: 'events',
|
|
60
|
+
label: 'Events',
|
|
61
|
+
sortable: true,
|
|
62
|
+
minWidth: '64px',
|
|
63
|
+
align: 'right'
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: 'modified_on',
|
|
67
|
+
label: 'Updated',
|
|
68
|
+
sortable: true,
|
|
69
|
+
minWidth: '96px',
|
|
70
|
+
maxWidth: '150px',
|
|
71
|
+
align: 'right'
|
|
72
|
+
}
|
|
73
|
+
];
|
|
74
|
+
this.bulkActions = [
|
|
75
|
+
{ key: 'archive', label: 'Archive', icon: Icon.archive }
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
protected getRowIcon(_item: Campaign): string | null {
|
|
80
|
+
return Icon.campaign;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
protected getRowHref(item: Campaign): string | null {
|
|
84
|
+
return item?.uuid ? `/campaign/read/${item.uuid}/` : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Clicking the group pill opens that group's contact list rather
|
|
88
|
+
* than falling through to the row click (which opens the campaign). */
|
|
89
|
+
private handleGroupClick(group: ObjectReference, event: MouseEvent): void {
|
|
90
|
+
// Stop the click from bubbling to the row's navigation handler.
|
|
91
|
+
event.stopPropagation();
|
|
92
|
+
if (!group?.uuid) return;
|
|
93
|
+
const href = `/contact/group/${group.uuid}/`;
|
|
94
|
+
// Guard the JSON-driven href against open-redirect, same as the
|
|
95
|
+
// row-click path in ContentList.handleRowClick.
|
|
96
|
+
if (!this.isSafeHref(href)) return;
|
|
97
|
+
// Meta/ctrl-click opens a new tab, matching ordinary links and the
|
|
98
|
+
// row-click behavior.
|
|
99
|
+
if (event.metaKey || event.ctrlKey) {
|
|
100
|
+
window.open(href, '_blank');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.fireCustomEvent(CustomEventType.Redirected, { url: href });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
protected renderCell(
|
|
107
|
+
item: Campaign,
|
|
108
|
+
column: ContentListColumn
|
|
109
|
+
): TemplateResult | string {
|
|
110
|
+
switch (column.key) {
|
|
111
|
+
case 'name':
|
|
112
|
+
return html`<span class="campaign-name" title=${item.name || ''}
|
|
113
|
+
>${item.name || ''}</span
|
|
114
|
+
>`;
|
|
115
|
+
case 'group':
|
|
116
|
+
return item.group
|
|
117
|
+
? html`<temba-label
|
|
118
|
+
type="group"
|
|
119
|
+
icon=${Icon.group}
|
|
120
|
+
clickable
|
|
121
|
+
@click=${(e: MouseEvent) => this.handleGroupClick(item.group, e)}
|
|
122
|
+
>${item.group.name}</temba-label
|
|
123
|
+
>`
|
|
124
|
+
: '';
|
|
125
|
+
case 'events':
|
|
126
|
+
return html`<span class="num"
|
|
127
|
+
>${(item.events ?? 0).toLocaleString()}</span
|
|
128
|
+
>`;
|
|
129
|
+
case 'contacts':
|
|
130
|
+
return html`<span class="num"
|
|
131
|
+
>${(item.contacts ?? 0).toLocaleString()}</span
|
|
132
|
+
>`;
|
|
133
|
+
case 'modified_on':
|
|
134
|
+
return item.modified_on
|
|
135
|
+
? html`<temba-date
|
|
136
|
+
value=${item.modified_on}
|
|
137
|
+
display="timedate"
|
|
138
|
+
></temba-date>`
|
|
139
|
+
: '';
|
|
140
|
+
default:
|
|
141
|
+
return super.renderCell(item, column);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
package/src/list/ContactList.ts
CHANGED
|
@@ -284,7 +284,9 @@ export class ContactList extends ContentList<Contact> {
|
|
|
284
284
|
if (i.urn) return i.urn;
|
|
285
285
|
if (Array.isArray(i.urns) && i.urns.length > 0) {
|
|
286
286
|
const u = i.urns[0];
|
|
287
|
-
return typeof u === 'string'
|
|
287
|
+
return typeof u === 'string'
|
|
288
|
+
? u.split(':')[1] || u
|
|
289
|
+
: u?.display || u?.path || '';
|
|
288
290
|
}
|
|
289
291
|
return '';
|
|
290
292
|
}
|
package/src/list/FlowList.ts
CHANGED
|
@@ -14,23 +14,38 @@ export class FlowList extends ContentList<Flow> {
|
|
|
14
14
|
static get styles() {
|
|
15
15
|
return css`
|
|
16
16
|
${ContentList.styles}
|
|
17
|
+
/* The name cell is a flex row (name text, issue icon, label
|
|
18
|
+
chips) that fills the grow column. When space runs out the
|
|
19
|
+
text spans ellipsize — the flex items shrink instead of the
|
|
20
|
+
cell hard-clipping them mid-glyph, which is what happens to
|
|
21
|
+
an inline-flex atomic inline under .cell-inner's ellipsis. */
|
|
17
22
|
.flow-name {
|
|
18
23
|
color: inherit;
|
|
19
24
|
font-weight: var(--w-medium);
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
gap: 6px;
|
|
28
|
+
min-width: 0;
|
|
29
|
+
}
|
|
30
|
+
.flow-name .name {
|
|
20
31
|
overflow: hidden;
|
|
21
32
|
text-overflow: ellipsis;
|
|
22
33
|
white-space: nowrap;
|
|
23
|
-
display: inline-flex;
|
|
24
|
-
align-items: center;
|
|
25
|
-
gap: 6px;
|
|
26
34
|
}
|
|
27
35
|
.flow-labels {
|
|
28
|
-
display:
|
|
36
|
+
display: flex;
|
|
29
37
|
align-items: center;
|
|
30
38
|
gap: 4px;
|
|
31
|
-
|
|
39
|
+
min-width: 0;
|
|
40
|
+
}
|
|
41
|
+
/* Chips shrink with the row and ellipsize internally (the
|
|
42
|
+
label's own slot handles that) rather than being clipped
|
|
43
|
+
mid-chip at the column edge. */
|
|
44
|
+
.flow-labels temba-label {
|
|
45
|
+
min-width: 0;
|
|
32
46
|
}
|
|
33
47
|
.issue-icon {
|
|
48
|
+
flex: none;
|
|
34
49
|
--icon-color: var(--warning);
|
|
35
50
|
}
|
|
36
51
|
.num {
|
|
@@ -84,13 +99,20 @@ export class FlowList extends ContentList<Flow> {
|
|
|
84
99
|
this.valueKey = 'uuid';
|
|
85
100
|
this.emptyMessage = 'No flows';
|
|
86
101
|
this.searchPlaceholder = 'Search flows';
|
|
102
|
+
// The name column is a `grow` column — it pools the table's
|
|
103
|
+
// leftover width so long names (and their label chips) get all
|
|
104
|
+
// available space before truncating, instead of clipping against
|
|
105
|
+
// a fixed cap while slack sits in the spacer. minTableWidth arms
|
|
106
|
+
// a horizontal scroll once the container is too narrow to keep
|
|
107
|
+
// the columns usable, rather than clipping anything.
|
|
108
|
+
this.minTableWidth = '640px';
|
|
87
109
|
this.columns = [
|
|
88
110
|
{
|
|
89
111
|
key: 'name',
|
|
90
112
|
label: 'Name',
|
|
91
113
|
sortable: true,
|
|
92
114
|
minWidth: '160px',
|
|
93
|
-
|
|
115
|
+
grow: true,
|
|
94
116
|
pinned: true
|
|
95
117
|
},
|
|
96
118
|
{
|
|
@@ -174,7 +196,9 @@ export class FlowList extends ContentList<Flow> {
|
|
|
174
196
|
case 'name': {
|
|
175
197
|
const labels = item.labels || [];
|
|
176
198
|
return html`<span class="flow-name"
|
|
177
|
-
|
|
199
|
+
><span class="name" title="${item.name || ''}"
|
|
200
|
+
>${item.name || ''}</span
|
|
201
|
+
>
|
|
178
202
|
${item.has_issues
|
|
179
203
|
? html`<temba-icon
|
|
180
204
|
class="issue-icon"
|
package/src/list/MsgList.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { css, html, TemplateResult } from 'lit';
|
|
|
2
2
|
import { ContentList, ContentListColumn } from './ContentList';
|
|
3
3
|
import { Icon } from '../Icons';
|
|
4
4
|
import { Msg } from '../interfaces';
|
|
5
|
+
import { attachmentAsString } from '../utils';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Message CRUDL list — drop-in replacement for the rapidpro
|
|
@@ -14,9 +15,9 @@ export class MsgList extends ContentList<Msg> {
|
|
|
14
15
|
static get styles() {
|
|
15
16
|
return css`
|
|
16
17
|
${ContentList.styles}
|
|
17
|
-
/* The message cell holds the body text
|
|
18
|
-
|
|
19
|
-
to the trailing edge. The text sizes to content and
|
|
18
|
+
/* The message cell holds the body text on the leading edge, with
|
|
19
|
+
its attachment thumbnails and the flow / label pills grouped
|
|
20
|
+
and pushed to the trailing edge. The text sizes to content and
|
|
20
21
|
ellipsizes when squeezed — resolved per row, so a busy row
|
|
21
22
|
doesn't widen a column for all of them. */
|
|
22
23
|
.msg-cell {
|
|
@@ -34,7 +35,7 @@ export class MsgList extends ContentList<Msg> {
|
|
|
34
35
|
text-overflow: ellipsis;
|
|
35
36
|
white-space: nowrap;
|
|
36
37
|
}
|
|
37
|
-
/* Attachment thumbnails
|
|
38
|
+
/* Attachment thumbnails, immediately after the message text. */
|
|
38
39
|
.msg-attachments {
|
|
39
40
|
flex: 0 0 auto;
|
|
40
41
|
display: flex;
|
|
@@ -44,10 +45,10 @@ export class MsgList extends ContentList<Msg> {
|
|
|
44
45
|
/* Flow + label pills, pushed to the trailing edge of the cell. */
|
|
45
46
|
.cell-pills {
|
|
46
47
|
flex: 0 0 auto;
|
|
48
|
+
margin-left: auto;
|
|
47
49
|
display: flex;
|
|
48
50
|
align-items: center;
|
|
49
51
|
gap: 6px;
|
|
50
|
-
margin-left: auto;
|
|
51
52
|
}
|
|
52
53
|
/* Attachment thumbnails are sized well below the 44px row so
|
|
53
54
|
they never grow its height — a small square preview rather
|
|
@@ -160,13 +161,18 @@ export class MsgList extends ContentList<Msg> {
|
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
163
|
|
|
163
|
-
/** The message cell — body text
|
|
164
|
-
*
|
|
165
|
-
* the
|
|
164
|
+
/** The message cell — body text on the leading edge with its
|
|
165
|
+
* attachment thumbnails immediately after it, and the flow / label
|
|
166
|
+
* pills pushed to the trailing edge. Each piece sizes to content, so
|
|
167
|
+
* the split is resolved independently for every row. A message with
|
|
168
|
+
* no text renders no text span at all — otherwise its min-width
|
|
169
|
+
* would strand an attachment-only row's thumbnails away from the
|
|
170
|
+
* leading edge. */
|
|
166
171
|
private renderMessageCell(item: Msg): TemplateResult {
|
|
172
|
+
const text = this.renderMessageText(item);
|
|
167
173
|
return html`
|
|
168
174
|
<div class="msg-cell">
|
|
169
|
-
|
|
175
|
+
${text ? html`<span class="msg-text">${text}</span>` : ''}
|
|
170
176
|
${this.renderAttachments(item)}${this.renderPills(item)}
|
|
171
177
|
</div>
|
|
172
178
|
`;
|
|
@@ -208,8 +214,8 @@ export class MsgList extends ContentList<Msg> {
|
|
|
208
214
|
return item.text || '';
|
|
209
215
|
}
|
|
210
216
|
|
|
211
|
-
/** Attachment thumbnails for a row,
|
|
212
|
-
*
|
|
217
|
+
/** Attachment thumbnails for a row, immediately after the message
|
|
218
|
+
* text, or '' when the row carries none. */
|
|
213
219
|
private renderAttachments(item: Msg): TemplateResult | string {
|
|
214
220
|
const attachments = item.attachments || [];
|
|
215
221
|
if (!attachments.length) return '';
|
|
@@ -219,7 +225,7 @@ export class MsgList extends ContentList<Msg> {
|
|
|
219
225
|
(a) => html`
|
|
220
226
|
<temba-thumbnail
|
|
221
227
|
class="msg-thumb"
|
|
222
|
-
attachment=${a}
|
|
228
|
+
attachment=${attachmentAsString(a)}
|
|
223
229
|
></temba-thumbnail>
|
|
224
230
|
`
|
|
225
231
|
)}
|
package/src/list/SortableList.ts
CHANGED
|
@@ -111,9 +111,11 @@ export class SortableList extends RapidElement {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
private getSortableElements(): Element[] {
|
|
114
|
+
// flatten so wrapper components can forward their host's children
|
|
115
|
+
// through a nested <slot>
|
|
114
116
|
const eles = this.shadowRoot
|
|
115
117
|
.querySelector('slot')
|
|
116
|
-
.assignedElements()
|
|
118
|
+
.assignedElements({ flatten: true })
|
|
117
119
|
.filter(
|
|
118
120
|
(ele) =>
|
|
119
121
|
ele.classList.contains('sortable') &&
|
|
@@ -539,11 +541,38 @@ export class SortableList extends RapidElement {
|
|
|
539
541
|
private beginDrag(
|
|
540
542
|
target: HTMLElement,
|
|
541
543
|
clientX: number,
|
|
542
|
-
clientY: number
|
|
544
|
+
clientY: number,
|
|
545
|
+
path: EventTarget[] = []
|
|
543
546
|
): boolean {
|
|
544
|
-
// if we have a drag handle, only allow dragging from that element
|
|
547
|
+
// if we have a drag handle, only allow dragging from that element. The
|
|
548
|
+
// handle may live inside a sortable child's shadow DOM (where event
|
|
549
|
+
// retargeting hides it from event.target), so walk the composed path up
|
|
550
|
+
// to the first .sortable host looking for it — and require that host to
|
|
551
|
+
// actually be one of ours, so a same-named class elsewhere in the path
|
|
552
|
+
// can't start a drag.
|
|
545
553
|
if (this.dragHandle) {
|
|
546
|
-
|
|
554
|
+
const searchPath = path.length > 0 ? path : [target];
|
|
555
|
+
let onHandle = false;
|
|
556
|
+
let handleHost: Element = null;
|
|
557
|
+
for (const hop of searchPath) {
|
|
558
|
+
// shadow roots appear as non-Element hops mid-path (e.g. an icon
|
|
559
|
+
// inside the handle) — skip them rather than giving up
|
|
560
|
+
if (!(hop instanceof Element)) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
if (!onHandle && hop.classList.contains(this.dragHandle)) {
|
|
564
|
+
onHandle = true;
|
|
565
|
+
}
|
|
566
|
+
if (hop.classList.contains('sortable')) {
|
|
567
|
+
handleHost = onHandle ? hop : null;
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (
|
|
572
|
+
!onHandle ||
|
|
573
|
+
!handleHost ||
|
|
574
|
+
!this.getSortableElements().includes(handleHost)
|
|
575
|
+
) {
|
|
547
576
|
return false;
|
|
548
577
|
}
|
|
549
578
|
}
|
|
@@ -572,7 +601,12 @@ export class SortableList extends RapidElement {
|
|
|
572
601
|
|
|
573
602
|
private handleMouseDown(event: MouseEvent) {
|
|
574
603
|
if (
|
|
575
|
-
this.beginDrag(
|
|
604
|
+
this.beginDrag(
|
|
605
|
+
event.target as HTMLElement,
|
|
606
|
+
event.clientX,
|
|
607
|
+
event.clientY,
|
|
608
|
+
event.composedPath()
|
|
609
|
+
)
|
|
576
610
|
) {
|
|
577
611
|
event.preventDefault();
|
|
578
612
|
event.stopPropagation();
|
|
@@ -586,7 +620,12 @@ export class SortableList extends RapidElement {
|
|
|
586
620
|
const touch = event.touches[0];
|
|
587
621
|
if (!touch) return;
|
|
588
622
|
if (
|
|
589
|
-
this.beginDrag(
|
|
623
|
+
this.beginDrag(
|
|
624
|
+
event.target as HTMLElement,
|
|
625
|
+
touch.clientX,
|
|
626
|
+
touch.clientY,
|
|
627
|
+
event.composedPath()
|
|
628
|
+
)
|
|
590
629
|
) {
|
|
591
630
|
event.stopPropagation();
|
|
592
631
|
document.addEventListener('touchmove', this.handleTouchMove, {
|