diff --git a/d.ts/antos.d.ts b/d.ts/antos.d.ts index 3ce3b5c..5ea55e3 100644 --- a/d.ts/antos.d.ts +++ b/d.ts/antos.d.ts @@ -3089,13 +3089,13 @@ declare namespace OS { */ private _onfileopen; /** - * Reference to the currently selected file meta-data + * Reference to the all selected files meta-datas * * @private - * @type {API.FileInfoType} + * @type {API.FileInfoType[]} * @memberof FileViewTag */ - private _selectedFile; + private _selectedFiles; /** * Data placeholder of the current working directory * @@ -3116,7 +3116,7 @@ declare namespace OS { * Header definition of the widget grid view * * @private - * @type {(GenericObject[])} + * @type {(GenericObject[])} * @memberof FileViewTag */ private _header; @@ -3227,6 +3227,19 @@ declare namespace OS { */ set showhidden(v: boolean); get showhidden(): boolean; + /** + * Setter: + * + * Allow multiple selection on file view + * + * Getter: + * + * Check whether the multiselection is actived + * + * @memberof FileViewTag + */ + set multiselect(v: boolean); + get multiselect(): boolean; /** * Get the current selected file * @@ -3235,6 +3248,14 @@ declare namespace OS { * @memberof FileViewTag */ get selectedFile(): API.FileInfoType; + /** + * Get all selected files + * + * @readonly + * @type {API.FileInfoType[]} + * @memberof FileViewTag + */ + get selectedFiles(): API.FileInfoType[]; /** * Setter: * @@ -3266,7 +3287,7 @@ declare namespace OS { * * @memberof FileViewTag */ - set ondragndrop(v: TagEventCallback>); + set ondragndrop(v: TagEventCallback>); /** * Sort file by its type * @@ -3593,7 +3614,7 @@ declare namespace OS { * @type {T} * @memberof DnDEventDataType */ - from: T; + from: T[]; /** * Reference to the target DOM element * @@ -4148,7 +4169,7 @@ declare namespace OS { * drag and drop on the list * * @private - * @type {{ from: ListViewItemTag; to: ListViewItemTag }} + * @type {{ from: ListViewItemTag[]; to: ListViewItemTag }} * @memberof ListViewTag */ private _dnd; @@ -5056,6 +5077,15 @@ declare namespace OS { */ set text(v: string | FormattedString); get text(): string | FormattedString; + /** + * Setter: Turn on/off text selection + * + * Getter: Check whether the label is selectable + * + * @memberof LabelTag + */ + set selectable(v: boolean); + get swon(): boolean; /** * Lqbel layout definition * @@ -5275,6 +5305,10 @@ interface Array { declare namespace OS { namespace GUI { namespace tag { + /** + * Row item event data type + */ + type GridRowEventData = TagEventDataType; /** * A grid Row is a simple element that * contains a group of grid cell @@ -5291,11 +5325,34 @@ declare namespace OS { * @memberof GridRowTag */ data: GenericObject[]; + /** + * placeholder for the row select event callback + * + * @private + * @type {TagEventCallback} + * @memberof ListViewItemTag + */ + private _onselect; /** *Creates an instance of GridRowTag. * @memberof GridRowTag */ constructor(); + /** + * Set item select event handle + * + * @memberof ListViewItemTag + */ + set onrowselect(v: TagEventCallback); + /** + * Setter: select/unselect the current item + * + * Getter: Check whether the current item is selected + * + * @memberof ListViewItemTag + */ + set selected(v: boolean); + get selected(): boolean; /** * Mount the tag, do nothing * @@ -5576,11 +5633,64 @@ declare namespace OS { * @memberof GridViewTag */ private _oncelldbclick; + /** + * Event data passing between mouse event when performing + * drag and drop on the list + * + * @private + * @type {{ from: GridRowTag[]; to: GridRowTag }} + * @memberof GridViewTag + */ + private _dnd; + /** + * placeholder of list drag and drop event handle + * + * @private + * @type {TagEventCallback>} + * @memberof GridViewTag + */ + private _ondragndrop; /** * Creates an instance of GridViewTag. * @memberof GridViewTag */ constructor(); + /** + * Set drag and drop event handle + * + * @memberof GridViewTag + */ + set ondragndrop(v: TagEventCallback>); + /** + * Setter: Enable/disable drag and drop event in the list + * + * Getter: Check whether the drag and drop event is enabled + * + * @memberof GridViewTag + */ + set dragndrop(v: boolean); + get dragndrop(): boolean; + /** + * placeholder of drag and drop mouse down event handle + * + * @private + * @memberof GridViewTag + */ + private _onmousedown; + /** + * placeholder of drag and drop mouse up event handle + * + * @private + * @memberof GridViewTag + */ + private _onmouseup; + /** + * placeholder of drag and drop mouse move event handle + * + * @private + * @memberof GridViewTag + */ + private _onmousemove; /** * Init the grid view before mounting. * Reset all the placeholders to default values @@ -5694,6 +5804,16 @@ declare namespace OS { */ set resizable(v: boolean); get resizable(): boolean; + /** + * Sort the grid using a sort function + * + * @param {context: any} context of the executed function + * @param {(a:GenericObject[], b:GenericObject[]) => boolean} a sort function that compares two rows data + * * @param {index: number} current header index + * @returns {void} + * @memberof GridViewTag + */ + sort(context: any, fn: (a: GenericObject[], b: GenericObject[], index?: number) => number): void; /** * Delete a grid rows * @@ -5732,11 +5852,18 @@ declare namespace OS { * This function triggers the row select event, a cell select * event will also trigger this event * - * @param {TagEventType} e + * @param {TagEventType} e * @returns {void} * @memberof GridViewTag */ private rowselect; + /** + * Unselect all the selected rows in the grid + * + * @returns {void} + * @memberof GridViewTag + */ + unselect(): void; /** * Check whether the grid has header * @@ -6584,7 +6711,7 @@ declare namespace OS { * Private data object passing between dragndrop mouse event * * @private - * @type {{ from: TreeViewTag; to: TreeViewTag }} + * @type {{ from: TreeViewTag[]; to: TreeViewTag }} * @memberof TreeViewTag */ private _dnd; @@ -6631,7 +6758,7 @@ declare namespace OS { * current tree. This function should return a promise on * a list of [[TreeViewDataType]] * - * @memberof TreeViewItemPrototype + * @memberof TreeViewTag */ fetch: (d: TreeViewItemPrototype) => Promise; /** diff --git a/src/core/BaseDialog.ts b/src/core/BaseDialog.ts index 55acb80..d529821 100644 --- a/src/core/BaseDialog.ts +++ b/src/core/BaseDialog.ts @@ -264,7 +264,6 @@ namespace OS { */ main(): void { const win = this.scheme as tag.WindowTag; - $(win).attr("tabindex", 0); $(win).on('keydown', (e) => { switch (e.which) { case 27: @@ -672,7 +671,7 @@ namespace OS { } for (let k in this.data) { const v = this.data[k]; - rows.push([{ text: k }, { text: v }]); + rows.push([{ text: k }, { text: v, selectable: true }]); } const grid = this.find("grid") as tag.GridViewTag; grid.header = [ @@ -1052,7 +1051,7 @@ namespace OS { __("Resource not found: {0}", path) ); } - return (fileview.path = path); + fileview.path = path; }; if (!this.data || !this.data.root) { @@ -1088,11 +1087,14 @@ namespace OS { } }; (this.find("btnOk") as tag.ButtonTag).onbtclick = (_e) => { - const f = fileview.selectedFile; + let f = fileview.selectedFile; if (!f) { - return this.notify( - __("Please select a file/fofler") - ); + const sel = location.selectedItem; + if(!sel) + return this.notify( + __("Please select a file/fofler") + ); + f = sel.data as API.FileInfoType; } if ( this.data && diff --git a/src/core/tags/FileViewTag.ts b/src/core/tags/FileViewTag.ts index e260676..c3c6ed1 100644 --- a/src/core/tags/FileViewTag.ts +++ b/src/core/tags/FileViewTag.ts @@ -28,13 +28,13 @@ namespace OS { private _onfileopen: TagEventCallback; /** - * Reference to the currently selected file meta-data + * Reference to the all selected files meta-datas * * @private - * @type {API.FileInfoType} + * @type {API.FileInfoType[]} * @memberof FileViewTag */ - private _selectedFile: API.FileInfoType; + private _selectedFiles: API.FileInfoType[]; /** * Data placeholder of the current working directory @@ -58,10 +58,10 @@ namespace OS { * Header definition of the widget grid view * * @private - * @type {(GenericObject[])} + * @type {(GenericObject[])} * @memberof FileViewTag */ - private _header: GenericObject[]; + private _header: GenericObject[]; /** * placeholder for the user-specified meta-data fetch function @@ -92,10 +92,37 @@ namespace OS { this.chdir = true; this.view = "list"; this._onfileopen = this._onfileselect = (e) => { }; + this._selectedFiles = []; + const fn = function(r1, r2, i) { + let t1 = r1[i].text; + let t2 = r2[i].text; + if(!t1 || !t2) return 0; + t1 = t1.toString().toLowerCase(); + t2 = t2.toString().toLowerCase(); + if(this.__f) + { + this.desc = ! this.desc; + if(t1 < t2) { return -1; } + if(t1 > t2) { return 1; } + } + else + { + this.desc = ! this.desc; + if(t1 > t2) { return -1; } + if(t1 < t2) { return 1; } + } + return 0; + }; this._header = [ - { text: "__(File name)" }, + { + text: "__(File name)", + sort: fn + }, { text: "__(Type)" }, - { text: "__(Size)" }, + { + text: "__(Size)", + sort: fn + }, ]; } @@ -227,6 +254,28 @@ namespace OS { return this.hasattr("showhidden"); } + /** + * Setter: + * + * Allow multiple selection on file view + * + * Getter: + * + * Check whether the multiselection is actived + * + * @memberof FileViewTag + */ + set multiselect(v: boolean) { + this.attsw(v, "multiselect"); + (this.refs.listview as ListViewTag).multiselect = v; + (this.refs.gridview as GridViewTag).multiselect = v; + } + get multiselect(): boolean { + return this.hasattr("multiselect"); + } + + + /** * Get the current selected file * @@ -235,7 +284,21 @@ namespace OS { * @memberof FileViewTag */ get selectedFile(): API.FileInfoType { - return this._selectedFile; + if(this._selectedFiles.length == 0) + return undefined; + return this._selectedFiles[this._selectedFiles.length - 1]; + } + + + /** + * Get all selected files + * + * @readonly + * @type {API.FileInfoType[]} + * @memberof FileViewTag + */ + get selectedFiles(): API.FileInfoType[] { + return this._selectedFiles; } /** @@ -286,7 +349,7 @@ namespace OS { * * @memberof FileViewTag */ - set data(v: API.FileInfoType[]) { + set data(v: API.FileInfoType[]) { if (!v) { return; } @@ -305,11 +368,21 @@ namespace OS { */ set ondragndrop( v: TagEventCallback< - DnDEventDataType + DnDEventDataType > ) { (this.refs.treeview as TreeViewTag).ondragndrop = v; (this.refs.listview as ListViewTag).ondragndrop = v; + (this.refs.gridview as GridViewTag).ondragndrop = (e) => { + const evt = { + id: this.aid, + data: { + from: e.data.from.map(x => x.data[0].domel), + to: e.data.to.data[0].domel + } + }; + v(evt); + }; } /** @@ -514,7 +587,7 @@ namespace OS { $(this.refs.listview).hide(); $(this.refs.gridview).hide(); $(this.refs.treecontainer).hide(); - this._selectedFile = undefined; + this._selectedFiles = []; switch (this.view) { case "icon": $(this.refs.listview).show(); @@ -552,7 +625,6 @@ namespace OS { ); } const evt = { id: this.aid, data: e }; - this._selectedFile = e; this._onfileselect(evt); this.observable.trigger("fileselect", evt); } @@ -612,18 +684,22 @@ namespace OS { grid.header = this._header; tree.dragndrop = true; list.dragndrop = true; + grid.dragndrop = true; // even handles list.onlistselect = (e) => { this.fileselect(e.data.item.data as API.FileInfoType); + this._selectedFiles = e.data.items.map( x => x.data as API.FileInfoType); }; grid.onrowselect = (e) => { this.fileselect( ($(e.data.item).children()[0] as GridCellPrototype) .data as API.FileInfoType ); + this._selectedFiles = e.data.items.map( x => ($(x).children()[0] as GridCellPrototype).data as API.FileInfoType); }; tree.ontreeselect = (e) => { this.fileselect(e.data.item.data as API.FileInfoType); + this._selectedFiles = [e.data.item.data as API.FileInfoType]; }; // dblclick list.onlistdbclick = (e) => { diff --git a/src/core/tags/GridViewTag.ts b/src/core/tags/GridViewTag.ts index 6403b96..90b4151 100644 --- a/src/core/tags/GridViewTag.ts +++ b/src/core/tags/GridViewTag.ts @@ -19,6 +19,10 @@ interface Array { namespace OS { export namespace GUI { export namespace tag { + /** + * Row item event data type + */ + export type GridRowEventData = TagEventDataType; /** * A grid Row is a simple element that * contains a group of grid cell @@ -36,6 +40,15 @@ namespace OS { */ data: GenericObject[]; + /** + * placeholder for the row select event callback + * + * @private + * @type {TagEventCallback} + * @memberof ListViewItemTag + */ + private _onselect: TagEventCallback; + /** *Creates an instance of GridRowTag. * @memberof GridRowTag @@ -44,6 +57,36 @@ namespace OS { super(); this.refs.yield = this; + this._onselect = (e) => {}; + } + + /** + * Set item select event handle + * + * @memberof ListViewItemTag + */ + set onrowselect(v: TagEventCallback) { + this._onselect = v; + } + + /** + * Setter: select/unselect the current item + * + * Getter: Check whether the current item is selected + * + * @memberof ListViewItemTag + */ + set selected(v: boolean) { + this.attsw(v, "selected"); + $(this).removeClass(); + if (!v) { + return; + } + $(this).addClass("afx-grid-row-selected"); + this._onselect({ id: this.aid, data: this }); + } + get selected(): boolean { + return this.hasattr("selected"); } /** @@ -420,6 +463,27 @@ namespace OS { */ private _oncelldbclick: TagEventCallback; + /** + * Event data passing between mouse event when performing + * drag and drop on the list + * + * @private + * @type {{ from: GridRowTag[]; to: GridRowTag }} + * @memberof GridViewTag + */ + private _dnd: { from: GridRowTag[]; to: GridRowTag }; + + /** + * placeholder of list drag and drop event handle + * + * @private + * @type {TagEventCallback>} + * @memberof GridViewTag + */ + private _ondragndrop: TagEventCallback< + DnDEventDataType + >; + /** * Creates an instance of GridViewTag. * @memberof GridViewTag @@ -428,6 +492,68 @@ namespace OS { super(); } + /** + * Set drag and drop event handle + * + * @memberof GridViewTag + */ + set ondragndrop( + v: TagEventCallback> + ) { + this._ondragndrop = v; + this.dragndrop = this.dragndrop; + } + + /** + * Setter: Enable/disable drag and drop event in the list + * + * Getter: Check whether the drag and drop event is enabled + * + * @memberof GridViewTag + */ + set dragndrop(v: boolean) { + this.attsw(v, "dragndrop"); + if(!v) + { + $(this.refs.container).off("mousedown", this._onmousedown); + } + else + { + $(this.refs.container).on( + "mousedown", + this._onmousedown + ); + } + } + get dragndrop(): boolean { + return this.hasattr("dragndrop"); + } + + /** + * placeholder of drag and drop mouse down event handle + * + * @private + * @memberof GridViewTag + */ + private _onmousedown: (e: JQuery.MouseEventBase) => void; + + /** + * placeholder of drag and drop mouse up event handle + * + * @private + * @memberof GridViewTag + */ + private _onmouseup: (e: JQuery.MouseEventBase) => void; + + /** + * placeholder of drag and drop mouse move event handle + * + * @private + * @memberof GridViewTag + */ + private _onmousemove: (e: JQuery.MouseEventBase) => void; + + /** * Init the grid view before mounting. * Reset all the placeholders to default values @@ -444,9 +570,13 @@ namespace OS { this._selectedRow = undefined; this._rows = []; this.resizable = false; + this.dragndrop = false; this._oncellselect = this._onrowselect = this._oncelldbclick = ( e: TagEventType ): void => {}; + this._ondragndrop = ( + e: TagEventType> + ) => {}; } /** @@ -507,7 +637,14 @@ namespace OS { * @memberof GridViewTag */ set cellitem(v: string) { + const currci = this.cellitem; $(this).attr("cellitem", v); + if(v != currci) + { + // force render data + $(this.refs.grid).empty(); + this.rows = this.rows; + } } get cellitem(): string { return $(this).attr("cellitem"); @@ -539,6 +676,12 @@ namespace OS { element.uify(this.observable); element.data = item; item.domel = element; + element.oncellselect = (e) => { + if(element.data.sort) + { + this.sort(element.data, element.data.sort); + } + }; i++; if (this.resizable) { if (i != v.length) { @@ -604,9 +747,48 @@ namespace OS { * @memberof GridViewTag */ set rows(rows: GenericObject[][]) { - $(this.refs.grid).empty(); this._rows = rows; - rows.map((row) => this.push(row, false)); + if(!rows) return; + // update existing row with new data + const ndrows = rows.length; + const ncrows = this.refs.grid.children.length; + const nmin = ndrows < ncrows? ndrows: ncrows; + if(this.selectedRow) + { + this.selectedRow.selected = false; + this._selectedRow = undefined; + this._selectedRows = []; + } + for(let i = 0; i < nmin; i++) + { + const rowel = (this.refs.grid.children[i] as GridRowTag); + rowel.data = rows[i]; + rowel.data.domel = rowel; + for(let celi = 0; celi < rowel.children.length; celi++) + { + const cel = (rowel.children[celi] as GridCellPrototype); + cel.data = rows[i][celi]; + cel.data.domel = cel; + } + } + // remove existing remaining rows + if(ndrows < ncrows) + { + const arr = Array.prototype.slice.call(this.refs.grid.children); + const blacklist = arr.slice(nmin, ncrows); + for(const r of blacklist) + { + this.delete(r); + } + } + // or add more rows + else if(ndrows > ncrows) + { + for(let i = nmin; i < ndrows; i++) + { + this.push(rows[i], false); + } + } } get rows(): GenericObject[][] { return this._rows; @@ -639,7 +821,24 @@ namespace OS { get resizable(): boolean { return this.hasattr("resizable"); } - + /** + * Sort the grid using a sort function + * + * @param {context: any} context of the executed function + * @param {(a:GenericObject[], b:GenericObject[]) => boolean} a sort function that compares two rows data + * * @param {index: number} current header index + * @returns {void} + * @memberof GridViewTag + */ + sort(context: any, fn: (a:GenericObject[], b:GenericObject[], index?: number) => number): void { + const index = this._header.indexOf(context); + const __fn = (a, b) => { + return fn.call(context,a, b, index); + } + this._rows.sort(__fn); + context.__f = ! context.__f; + this.rows = this._rows; + } /** * Delete a grid rows * @@ -713,6 +912,10 @@ namespace OS { element.oncelldbclick = (e) => this.cellselect(e, true); element.data = cell; } + el.onrowselect = (e) => this.rowselect({ + id: el.aid, + data: {item: el} + }); } /** @@ -751,7 +954,10 @@ namespace OS { } else { this.observable.trigger("cellselect", e); this._oncellselect(e); - return this.rowselect(e); + const row = ($( + e.data.item + ).parent()[0] as any) as GridRowTag; + row.selected = true; } } @@ -759,11 +965,11 @@ namespace OS { * This function triggers the row select event, a cell select * event will also trigger this event * - * @param {TagEventType} e + * @param {TagEventType} e * @returns {void} * @memberof GridViewTag */ - private rowselect(e: TagEventType): void { + private rowselect(e: TagEventType): void { if (!e.data.item) { return; } @@ -774,39 +980,58 @@ namespace OS { items: [], }, }; - const row = ($( - e.data.item - ).parent()[0] as any) as GridRowTag; + const row = e.data.item as GridRowTag; if (this.multiselect) { if (this.selectedRows.includes(row)) { this.selectedRows.splice( this.selectedRows.indexOf(row), 1 ); - $(row).removeClass(); + row.selected = false; + return; } else { this.selectedRows.push(row); - $(row) - .removeClass() - .addClass("afx-grid-row-selected"); } evt.data.items = this.selectedRows; } else { + if(this.selectedRows.length > 0) + { + for(const item of this.selectedRows) + { + if(item != row) + { + item.selected = false; + } + } + } if (this.selectedRow === row) { return; } - $(this.selectedRow).removeClass(); - this._selectedRows = [row]; - evt.data.item = row; + if(this.selectedRow) + this.selectedRow.selected = false; evt.data.items = [row]; - $(row).removeClass().addClass("afx-grid-row-selected"); this._selectedRows = [row]; } + evt.data.item = row; this._selectedRow = row; this._onrowselect(evt); return this.observable.trigger("rowselect", evt); } + /** + * Unselect all the selected rows in the grid + * + * @returns {void} + * @memberof GridViewTag + */ + unselect(): void { + for (let v of this.selectedRows) { + v.selected = false; + } + this._selectedRows = []; + this._selectedRow = undefined; + } + /** * Check whether the grid has header * @@ -904,7 +1129,6 @@ namespace OS { */ protected mount(): void { $(this).css("overflow", "hidden"); - $(this.refs.grid).css("display", "grid"); $(this.refs.header).css("display", "grid"); this.observable.on("resize", (e) => this.calibrate()); @@ -912,6 +1136,73 @@ namespace OS { .css("width", "100%") .css("overflow-x", "hidden") .css("overflow-y", "auto"); + // drag and drop + this._dnd = { + from: undefined, + to: undefined, + }; + this._onmousedown = (e) => { + if(this.multiselect || this.selectedRows == undefined || this.selectedRows.length == 0) + { + return; + } + let el: any = $(e.target).closest("afx-grid-row"); + if (el.length === 0) { + return; + } + el = el[0]; + if(!this.selectedRows.includes(el)) + { + return; + } + this._dnd.from = this.selectedRows; + this._dnd.to = undefined; + $(window).on("mouseup", this._onmouseup); + $(window).on("mousemove", this._onmousemove); + }; + + this._onmouseup = (e) => { + $(window).off("mouseup", this._onmouseup); + $(window).off("mousemove", this._onmousemove); + $("#systooltip").hide(); + let el: any = $(e.target).closest("afx-grid-row"); + if (el.length === 0) { + return; + } + el = el[0]; + if (this._dnd.from.includes(el)) { + return; + } + this._dnd.to = el; + this._ondragndrop({ id: this.aid, data: this._dnd }); + this._dnd = { + from: undefined, + to: undefined, + }; + }; + + this._onmousemove = (e) => { + if (!e) { + return; + } + if (!this._dnd.from) { + return; + } + const data = { + text: __("{0} selected elements", this._dnd.from.length).__(), + items: this._dnd.from + }; + const $label = $("#systooltip"); + const top = e.clientY + 5; + const left = e.clientX + 5; + $label.show(); + const label = $label[0] as LabelTag; + label.set(data); + return $label + .css("top", top + "px") + .css("left", left + "px"); + }; + return this.calibrate(); } diff --git a/src/core/tags/LabelTag.ts b/src/core/tags/LabelTag.ts index 1a87414..77dd0a0 100644 --- a/src/core/tags/LabelTag.ts +++ b/src/core/tags/LabelTag.ts @@ -56,6 +56,7 @@ namespace OS { this.icon = undefined; this.iconclass = undefined; this.text = undefined; + this.selectable = false; } /** @@ -121,6 +122,33 @@ namespace OS { return this._text; } + + /** + * Setter: Turn on/off text selection + * + * Getter: Check whether the label is selectable + * + * @memberof LabelTag + */ + set selectable(v: boolean) { + this.attsw(v, "selectable"); + if(v) + { + $(this.refs.text) + .css("user-select", "text") + .css("cursor", "text"); + } + else + { + $(this.refs.text) + .css("user-select", "none") + .css("cursor", "default"); + } + } + get swon(): boolean { + return this.hasattr("selectable"); + } + /** * Lqbel layout definition * diff --git a/src/core/tags/ListViewTag.ts b/src/core/tags/ListViewTag.ts index 8625864..84f3925 100644 --- a/src/core/tags/ListViewTag.ts +++ b/src/core/tags/ListViewTag.ts @@ -444,10 +444,10 @@ namespace OS { * drag and drop on the list * * @private - * @type {{ from: ListViewItemTag; to: ListViewItemTag }} + * @type {{ from: ListViewItemTag[]; to: ListViewItemTag }} * @memberof ListViewTag */ - private _dnd: { from: ListViewItemTag; to: ListViewItemTag }; + private _dnd: { from: ListViewItemTag[]; to: ListViewItemTag }; /** *Creates an instance of ListViewTag. @@ -990,6 +990,16 @@ namespace OS { this.selectedItems.push(e.data); edata.items = this.selectedItems; } else { + if(this.selectedItems.length > 0) + { + for(const item of this.selectedItems) + { + if(item != e.data) + { + item.selected = false; + } + } + } if (this.selectedItem === e.data) { return; } @@ -1044,14 +1054,22 @@ namespace OS { to: undefined, }; this._onmousedown = (e) => { + if(this.multiselect || this.selectedItems == undefined || this.selectedItems.length == 0) + { + return; + } let el: any = $(e.target).closest( "li[dataref='afx-list-item']" ); if (el.length === 0) { return; } - el = el.parent()[0] as ListViewItemTag; - this._dnd.from = el; + el = el.parent()[0]; + if(!this.selectedItems.includes(el)) + { + return; + } + this._dnd.from = this.selectedItems; this._dnd.to = undefined; $(window).on("mouseup", this._onmouseup); $(window).on("mousemove", this._onmousemove); @@ -1068,7 +1086,7 @@ namespace OS { return; } el = el.parent()[0]; - if (el === this._dnd.from) { + if (this._dnd.from.includes(el)) { return; } this._dnd.to = el; @@ -1086,7 +1104,18 @@ namespace OS { if (!this._dnd.from) { return; } - const data = this._dnd.from.data; + const data = { + text: '', + items: this._dnd.from + }; + if(this._dnd.from.length == 1) + { + data.text = this._dnd.from[0].data.text; + } + else + { + data.text = __("{0} selected elements", this._dnd.from.length).__(); + } const $label = $("#systooltip"); const top = e.clientY + 5; const left = e.clientX + 5; diff --git a/src/core/tags/TreeViewTag.ts b/src/core/tags/TreeViewTag.ts index 4d3adce..0c8646c 100644 --- a/src/core/tags/TreeViewTag.ts +++ b/src/core/tags/TreeViewTag.ts @@ -586,10 +586,10 @@ namespace OS { * Private data object passing between dragndrop mouse event * * @private - * @type {{ from: TreeViewTag; to: TreeViewTag }} + * @type {{ from: TreeViewTag[]; to: TreeViewTag }} * @memberof TreeViewTag */ - private _dnd: { from: TreeViewTag; to: TreeViewTag }; + private _dnd: { from: TreeViewTag[]; to: TreeViewTag }; /** * Reference to parent tree of the current tree. @@ -638,7 +638,7 @@ namespace OS { * current tree. This function should return a promise on * a list of [[TreeViewDataType]] * - * @memberof TreeViewItemPrototype + * @memberof TreeViewTag */ fetch: ( d: TreeViewItemPrototype @@ -920,7 +920,7 @@ namespace OS { */ protected mount(): void { this._dnd = { - from: undefined, + from: [], to: undefined, }; this._treemousedown = (e) => { @@ -932,7 +932,7 @@ namespace OS { if (el === this) { return; } - this._dnd.from = el; + this._dnd.from = [el]; this._dnd.to = undefined; $(window).on("mouseup", this._treemouseup); return $(window).on("mousemove", this._treemousemove); @@ -951,8 +951,8 @@ namespace OS { el = el.parent; } if ( - el === this._dnd.from || - el === this._dnd.from.parent + el === this._dnd.from[0] || + el === this._dnd.from[0].parent ) { return; } @@ -962,7 +962,7 @@ namespace OS { data: this._dnd, }); this._dnd = { - from: undefined, + from: [], to: undefined, }; }; @@ -974,7 +974,7 @@ namespace OS { if (!this._dnd.from) { return; } - const data = this._dnd.from.data; + const data = this._dnd.from[0].data; const $label = $("#systooltip"); const top = e.clientY + 5; const left = e.clientX + 5; diff --git a/src/core/tags/WindowTag.ts b/src/core/tags/WindowTag.ts index 06746e2..f5a939e 100644 --- a/src/core/tags/WindowTag.ts +++ b/src/core/tags/WindowTag.ts @@ -306,6 +306,7 @@ namespace OS { .removeClass("unactive"); this._shown = true; $(this.refs.win_overlay).hide(); + $(this).trigger("focus"); }); this.observable.on("blur", () => { @@ -336,6 +337,7 @@ namespace OS { w: this.width, h: this.height, }); + $(this).attr("tabindex", 0).css("outline", "none"); return this.observable.trigger("rendered", { id: this.aid, }); diff --git a/src/core/tags/tag.ts b/src/core/tags/tag.ts index 3c80c98..83f742a 100644 --- a/src/core/tags/tag.ts +++ b/src/core/tags/tag.ts @@ -233,7 +233,7 @@ namespace OS { * @type {T} * @memberof DnDEventDataType */ - from: T; + from: T[]; /** * Reference to the target DOM element diff --git a/src/packages/Files/main.ts b/src/packages/Files/main.ts index b1790e1..30c2eb0 100644 --- a/src/packages/Files/main.ts +++ b/src/packages/Files/main.ts @@ -21,7 +21,7 @@ namespace OS { interface FilesClipboardType { cut: boolean; - file: API.VFS.BaseFileHandle; + files: API.VFS.BaseFileHandle[]; } interface FilesViewType { icon: boolean; @@ -228,43 +228,54 @@ namespace OS { }; this.vfs_event_flag = true; - this.view.ondragndrop = (e) => { + this.view.ondragndrop = async (e) => { if (!e) { return; } - const src = e.data.from.data; + const src = e.data.from; const des = e.data.to.data; if (des.type === "file") { return; } - const file = src.path.asFileHandle(); + // ask to confirm + const r = await this.ask({ + title: __("Move files"), + text: __("Move selected file to {0}?", des.text) + }); + if(!r) + { + return; + } // disable the vfs event on // we update it manually this.vfs_event_flag = false; - return file - .move(`${des.path}/${file.basename}`) - .then(() => { - if (this.view.view === "icon") { - this.view.path = this.view.path; - } else { - this.view.update(file.parent().path); - this.view.update(des.path); - } - //reenable the vfs event - return (this.vfs_event_flag = true); - }) - .catch((e: Error) => { - // reenable the vfs event - this.vfs_event_flag = true; - return this.error( + const promises = []; + for(const item of src) + { + let file = item.data.path.asFileHandle(); + promises.push( + file.move(`${des.path}/${file.basename}`)); + } + try{ + await Promise.all(promises); + if (this.view.view === "tree") { + this.view.update(src[0].data.path.asFileHandle().parent().path); + this.view.update(des.path); + } else { + this.view.path = this.view.path; + } + } + catch(error) + { + this.error( __( - "Unable to move: {0} -> {1}", - src.path, + "Unable to move files to: {0}", des.path ), - e + error ); - }); + } + this.vfs_event_flag = true; }; // application setting @@ -355,6 +366,23 @@ namespace OS { this.view.view = "list"; this.viewType.list = true; }; + // enable or disable multi-select by CTRL key + $(this.scheme).on("keydown", (evt)=>{ + if(evt.ctrlKey && evt.which == 17) + { + this.view.multiselect = true; + } + else + { + this.view.multiselect = false; + } + }); + $(this.scheme).on("keyup", (evt)=>{ + if(!evt.ctrlKey) + { + this.view.multiselect = false; + } + }); this.view.path = this.currdir.path; } @@ -585,20 +613,22 @@ namespace OS { title: "__(Delete)", iconclass: "fa fa-question-circle", text: __( - "Do you really want to delete: {0}?", - file.filename + "Do you really want to delete selected files?" ), }).then(async (d) => { if (!d) { return; } + const promises = []; + for(const f of this.view.selectedFiles) + { + promises.push(f.path.asFileHandle().remove()); + } try { - return file.path - .asFileHandle() - .remove(); + await Promise.all(promises); } catch (e) { - return this.error(__("Fail to delete: {0}", file.path), e); + return this.error(__("Fail to delete selected files"), e); } }); break; @@ -609,9 +639,9 @@ namespace OS { } this.clipboard = { cut: true, - file: file.path.asFileHandle(), + files: this.view.selectedFiles.map(x => x.path.asFileHandle()), }; - return this.notify(__("File {0} cut", file.filename)); + return this.notify(__("{0} files cut", this.clipboard.files.length)); case `${this.name}-copy`: if (!file) { @@ -619,10 +649,10 @@ namespace OS { } this.clipboard = { cut: false, - file: file.path.asFileHandle(), + files: this.view.selectedFiles.map(x => x.path.asFileHandle()), }; return this.notify( - __("File {0} copied", file.filename) + __("{0} files copied", this.clipboard.files.length) ); case `${this.name}-paste`: @@ -630,29 +660,33 @@ namespace OS { return; } if (this.clipboard.cut) { - this.clipboard.file - .move( - `${this.currdir.path}/${this.clipboard.file.basename}` - ) + const promises = []; + for(const file of this.clipboard.files) + { + promises.push(file.move( + `${this.currdir.path}/${file.basename}` + )); + } + Promise.all(promises) .then((r) => { return (this.clipboard = undefined); }) .catch((e) => { return this.error( __( - "Fail to paste: {0}", - this.clipboard.file.path + "Fail to paste to: {0}", + this.currdir.path ), e ); }); } else { - API.VFS.copy([this.clipboard.file.path],this.currdir.path) + API.VFS.copy(this.clipboard.files.map(x => x.path),this.currdir.path) .then(() => { return (this.clipboard = undefined); }) .catch((e) => { - return this.error(__("Fail to paste: {0}", this.clipboard.file.path), e); + return this.error(__("Fail to paste to: {0}", this.currdir.path), e); }); } break; diff --git a/src/packages/Files/package.json b/src/packages/Files/package.json index 02d52d3..76800fb 100644 --- a/src/packages/Files/package.json +++ b/src/packages/Files/package.json @@ -6,7 +6,7 @@ "author": "Xuan Sang LE", "email": "xsang.le@gmail.com" }, - "version":"0.1.4-b", + "version":"0.1.5-b", "category":"System", "iconclass":"fa fa-hdd-o", "mimes":["dir"], diff --git a/src/themes/system/afx-label.css b/src/themes/system/afx-label.css index 92b9426..5ed7fc9 100644 --- a/src/themes/system/afx-label.css +++ b/src/themes/system/afx-label.css @@ -5,5 +5,4 @@ afx-label i.label-text{ font-weight: normal; font-style: normal; margin-left: 3px; - user-select:text; } \ No newline at end of file