x4js 2.3.17 → 2.3.19
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/aicli.md +3 -3
- package/aicontext.md +211 -47
- package/cli/config.mjs +14 -7
- package/cli/copy-plugin.mjs +8 -0
- package/cli/create.mjs +8 -2
- package/cli/dev.mjs +4 -1
- package/cli/less-plugin.mjs +5 -1
- package/cli/main.mjs +15 -5
- package/cli/rawfile-plugin.mjs +23 -3
- package/cli/templates.mjs +5 -1
- package/package.json +1 -1
- package/src/components/calendar/calendar.module.scss +16 -1
- package/src/components/calendar/calendar.ts +52 -9
- package/src/components/checkbox/checkbox.ts +1 -0
- package/src/components/combobox/combobox.ts +51 -23
- package/src/components/dialog/dialog.ts +4 -1
- package/src/components/form/form.ts +9 -5
- package/src/components/gridview/gridview.ts +352 -176
- package/src/components/input/input.ts +63 -38
- package/src/components/keyboard/keyboard.module.scss +35 -4
- package/src/components/keyboard/keyboard.ts +295 -80
- package/src/components/listbox/listbox.ts +6 -1
- package/src/components/messages/messages.ts +12 -3
- package/src/components/popup/popup.ts +19 -20
- package/src/components/radio/radio.ts +8 -4
- package/src/components/rating/rating.ts +38 -48
- package/src/components/slider/slider.ts +38 -8
- package/src/components/spreadsheet/spreadsheet.ts +167 -132
- package/src/components/switch/switch.ts +2 -1
- package/src/components/tag/tag.ts +1 -2
- package/src/components/textarea/textarea.module.scss +6 -2
- package/src/components/textarea/textarea.ts +50 -11
- package/src/components/textedit/textedit.ts +12 -2
- package/src/core/component.ts +16 -9
- package/src/core/core_application.ts +8 -4
- package/src/core/core_clipboard.ts +239 -0
- package/src/core/core_data.ts +118 -49
- package/src/core/core_dom.ts +36 -6
- package/src/core/core_dragdrop.ts +11 -5
- package/src/core/core_element.ts +1 -1
- package/src/core/core_pdf.ts +696 -46
- package/src/core/core_router.ts +42 -44
- package/src/core/core_shortcuts.ts +1 -1
- package/src/core/core_state.ts +22 -5
- package/src/x4.d.ts +1 -0
- package/src/x4.ts +1 -0
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
**/
|
|
18
18
|
|
|
19
19
|
import { BaseProps } from '../input/input';
|
|
20
|
-
import { Component, ComponentProps } from '../../core/component';
|
|
20
|
+
import { Component, ComponentEvents, ComponentProps, EvChange, EvFocus } from '../../core/component';
|
|
21
21
|
|
|
22
22
|
import { Label } from '../label/label';
|
|
23
23
|
import { VBox } from '../boxes/boxes';
|
|
@@ -29,37 +29,64 @@ import { class_ns, IFormElement } from '../../core/core_tools';
|
|
|
29
29
|
*
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
interface
|
|
33
|
-
label?: string;
|
|
32
|
+
export interface SimpleTextAreaProps extends BaseProps {
|
|
34
33
|
value?: string;
|
|
35
34
|
resize?: boolean | "none" | "vertical" | "horizontal" | "both";
|
|
36
35
|
readonly?: boolean;
|
|
37
36
|
trim?: boolean;
|
|
38
37
|
}
|
|
39
38
|
|
|
39
|
+
export interface TextAreaProps extends SimpleTextAreaProps {
|
|
40
|
+
label?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TextAreaEvents extends ComponentEvents {
|
|
44
|
+
focus: EvFocus;
|
|
45
|
+
change: EvChange;
|
|
46
|
+
}
|
|
47
|
+
|
|
40
48
|
|
|
41
49
|
/**
|
|
42
|
-
*
|
|
50
|
+
*
|
|
43
51
|
*/
|
|
44
52
|
|
|
45
53
|
@class_ns( "x4" )
|
|
46
|
-
export class SimpleTextArea extends Component<
|
|
54
|
+
export class SimpleTextArea extends Component<SimpleTextAreaProps,TextAreaEvents> {
|
|
47
55
|
|
|
48
|
-
constructor( props:
|
|
56
|
+
constructor( props: SimpleTextAreaProps ) {
|
|
49
57
|
super( { ...props, tag: "textarea" } );
|
|
50
58
|
|
|
59
|
+
this.mapPropEvents( props, "focus", "change" );
|
|
60
|
+
|
|
51
61
|
this.setAttribute( "name", props.name );
|
|
52
62
|
if( props.value ) {
|
|
53
63
|
this.setText( props.value+'' );
|
|
54
64
|
}
|
|
55
65
|
|
|
66
|
+
// none by default (the stylesheet); true: "both", false: "none"
|
|
56
67
|
if( props.resize!==undefined ) {
|
|
57
|
-
this.setStyleValue( "resize", props.resize===false ? "none" : props.resize
|
|
68
|
+
this.setStyleValue( "resize", props.resize===true ? "both" : props.resize===false ? "none" : props.resize );
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
if( props.readonly ) {
|
|
61
72
|
this.setAttribute( "readonly", true );
|
|
62
73
|
}
|
|
74
|
+
|
|
75
|
+
if( props.placeholder ) {
|
|
76
|
+
this.setAttribute( "placeholder", props.placeholder );
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if( props.required ) {
|
|
80
|
+
this.setAttribute( "required", true );
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if( props.autofocus ) {
|
|
84
|
+
this.setAttribute( "autofocus", true );
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.addDOMEvent( "blur", ( ) => this.fire( "focus", { focus_out: true } ) );
|
|
88
|
+
this.addDOMEvent( "focus", ( ) => this.fire( "focus", { focus_out: false } ) );
|
|
89
|
+
this.addDOMEvent( "input", ( ) => this.fire( "change", { value: this.getText( ) } ) );
|
|
63
90
|
}
|
|
64
91
|
|
|
65
92
|
setText( text: string ) {
|
|
@@ -79,12 +106,18 @@ export class SimpleTextArea extends Component<TextAreaProps> {
|
|
|
79
106
|
this.dom.scrollTop = this.dom.scrollHeight;
|
|
80
107
|
}
|
|
81
108
|
|
|
109
|
+
/** false when it is required and empty */
|
|
110
|
+
|
|
111
|
+
isValid( ) {
|
|
112
|
+
return !this.props.required || this.getText( )!=="";
|
|
113
|
+
}
|
|
114
|
+
|
|
82
115
|
queryInterface<T>(name: string): T {
|
|
83
116
|
if( name=="form-element" ) {
|
|
84
117
|
const i: IFormElement = {
|
|
85
118
|
getRawValue: ( ): any => { return this.getText(); },
|
|
86
119
|
setRawValue: ( v: any ) => { this.setText( v ); },
|
|
87
|
-
isValid: ( ) => { return
|
|
120
|
+
isValid: ( ) => { return this.isValid( ); }
|
|
88
121
|
};
|
|
89
122
|
|
|
90
123
|
return i as unknown as T;
|
|
@@ -115,11 +148,11 @@ export class TextArea extends VBox {
|
|
|
115
148
|
super( props );
|
|
116
149
|
|
|
117
150
|
|
|
118
|
-
const {
|
|
151
|
+
const { value, resize, readonly, trim, name, placeholder, required, autofocus, focus, change } = props;
|
|
119
152
|
|
|
120
153
|
this.setContent( [
|
|
121
154
|
new Label( { text: props.label }),
|
|
122
|
-
this._input = new SimpleTextArea( {
|
|
155
|
+
this._input = new SimpleTextArea( { value,resize,readonly,trim,name,placeholder,required,autofocus,focus,change } )
|
|
123
156
|
])
|
|
124
157
|
}
|
|
125
158
|
|
|
@@ -135,12 +168,18 @@ export class TextArea extends VBox {
|
|
|
135
168
|
this._input.scrollToBottom( );
|
|
136
169
|
}
|
|
137
170
|
|
|
171
|
+
/** false when it is required and empty */
|
|
172
|
+
|
|
173
|
+
isValid( ) {
|
|
174
|
+
return this._input.isValid( );
|
|
175
|
+
}
|
|
176
|
+
|
|
138
177
|
queryInterface<T>(name: string): T {
|
|
139
178
|
if( name=="form-element" ) {
|
|
140
179
|
const i: IFormElement = {
|
|
141
180
|
getRawValue: ( ): any => { return this.getText(); },
|
|
142
181
|
setRawValue: ( v: any ) => { this.setText( v ); },
|
|
143
|
-
isValid: ( ) => { return
|
|
182
|
+
isValid: ( ) => { return this.isValid( ); }
|
|
144
183
|
};
|
|
145
184
|
|
|
146
185
|
return i as unknown as T;
|
|
@@ -87,7 +87,9 @@ export class TextEdit<E extends ComponentEvents = ComponentEvents> extends HBox<
|
|
|
87
87
|
private input: Input;
|
|
88
88
|
|
|
89
89
|
constructor( props: TextEditProps ) {
|
|
90
|
-
|
|
90
|
+
// dom_events are for the input (focus, blur, keydown...): not on the box too,
|
|
91
|
+
// where keydown would run a second time, and focus never (it does not bubble)
|
|
92
|
+
super( { ...props, dom_events: undefined } );
|
|
91
93
|
|
|
92
94
|
if( !props.inputId ) {
|
|
93
95
|
props.inputId = makeUniqueComponentId()
|
|
@@ -104,8 +106,16 @@ export class TextEdit<E extends ComponentEvents = ComponentEvents> extends HBox<
|
|
|
104
106
|
|
|
105
107
|
|
|
106
108
|
const iprops: InputProps = { ...props, id: props.inputId, attrs: props.inputAttrs, width: props.inputWidth };
|
|
107
|
-
|
|
109
|
+
|
|
110
|
+
// the props of the box stay on it: given to the input too, a style would be put
|
|
111
|
+
// twice, a tooltip twice, the height of the box on the field...
|
|
108
112
|
delete iprops.cls;
|
|
113
|
+
delete iprops.style;
|
|
114
|
+
delete iprops.tooltip;
|
|
115
|
+
delete iprops.hidden;
|
|
116
|
+
delete iprops.flex;
|
|
117
|
+
delete iprops.stretch;
|
|
118
|
+
delete iprops.height;
|
|
109
119
|
|
|
110
120
|
this.setContent( [
|
|
111
121
|
props.label ? new HBox( { id: "label", width: props.labelWidth, content: [
|
package/src/core/component.ts
CHANGED
|
@@ -234,7 +234,7 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
|
|
|
234
234
|
this.setStyle( props.style );
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
if( props.content ) {
|
|
237
|
+
if( props.content || props.content===0 ) {
|
|
238
238
|
this.setContent( props.content );
|
|
239
239
|
}
|
|
240
240
|
|
|
@@ -321,7 +321,7 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
|
|
|
321
321
|
}
|
|
322
322
|
|
|
323
323
|
if( cls.indexOf(' ')>=0 ) {
|
|
324
|
-
const ccs = cls.split( " " );
|
|
324
|
+
const ccs = cls.split( " " ).filter( x => x );
|
|
325
325
|
this.dom.classList.remove(...ccs);
|
|
326
326
|
}
|
|
327
327
|
else {
|
|
@@ -358,7 +358,7 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
|
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
if( cls.indexOf(' ')>=0 ) {
|
|
361
|
-
const ccs = cls.split( " " );
|
|
361
|
+
const ccs = cls.split( " " ).filter( x => x );
|
|
362
362
|
ccs.forEach( toggle );
|
|
363
363
|
}
|
|
364
364
|
else {
|
|
@@ -897,7 +897,8 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
|
|
|
897
897
|
|
|
898
898
|
/**
|
|
899
899
|
* Enables or disables the component.
|
|
900
|
-
* This sets the `disabled`
|
|
900
|
+
* This sets or removes the `disabled` and `inert` attributes of the component itself. Its children
|
|
901
|
+
* are not changed: the browser makes all that is inside an `inert` element inactive.
|
|
901
902
|
* @param ena - If `true`, the component is enabled; if `false`, it's disabled. Defaults to `true`.
|
|
902
903
|
* @returns The component instance for chaining.
|
|
903
904
|
*/
|
|
@@ -1195,8 +1196,8 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
|
|
|
1195
1196
|
if( !this.#shortcuts ) {
|
|
1196
1197
|
const shortcuts = this.#shortcuts = new Shortcuts( );
|
|
1197
1198
|
|
|
1198
|
-
// straight on the element, not with addDOMEvent:
|
|
1199
|
-
// children are called
|
|
1199
|
+
// straight on the element, not with addDOMEvent: it does not change
|
|
1200
|
+
// how the handlers of the children are called
|
|
1200
1201
|
this.dom.addEventListener( "keydown", ( ev ) => shortcuts.handle( ev as KeyboardEvent ) );
|
|
1201
1202
|
this.dom.addEventListener( "focusin", ( ) => Application.instance( ).setActiveShortcuts( this, shortcuts ) );
|
|
1202
1203
|
}
|
|
@@ -1230,11 +1231,17 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
|
|
|
1230
1231
|
|
|
1231
1232
|
const state = makeState( raw );
|
|
1232
1233
|
|
|
1233
|
-
// the state is what is saved, not raw: makeState works on a copy of it
|
|
1234
|
+
// the state is what is saved, not raw: makeState works on a copy of it.
|
|
1235
|
+
// The write waits for the changes to stop. Its timer is the one of the
|
|
1236
|
+
// browser, not this.setTimeout: the timers of a component are cleared
|
|
1237
|
+
// when it leaves the DOM, and what changed just before must still be written
|
|
1238
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
1239
|
+
|
|
1234
1240
|
state.on( "change", ( ) => {
|
|
1235
|
-
|
|
1241
|
+
clearTimeout( timer );
|
|
1242
|
+
timer = setTimeout( ( ) => {
|
|
1236
1243
|
Application.instance().setStorage( key, JSON.stringify( state ) );
|
|
1237
|
-
|
|
1244
|
+
}, 500 );
|
|
1238
1245
|
});
|
|
1239
1246
|
|
|
1240
1247
|
this.#pstate[name] = state;
|
|
@@ -104,8 +104,10 @@ export class Application<E extends ApplicationEvents = ApplicationEvents> extend
|
|
|
104
104
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
105
105
|
main_app = this;
|
|
106
106
|
|
|
107
|
+
this.props = props;
|
|
108
|
+
|
|
107
109
|
const loaded = ( ) => {
|
|
108
|
-
this.mount( props.mountPoint
|
|
110
|
+
this.mount( this.props.mountPoint )
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
if( document.readyState=='complete' ) {
|
|
@@ -141,13 +143,14 @@ export class Application<E extends ApplicationEvents = ApplicationEvents> extend
|
|
|
141
143
|
|
|
142
144
|
/**
|
|
143
145
|
* Sets the main view component for the application.
|
|
144
|
-
* This component will be mounted to the DOM
|
|
146
|
+
* This component will be mounted to the DOM, in `mountPoint`.
|
|
147
|
+
* Called once: each call sets up the keyboard (Tab, shortcuts) again.
|
|
145
148
|
* @param view - The component to set as the main view.
|
|
146
149
|
*/
|
|
147
150
|
|
|
148
151
|
setMainView( view: Component ) {
|
|
149
152
|
this.mainview = view;
|
|
150
|
-
this.mount( );
|
|
153
|
+
this.mount( this.props.mountPoint );
|
|
151
154
|
this._setupKeyboard( );
|
|
152
155
|
}
|
|
153
156
|
|
|
@@ -321,7 +324,8 @@ export class Application<E extends ApplicationEvents = ApplicationEvents> extend
|
|
|
321
324
|
return;
|
|
322
325
|
}
|
|
323
326
|
|
|
324
|
-
|
|
327
|
+
// send throws while the socket is connecting
|
|
328
|
+
if( msg_socket && opened ) {
|
|
325
329
|
msg_socket.send( JSON.stringify( {
|
|
326
330
|
msg: e.msg,
|
|
327
331
|
params: e.params,
|
|
@@ -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
|
+
}
|