x4js 2.3.17 → 2.3.18
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/aicontext.md +163 -25
- package/package.json +1 -1
- package/src/components/gridview/gridview.ts +350 -175
- package/src/components/keyboard/keyboard.ts +181 -65
- package/src/components/spreadsheet/spreadsheet.ts +27 -1
- package/src/components/tag/tag.ts +1 -2
- package/src/components/textarea/textarea.module.scss +6 -0
- package/src/components/textarea/textarea.ts +48 -10
- package/src/core/component.ts +16 -9
- package/src/core/core_clipboard.ts +239 -0
- package/src/core/core_data.ts +54 -23
- package/src/core/core_dom.ts +27 -5
- package/src/core/core_dragdrop.ts +11 -5
- package/src/core/core_element.ts +1 -1
- package/src/core/core_shortcuts.ts +1 -1
- package/src/x4.d.ts +1 -0
- package/src/x4.ts +1 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ___ ___ __
|
|
3
|
+
* \ \/ / / _
|
|
4
|
+
* \ / /_| |_
|
|
5
|
+
* / \____ _|
|
|
6
|
+
* /__/\__\ |_|
|
|
7
|
+
*
|
|
8
|
+
* @file core_clipboard.ts
|
|
9
|
+
* @author Etienne Cochard
|
|
10
|
+
*
|
|
11
|
+
* @copyright (c) 2026 R-libre ingenierie
|
|
12
|
+
*
|
|
13
|
+
* Use of this source code is governed by an MIT-style license
|
|
14
|
+
* that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
|
|
15
|
+
**/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The clipboard of the system: a text, an image, files.
|
|
19
|
+
*
|
|
20
|
+
* There are two ways to it, and they do not give the same things.
|
|
21
|
+
*
|
|
22
|
+
* In a "copy", "cut" or "paste" event (the keys of the user, the menu of
|
|
23
|
+
* the browser), the browser gives its clipboard at once and asks nothing:
|
|
24
|
+
* setEventClipboard, getEventClipboard. It is the only way to read files.
|
|
25
|
+
*
|
|
26
|
+
* Out of an event (a menu, a button), the system is asked: setClipboard,
|
|
27
|
+
* getClipboard. It is asynchronous, the browser may ask the user for a
|
|
28
|
+
* permission or refuse, and there is nothing to ask on a page that is not
|
|
29
|
+
* served in https. So what the application set last is kept: it is what is
|
|
30
|
+
* read when the system does not answer, and copy and paste still work
|
|
31
|
+
* inside the application.
|
|
32
|
+
*
|
|
33
|
+
* Files cannot be written, in neither way.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export interface ClipboardContent {
|
|
37
|
+
text?: string;
|
|
38
|
+
/** written as a PNG, the format every browser takes: another one is converted */
|
|
39
|
+
image?: Blob;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ClipboardRead extends ClipboardContent {
|
|
43
|
+
/** all the files, the images too; none out of an event */
|
|
44
|
+
files: File[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** what the application set last */
|
|
48
|
+
let kept: ClipboardContent = null;
|
|
49
|
+
|
|
50
|
+
function toPng( image: Blob ): Promise<Blob> {
|
|
51
|
+
return new Promise( ( resolve, reject ) => {
|
|
52
|
+
const url = URL.createObjectURL( image );
|
|
53
|
+
const img = document.createElement( "img" );
|
|
54
|
+
|
|
55
|
+
img.onload = ( ) => {
|
|
56
|
+
URL.revokeObjectURL( url );
|
|
57
|
+
|
|
58
|
+
const canvas = document.createElement( "canvas" );
|
|
59
|
+
canvas.width = img.naturalWidth;
|
|
60
|
+
canvas.height = img.naturalHeight;
|
|
61
|
+
canvas.getContext( "2d" ).drawImage( img, 0, 0 );
|
|
62
|
+
|
|
63
|
+
canvas.toBlob( png => png ? resolve( png ) : reject( new Error( "not an image" ) ), "image/png" );
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
img.onerror = ( ) => {
|
|
67
|
+
URL.revokeObjectURL( url );
|
|
68
|
+
reject( new Error( "not an image" ) );
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
img.src = url;
|
|
72
|
+
} );
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Set the clipboard, from a menu or a button.
|
|
77
|
+
*
|
|
78
|
+
* @returns true when the system took it. When it did not, getClipboard
|
|
79
|
+
* still gives it back: it can be pasted in the application.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* setClipboard( { text: "hello" } );
|
|
83
|
+
* setClipboard( { image: blob, text: "the same, for what cannot paste an image" } );
|
|
84
|
+
*/
|
|
85
|
+
|
|
86
|
+
export async function setClipboard( data: ClipboardContent ): Promise<boolean> {
|
|
87
|
+
kept = { text: data.text, image: data.image };
|
|
88
|
+
|
|
89
|
+
// not there on a page that is not served in https
|
|
90
|
+
const system = navigator.clipboard;
|
|
91
|
+
if( !system ) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
if( !data.image ) {
|
|
97
|
+
await system.writeText( data.text ?? "" );
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// the conversion is given as a promise: the system has to be asked
|
|
102
|
+
// now, while the click of the user still allows it
|
|
103
|
+
const parts: Record<string,Blob | Promise<Blob>> = {
|
|
104
|
+
"image/png": data.image.type==="image/png" ? data.image : toPng( data.image ),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if( data.text ) {
|
|
108
|
+
parts["text/plain"] = new Blob( [data.text], { type: "text/plain" } );
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await system.write( [new ClipboardItem( parts )] );
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
catch( e ) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Read the clipboard, from a menu or a button.
|
|
121
|
+
*
|
|
122
|
+
* @returns what the system holds. When it refuses or cannot be asked, what
|
|
123
|
+
* the application set last. Never files: see getEventClipboard.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* const data = await getClipboard( );
|
|
127
|
+
* if( data.text ) {
|
|
128
|
+
* input.setValue( data.text );
|
|
129
|
+
* }
|
|
130
|
+
*/
|
|
131
|
+
|
|
132
|
+
export async function getClipboard( ): Promise<ClipboardRead> {
|
|
133
|
+
const system = navigator.clipboard;
|
|
134
|
+
|
|
135
|
+
if( system ) {
|
|
136
|
+
try {
|
|
137
|
+
// read gives the images too, but some browsers only have readText
|
|
138
|
+
if( !system.read ) {
|
|
139
|
+
const text = await system.readText( );
|
|
140
|
+
return { text: text || undefined, files: [] };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const data: ClipboardRead = { files: [] };
|
|
144
|
+
|
|
145
|
+
for( const item of await system.read( ) ) {
|
|
146
|
+
for( const type of item.types ) {
|
|
147
|
+
if( type==="text/plain" && !data.text ) {
|
|
148
|
+
const blob = await item.getType( type );
|
|
149
|
+
data.text = ( await blob.text( ) ) || undefined;
|
|
150
|
+
}
|
|
151
|
+
else if( type.startsWith( "image/" ) && !data.image ) {
|
|
152
|
+
data.image = await item.getType( type );
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return data;
|
|
158
|
+
}
|
|
159
|
+
catch( e ) {
|
|
160
|
+
// refused: what the application has
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if( kept ) {
|
|
165
|
+
return { text: kept.text, image: kept.image, files: [] };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { files: [] };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* false when getClipboard has nothing to give: the system cannot be asked,
|
|
173
|
+
* and the application did not set anything. What the system holds is not
|
|
174
|
+
* known before it is asked for.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* { text: _tr.global.paste, disabled: !canReadClipboard( ), click: ( ) => this.paste( ) }
|
|
178
|
+
*/
|
|
179
|
+
|
|
180
|
+
export function canReadClipboard( ): boolean {
|
|
181
|
+
const system = navigator.clipboard;
|
|
182
|
+
if( system && ( system.read || system.readText ) ) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return !!kept;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Set the clipboard in a "copy" or a "cut" event. The default of the event
|
|
191
|
+
* is prevented: the browser would copy its own selection instead.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* this.addDOMEvent( "copy", ev => {
|
|
195
|
+
* setEventClipboard( ev, { text: this.getSelectedText( ) } );
|
|
196
|
+
* } );
|
|
197
|
+
*/
|
|
198
|
+
|
|
199
|
+
export function setEventClipboard( ev: ClipboardEvent, data: ClipboardContent ): void {
|
|
200
|
+
kept = { text: data.text, image: data.image };
|
|
201
|
+
|
|
202
|
+
ev.preventDefault( );
|
|
203
|
+
|
|
204
|
+
if( data.text!==undefined && ev.clipboardData ) {
|
|
205
|
+
ev.clipboardData.setData( "text/plain", data.text );
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// the event only takes texts: the image is given to the system
|
|
209
|
+
if( data.image ) {
|
|
210
|
+
setClipboard( data );
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Read the clipboard in a "paste" event. The default of the event is left
|
|
216
|
+
* as it is: prevent it when what was read is used.
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* this.addDOMEvent( "paste", ev => {
|
|
220
|
+
* const data = getEventClipboard( ev );
|
|
221
|
+
* if( data.image ) {
|
|
222
|
+
* ev.preventDefault( );
|
|
223
|
+
* this.setPicture( data.image );
|
|
224
|
+
* }
|
|
225
|
+
* } );
|
|
226
|
+
*/
|
|
227
|
+
|
|
228
|
+
export function getEventClipboard( ev: ClipboardEvent ): ClipboardRead {
|
|
229
|
+
const data: ClipboardRead = { files: [] };
|
|
230
|
+
|
|
231
|
+
const transfer = ev.clipboardData;
|
|
232
|
+
if( transfer ) {
|
|
233
|
+
data.text = transfer.getData( "text/plain" ) || undefined;
|
|
234
|
+
data.files = Array.from( transfer.files );
|
|
235
|
+
data.image = data.files.find( file => file.type.startsWith( "image/" ) );
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return data;
|
|
239
|
+
}
|
package/src/core/core_data.ts
CHANGED
|
@@ -1152,41 +1152,72 @@ export class DataView extends CoreElement<DataViewEventMap>
|
|
|
1152
1152
|
|
|
1153
1153
|
protected m_props: DataViewProps;
|
|
1154
1154
|
|
|
1155
|
+
private m_unlink: ( ) => void; // stops listening to the store
|
|
1156
|
+
|
|
1155
1157
|
constructor( props: DataViewProps ) {
|
|
1156
1158
|
super( );
|
|
1157
1159
|
|
|
1158
1160
|
this.m_props = props;
|
|
1159
1161
|
this.m_store = props.store;
|
|
1160
1162
|
this.m_index = null;
|
|
1161
|
-
this.m_filter = null;
|
|
1162
|
-
this.m_sort = null;
|
|
1163
1163
|
this.m_model = this.m_store.getModel();
|
|
1164
|
+
this.m_filter = props.filter;
|
|
1164
1165
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
}
|
|
1174
|
-
else {
|
|
1175
|
-
this.sort( [props.order] );
|
|
1176
|
-
}
|
|
1166
|
+
if( !props.order ) {
|
|
1167
|
+
this.m_sort = null;
|
|
1168
|
+
}
|
|
1169
|
+
else if( isString(props.order) ) {
|
|
1170
|
+
this.m_sort = [ { field: props.order, ascending: true } ];
|
|
1171
|
+
}
|
|
1172
|
+
else if( isArray(props.order) ) {
|
|
1173
|
+
this.m_sort = props.order;
|
|
1177
1174
|
}
|
|
1178
1175
|
else {
|
|
1179
|
-
this.
|
|
1176
|
+
this.m_sort = [props.order];
|
|
1180
1177
|
}
|
|
1181
1178
|
|
|
1182
|
-
|
|
1179
|
+
// listens to the store, then filters and sorts
|
|
1180
|
+
this.open( );
|
|
1183
1181
|
}
|
|
1184
1182
|
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1183
|
+
/**
|
|
1184
|
+
* The view follows its store: it is filtered and sorted with what the
|
|
1185
|
+
* store holds now, and again at each of its changes.
|
|
1186
|
+
* Called when the view is made, and to use it again after a close( ).
|
|
1187
|
+
*/
|
|
1188
|
+
|
|
1189
|
+
open( ) {
|
|
1190
|
+
if( this.m_unlink ) {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
this.m_unlink = this.m_store.addListener( 'data_change', ( ) => this._storeChange( ) );
|
|
1195
|
+
this._storeChange( );
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* The view stops following its store, until open( ) is called.
|
|
1200
|
+
* A store keeps every view that listens to it: without this, a view
|
|
1201
|
+
* that is not used anymore would still be filtered, sorted and heard at
|
|
1202
|
+
* each change.
|
|
1203
|
+
*/
|
|
1204
|
+
|
|
1205
|
+
close( ) {
|
|
1206
|
+
if( this.m_unlink ) {
|
|
1207
|
+
this.m_unlink( );
|
|
1208
|
+
this.m_unlink = null;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
private _storeChange( ) {
|
|
1213
|
+
|
|
1214
|
+
// filtered and sorted again without telling: the event that follows says it all.
|
|
1215
|
+
// _filter sorts when the view is sorted; when it is not, it is by id
|
|
1216
|
+
this._filter( this.m_filter, false );
|
|
1217
|
+
if( !this.m_sort ) {
|
|
1218
|
+
this._sort( null, false );
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1190
1221
|
this.fire( 'view_change', { change_type: 'change' } );
|
|
1191
1222
|
}
|
|
1192
1223
|
|
|
@@ -1206,9 +1237,9 @@ export class DataView extends CoreElement<DataViewEventMap>
|
|
|
1206
1237
|
this.m_index = this.m_store.createIndex( filter );
|
|
1207
1238
|
this.m_filter = filter;
|
|
1208
1239
|
|
|
1209
|
-
// need to sort again:
|
|
1240
|
+
// need to sort again, without telling: the filter is what changed
|
|
1210
1241
|
if( this.m_sort ) {
|
|
1211
|
-
this.
|
|
1242
|
+
this._sort( this.m_sort, false );
|
|
1212
1243
|
}
|
|
1213
1244
|
|
|
1214
1245
|
if( notify ) {
|
package/src/core/core_dom.ts
CHANGED
|
@@ -19,7 +19,7 @@ export const COMPONENT = Symbol( "component" );
|
|
|
19
19
|
/** @ignore this events must be defined on domNode (do not bubble) */
|
|
20
20
|
export const unbubbleEvents = {
|
|
21
21
|
mouseleave: 1, mouseenter: 1, load: 1, unload: 1, scroll: 1, focus: 1, blur: 1, rowexit: 1, beforeunload: 1, stop: 1,
|
|
22
|
-
dragdrop: 1, dragenter: 1, dragexit: 1, draggesture: 1, dragover: 1, contextmenu: 1, created: 2, removed: 2,
|
|
22
|
+
dragdrop: 1, dragenter: 1, dragexit: 1, draggesture: 1, dragover: 1, contextmenu: 1, created: 2, removed: 2, resized: 2
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
export type DOMEventHandler = ( ev: Event ) => void;
|
|
@@ -118,8 +118,12 @@ function observeSize(entries: ResizeObserverEntry[]) {
|
|
|
118
118
|
|
|
119
119
|
export function dispatchEvent(ev: Event) {
|
|
120
120
|
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
// 2: an event of x4, for its node only.
|
|
122
|
+
// 1: stops at its node when the browser does not bubble it (focus, mouseenter...)
|
|
123
|
+
const flag = (unbubbleEvents as any)[ev.type];
|
|
124
|
+
const noup = flag===2 || ( flag===1 && !ev.bubbles );
|
|
125
|
+
|
|
126
|
+
let target = ev.target as Node;
|
|
123
127
|
|
|
124
128
|
while (target) {
|
|
125
129
|
const store = event_handlers.get( target );
|
|
@@ -127,8 +131,11 @@ export function dispatchEvent(ev: Event) {
|
|
|
127
131
|
const callback = store[ev.type];
|
|
128
132
|
if( callback ) {
|
|
129
133
|
if( Array.isArray(callback) ) {
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
// a handler that returns true stops those that follow it on this node.
|
|
135
|
+
// true only: an arrow function returns what its expression does
|
|
136
|
+
// (ev => this.show( ) returns this), and this must stop nothing
|
|
137
|
+
callback.some( c => (c( ev ) as any)===true );
|
|
138
|
+
}
|
|
132
139
|
else {
|
|
133
140
|
callback( ev );
|
|
134
141
|
}
|
|
@@ -255,7 +262,17 @@ export interface GlobalDOMEvents {
|
|
|
255
262
|
* @param ev The mouse event.
|
|
256
263
|
*/
|
|
257
264
|
contextmenu?: (ev: MouseEvent) => any;
|
|
265
|
+
/**
|
|
266
|
+
* Fires when the user copies (the keys, or the menu of the browser).
|
|
267
|
+
* @param ev The clipboard event: see setEventClipboard.
|
|
268
|
+
*/
|
|
269
|
+
copy?: (ev: ClipboardEvent) => any;
|
|
258
270
|
cuechange?: (ev: Event) => any;
|
|
271
|
+
/**
|
|
272
|
+
* Fires when the user cuts.
|
|
273
|
+
* @param ev The clipboard event: see setEventClipboard.
|
|
274
|
+
*/
|
|
275
|
+
cut?: (ev: ClipboardEvent) => any;
|
|
259
276
|
/**
|
|
260
277
|
* Fires when the user double-clicks the object.
|
|
261
278
|
* @param ev The mouse event.
|
|
@@ -391,6 +408,11 @@ export interface GlobalDOMEvents {
|
|
|
391
408
|
* @param ev The event.
|
|
392
409
|
*/
|
|
393
410
|
pause?: (ev: Event) => any;
|
|
411
|
+
/**
|
|
412
|
+
* Fires when the user pastes.
|
|
413
|
+
* @param ev The clipboard event: see getEventClipboard.
|
|
414
|
+
*/
|
|
415
|
+
paste?: (ev: ClipboardEvent) => any;
|
|
394
416
|
/**
|
|
395
417
|
* Occurs when the play method is requested.
|
|
396
418
|
* @param ev The event.
|
|
@@ -19,13 +19,13 @@ import { Point } from './core_tools';
|
|
|
19
19
|
|
|
20
20
|
const x_drag_cb = Symbol( 'x-drag-cb' );
|
|
21
21
|
|
|
22
|
-
interface DropInfo {
|
|
22
|
+
export interface DropInfo {
|
|
23
23
|
pt: Point;
|
|
24
24
|
data: DataTransfer;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
type DropCallback = ( command: 'enter' | 'leave' | 'drag' | 'drop', el: Component, infos: DropInfo ) => void;
|
|
28
|
-
type
|
|
27
|
+
export type DropCallback = ( command: 'enter' | 'leave' | 'drag' | 'drop', el: Component, infos: DropInfo ) => void;
|
|
28
|
+
export type DropFilterCallback = ( el: Component, data: DataTransfer ) => boolean;
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Manages drag and drop operations within the application.
|
|
@@ -78,6 +78,12 @@ class DragManager {
|
|
|
78
78
|
el.addDOMEvent('dragend', (ev: DragEvent) => {
|
|
79
79
|
el.removeClass( 'dragging' );
|
|
80
80
|
this.dragGhost.remove();
|
|
81
|
+
|
|
82
|
+
// the target that was left is told now, while the source is known.
|
|
83
|
+
// Then the source is forgotten: the next drag may be one of files
|
|
84
|
+
// that come from the desktop, which has none
|
|
85
|
+
this._check( );
|
|
86
|
+
this.dragSource = null;
|
|
81
87
|
});
|
|
82
88
|
|
|
83
89
|
el.setAttribute('draggable', "true");
|
|
@@ -86,7 +92,7 @@ class DragManager {
|
|
|
86
92
|
/**
|
|
87
93
|
* Registers a component as a drop target.
|
|
88
94
|
* This sets up DOM event listeners for `dragenter`, `dragover`, `dragleave`, and `drop`.
|
|
89
|
-
* It uses a `DropCallback` to notify the component about drag events and an optional `
|
|
95
|
+
* It uses a `DropCallback` to notify the component about drag events and an optional `DropFilterCallback`
|
|
90
96
|
* to determine if the target should accept the dragged item.
|
|
91
97
|
*
|
|
92
98
|
* @param el - The component to make a drop target.
|
|
@@ -94,7 +100,7 @@ class DragManager {
|
|
|
94
100
|
* @param filterCB - An optional callback to filter which draggable items can be dropped.
|
|
95
101
|
*/
|
|
96
102
|
|
|
97
|
-
registerDropTarget(el: Component, cb: DropCallback, filterCB?:
|
|
103
|
+
registerDropTarget(el: Component, cb: DropCallback, filterCB?: DropFilterCallback ) {
|
|
98
104
|
|
|
99
105
|
const dragEnter = (ev: DragEvent) => {
|
|
100
106
|
if( filterCB && !filterCB(this.dragSource,ev.dataTransfer) ) {
|
package/src/core/core_element.ts
CHANGED
|
@@ -232,7 +232,7 @@ export class CoreElement<E extends EventMap = EventMap> {
|
|
|
232
232
|
return;
|
|
233
233
|
}
|
|
234
234
|
|
|
235
|
-
//
|
|
235
|
+
// clear() of an entry does not touch the map, so plain iteration is safe.
|
|
236
236
|
for (const entry of this.#timers.values()) {
|
|
237
237
|
entry.clear();
|
|
238
238
|
}
|
|
@@ -51,7 +51,7 @@ interface Keys {
|
|
|
51
51
|
key: string;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
const isMac =
|
|
54
|
+
const isMac = /mac|iphone|ipad/i.test( navigator.platform ?? "" );
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* "Shift+Mod+K" -> shift, ctrl (cmd on a Mac), k
|
package/src/x4.d.ts
CHANGED