diff --git a/api/js/etemplate/et2_core_DOMWidget.js b/api/js/etemplate/et2_core_DOMWidget.js index 8ecf911aa3..d3770dd159 100644 --- a/api/js/etemplate/et2_core_DOMWidget.js +++ b/api/js/etemplate/et2_core_DOMWidget.js @@ -698,6 +698,9 @@ var et2_surroundingsMgr = /** @class */ (function (_super) { // Return the widget container return this._widgetContainer; }; + et2_surroundingsMgr.prototype.getWidgetSurroundings = function () { + return this._widgetSurroundings; + }; return et2_surroundingsMgr; }(et2_core_inheritance_1.ClassWithAttributes)); /** diff --git a/api/js/etemplate/et2_core_DOMWidget.ts b/api/js/etemplate/et2_core_DOMWidget.ts index 5439f82dfb..25870e8492 100644 --- a/api/js/etemplate/et2_core_DOMWidget.ts +++ b/api/js/etemplate/et2_core_DOMWidget.ts @@ -875,6 +875,11 @@ class et2_surroundingsMgr extends ClassWithAttributes // Return the widget container return this._widgetContainer; } + + getWidgetSurroundings() : HTMLElement [] + { + return this._widgetSurroundings; + } } /** diff --git a/api/js/etemplate/et2_types.d.ts b/api/js/etemplate/et2_types.d.ts index f2b4f913b9..c03bed2275 100644 --- a/api/js/etemplate/et2_types.d.ts +++ b/api/js/etemplate/et2_types.d.ts @@ -191,4 +191,7 @@ declare function nm_open_popup(_action, _selected) : void; declare function nm_submit_popup(button) : void; declare function nm_hide_popup(element, div_id) : false; declare function nm_activate_link(_action, _senders) : void; -declare function egw_seperateJavaScript(_html) : void; \ No newline at end of file +declare function egw_seperateJavaScript(_html) : void; +declare class Resumable { + constructor(asyncOptions: any); +} \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_description.js b/api/js/etemplate/et2_widget_description.js index 7612701a35..a940abaf2f 100644 --- a/api/js/etemplate/et2_widget_description.js +++ b/api/js/etemplate/et2_widget_description.js @@ -352,5 +352,6 @@ var et2_description = /** @class */ (function (_super) { }; return et2_description; }(et2_core_baseWidget_1.et2_baseWidget)); +exports.et2_description = et2_description; et2_core_widget_1.et2_register_widget(et2_description, ["description", "label"]); //# sourceMappingURL=et2_widget_description.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_description.ts b/api/js/etemplate/et2_widget_description.ts index 2934ae52ce..5593921669 100644 --- a/api/js/etemplate/et2_widget_description.ts +++ b/api/js/etemplate/et2_widget_description.ts @@ -24,7 +24,7 @@ import './et2_types'; /** * Class which implements the "description" XET-Tag */ -class et2_description extends et2_baseWidget implements et2_IDetachedDOM +export class et2_description extends et2_baseWidget implements et2_IDetachedDOM { static readonly _attributes : any = { "label": { diff --git a/api/js/etemplate/et2_widget_dialog.js b/api/js/etemplate/et2_widget_dialog.js index e56e0a48c4..7a0d541bed 100644 --- a/api/js/etemplate/et2_widget_dialog.js +++ b/api/js/etemplate/et2_widget_dialog.js @@ -799,5 +799,6 @@ var et2_dialog = /** @class */ (function (_super) { et2_dialog.NO_BUTTON = 3; return et2_dialog; }(et2_core_widget_2.et2_widget)); +exports.et2_dialog = et2_dialog; et2_core_widget_1.et2_register_widget(et2_dialog, ["dialog"]); //# sourceMappingURL=et2_widget_dialog.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_dialog.ts b/api/js/etemplate/et2_widget_dialog.ts index cee19a5e0d..bf263375e0 100644 --- a/api/js/etemplate/et2_widget_dialog.ts +++ b/api/js/etemplate/et2_widget_dialog.ts @@ -93,7 +93,7 @@ import {et2_DOMWidget} from "./et2_core_DOMWidget"; * @augments et2_widget * @see http://api.jqueryui.com/dialog/ */ -class et2_dialog extends et2_widget { +export class et2_dialog extends et2_widget { static readonly _attributes: any = { callback: { name: "Callback", diff --git a/api/js/etemplate/et2_widget_file.js b/api/js/etemplate/et2_widget_file.js index b274069d28..78b5254108 100644 --- a/api/js/etemplate/et2_widget_file.js +++ b/api/js/etemplate/et2_widget_file.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS Number object * @@ -9,710 +10,611 @@ * @copyright Nathan Gray 2011 * @version $Id$ */ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - et2_core_inputWidget; - phpgwapi.Resumable.resumable; + et2_core_inputWidget; + phpgwapi.Resumable.resumable; */ - +var et2_core_inputWidget_1 = require("./et2_core_inputWidget"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); /** * Class which implements file upload * * @augments et2_inputWidget */ -var et2_file = (function(){ "use strict"; return et2_inputWidget.extend( -{ - attributes: { - "multiple": { - "name": "Multiple files", - "type": "boolean", - "default": false, - "description": "Allow the user to select more than one file to upload at a time. Subject to browser support." - }, - "max_file_size": { - "name": "Maximum file size", - "type": "integer", - "default":0, - "description": "Largest file accepted, in bytes. Subject to server limitations. 8MB = 8388608" - }, - "mime": { - "name": "Allowed file types", - "type": "string", - "default": et2_no_init, - "description": "Mime type (eg: image/png) or regex (eg: /^text\//i) for allowed file types" - }, - "blur": { - "name": "Placeholder", - "type": "string", - "default": "", - "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." - }, - "progress": { - "name": "Progress node", - "type": "string", - "default": et2_no_init, - "description": "The ID of an alternate node (div) to display progress and results. The Node is fetched with et2 getWidgetById so you MUST use the id assigned in XET-File (it may not be available at creation time, so we (re)check on createStatus time)" - }, - "onStart": { - "name": "Start event handler", - "type": "any", - "default": et2_no_init, - "description": "A (js) function called when an upload starts. Return true to continue with upload, false to cancel." - }, - "onFinish": { - "name": "Finish event handler", - "type": "any", - "default": et2_no_init, - "description": "A (js) function called when all files to be uploaded are finished." - }, - drop_target: { - "name": "Optional, additional drop target for HTML5 uploads", - "type": "string", - "default": et2_no_init, - "description": "The ID of an additional drop target for HTML5 drag-n-drop file uploads" - }, - label: { - "name": "Label of file upload", - "type": "string", - "default": "Choose file...", - "description": "String caption to be displayed on file upload span" - }, - progress_dropdownlist: { - "name": "List on files in progress like dropdown", - "type": "boolean", - "default": false, - "description": "Style list of files in uploading progress like dropdown list with a total upload progress indicator" - }, - onFinishOne: { - "name": "Finish event handler for each one", - "type": "any", - "default": et2_no_init, - "description": "A (js) function called when a file to be uploaded is finished." - }, - accept: { - "name": "Acceptable extensions", - "type": "string", - "default": '', - "description": "Define types of files that the server accepts. Multiple types can be seperated by comma and the default is to accept everything." - }, - chunk_size: { - "name": "Chunk size", - "type": "integer", - "default": 1024*1024, - "description": "Max chunk size, gets set from server-side PHP (max_upload_size-1M)/2" // last chunk can be up to 2*chunk_size! - } - }, - - asyncOptions: {}, - - /** - * Constructor - * - * @memberOf et2_file - */ - init: function(_parent, attrs) { - this._super.apply(this, arguments); - - this.node = null; - this.input = null; - this.progress = null; - this.span = null; - // Contains all submit buttons need to be disabled during upload process - this.disabled_buttons = jQuery("input[type='submit'], button"); - if(!this.options.value) this.options.value = {}; - - if(!this.options.id) { - console.warn("File widget needs an ID. Used 'file_widget'."); - this.options.id = "file_widget"; - } - - // Legacy - id ending in [] means multiple - if(this.options.id.substr(-2) == "[]") - { - this.options.multiple = true; - } - // If ID ends in /, it's a directory - allow multiple - else if (this.options.id.substr(-1) === "/") - { - this.options.multiple = true; - attrs.multiple = true; - } - - // Set up the URL to have the request ID & the widget ID - var instance = this.getInstanceManager(); - - var self = this; - - this.asyncOptions = jQuery.extend({ - // Callbacks - onStart: function(event, file_count) { - return self.onStart(event, file_count); - }, - onFinish: function(event, file_count) { - self.onFinish.apply(self, [event, file_count]) - }, - onStartOne: function(event, file_name, index, file_count) { - - }, - onFinishOne: function(event, response, name, number, total) { return self.finishUpload(event,response,name,number,total);}, - onProgress: function(event, progress, name, number, total) { return self.onProgress(event,progress,name,number,total);}, - onError: function(event, name, error) { return self.onError(event,name,error);}, - beforeSend: function(form) { return self.beforeSend(form);}, - - chunkSize: this.options.chunk_size || 1024*1024, - - target: egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\File::ajax_upload"), - query: function(file) {return self.beforeSend(file);}, - // Disable checking for already uploaded chunks - testChunks: false - },this.asyncOptions); - this.asyncOptions.fieldName = this.options.id; - this.createInputWidget(); - this.set_readonly(this.options.readonly); - }, - - destroy: function() { - this._super.apply(this, arguments); - this.set_drop_target(null); - this.node = null; - this.input = null; - this.span = null; - this.progress = null; - }, - - createInputWidget: function() { - this.node = jQuery(document.createElement("div")).addClass("et2_file"); - this.span = jQuery(document.createElement("span")) - .addClass('et2_file_span et2_button') - .appendTo (this.node); - if (this.options.label != '') this.span.addClass('et2_button_text'); - var span = this.span; - this.input = jQuery(document.createElement("input")) - .attr("type", "file").attr("placeholder", this.options.blur) - .addClass ("et2_file_upload") - .appendTo(this.node) - .hover(function(e){ - jQuery(span) - .toggleClass('et2_file_spanHover'); - }) - .on({ - mousedown:function (e){ - jQuery(span).addClass('et2_file_spanActive'); - }, - mouseup:function (e){ - jQuery(span).removeClass('et2_file_spanActive'); - } - }); - if (this.options.accept) this.input.attr('accept', this.options.accept); - var self = this; - // trigger native input upload file - if (!this.options.readonly) this.span.click(function(){self.input.click()}); - // Check for File interface, should fall back to normal form submit if missing - if(typeof File != "undefined" && typeof (new XMLHttpRequest()).upload != "undefined") - { - this.resumable = new Resumable(this.asyncOptions); - this.resumable.assignBrowse(this.input); - this.resumable.on('fileAdded', jQuery.proxy(this._fileAdded, this)); - this.resumable.on('fileProgress', jQuery.proxy(this._fileProgress, this)); - this.resumable.on('fileSuccess', jQuery.proxy(this.finishUpload, this)); - this.resumable.on('complete', jQuery.proxy(this.onFinish, this)); - } - else - { - // This may be a problem submitting via ajax - } - if(this.options.progress) - { - var widget = this.getRoot().getWidgetById(this.options.progress); - if(widget) - { - //may be not available at createInputWidget time - this.progress = jQuery(widget.getDOMNode()); - } - } - if(!this.progress) - { - this.progress = jQuery(document.createElement("div")).appendTo(this.node); - } - this.progress.addClass("progress"); - - if(this.options.multiple) - { - this.input.attr("multiple","multiple"); - } - - this.setDOMNode(this.node[0]); - }, - - /** - * Set a widget or DOM node as a HTML5 file drop target - * - * @param String new_target widget ID or DOM node ID to be used as a new target - */ - set_drop_target: function(new_target) - { - // Cancel old drop target - if(this.options.drop_target) - { - var widget = this.getRoot().getWidgetById(this.options.drop_target); - var drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target); - if(drop_target) - { - this.resumable.unAssignDrop(drop_target); - } - } - - this.options.drop_target = new_target; - - if(!this.options.drop_target) return; - - // Set up new drop target - var widget = this.getRoot().getWidgetById(this.options.drop_target); - var drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target); - if(drop_target) - { - this.resumable.assignDrop([drop_target]); - } - else - { - this.egw().debug("warn", "Did not find file drop target %s", this.options.drop_target); - } - - }, - attachToDOM: function() { - this._super.apply(this, arguments); - // Override parent's change, file widget will fire change when finished uploading - this.input.unbind("change.et2_inputWidget"); - }, - getValue: function() { - var value = this.options.value ? this.options.value : this.input.val(); - return value; - }, - - /** - * Set the value of the file widget. - * - * If you pass a FileList or list of files, it will trigger the async upload - * - * @param {FileList|File[]|false} value List of files to be uploaded, or false to reset. - * @param {Event} event Most browsers require the user to initiate file transfers in some way. - * Pass the event in, if you have it. - */ - set_value: function(value, event) { - if(!value || typeof value == "undefined") - { - value = {}; - } - if(jQuery.isEmptyObject(value)) - { - this.options.value = {}; - if (this.resumable.progress() == 1) this.progress.empty(); - - // Reset the HTML element - this.input.wrap('
').closest('form').get(0).reset(); - this.input.unwrap(); - - return; - } - - var addFile = jQuery.proxy(function(i, file) { - this.resumable.addFile(file,event); - }, this); - if(typeof value == 'object' && value.length && typeof value[0] == 'object' && value[0].name) - { - try - { - this.input[0].files = value; - - jQuery.each(value, addFile); - } - catch (e) - { - var self = this; - var args = arguments; - jQuery.each(value, addFile); - } - } - }, - - /** - * Set the value for label - * The label is used as caption for span tag which customize the HTML file upload styling - * - * @param {string} value text value of label - */ - set_label: function (value) - { - if (this.span != null && value != null) - { - this.span.text(value); - } - }, - - getInputNode: function() { - if (typeof this.input == 'undefined') return false; - return this.input[0]; - }, - - - set_mime: function(mime) { - if(!mime) - { - this.options.mime = null; - } - if(mime.indexOf("/") != 0) - { - // Lower case it now, if it's not a regex - this.options.mime = mime.toLowerCase(); - } - else - { - // Convert into a js regex - var parts = mime.substr(1).match(/(.*)\/([igm]?)$/); - this.options.mime = new RegExp(parts[1],parts.length > 2 ? parts[2] : ""); - } - }, - - set_multiple: function(_multiple) { - this.options.multiple = _multiple; - if(_multiple) - { - return this.input.attr("multiple", "multiple"); - } - return this.input.removeAttr("multiple"); - }, - /** - * Check to see if the provided file's mimetype matches - * - * @param f File object - * @return boolean - */ - checkMime: function(f) { - // If missing, let the server handle it - if(!this.options.mime || !f.type) return true; - - var is_preg = (typeof this.options.mime == "object"); - if(!is_preg && f.type.toLowerCase() == this.options.mime || is_preg && this.options.mime.test(f.type)) - { - return true; - } - - // Not right mime - return false; - }, - - _fileAdded: function(file,event) { - // Manual additions have no event - if(typeof event == 'undefined') - { - event = {}; - } - // Trigger start of uploading, calls callback - if(!this.resumable.isUploading()) - { - if (!(this.onStart(event,this.resumable.files.length))) return; - } - - // Here 'this' is the input - if(this.checkMime(file.file)) - { - if(this.createStatus(event,file)) - { - - // Disable buttons - this.disabled_buttons - .not("[disabled]") - .attr("disabled", true) - .addClass('et2_button_ro') - .removeClass('et2_clickable') - .css('cursor', 'default'); - - // Actually start uploading - this.resumable.upload(); - } - } - else - { - // Wrong mime type - show in the list of files - return this.createStatus( - this.egw().lang("File is of wrong type (%1 != %2)!", file.file.type, this.options.mime), - file - ); - } - - }, - - /** - * Add in the request id - */ - beforeSend: function(form) { - var instance = this.getInstanceManager(); - - return { - request_id: instance.etemplate_exec_id, - widget_id: this.id - }; - }, - - /** - * Disables submit buttons while uploading - */ - onStart: function(event, file_count) { - // Hide any previous errors - this.hideMessage(); - - - event.data = this; - - //Add dropdown_progress - if (this.options.progress_dropdownlist) - { - this._build_progressDropDownList(); - } - - // Callback - if(this.options.onStart) return et2_call(this.options.onStart, event, file_count); - return true; - }, - - /** - * Re-enables submit buttons when done - */ - onFinish: function() { - this.disabled_buttons.attr("disabled", false).css('cursor','pointer').removeClass('et2_button_ro'); - - var file_count = this.resumable.files.length; - - // Remove files from list - while(this.resumable.files.length > 0) - { - this.resumable.removeFile(this.resumable.files[this.resumable.files.length -1]); - } - - var event = jQuery.Event('upload'); - - event.data = this; - - var result = false; - - //Remove progress_dropDown_fileList class and unbind the click handler from body - if (this.options.progress_dropdownlist) - { - this.progress.removeClass("progress_dropDown_fileList"); - jQuery(this.node).find('span').removeClass('totalProgress_loader'); - jQuery('body').off('click'); - } - - if(this.options.onFinish && !jQuery.isEmptyObject(this.getValue())) - { - result = et2_call(this.options.onFinish, event, file_count); - } - else - { - result = (file_count == 0 || !jQuery.isEmptyObject(this.getValue())); - } - if(result) - { - // Fire legacy change action when done - this.change(this.input); - } - }, - - /** - * Build up dropdown progress with total count indicator - * - * @todo Implement totalProgress bar instead of ajax-loader, in order to show how much percent of uploading is completed - */ - _build_progressDropDownList: function () - { - this.progress.addClass("progress_dropDown_fileList"); - - //Add uploading indicator and bind hover handler on it - jQuery(this.node).find('span').addClass('totalProgress_loader'); - - jQuery(this.node).find('span.et2_file_span').hover(function(){ - jQuery('.progress_dropDown_fileList').show(); - }); - //Bind click handler to dismiss the dropdown while uploading - jQuery('body').on('click', function(event){ - if (event.target.className != 'remove') - { - jQuery('.progress_dropDown_fileList').hide(); - } - }); - - }, - - /** - * Creates the elements used for displaying the file, and it's upload status, and - * attaches them to the DOM - * - * @param _event Either the event, or an error message - */ - createStatus: function(_event, file) { - var error = (typeof _event == "object" ? "" : _event); - - if(this.options.max_file_size && file.size > this.options.max_file_size) { - error = this.egw().lang("File too large. Maximum %1", et2_vfsSize.prototype.human_size(this.options.max_file_size)); - } - - if(this.options.progress) - { - var widget = this.getRoot().getWidgetById(this.options.progress); - if(widget) - { - this.progress = jQuery(widget.getDOMNode()); - this.progress.addClass("progress"); - } - } - if(this.progress) - { - var fileName = file.fileName || 'file'; - var status = jQuery("
  • "+fileName - +"

  • ") - .appendTo(this.progress); - jQuery("div.remove",status).on('click', file, jQuery.proxy(this.cancel,this)); - if(error != "") - { - status.addClass("message ui-state-error"); - status.append("
    "+error+""); - jQuery(".progressBar",status).css("display", "none"); - } - } - return error == ""; - }, - - _fileProgress: function(file) { - if(this.progress) - { - jQuery("li[data-file='"+file.fileName.replace(/'/g, '"')+"'] > span.progressBar > p").css("width", Math.ceil(file.progress()*100)+"%"); - - } - return true; - }, - - onError: function(event, name, error) { - console.warn(event,name,error); - }, - - /** - * A file upload is finished, update the UI - */ - finishUpload: function(file, response) { - var name = file.fileName || 'file'; - - if(typeof response == 'string') response = jQuery.parseJSON(response); - if(response.response[0] && typeof response.response[0].data.length == 'undefined') { - if(typeof this.options.value !== 'object' || !this.options.multiple) - { - this.set_value({}); - } - for(var key in response.response[0].data) { - if(typeof response.response[0].data[key] == "string") - { - // Message from server - probably error - jQuery("[data-file='"+name.replace(/'/g, '"')+"']",this.progress) - .addClass("error") - .css("display", "block") - .text(response.response[0].data[key]); - } - else - { - this.options.value[key] = response.response[0].data[key]; - // If not multiple, we already destroyed the status, so re-create it - if(!this.options.multiple) - { - this.createStatus({}, file); - } - if(this.progress) - { - jQuery("[data-file='"+name.replace(/'/g, '"')+"']",this.progress).addClass("message success"); - } - } - } - } - else if (this.progress) - { - jQuery("[data-file='"+name.replace(/'/g, '"')+"']",this.progress) - .addClass("ui-state-error") - .css("display", "block") - .text(this.egw().lang("Server error")); - } - var event = jQuery.Event('upload'); - - event.data = this; - - // Callback - if(this.options.onFinishOne) - { - return et2_call(this.options.onFinishOne,event,response,name); - } - return true; - }, - - /** - * Remove a file from the list of values - * - * @param {File|string} File object, or file name, to remove - */ - remove_file: function(file) - { - //console.info(filename); - if(typeof file == 'string') - { - file = {fileName: file}; - } - for(var key in this.options.value) - { - if(this.options.value[key].name == file.fileName) - { - delete this.options.value[key]; - jQuery('[data-file="'+file.fileName.replace(/'/g, '"')+'"]',this.node).remove(); - return; - } - } - if(file.isComplete && !file.isComplete() && file.cancel) file.cancel(); - }, - - /** - * Cancel a file - event callback - */ - cancel: function(e) - { - e.preventDefault(); - // Look for file name in list - var target = jQuery(e.target).parents("li"); - - this.remove_file(e.data); - - // In case it didn't make it to the list (error) - target.remove(); - jQuery(e.target).remove(); - }, - - /** - * Set readonly - * - * @param {boolean} _ro boolean readonly state, true means readonly - */ - set_readonly: function(_ro) - { - if (typeof _ro != "undefined") - { - this.options.readonly = _ro; - this.span.toggleClass('et2_file_ro',_ro); - if (this.options.readonly) - { - this.span.unbind('click'); - } - else - { - var self = this; - this.span.off().bind('click',function(){self.input.click()}); - } - } - } -});}).call(this); - -et2_register_widget(et2_file, ["file"]); - +var et2_file = /** @class */ (function (_super) { + __extends(et2_file, _super); + /** + * Constructor + * + * @memberOf et2_file + */ + function et2_file(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_file._attributes, _child || {})) || this; + _this.asyncOptions = {}; + _this.input = null; + _this.progress = null; + _this.span = null; + _this.node = null; + _this.input = null; + _this.progress = null; + _this.span = null; + // Contains all submit buttons need to be disabled during upload process + _this.disabled_buttons = jQuery("input[type='submit'], button"); + if (!_this.options.value) + _this.options.value = {}; + if (!_this.options.id) { + console.warn("File widget needs an ID. Used 'file_widget'."); + _this.options.id = "file_widget"; + } + // Legacy - id ending in [] means multiple + if (_this.options.id.substr(-2) == "[]") { + _this.options.multiple = true; + } + // If ID ends in /, it's a directory - allow multiple + else if (_this.options.id.substr(-1) === "/") { + _this.options.multiple = true; + _attrs.multiple = true; + } + // Set up the URL to have the request ID & the widget ID + var instance = _this.getInstanceManager(); + var self = _this; + _this.asyncOptions = jQuery.extend({ + // Callbacks + onStart: function (event, file_count) { + return self.onStart(event, file_count); + }, + onFinish: function (event, file_count) { + self.onFinish.apply(self, [event, file_count]); + }, + onStartOne: function (event, file_name, index, file_count) { + }, + onFinishOne: function (event, response, name, number, total) { return self.finishUpload(event, response, name, number, total); }, + onProgress: function (event, progress, name, number, total) { return self.onProgress(event, progress, name, number, total); }, + onError: function (event, name, error) { return self.onError(event, name, error); }, + beforeSend: function (form) { return self.beforeSend(form); }, + chunkSize: _this.options.chunk_size || 1024 * 1024, + target: egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\File::ajax_upload"), + query: function (file) { return self.beforeSend(file); }, + // Disable checking for already uploaded chunks + testChunks: false + }, _this.asyncOptions); + _this.asyncOptions.fieldName = _this.options.id; + _this.createInputWidget(); + _this.set_readonly(_this.options.readonly); + return _this; + } + et2_file.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.set_drop_target(null); + this.node = null; + this.input = null; + this.span = null; + this.progress = null; + }; + et2_file.prototype.createInputWidget = function () { + this.node = jQuery(document.createElement("div")).addClass("et2_file"); + this.span = jQuery(document.createElement("span")) + .addClass('et2_file_span et2_button') + .appendTo(this.node); + if (this.options.label != '') + this.span.addClass('et2_button_text'); + var span = this.span; + this.input = jQuery(document.createElement("input")) + .attr("type", "file").attr("placeholder", this.options.blur) + .addClass("et2_file_upload") + .appendTo(this.node) + .hover(function () { + jQuery(span) + .toggleClass('et2_file_spanHover'); + }) + .on({ + mousedown: function () { + jQuery(span).addClass('et2_file_spanActive'); + }, + mouseup: function () { + jQuery(span).removeClass('et2_file_spanActive'); + } + }); + if (this.options.accept) + this.input.attr('accept', this.options.accept); + var self = this; + // trigger native input upload file + if (!this.options.readonly) + this.span.click(function () { self.input.click(); }); + // Check for File interface, should fall back to normal form submit if missing + if (typeof File != "undefined" && typeof (new XMLHttpRequest()).upload != "undefined") { + this.resumable = new Resumable(this.asyncOptions); + this.resumable.assignBrowse(this.input); + this.resumable.on('fileAdded', jQuery.proxy(this._fileAdded, this)); + this.resumable.on('fileProgress', jQuery.proxy(this._fileProgress, this)); + this.resumable.on('fileSuccess', jQuery.proxy(this.finishUpload, this)); + this.resumable.on('complete', jQuery.proxy(this.onFinish, this)); + } + else { + // This may be a problem submitting via ajax + } + if (this.options.progress) { + var widget = this.getRoot().getWidgetById(this.options.progress); + if (widget) { + //may be not available at createInputWidget time + this.progress = jQuery(widget.getDOMNode()); + } + } + if (!this.progress) { + this.progress = jQuery(document.createElement("div")).appendTo(this.node); + } + this.progress.addClass("progress"); + if (this.options.multiple) { + this.input.attr("multiple", "multiple"); + } + this.setDOMNode(this.node[0]); + }; + /** + * Set a widget or DOM node as a HTML5 file drop target + * + * @param {string} new_target widget ID or DOM node ID to be used as a new target + */ + et2_file.prototype.set_drop_target = function (new_target) { + // Cancel old drop target + if (this.options.drop_target) { + var widget_1 = this.getRoot().getWidgetById(this.options.drop_target); + var drop_target_1 = widget_1 && widget_1.getDOMNode() || document.getElementById(this.options.drop_target); + if (drop_target_1) { + this.resumable.unAssignDrop(drop_target_1); + } + } + this.options.drop_target = new_target; + if (!this.options.drop_target) + return; + // Set up new drop target + var widget = this.getRoot().getWidgetById(this.options.drop_target); + var drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target); + if (drop_target) { + this.resumable.assignDrop([drop_target]); + } + else { + this.egw().debug("warn", "Did not find file drop target %s", this.options.drop_target); + } + }; + et2_file.prototype.attachToDOM = function () { + var res = _super.prototype.attachToDOM.call(this); + // Override parent's change, file widget will fire change when finished uploading + this.input.unbind("change.et2_inputWidget"); + return res; + }; + et2_file.prototype.getValue = function () { + return this.options.value ? this.options.value : this.input.val(); + }; + /** + * Set the value of the file widget. + * + * If you pass a FileList or list of files, it will trigger the async upload + * + * @param {FileList|File[]|false} value List of files to be uploaded, or false to reset. + * @param {Event} event Most browsers require the user to initiate file transfers in some way. + * Pass the event in, if you have it. + */ + et2_file.prototype.set_value = function (value, event) { + if (!value || typeof value == "undefined") { + value = {}; + } + if (jQuery.isEmptyObject(value)) { + this.options.value = {}; + if (this.resumable.progress() == 1) + this.progress.empty(); + // Reset the HTML element + this.input.wrap('').closest('form').get(0).reset(); + this.input.unwrap(); + return; + } + var addFile = jQuery.proxy(function (i, file) { + this.resumable.addFile(file, event); + }, this); + if (typeof value == 'object' && value.length && typeof value[0] == 'object' && value[0].name) { + try { + this.input[0].files = value; + jQuery.each(value, addFile); + } + catch (e) { + var self = this; + var args = arguments; + jQuery.each(value, addFile); + } + } + }; + /** + * Set the value for label + * The label is used as caption for span tag which customize the HTML file upload styling + * + * @param {string} value text value of label + */ + et2_file.prototype.set_label = function (value) { + if (this.span != null && value != null) { + this.span.text(value); + } + }; + et2_file.prototype.getInputNode = function () { + if (typeof this.input == 'undefined') + return false; + return this.input[0]; + }; + et2_file.prototype.set_mime = function (mime) { + if (!mime) { + this.options.mime = null; + } + if (mime.indexOf("/") != 0) { + // Lower case it now, if it's not a regex + this.options.mime = mime.toLowerCase(); + } + else { + // Convert into a js regex + var parts = mime.substr(1).match(/(.*)\/([igm]?)$/); + this.options.mime = new RegExp(parts[1], parts.length > 2 ? parts[2] : ""); + } + }; + et2_file.prototype.set_multiple = function (_multiple) { + this.options.multiple = _multiple; + if (_multiple) { + return this.input.attr("multiple", "multiple"); + } + return this.input.removeAttr("multiple"); + }; + /** + * Check to see if the provided file's mimetype matches + * + * @param f File object + * @return boolean + */ + et2_file.prototype.checkMime = function (f) { + // If missing, let the server handle it + if (!this.options.mime || !f.type) + return true; + var is_preg = (typeof this.options.mime == "object"); + if (!is_preg && f.type.toLowerCase() == this.options.mime || is_preg && this.options.mime.test(f.type)) { + return true; + } + // Not right mime + return false; + }; + et2_file.prototype._fileAdded = function (file, event) { + // Manual additions have no event + if (typeof event == 'undefined') { + event = {}; + } + // Trigger start of uploading, calls callback + if (!this.resumable.isUploading()) { + if (!(this.onStart(event, this.resumable.files.length))) + return; + } + // Here 'this' is the input + if (this.checkMime(file.file)) { + if (this.createStatus(event, file)) { + // Disable buttons + this.disabled_buttons + .not("[disabled]") + .attr("disabled", true) + .addClass('et2_button_ro') + .removeClass('et2_clickable') + .css('cursor', 'default'); + // Actually start uploading + this.resumable.upload(); + } + } + else { + // Wrong mime type - show in the list of files + return this.createStatus(this.egw().lang("File is of wrong type (%1 != %2)!", file.file.type, this.options.mime), file); + } + }; + /** + * Add in the request id + */ + et2_file.prototype.beforeSend = function (form) { + var instance = this.getInstanceManager(); + return { + request_id: instance.etemplate_exec_id, + widget_id: this.id + }; + }; + /** + * Disables submit buttons while uploading + */ + et2_file.prototype.onStart = function (event, file_count) { + // Hide any previous errors + this.hideMessage(); + event.data = this; + //Add dropdown_progress + if (this.options.progress_dropdownlist) { + this._build_progressDropDownList(); + } + // Callback + if (this.options.onStart) + return et2_call(this.options.onStart, event, file_count); + return true; + }; + /** + * Re-enables submit buttons when done + */ + et2_file.prototype.onFinish = function () { + this.disabled_buttons.attr("disabled", 0).css('cursor', 'pointer').removeClass('et2_button_ro'); + var file_count = this.resumable.files.length; + // Remove files from list + while (this.resumable.files.length > 0) { + this.resumable.removeFile(this.resumable.files[this.resumable.files.length - 1]); + } + var event = jQuery.Event('upload'); + event.data = this; + var result = false; + //Remove progress_dropDown_fileList class and unbind the click handler from body + if (this.options.progress_dropdownlist) { + this.progress.removeClass("progress_dropDown_fileList"); + jQuery(this.node).find('span').removeClass('totalProgress_loader'); + jQuery('body').off('click'); + } + if (this.options.onFinish && !jQuery.isEmptyObject(this.getValue())) { + result = et2_call(this.options.onFinish, event, file_count); + } + else { + result = (file_count == 0 || !jQuery.isEmptyObject(this.getValue())); + } + if (result) { + // Fire legacy change action when done + this.change(this.input); + } + }; + /** + * Build up dropdown progress with total count indicator + * + * @todo Implement totalProgress bar instead of ajax-loader, in order to show how much percent of uploading is completed + */ + et2_file.prototype._build_progressDropDownList = function () { + this.progress.addClass("progress_dropDown_fileList"); + //Add uploading indicator and bind hover handler on it + jQuery(this.node).find('span').addClass('totalProgress_loader'); + jQuery(this.node).find('span.et2_file_span').hover(function () { + jQuery('.progress_dropDown_fileList').show(); + }); + //Bind click handler to dismiss the dropdown while uploading + jQuery('body').on('click', function (event) { + if (event.target.className != 'remove') { + jQuery('.progress_dropDown_fileList').hide(); + } + }); + }; + /** + * Creates the elements used for displaying the file, and it's upload status, and + * attaches them to the DOM + * + * @param _event Either the event, or an error message + */ + et2_file.prototype.createStatus = function (_event, file) { + var error = (typeof _event == "object" ? "" : _event); + if (this.options.max_file_size && file.size > this.options.max_file_size) { + error = this.egw().lang("File too large. Maximum %1", et2_vfsSize.prototype.human_size(this.options.max_file_size)); + } + if (this.options.progress) { + var widget = this.getRoot().getWidgetById(this.options.progress); + if (widget) { + this.progress = jQuery(widget.getDOMNode()); + this.progress.addClass("progress"); + } + } + if (this.progress) { + var fileName = file.fileName || 'file'; + var status = jQuery("
  • " + fileName + + "

  • ") + .appendTo(this.progress); + jQuery("div.remove", status).on('click', file, jQuery.proxy(this.cancel, this)); + if (error != "") { + status.addClass("message ui-state-error"); + status.append("
    " + error + ""); + jQuery(".progressBar", status).css("display", "none"); + } + } + return error == ""; + }; + et2_file.prototype._fileProgress = function (file) { + if (this.progress) { + jQuery("li[data-file='" + file.fileName.replace(/'/g, '"') + "'] > span.progressBar > p").css("width", Math.ceil(file.progress() * 100) + "%"); + } + return true; + }; + et2_file.prototype.onError = function (event, name, error) { + console.warn(event, name, error); + }; + /** + * A file upload is finished, update the UI + */ + et2_file.prototype.finishUpload = function (file, response) { + var name = file.fileName || 'file'; + if (typeof response == 'string') + response = jQuery.parseJSON(response); + if (response.response[0] && typeof response.response[0].data.length == 'undefined') { + if (typeof this.options.value !== 'object' || !this.options.multiple) { + this.set_value({}); + } + for (var key in response.response[0].data) { + if (typeof response.response[0].data[key] == "string") { + // Message from server - probably error + jQuery("[data-file='" + name.replace(/'/g, '"') + "']", this.progress) + .addClass("error") + .css("display", "block") + .text(response.response[0].data[key]); + } + else { + this.options.value[key] = response.response[0].data[key]; + // If not multiple, we already destroyed the status, so re-create it + if (!this.options.multiple) { + this.createStatus({}, file); + } + if (this.progress) { + jQuery("[data-file='" + name.replace(/'/g, '"') + "']", this.progress).addClass("message success"); + } + } + } + } + else if (this.progress) { + jQuery("[data-file='" + name.replace(/'/g, '"') + "']", this.progress) + .addClass("ui-state-error") + .css("display", "block") + .text(this.egw().lang("Server error")); + } + var event = jQuery.Event('upload'); + event.data = this; + // Callback + if (this.options.onFinishOne) { + return et2_call(this.options.onFinishOne, event, response, name); + } + return true; + }; + /** + * Remove a file from the list of values + * + * @param {File|string} File object, or file name, to remove + */ + et2_file.prototype.remove_file = function (file) { + //console.info(filename); + if (typeof file == 'string') { + file = { fileName: file }; + } + for (var key in this.options.value) { + if (this.options.value[key].name == file.fileName) { + delete this.options.value[key]; + jQuery('[data-file="' + file.fileName.replace(/'/g, '"') + '"]', this.node).remove(); + return; + } + } + if (file.isComplete && !file.isComplete() && file.cancel) + file.cancel(); + }; + /** + * Cancel a file - event callback + */ + et2_file.prototype.cancel = function (e) { + e.preventDefault(); + // Look for file name in list + var target = jQuery(e.target).parents("li"); + this.remove_file(e.data); + // In case it didn't make it to the list (error) + target.remove(); + jQuery(e.target).remove(); + }; + /** + * Set readonly + * + * @param {boolean} _ro boolean readonly state, true means readonly + */ + et2_file.prototype.set_readonly = function (_ro) { + if (typeof _ro != "undefined") { + this.options.readonly = _ro; + this.span.toggleClass('et2_file_ro', _ro); + if (this.options.readonly) { + this.span.unbind('click'); + } + else { + var self = this; + this.span.off().bind('click', function () { self.input.click(); }); + } + } + }; + et2_file._attributes = { + "multiple": { + "name": "Multiple files", + "type": "boolean", + "default": false, + "description": "Allow the user to select more than one file to upload at a time. Subject to browser support." + }, + "max_file_size": { + "name": "Maximum file size", + "type": "integer", + "default": 0, + "description": "Largest file accepted, in bytes. Subject to server limitations. 8MB = 8388608" + }, + "mime": { + "name": "Allowed file types", + "type": "string", + "default": et2_no_init, + "description": "Mime type (eg: image/png) or regex (eg: /^text\//i) for allowed file types" + }, + "blur": { + "name": "Placeholder", + "type": "string", + "default": "", + "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." + }, + "progress": { + "name": "Progress node", + "type": "string", + "default": et2_no_init, + "description": "The ID of an alternate node (div) to display progress and results. The Node is fetched with et2 getWidgetById so you MUST use the id assigned in XET-File (it may not be available at creation time, so we (re)check on createStatus time)" + }, + "onStart": { + "name": "Start event handler", + "type": "any", + "default": et2_no_init, + "description": "A (js) function called when an upload starts. Return true to continue with upload, false to cancel." + }, + "onFinish": { + "name": "Finish event handler", + "type": "any", + "default": et2_no_init, + "description": "A (js) function called when all files to be uploaded are finished." + }, + drop_target: { + "name": "Optional, additional drop target for HTML5 uploads", + "type": "string", + "default": et2_no_init, + "description": "The ID of an additional drop target for HTML5 drag-n-drop file uploads" + }, + label: { + "name": "Label of file upload", + "type": "string", + "default": "Choose file...", + "description": "String caption to be displayed on file upload span" + }, + progress_dropdownlist: { + "name": "List on files in progress like dropdown", + "type": "boolean", + "default": false, + "description": "Style list of files in uploading progress like dropdown list with a total upload progress indicator" + }, + onFinishOne: { + "name": "Finish event handler for each one", + "type": "any", + "default": et2_no_init, + "description": "A (js) function called when a file to be uploaded is finished." + }, + accept: { + "name": "Acceptable extensions", + "type": "string", + "default": '', + "description": "Define types of files that the server accepts. Multiple types can be seperated by comma and the default is to accept everything." + }, + chunk_size: { + "name": "Chunk size", + "type": "integer", + "default": 1024 * 1024, + "description": "Max chunk size, gets set from server-side PHP (max_upload_size-1M)/2" // last chunk can be up to 2*chunk_size! + } + }; + return et2_file; +}(et2_core_inputWidget_1.et2_inputWidget)); +exports.et2_file = et2_file; +et2_core_widget_1.et2_register_widget(et2_file, ["file"]); +//# sourceMappingURL=et2_widget_file.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_file.ts b/api/js/etemplate/et2_widget_file.ts new file mode 100644 index 0000000000..29bd61fe31 --- /dev/null +++ b/api/js/etemplate/et2_widget_file.ts @@ -0,0 +1,740 @@ +/** + * EGroupware eTemplate2 - JS Number object + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2011 + * @version $Id$ + */ + +/*egw:uses + et2_core_inputWidget; + phpgwapi.Resumable.resumable; +*/ + +import {et2_inputWidget} from "./et2_core_inputWidget"; +import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; + +/** + * Class which implements file upload + * + * @augments et2_inputWidget + */ +export class et2_file extends et2_inputWidget +{ + static readonly _attributes : any = { + "multiple": { + "name": "Multiple files", + "type": "boolean", + "default": false, + "description": "Allow the user to select more than one file to upload at a time. Subject to browser support." + }, + "max_file_size": { + "name": "Maximum file size", + "type": "integer", + "default":0, + "description": "Largest file accepted, in bytes. Subject to server limitations. 8MB = 8388608" + }, + "mime": { + "name": "Allowed file types", + "type": "string", + "default": et2_no_init, + "description": "Mime type (eg: image/png) or regex (eg: /^text\//i) for allowed file types" + }, + "blur": { + "name": "Placeholder", + "type": "string", + "default": "", + "description": "This text get displayed if an input-field is empty and does not have the input-focus (blur). It can be used to show a default value or a kind of help-text." + }, + "progress": { + "name": "Progress node", + "type": "string", + "default": et2_no_init, + "description": "The ID of an alternate node (div) to display progress and results. The Node is fetched with et2 getWidgetById so you MUST use the id assigned in XET-File (it may not be available at creation time, so we (re)check on createStatus time)" + }, + "onStart": { + "name": "Start event handler", + "type": "any", + "default": et2_no_init, + "description": "A (js) function called when an upload starts. Return true to continue with upload, false to cancel." + }, + "onFinish": { + "name": "Finish event handler", + "type": "any", + "default": et2_no_init, + "description": "A (js) function called when all files to be uploaded are finished." + }, + drop_target: { + "name": "Optional, additional drop target for HTML5 uploads", + "type": "string", + "default": et2_no_init, + "description": "The ID of an additional drop target for HTML5 drag-n-drop file uploads" + }, + label: { + "name": "Label of file upload", + "type": "string", + "default": "Choose file...", + "description": "String caption to be displayed on file upload span" + }, + progress_dropdownlist: { + "name": "List on files in progress like dropdown", + "type": "boolean", + "default": false, + "description": "Style list of files in uploading progress like dropdown list with a total upload progress indicator" + }, + onFinishOne: { + "name": "Finish event handler for each one", + "type": "any", + "default": et2_no_init, + "description": "A (js) function called when a file to be uploaded is finished." + }, + accept: { + "name": "Acceptable extensions", + "type": "string", + "default": '', + "description": "Define types of files that the server accepts. Multiple types can be seperated by comma and the default is to accept everything." + }, + chunk_size: { + "name": "Chunk size", + "type": "integer", + "default": 1024*1024, + "description": "Max chunk size, gets set from server-side PHP (max_upload_size-1M)/2" // last chunk can be up to 2*chunk_size! + } + }; + + asyncOptions : any = {}; + input : JQuery = null; + progress : JQuery = null; + span : JQuery = null; + disabled_buttons : JQuery; + resumable : any; + + /** + * Constructor + * + * @memberOf et2_file + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_file._attributes, _child || {})); + + this.node = null; + this.input = null; + this.progress = null; + this.span = null; + // Contains all submit buttons need to be disabled during upload process + this.disabled_buttons = jQuery("input[type='submit'], button"); + if(!this.options.value) this.options.value = {}; + + if(!this.options.id) { + console.warn("File widget needs an ID. Used 'file_widget'."); + this.options.id = "file_widget"; + } + + // Legacy - id ending in [] means multiple + if(this.options.id.substr(-2) == "[]") + { + this.options.multiple = true; + } + // If ID ends in /, it's a directory - allow multiple + else if (this.options.id.substr(-1) === "/") + { + this.options.multiple = true; + _attrs.multiple = true; + } + + // Set up the URL to have the request ID & the widget ID + var instance = this.getInstanceManager(); + + let self = this; + + this.asyncOptions = jQuery.extend({ + // Callbacks + onStart: function(event, file_count) { + return self.onStart(event, file_count); + }, + onFinish: function(event, file_count) { + self.onFinish.apply(self, [event, file_count]) + }, + onStartOne: function(event, file_name, index, file_count) { + + }, + onFinishOne: function(event, response, name, number, total) { return self.finishUpload(event,response,name,number,total);}, + onProgress: function(event, progress, name, number, total) { return self.onProgress(event,progress,name,number,total);}, + onError: function(event, name, error) { return self.onError(event,name,error);}, + beforeSend: function(form) { return self.beforeSend(form);}, + + chunkSize: this.options.chunk_size || 1024*1024, + + target: egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\File::ajax_upload"), + query: function(file) {return self.beforeSend(file);}, + // Disable checking for already uploaded chunks + testChunks: false + },this.asyncOptions); + this.asyncOptions.fieldName = this.options.id; + this.createInputWidget(); + this.set_readonly(this.options.readonly); + } + + destroy() + { + super.destroy(); + this.set_drop_target(null); + this.node = null; + this.input = null; + this.span = null; + this.progress = null; + } + + createInputWidget() + { + this.node = jQuery(document.createElement("div")).addClass("et2_file"); + this.span = jQuery(document.createElement("span")) + .addClass('et2_file_span et2_button') + .appendTo (this.node); + if (this.options.label != '') this.span.addClass('et2_button_text'); + let span = this.span; + this.input = jQuery(document.createElement("input")) + .attr("type", "file").attr("placeholder", this.options.blur) + .addClass ("et2_file_upload") + .appendTo(this.node) + .hover(function() { + jQuery(span) + .toggleClass('et2_file_spanHover'); + }) + .on({ + mousedown:function (){ + jQuery(span).addClass('et2_file_spanActive'); + }, + mouseup:function (){ + jQuery(span).removeClass('et2_file_spanActive'); + } + }); + if (this.options.accept) this.input.attr('accept', this.options.accept); + let self = this; + // trigger native input upload file + if (!this.options.readonly) this.span.click(function(){self.input.click()}); + // Check for File interface, should fall back to normal form submit if missing + if(typeof File != "undefined" && typeof (new XMLHttpRequest()).upload != "undefined") + { + this.resumable = new Resumable(this.asyncOptions); + this.resumable.assignBrowse(this.input); + this.resumable.on('fileAdded', jQuery.proxy(this._fileAdded, this)); + this.resumable.on('fileProgress', jQuery.proxy(this._fileProgress, this)); + this.resumable.on('fileSuccess', jQuery.proxy(this.finishUpload, this)); + this.resumable.on('complete', jQuery.proxy(this.onFinish, this)); + } + else + { + // This may be a problem submitting via ajax + } + if(this.options.progress) + { + let widget = this.getRoot().getWidgetById(this.options.progress); + if(widget) + { + //may be not available at createInputWidget time + this.progress = jQuery(widget.getDOMNode()); + } + } + if(!this.progress) + { + this.progress = jQuery(document.createElement("div")).appendTo(this.node); + } + this.progress.addClass("progress"); + + if(this.options.multiple) + { + this.input.attr("multiple","multiple"); + } + + this.setDOMNode(this.node[0]); + } + + /** + * Set a widget or DOM node as a HTML5 file drop target + * + * @param {string} new_target widget ID or DOM node ID to be used as a new target + */ + set_drop_target(new_target : string) + { + // Cancel old drop target + if(this.options.drop_target) + { + let widget = this.getRoot().getWidgetById(this.options.drop_target); + let drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target); + if(drop_target) + { + this.resumable.unAssignDrop(drop_target); + } + } + + this.options.drop_target = new_target; + + if(!this.options.drop_target) return; + + // Set up new drop target + let widget = this.getRoot().getWidgetById(this.options.drop_target); + let drop_target = widget && widget.getDOMNode() || document.getElementById(this.options.drop_target); + if(drop_target) + { + this.resumable.assignDrop([drop_target]); + } + else + { + this.egw().debug("warn", "Did not find file drop target %s", this.options.drop_target); + } + + } + + attachToDOM() + { + let res = super.attachToDOM(); + // Override parent's change, file widget will fire change when finished uploading + this.input.unbind("change.et2_inputWidget"); + return res; + } + + getValue() + { + return this.options.value ? this.options.value : this.input.val(); + } + + /** + * Set the value of the file widget. + * + * If you pass a FileList or list of files, it will trigger the async upload + * + * @param {FileList|File[]|false} value List of files to be uploaded, or false to reset. + * @param {Event} event Most browsers require the user to initiate file transfers in some way. + * Pass the event in, if you have it. + */ + set_value(value, event?) : boolean + { + if (!value || typeof value == "undefined") { + value = {}; + } + if (jQuery.isEmptyObject(value)) { + this.options.value = {}; + if (this.resumable.progress() == 1) this.progress.empty(); + + // Reset the HTML element + this.input.wrap('').closest('form').get(0).reset(); + this.input.unwrap(); + + return; + } + + let addFile = jQuery.proxy(function (i, file) { + this.resumable.addFile(file, event); + }, this); + if (typeof value == 'object' && value.length && typeof value[0] == 'object' && value[0].name) { + try { + this.input[0].files = value; + + jQuery.each(value, addFile); + } catch (e) { + var self = this; + var args = arguments; + jQuery.each(value, addFile); + } + } + } + + /** + * Set the value for label + * The label is used as caption for span tag which customize the HTML file upload styling + * + * @param {string} value text value of label + */ + set_label(value) + { + if (this.span != null && value != null) + { + this.span.text(value); + } + } + + getInputNode() + { + if (typeof this.input == 'undefined') return false; + return this.input[0]; + } + + + set_mime(mime) + { + if(!mime) + { + this.options.mime = null; + } + if(mime.indexOf("/") != 0) + { + // Lower case it now, if it's not a regex + this.options.mime = mime.toLowerCase(); + } + else + { + // Convert into a js regex + var parts = mime.substr(1).match(/(.*)\/([igm]?)$/); + this.options.mime = new RegExp(parts[1],parts.length > 2 ? parts[2] : ""); + } + } + + set_multiple(_multiple) + { + this.options.multiple = _multiple; + if(_multiple) + { + return this.input.attr("multiple", "multiple"); + } + return this.input.removeAttr("multiple"); + } + + /** + * Check to see if the provided file's mimetype matches + * + * @param f File object + * @return boolean + */ + checkMime(f) + { + // If missing, let the server handle it + if(!this.options.mime || !f.type) return true; + + var is_preg = (typeof this.options.mime == "object"); + if(!is_preg && f.type.toLowerCase() == this.options.mime || is_preg && this.options.mime.test(f.type)) + { + return true; + } + + // Not right mime + return false; + } + + private _fileAdded(file,event) + { + // Manual additions have no event + if(typeof event == 'undefined') + { + event = {}; + } + // Trigger start of uploading, calls callback + if(!this.resumable.isUploading()) + { + if (!(this.onStart(event,this.resumable.files.length))) return; + } + + // Here 'this' is the input + if(this.checkMime(file.file)) + { + if(this.createStatus(event,file)) + { + + // Disable buttons + this.disabled_buttons + .not("[disabled]") + .attr("disabled", true) + .addClass('et2_button_ro') + .removeClass('et2_clickable') + .css('cursor', 'default'); + + // Actually start uploading + this.resumable.upload(); + } + } + else + { + // Wrong mime type - show in the list of files + return this.createStatus( + this.egw().lang("File is of wrong type (%1 != %2)!", file.file.type, this.options.mime), + file + ); + } + + } + + /** + * Add in the request id + */ + beforeSend(form) { + var instance = this.getInstanceManager(); + + return { + request_id: instance.etemplate_exec_id, + widget_id: this.id + }; + } + + /** + * Disables submit buttons while uploading + */ + onStart(event, file_count) { + // Hide any previous errors + this.hideMessage(); + + + event.data = this; + + //Add dropdown_progress + if (this.options.progress_dropdownlist) + { + this._build_progressDropDownList(); + } + + // Callback + if(this.options.onStart) return et2_call(this.options.onStart, event, file_count); + return true; + } + + /** + * Re-enables submit buttons when done + */ + onFinish() { + this.disabled_buttons.attr("disabled", 0).css('cursor','pointer').removeClass('et2_button_ro'); + + var file_count = this.resumable.files.length; + + // Remove files from list + while(this.resumable.files.length > 0) + { + this.resumable.removeFile(this.resumable.files[this.resumable.files.length -1]); + } + + var event = jQuery.Event('upload'); + + event.data = this; + + var result = false; + + //Remove progress_dropDown_fileList class and unbind the click handler from body + if (this.options.progress_dropdownlist) + { + this.progress.removeClass("progress_dropDown_fileList"); + jQuery(this.node).find('span').removeClass('totalProgress_loader'); + jQuery('body').off('click'); + } + + if(this.options.onFinish && !jQuery.isEmptyObject(this.getValue())) + { + result = et2_call(this.options.onFinish, event, file_count); + } + else + { + result = (file_count == 0 || !jQuery.isEmptyObject(this.getValue())); + } + if(result) + { + // Fire legacy change action when done + this.change(this.input); + } + } + + /** + * Build up dropdown progress with total count indicator + * + * @todo Implement totalProgress bar instead of ajax-loader, in order to show how much percent of uploading is completed + */ + private _build_progressDropDownList() + { + this.progress.addClass("progress_dropDown_fileList"); + + //Add uploading indicator and bind hover handler on it + jQuery(this.node).find('span').addClass('totalProgress_loader'); + + jQuery(this.node).find('span.et2_file_span').hover(function(){ + jQuery('.progress_dropDown_fileList').show(); + }); + //Bind click handler to dismiss the dropdown while uploading + jQuery('body').on('click', function(event){ + if (event.target.className != 'remove') + { + jQuery('.progress_dropDown_fileList').hide(); + } + }); + + } + + /** + * Creates the elements used for displaying the file, and it's upload status, and + * attaches them to the DOM + * + * @param _event Either the event, or an error message + */ + createStatus(_event, file) + { + var error = (typeof _event == "object" ? "" : _event); + + if(this.options.max_file_size && file.size > this.options.max_file_size) { + error = this.egw().lang("File too large. Maximum %1", et2_vfsSize.prototype.human_size(this.options.max_file_size)); + } + + if(this.options.progress) + { + var widget = this.getRoot().getWidgetById(this.options.progress); + if(widget) + { + this.progress = jQuery(widget.getDOMNode()); + this.progress.addClass("progress"); + } + } + if(this.progress) + { + var fileName = file.fileName || 'file'; + var status = jQuery("
  • "+fileName + +"

  • ") + .appendTo(this.progress); + jQuery("div.remove",status).on('click', file, jQuery.proxy(this.cancel,this)); + if(error != "") + { + status.addClass("message ui-state-error"); + status.append("
    "+error+""); + jQuery(".progressBar",status).css("display", "none"); + } + } + return error == ""; + } + + private _fileProgress(file) + { + if(this.progress) + { + jQuery("li[data-file='"+file.fileName.replace(/'/g, '"')+"'] > span.progressBar > p").css("width", Math.ceil(file.progress()*100)+"%"); + + } + return true; + } + + onError(event, name, error) + { + console.warn(event,name,error); + } + + /** + * A file upload is finished, update the UI + */ + finishUpload(file, response) + { + var name = file.fileName || 'file'; + + if(typeof response == 'string') response = jQuery.parseJSON(response); + if(response.response[0] && typeof response.response[0].data.length == 'undefined') { + if(typeof this.options.value !== 'object' || !this.options.multiple) + { + this.set_value({}); + } + for(var key in response.response[0].data) { + if(typeof response.response[0].data[key] == "string") + { + // Message from server - probably error + jQuery("[data-file='"+name.replace(/'/g, '"')+"']",this.progress) + .addClass("error") + .css("display", "block") + .text(response.response[0].data[key]); + } + else + { + this.options.value[key] = response.response[0].data[key]; + // If not multiple, we already destroyed the status, so re-create it + if(!this.options.multiple) + { + this.createStatus({}, file); + } + if(this.progress) + { + jQuery("[data-file='"+name.replace(/'/g, '"')+"']",this.progress).addClass("message success"); + } + } + } + } + else if (this.progress) + { + jQuery("[data-file='"+name.replace(/'/g, '"')+"']",this.progress) + .addClass("ui-state-error") + .css("display", "block") + .text(this.egw().lang("Server error")); + } + var event = jQuery.Event('upload'); + + event.data = this; + + // Callback + if(this.options.onFinishOne) + { + return et2_call(this.options.onFinishOne,event,response,name); + } + return true; + } + + /** + * Remove a file from the list of values + * + * @param {File|string} File object, or file name, to remove + */ + remove_file(file) + { + //console.info(filename); + if(typeof file == 'string') + { + file = {fileName: file}; + } + for(var key in this.options.value) + { + if(this.options.value[key].name == file.fileName) + { + delete this.options.value[key]; + jQuery('[data-file="'+file.fileName.replace(/'/g, '"')+'"]',this.node).remove(); + return; + } + } + if(file.isComplete && !file.isComplete() && file.cancel) file.cancel(); + } + + /** + * Cancel a file - event callback + */ + cancel(e) + { + e.preventDefault(); + // Look for file name in list + var target = jQuery(e.target).parents("li"); + + this.remove_file(e.data); + + // In case it didn't make it to the list (error) + target.remove(); + jQuery(e.target).remove(); + } + + /** + * Set readonly + * + * @param {boolean} _ro boolean readonly state, true means readonly + */ + set_readonly(_ro) + { + if (typeof _ro != "undefined") + { + this.options.readonly = _ro; + this.span.toggleClass('et2_file_ro',_ro); + if (this.options.readonly) + { + this.span.unbind('click'); + } + else + { + var self = this; + this.span.off().bind('click',function(){self.input.click()}); + } + } + } +} +et2_register_widget(et2_file, ["file"]); + + diff --git a/api/js/etemplate/et2_widget_selectAccount.js b/api/js/etemplate/et2_widget_selectAccount.js index 7d0be43110..7ce48e1396 100644 --- a/api/js/etemplate/et2_widget_selectAccount.js +++ b/api/js/etemplate/et2_widget_selectAccount.js @@ -35,6 +35,7 @@ var et2_core_widget_1 = require("./et2_core_widget"); var et2_core_inheritance_1 = require("./et2_core_inheritance"); var et2_widget_link_1 = require("./et2_widget_link"); var et2_widget_dialog_1 = require("./et2_widget_dialog"); +var et2_widget_link_2 = require("./et2_widget_link"); /** * Account selection widget * Changes according to the user's account_selection preference @@ -719,7 +720,7 @@ var et2_selectAccount_ro = /** @class */ (function (_super) { } }; return et2_selectAccount_ro; -}(et2_link_string)); +}(et2_widget_link_2.et2_link_string)); exports.et2_selectAccount_ro = et2_selectAccount_ro; et2_core_widget_1.et2_register_widget(et2_selectAccount_ro, ["select-account_ro"]); //# sourceMappingURL=et2_widget_selectAccount.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_selectAccount.ts b/api/js/etemplate/et2_widget_selectAccount.ts index 8cf6ae109b..879380fcc1 100644 --- a/api/js/etemplate/et2_widget_selectAccount.ts +++ b/api/js/etemplate/et2_widget_selectAccount.ts @@ -18,10 +18,11 @@ */ import {et2_selectbox} from "./et2_widget_selectbox"; -import {et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {et2_register_widget, WidgetConfig, et2_widget} from "./et2_core_widget"; import {ClassWithAttributes} from "./et2_core_inheritance"; import {et2_link_entry} from "./et2_widget_link"; import {et2_dialog} from "./et2_widget_dialog"; +import {et2_link_string} from "./et2_widget_link"; /** * Account selection widget @@ -788,7 +789,7 @@ export class et2_selectAccount_ro extends et2_link_string // Legacy options could have row count or empty label in first slot if(typeof this.options.empty_label == "string") { - if(isNaN(this.options.empty_label)) + if(isNaN(this.options.empty_label)) { this.options.empty_label = this.egw().lang(this.options.empty_label); } diff --git a/api/js/etemplate/et2_widget_textbox.js b/api/js/etemplate/et2_widget_textbox.js index 7e140dd2e6..8a857e875b 100644 --- a/api/js/etemplate/et2_widget_textbox.js +++ b/api/js/etemplate/et2_widget_textbox.js @@ -383,6 +383,7 @@ var et2_textbox_ro = /** @class */ (function (_super) { }; return et2_textbox_ro; }(et2_core_valueWidget_1.et2_valueWidget)); +exports.et2_textbox_ro = et2_textbox_ro; et2_core_widget_1.et2_register_widget(et2_textbox_ro, ["textbox_ro"]); /** * et2_searchbox is a widget which provides a collapsable input search diff --git a/api/js/etemplate/et2_widget_textbox.ts b/api/js/etemplate/et2_widget_textbox.ts index cb4f4504cd..d947485921 100644 --- a/api/js/etemplate/et2_widget_textbox.ts +++ b/api/js/etemplate/et2_widget_textbox.ts @@ -321,7 +321,7 @@ et2_register_widget(et2_textbox, ["textbox", "passwd", "hidden"]); * * @augments et2_valueWidget */ -class et2_textbox_ro extends et2_valueWidget implements et2_IDetachedDOM +export class et2_textbox_ro extends et2_valueWidget implements et2_IDetachedDOM { /** * Ignore all more advanced attributes. diff --git a/api/js/etemplate/et2_widget_vfs.js b/api/js/etemplate/et2_widget_vfs.js index 47cdcc6024..eb642645eb 100644 --- a/api/js/etemplate/et2_widget_vfs.js +++ b/api/js/etemplate/et2_widget_vfs.js @@ -1,3 +1,4 @@ +"use strict"; /** * EGroupware eTemplate2 - JS VFS widgets * @@ -8,1445 +9,1310 @@ * @author Nathan Gray * @copyright Nathan Gray 2012 */ - +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); /*egw:uses - /vendor/bower-asset/jquery/dist/jquery.js; - vfsSelectUI; - et2_core_inputWidget; - et2_core_valueWidget; - et2_widget_description; - et2_widget_file; - expose; + /vendor/bower-asset/jquery/dist/jquery.js; + vfsSelectUI; + et2_core_inputWidget; + et2_core_valueWidget; + et2_widget_description; + et2_widget_file; + expose; */ - +var et2_core_valueWidget_1 = require("./et2_core_valueWidget"); +var et2_core_widget_1 = require("./et2_core_widget"); +var et2_core_inheritance_1 = require("./et2_core_inheritance"); +var egw_action_js_1 = require("../egw_action/egw_action.js"); +var egw_keymanager_js_1 = require("../egw_action/egw_keymanager.js"); +var et2_widget_textbox_1 = require("./et2_widget_textbox"); +var et2_widget_description_1 = require("./et2_widget_description"); +var et2_widget_selectAccount_1 = require("./et2_widget_selectAccount"); +var et2_widget_file_1 = require("./et2_widget_file"); +var et2_widget_dialog_1 = require("./et2_widget_dialog"); +var et2_core_inputWidget_1 = require("./et2_core_inputWidget"); /** * Class which implements the "vfs" XET-Tag * * @augments et2_valueWidget */ -var et2_vfs = (function(){ "use strict"; return et2_valueWidget.extend([et2_IDetachedDOM], -{ - attributes: { - "value": { - "type": "any", // Object - "description": "Array of (stat) information about the file" - } - }, - - /** - * Mime type of directories - */ - DIR_MIME_TYPE: 'httpd/unix-directory', - - /** - * Constructor - * - * @memberOf et2_vfs - */ - init: function() { - this._super.apply(this, arguments); - - this.value = ""; - this.span = jQuery(document.createElement("ul")) - .addClass('et2_vfs'); - - this.setDOMNode(this.span[0]); - }, - - getValue: function() { - return this.value; - }, - - set_value: function(_value) { - if (typeof _value !== 'object') - { - // Only warn if it's an actual value, just blank for falsy values - if(_value) - { - this.egw().debug("warn", "%s only has path, needs full array", this.id, _value); - } - this.span.empty().text(_value); - return; - } - this.span.empty(); - this.value = _value; - var path = _value.path ? _value.path : '/'; - // calculate path as parent of name, which can contain slashes - // eg. _value.path=/home/ralf/sub/file, _value.name=sub/file --> path=/home/ralf - // --> generate clickable fields for sub/ + file - var sub_path = path.substring(0, _value.path.length-_value.name.length-1); - if(_value.path.indexOf(_value.name) >= 0 && sub_path[sub_path.length-1] === '/') - { - path = sub_path; - var path_offset = path.split('/').length; - var path_parts = _value.path.split('/'); - } - else - { - if(_value.path.indexOf(_value.name) >= 0) - { - // Remove name from end, so we can add it again later - path = sub_path; - } - var path_offset = 0; - var path_parts = _value.name.split('/'); - } - - var text; - for(var i = path_offset; i < path_parts.length; i++) - { - path += (path=='/'?'':'/')+path_parts[i]; - text = egw.decodePath(path_parts[i]); - - // Nice human-readable stuff for apps - if(path_parts[1] == 'apps') - { - switch(path_parts.length) - { - case 2: - if(i == 1) - { - text = this.egw().lang('applications'); - } - break; - case 3: - if( i == 2) - { - text = this.egw().lang(path_parts[2]); - } - break; - case 4: - if(!isNaN(text)) - { - var link_title = this.egw().link_title(path_parts[2],path_parts[3], - function(title) { - if(!title || this.value.name == title) return; - jQuery('li',this.span).last().text(title); - }, this - ); - if(link_title && typeof link_title !== 'undefined') text = link_title; - } - break; - } - } - var self = this; - var data = {path: path, type: i < path_parts.length-1 ? this.DIR_MIME_TYPE : _value.mime }; - var node = jQuery(document.createElement("li")) - .addClass("vfsFilename") - .text(text + (i < path_parts.length-1 ? '/' : '')) - //.attr('title', egw.decodePath(path)) - .addClass("et2_clickable et2_link") - .click({data:data, egw: this.egw()}, function(e) { - if(!self.onclick) { - e.data.egw.open(e.data.data, "file"); - } - else if (self.click(e)) - { - e.data.egw.open(e.data.data, "file"); - } - }) - .appendTo(this.span); - } - - // Last part of path do default action - this._bind_default_action(node, data); - - }, - - _bind_default_action: function(node, data) - { - var links = []; - var widget = this; - var defaultAction = null; - var object = null; - var app = this.getInstanceManager().app; - while(links.length === 0 && widget.getParent()) - { - object = egw_getAppObjectManager(app).getObjectById(widget.id); - if(object && object.manager && object.manager.children) - { - links = object.manager.children; - } - widget = widget.getParent(); - } - for (var k in links) - { - if (links[k].default && links[k].enabled.exec(links[k])) - { - defaultAction = links[k]; - break; - } - } - if(defaultAction && !this.onclick) - { - node.off('click').click({data:data, egw: this.egw()}, function(e) { - // Wait until object selection happens - window.setTimeout(function() { - // execute default action - egw_keyHandler(EGW_KEY_ENTER, false, false, false); - }); - // Select row - return true; - }.bind({data: data, object: object})); - } - }, - - /** - * Code for implementing et2_IDetachedDOM (data grid) - * - * @param {array} _attrs array of attribute-names to push further names onto - */ - getDetachedAttributes: function(_attrs) - { - _attrs.push("value"); - }, - - getDetachedNodes: function() - { - return [this.span[0]]; - }, - - setDetachedAttributes: function(_nodes, _values) - { - this.span = jQuery(_nodes[0]); - if(typeof _values["value"] != 'undefined') - { - this.set_value(_values["value"]); - } - } - - -});}).call(this); -et2_register_widget(et2_vfs, ["vfs"]); - +var et2_vfs = /** @class */ (function (_super) { + __extends(et2_vfs, _super); + /** + * Constructor + * + * @memberOf et2_vfs + */ + function et2_vfs(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_vfs._attributes, _child || {})) || this; + _this.span = null; + _this.value = ""; + _this.span = jQuery(document.createElement("ul")) + .addClass('et2_vfs'); + _this.setDOMNode(_this.span[0]); + return _this; + } + et2_vfs.prototype.getValue = function () { + return this.value; + }; + et2_vfs.prototype.set_value = function (_value) { + if (typeof _value !== 'object') { + // Only warn if it's an actual value, just blank for falsy values + if (_value) { + this.egw().debug("warn", "%s only has path, needs full array", this.id, _value); + } + this.span.empty().text(_value); + return; + } + this.span.empty(); + this.value = _value; + var path = _value.path ? _value.path : '/'; + // calculate path as parent of name, which can contain slashes + // eg. _value.path=/home/ralf/sub/file, _value.name=sub/file --> path=/home/ralf + // --> generate clickable fields for sub/ + file + var sub_path = path.substring(0, _value.path.length - _value.name.length - 1); + var path_offset, path_parts; + if (_value.path.indexOf(_value.name) >= 0 && sub_path[sub_path.length - 1] === '/') { + path = sub_path; + path_offset = path.split('/').length; + path_parts = _value.path.split('/'); + } + else { + if (_value.path.indexOf(_value.name) >= 0) { + // Remove name from end, so we can add it again later + path = sub_path; + } + path_offset = 0; + path_parts = _value.name.split('/'); + } + var text; + var _loop_1 = function (i) { + path += (path == '/' ? '' : '/') + path_parts[i]; + text = egw.decodePath(path_parts[i]); + // Nice human-readable stuff for apps + if (path_parts[1] == 'apps') { + switch (path_parts.length) { + case 2: + if (i == 1) { + text = this_1.egw().lang('applications'); + } + break; + case 3: + if (i == 2) { + text = this_1.egw().lang(path_parts[2]); + } + break; + case 4: + if (!isNaN(text)) { + var link_title = this_1.egw().link_title(path_parts[2], path_parts[3], function (title) { + if (!title || this.value.name == title) + return; + jQuery('li', this.span).last().text(title); + }, this_1); + if (link_title && typeof link_title !== 'undefined') + text = link_title; + } + break; + } + } + var self_1 = this_1; + data = { path: path, type: i < path_parts.length - 1 ? et2_vfs.DIR_MIME_TYPE : _value.mime }; + node = jQuery(document.createElement("li")) + .addClass("vfsFilename") + .text(text + (i < path_parts.length - 1 ? '/' : '')) + //.attr('title', egw.decodePath(path)) + .addClass("et2_clickable et2_link") + .click({ data: data, egw: this_1.egw() }, function (e) { + if (!self_1.onclick) { + e.data.egw.open(e.data.data, "file"); + } + else if (self_1.click(e)) { + e.data.egw.open(e.data.data, "file"); + } + }) + .appendTo(this_1.span); + }; + var this_1 = this, data, node; + for (var i = path_offset; i < path_parts.length; i++) { + _loop_1(i); + } + // Last part of path do default action + this._bind_default_action(node, data); + }; + et2_vfs.prototype._bind_default_action = function (node, data) { + var links = []; + var widget = this; + var defaultAction = null; + var object = null; + var app = this.getInstanceManager().app; + while (links.length === 0 && widget.getParent()) { + object = egw_action_js_1.egw_getAppObjectManager(app).getObjectById(widget.id); + if (object && object.manager && object.manager.children) { + links = object.manager.children; + } + widget = widget.getParent(); + } + for (var k in links) { + if (links[k].default && links[k].enabled.exec(links[k])) { + defaultAction = links[k]; + break; + } + } + if (defaultAction && !this.onclick) { + node.off('click').click({ data: data, egw: this.egw() }, function (e) { + // Wait until object selection happens + window.setTimeout(function () { + // execute default action + egw_keymanager_js_1.egw_keyHandler(EGW_KEY_ENTER, false, false, false); + }); + // Select row + return true; + }.bind({ data: data, object: object })); + } + }; + /** + * Code for implementing et2_IDetachedDOM (data grid) + * + * @param {array} _attrs array of attribute-names to push further names onto + */ + et2_vfs.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("value"); + }; + et2_vfs.prototype.getDetachedNodes = function () { + return [this.span[0]]; + }; + et2_vfs.prototype.setDetachedAttributes = function (_nodes, _values) { + this.span = jQuery(_nodes[0]); + if (typeof _values["value"] != 'undefined') { + this.set_value(_values["value"]); + } + }; + et2_vfs._attributes = { + "value": { + "type": "any", + "description": "Array of (stat) information about the file" + } + }; + /** + * Mime type of directories + */ + et2_vfs.DIR_MIME_TYPE = 'httpd/unix-directory'; + return et2_vfs; +}(et2_core_valueWidget_1.et2_valueWidget)); +et2_core_widget_1.et2_register_widget(et2_vfs, ["vfs"]); /** - * vfs-name - * filename automatically urlencoded on return (urldecoded on display to user) - * - * @augments et2_textbox - */ -var et2_vfsName = (function(){ "use strict"; return et2_textbox.extend( -{ - /** - * Constructor - * - * @memberOf et2_vfsName - */ - init: function() { - this._super.apply(this, arguments); - this.input.addClass("et2_vfs"); - }, - set_value: function(_value) { - if(_value.path) - { - _value = _value.path; - } - try - { - _value = egw.decodePath(_value); - } catch (e) - { - _value = 'Error! ' + _value; - } - this._super.apply(this,[_value]); - }, - getValue: function() { - return egw.encodePath(this._super.apply(this)||''); - } -});}).call(this); -et2_register_widget(et2_vfsName, ["vfs-name"]); - +* vfs-name +* filename automatically urlencoded on return (urldecoded on display to user) +* +* @augments et2_textbox +*/ +var et2_vfsName = /** @class */ (function (_super) { + __extends(et2_vfsName, _super); + /** + * Constructor + * + * @memberOf et2_vfsName + */ + function et2_vfsName(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_vfsName._attributes, _child || {})) || this; + _this.input.addClass("et2_vfs"); + return _this; + } + et2_vfsName.prototype.set_value = function (_value) { + if (_value.path) { + _value = _value.path; + } + try { + _value = egw.decodePath(_value); + } + catch (e) { + _value = 'Error! ' + _value; + } + _super.prototype.set_value.call(this, _value); + }; + et2_vfsName.prototype.getValue = function () { + return egw.encodePath(_super.prototype.getValue.call(this) || ''); + }; + return et2_vfsName; +}(et2_widget_textbox_1.et2_textbox)); +et2_core_widget_1.et2_register_widget(et2_vfsName, ["vfs-name"]); /** - * vfs-name - * filename automatically urlencoded on return (urldecoded on display to user) - * - * @augments et2_textbox - */ -var et2_vfsPath = (function(){ "use strict"; return et2_vfsName.extend( -{ - attributes: { - noicon: { - type: "boolean", - description: "suppress folder icons", - default: true - } - }, - - /** - * Constructor - * - * @memberOf et2_vfsName - */ - init: function() - { - this.div = jQuery(document.createElement("div")) - .addClass('et2_vfsPath'); - this.span = jQuery(document.createElement("ul")) - .appendTo(this.div); - this._super.apply(this, arguments); - }, - createInputWidget: function() - { - this._super.apply(this, arguments); - - this.div.prepend(this.input); - this.setDOMNode(this.div[0]); - this.span.on('wheel', function(e){ - var delta = e.originalEvent.deltaY > 0 ? 30 : -30; - this.scrollLeft = this.scrollLeft - delta; - }); - this.span.on('mouseover', function (e){ - if (this.scrollWidth > this.clientWidth) - { - jQuery(this).addClass('scrollable'); - } - else - { - jQuery(this).removeClass('scrollable'); - } - }); - this.input.on('focus', function() { - this.input.val(this.options.value); - this.span.hide(); - }.bind(this)) - .on('focusout', function() { - // Can't use show() because it uses the wrong display - this.span.css('display', 'flex'); - this.input.val(''); - }.bind(this)); - }, - change: function(_node) - { - if(this.input.val()) - { - this.set_value(this.input.val()); - } - return this._super.apply(this, arguments); - }, - - set_value: function(_value) - { - if(_value.path) - { - _value = _value.path; - } - if(_value === this.options.value && this._oldValue !== et2_no_init) return; - - var path_parts = _value.split('/'); - if(_value === '/') path_parts = ['']; - var path = "/"; - var text = ''; - this.span.empty().css('display', 'flex'); - this.input.val(''); - for(var i = 0; i < path_parts.length; i++) - { - path += (path=='/'?'':'/')+path_parts[i]; - text = egw.decodePath(path_parts[i]); - - var image = path=='/' ? this.egw().image('navbar','api') : this.egw().image(text); - - // Nice human-readable stuff for apps - if(path_parts[1] == 'apps') - { - if(i === 1) - { - text = this.egw().lang('applications'); - } - else if( i === 2) - { - text = this.egw().lang(path_parts[2]); - image = this.egw().image('navbar',path_parts[2].toLowerCase()); - } - else if(!isNaN(text)) - { - var link_title = this.egw().link_title(path_parts[2],path_parts[3], - function(title) { - if(!title) return; - jQuery('li',this.span).first().text(title); - }, this - ); - if(link_title && typeof link_title !== 'undefined') text = link_title; - } - - } - var self = this; - var node = jQuery(document.createElement("li")) - .addClass("vfsPath et2_clickable") - .text(text) - //.attr('title', egw.decodePath(path)) - .click({data:path, egw: this.egw()}, function(e) { - return self.set_value(e.data.data); - }) - .prependTo(this.span); - if(image && !this.options.noicon) - { - node.prepend(this.egw().image_element(image)); - } - jQuery(this.getDOMNode()).append(this.span); - } - - if(this.isAttached() && this.options.value !== _value) - { - this._oldValue = this.options.value; - this.options.value = _value; - this.change(); - } - }, - getValue: function() { - return this.options ? this.options.value : null; - } -});}).call(this); -et2_register_widget(et2_vfsPath, ["vfs-path"]); - +* vfs-name +* filename automatically urlencoded on return (urldecoded on display to user) +* +* @augments et2_textbox +*/ +var et2_vfsPath = /** @class */ (function (_super) { + __extends(et2_vfsPath, _super); + /** + * Constructor + * + * @memberOf et2_vfsName + */ + function et2_vfsPath(_parent, _attrs, _child) { + // Call the inherited constructor + return _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_vfsPath._attributes, _child || {})) || this; + } + et2_vfsPath.prototype.createInputWidget = function () { + _super.prototype.createInputWidget.call(this); + this.div = jQuery(document.createElement("div")) + .addClass('et2_vfsPath'); + this.span = jQuery(document.createElement("ul")) + .appendTo(this.div); + this.div.prepend(this.input); + this.setDOMNode(this.div[0]); + this.span.on('wheel', function (e) { + var delta = e.originalEvent["deltaY"] > 0 ? 30 : -30; + this.scrollLeft = this.scrollLeft - delta; + }); + this.span.on('mouseover', function (e) { + if (this.scrollWidth > this.clientWidth) { + jQuery(this).addClass('scrollable'); + } + else { + jQuery(this).removeClass('scrollable'); + } + }); + this.input.on('focus', function () { + this.input.val(this.options.value); + this.span.hide(); + }.bind(this)) + .on('focusout', function () { + // Can't use show() because it uses the wrong display + this.span.css('display', 'flex'); + this.input.val(''); + }.bind(this)); + }; + et2_vfsPath.prototype.change = function (_node) { + if (this.input.val()) { + this.set_value(this.input.val()); + } + return _super.prototype.change.call(this, _node); + }; + et2_vfsPath.prototype.set_value = function (_value) { + if (_value.path) { + _value = _value.path; + } + if (_value === this.options.value && this._oldValue !== et2_no_init) + return; + var path_parts = _value.split('/'); + if (_value === '/') + path_parts = ['']; + var path = "/"; + var text = ''; + if (this.span) + this.span.empty().css('display', 'flex'); + this.input.val(''); + var _loop_2 = function (i) { + path += (path == '/' ? '' : '/') + path_parts[i]; + text = egw.decodePath(path_parts[i]); + var image = path == '/' ? this_2.egw().image('navbar', 'api') : this_2.egw().image(text); + // Nice human-readable stuff for apps + if (path_parts[1] == 'apps') { + if (i === 1) { + text = this_2.egw().lang('applications'); + } + else if (i === 2) { + text = this_2.egw().lang(path_parts[2]); + image = this_2.egw().image('navbar', path_parts[2].toLowerCase()); + } + else if (!isNaN(text)) { + var link_title = this_2.egw().link_title(path_parts[2], path_parts[3], function (title) { + if (!title) + return; + jQuery('li', this.span).first().text(title); + }, this_2); + if (link_title && typeof link_title !== 'undefined') + text = link_title; + } + } + var self_2 = this_2; + var node = jQuery(document.createElement("li")) + .addClass("vfsPath et2_clickable") + .text(text) + //.attr('title', egw.decodePath(path)) + .click({ data: path, egw: this_2.egw() }, function (e) { + return self_2.set_value(e.data.data); + }) + .prependTo(this_2.span); + if (image && !this_2.options.noicon) { + node.prepend(this_2.egw().image_element(image)); + } + jQuery(this_2.getDOMNode()).append(this_2.span); + }; + var this_2 = this; + for (var i = 0; i < path_parts.length; i++) { + _loop_2(i); + } + if (this.isAttached() && this.options.value !== _value) { + this._oldValue = this.options.value; + this.options.value = _value; + this.change(); + } + }; + et2_vfsPath.prototype.getValue = function () { + return this.options ? this.options.value : null; + }; + et2_vfsPath._attributes = { + noicon: { + type: "boolean", + description: "suppress folder icons", + default: true + } + }; + return et2_vfsPath; +}(et2_vfsName)); +et2_core_widget_1.et2_register_widget(et2_vfsPath, ["vfs-path"]); /** - * vfs-name - * filename automatically urlencoded on return (urldecoded on display to user) - * - * @augments et2_textbox_ro - */ -var et2_vfsName_ro = (function(){ "use strict"; return et2_textbox_ro.extend( -{ - /** - * Constructor - * - * @memberOf et2_vfsName_ro - */ - init: function() { - this._super.apply(this, arguments); - }, - set_value: function(_value) { - if(_value.path) - { - _value = _value.path; - } - try - { - _value = egw.decodePath(_value); - } catch (e) - { - _value = 'Error! ' + _value; - } - this._super.apply(this,[_value]); - }, - getValue: function() { - return egw.encodePath(this._super.apply(this)); - } -});}).call(this); -et2_register_widget(et2_vfsName_ro, ["vfs-name_ro"]); - +* vfs-name +* filename automatically urlencoded on return (urldecoded on display to user) +* +* @augments et2_textbox_ro +*/ +var et2_vfsName_ro = /** @class */ (function (_super) { + __extends(et2_vfsName_ro, _super); + /** + * Constructor + * + * @memberOf et2_vfsName_ro + */ + /** + * Constructor + */ + function et2_vfsName_ro(_parent, _attrs, _child) { + // Call the inherited constructor + return _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_vfsName_ro._attributes, _child || {})) || this; + } + et2_vfsName_ro.prototype.set_value = function (_value) { + if (_value.path) { + _value = _value.path; + } + try { + _value = egw.decodePath(_value); + } + catch (e) { + _value = 'Error! ' + _value; + } + _super.prototype.set_value.call(this, _value); + }; + et2_vfsName_ro.prototype.getValue = function () { + return egw.encodePath(_super.prototype.getValue.call(this) || ''); + }; + return et2_vfsName_ro; +}(et2_widget_textbox_1.et2_textbox_ro)); +et2_core_widget_1.et2_register_widget(et2_vfsName_ro, ["vfs-name_ro"]); /** - * vfs-mime: icon for mimetype of file, or thumbnail - * incl. optional link overlay icon, if file is a symlink - * - * Creates following structure - * - * - * - * - * - * - * - * span.overlayContainer is optional and only generated for symlinks - * - * @augments et2_valueWidget - */ -var et2_vfsMime = (function(){ "use strict"; return expose(et2_valueWidget.extend([et2_IDetachedDOM], -{ - attributes: { - "value": { - "type": "any", // Object - "description": "Array of (stat) information about the file" - }, - "size": { - "name": "Icon size", - "type": "integer", - "description": "Size of icon / thumbnail, in pixels", - "default": et2_no_init - }, - "expose_callback":{ - "name": "expose_callback", - "type": "js", - "default": et2_no_init, - "description": "JS code which is executed when expose slides." - }, - expose_view: { - name: "Expose view", - type: "boolean", - default: true, - description: "Clicking on an image would popup an expose view" - }, - thumb_mime_size:{ - name: "Image thumbnail size", - type: "string", - default:"", - description:" Size of thumbnail in pixel for specified mime type with syntax of: mime_type(s),size (eg. image,video,128)" - } - - }, - - legacyOptions:["size"], - - /** - * Constructor - * - * @memberOf et2_vfsMime - */ - init: function() { - this._super.apply(this, arguments); - this.iconOverlayContainer = jQuery(document.createElement('span')).addClass('iconOverlayContainer'); - this.image = jQuery(document.createElement("img")); - this.image.addClass("et2_vfs vfsMimeIcon"); - this.iconOverlayContainer.append(this.image); - this.setDOMNode(this.iconOverlayContainer[0]); - }, - - /** - * Handler for expose slide action, from expose - * Returns data needed for the given index, or false to let expose handle it - * - * @param {Gallery} gallery - * @param {integer} index - * @param {DOMNode} slide - * @return {Array} array of objects consist of media contnet - */ - expose_onslide: function(gallery, index, slide) - { - var content = false; - if (this.options.expose_callback && typeof this.options.expose_callback == 'function') - { - //Call the callback to load more items - content = this.options.expose_callback.call(this,[gallery, index]); - if (content) this.add(content); - } - return content; - }, - - /** - * Function to get media content to feed the expose - * - * @param {type} _value - * @returns {Array} return an array of object consists of media content - */ - getMedia: function (_value) - { - var base_url = egw.webserverUrl.match(/^\//,'ig')?egw(window).window.location.origin + egw.webserverUrl:egw.webserverUrl; - var mediaContent = [{ - title: _value.name, - type: _value.mime, - href: _value.download_url - }]; - // check if download_url is not already an url (some stream-wrappers allow to specify that!) - if (_value.download_url && (_value.download_url[0] == '/' || _value.download_url.substr(0, 4) != 'http')) - { - mediaContent[0].href = base_url + _value.download_url; - - if (mediaContent[0].href && mediaContent[0].href.match(/\/webdav.php/,'ig')) - { - mediaContent[0].download_href = mediaContent[0].href + '?download'; - } - } - if (_value && _value.mime && _value.mime.match(/video\//,'ig')) - { - mediaContent[0].thumbnail = this.egw().mime_icon(_value.mime, _value.path, undefined, _value.mtime); - } - else - { - mediaContent[0].thumbnail = _value.path && _value.mime ? - this.egw().mime_icon(_value.mime, _value.path, undefined, _value.mtime) : - this.image.attr('src')+ '&thheight=128'; - } - return mediaContent; - }, - - set_value: function(_value) { - if (typeof _value !== 'object') - { - this.egw().debug("warn", "%s only has path, needs array with path & mime", this.id, _value); - // Keep going, will be 'unknown type' - } - var src = this.egw().mime_icon(_value.mime, _value.path, undefined, _value.mtime); - if(src) - { - // Set size of thumbnail - if(src.indexOf("thumbnail.php") > -1) - { - if(this.options.size) - { - src += "&thsize="+this.options.size; - } - else if (this.options.thumb_mime_size) - { - var mime_size = this.options.thumb_mime_size.split(','); - var mime_regex = RegExp(_value.mime.split('/')[0]); - if (typeof mime_size != 'undefined' && jQuery.isArray(mime_size) - && !isNaN(mime_size[mime_size.length-1]) && isNaN(mime_size[0]) && this.options.thumb_mime_size.match(mime_regex[0], 'ig')) - { - src += "&thsize=" + mime_size[mime_size.length-1]; - } - } - this.image.css("max-width", "100%"); - } - this.image.attr("src", src); - // tooltip for mimetypes with available detailed thumbnail - if (_value.mime && _value.mime.match(/application\/vnd\.oasis\.opendocument\.(text|presentation|spreadsheet|chart)/)) - { - var tooltip_target = this.image.parent().parent().parent().length > 0 ? - // Nextmatch row - this.image.parent().parent().parent() : - // Not in nextmatch - this.image.parent(); - tooltip_target.tooltip({ - items:"img", - position: {my:"right top", at:"left top", collision:"flipfit"}, - content: function(){ - return ''; - } - }); - } - } - // add/remove link icon, if file is (not) a symlink - if ((_value.mode & et2_vfsMode.prototype.types.l) == et2_vfsMode.prototype.types.l) - { - if (typeof this.overlayContainer == 'undefined') - { - - this.overlayContainer = jQuery(document.createElement('span')).addClass('overlayContainer'); - this.overlayContainer.append(jQuery(document.createElement('img')) - .addClass('overlay').attr('src', this.egw().image('link', 'etemplate'))); - this.iconOverlayContainer.append(this.overlayContainer); - } - } - else if (typeof this.overlayContainer != 'undefined') - { - this.overlayContainer.remove(); - delete this.overlayContainer; - } - }, - /** - * Implementation of "et2_IDetachedDOM" for fast viewing in gridview - * Override to add needed attributes - * - * @param {array} _attrs array of attribute-names to push further names onto - */ - getDetachedAttributes: function(_attrs) { - _attrs.push("value", "class"); - }, - getDetachedNodes: function() { - return [this.node, this.iconOverlayContainer[0], this.image[0]]; - }, - - setDetachedAttributes: function(_nodes, _values) { - this.iconOverlayContainer = jQuery(_nodes[1]); - this.image = jQuery(_nodes[2]); - this.node = _nodes[0]; - this.overlayContainer = _nodes[0].children[1]; - if(typeof _values['class'] != "undefined") { - this.image.addClass(_values['class']); - } - if(typeof _values['value'] != "undefined") { - this.set_value(_values['value']); - } - } -}));}).call(this); -et2_register_widget(et2_vfsMime, ["vfs-mime"]); - +* vfs-mime: icon for mimetype of file, or thumbnail +* incl. optional link overlay icon, if file is a symlink +* +* Creates following structure +* +* +* +* +* +* +* +* span.overlayContainer is optional and only generated for symlinks +* @todo implement mixin Expose class +* @augments et2_valueWidget +*/ +var et2_vfsMime = /** @class */ (function (_super) { + __extends(et2_vfsMime, _super); + /** + * Constructor + * + * @memberOf et2_vfsMime + */ + function et2_vfsMime(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_vfsMime._attributes, _child || {})) || this; + _this.legacyOptions = ["size"]; + _this.iconOverlayContainer = null; + _this.image = null; + _this.iconOverlayContainer = jQuery(document.createElement('span')).addClass('iconOverlayContainer'); + _this.image = jQuery(document.createElement("img")); + _this.image.addClass("et2_vfs vfsMimeIcon"); + _this.iconOverlayContainer.append(_this.image); + _this.setDOMNode(_this.iconOverlayContainer[0]); + return _this; + } + /** + * Handler for expose slide action, from expose + * Returns data needed for the given index, or false to let expose handle it + * + * @param {Gallery} gallery + * @param {integer} index + * @param {DOMNode} slide + * @return {Array} array of objects consist of media contnet + */ + et2_vfsMime.prototype.expose_onslide = function (gallery, index, slide) { + var content = false; + if (this.options.expose_callback && typeof this.options.expose_callback == 'function') { + //Call the callback to load more items + content = this.options.expose_callback.call(this, [gallery, index]); + if (content) + this.add(content); + } + return content; + }; + /** + * Function to get media content to feed the expose + * + * @param {type} _value + * @returns {Array} return an array of object consists of media content + */ + et2_vfsMime.prototype.getMedia = function (_value) { + var base_url = egw.webserverUrl.match(/^\/ig/) ? egw(window).window.location.origin + egw.webserverUrl : egw.webserverUrl; + var mediaContent = [{ + title: _value.name, + type: _value.mime, + href: _value.download_url + }]; + // check if download_url is not already an url (some stream-wrappers allow to specify that!) + if (_value.download_url && (_value.download_url[0] == '/' || _value.download_url.substr(0, 4) != 'http')) { + mediaContent[0].href = base_url + _value.download_url; + if (mediaContent[0].href && mediaContent[0].href.match(/\/webdav.php/, 'ig')) { + mediaContent[0]["download_href"] = mediaContent[0].href + '?download'; + } + } + if (_value && _value.mime && _value.mime.match(/video\//, 'ig')) { + mediaContent[0]["thumbnail"] = this.egw().mime_icon(_value.mime, _value.path, undefined, _value.mtime); + } + else { + mediaContent[0]["thumbnail"] = _value.path && _value.mime ? + this.egw().mime_icon(_value.mime, _value.path, undefined, _value.mtime) : + this.image.attr('src') + '&thheight=128'; + } + return mediaContent; + }; + et2_vfsMime.prototype.set_value = function (_value) { + if (typeof _value !== 'object') { + this.egw().debug("warn", "%s only has path, needs array with path & mime", this.id, _value); + // Keep going, will be 'unknown type' + } + var src = this.egw().mime_icon(_value.mime, _value.path, undefined, _value.mtime); + if (src) { + // Set size of thumbnail + if (src.indexOf("thumbnail.php") > -1) { + if (this.options.size) { + src += "&thsize=" + this.options.size; + } + else if (this.options.thumb_mime_size) { + var mime_size = this.options.thumb_mime_size.split(','); + var mime_regex = RegExp(_value.mime.split('/')[0]); + if (typeof mime_size != 'undefined' && jQuery.isArray(mime_size) + && !isNaN(mime_size[mime_size.length - 1]) && isNaN(mime_size[0]) && this.options.thumb_mime_size.match(mime_regex[0], 'ig')) { + src += "&thsize=" + mime_size[mime_size.length - 1]; + } + } + this.image.css("max-width", "100%"); + } + this.image.attr("src", src); + // tooltip for mimetypes with available detailed thumbnail + if (_value.mime && _value.mime.match(/application\/vnd\.oasis\.opendocument\.(text|presentation|spreadsheet|chart)/)) { + var tooltip_target = this.image.parent().parent().parent().length > 0 ? + // Nextmatch row + this.image.parent().parent().parent() : + // Not in nextmatch + this.image.parent(); + tooltip_target.tooltip({ + items: "img", + position: { my: "right top", at: "left top", collision: "flipfit" }, + content: function () { + return ''; + } + }); + } + } + // add/remove link icon, if file is (not) a symlink + if ((_value.mode & et2_vfsMode.types.l) == et2_vfsMode.types.l) { + if (typeof this.overlayContainer == 'undefined') { + this.overlayContainer = jQuery(document.createElement('span')).addClass('overlayContainer'); + this.overlayContainer.append(jQuery(document.createElement('img')) + .addClass('overlay').attr('src', this.egw().image('link', 'etemplate'))); + this.iconOverlayContainer.append(this.overlayContainer); + } + } + else if (typeof this.overlayContainer != 'undefined') { + this.overlayContainer.remove(); + delete this.overlayContainer; + } + }; + /** + * Implementation of "et2_IDetachedDOM" for fast viewing in gridview + * Override to add needed attributes + * + * @param {array} _attrs array of attribute-names to push further names onto + */ + et2_vfsMime.prototype.getDetachedAttributes = function (_attrs) { + _attrs.push("value", "class"); + }; + et2_vfsMime.prototype.getDetachedNodes = function () { + return [this.node, this.iconOverlayContainer[0], this.image[0]]; + }; + et2_vfsMime.prototype.setDetachedAttributes = function (_nodes, _values) { + this.iconOverlayContainer = jQuery(_nodes[1]); + this.image = jQuery(_nodes[2]); + this.node = _nodes[0]; + this.overlayContainer = _nodes[0].children[1]; + if (typeof _values['class'] != "undefined") { + this.image.addClass(_values['class']); + } + if (typeof _values['value'] != "undefined") { + this.set_value(_values['value']); + } + }; + et2_vfsMime._attributes = { + "value": { + "type": "any", + "description": "Array of (stat) information about the file" + }, + "size": { + "name": "Icon size", + "type": "integer", + "description": "Size of icon / thumbnail, in pixels", + "default": et2_no_init + }, + "expose_callback": { + "name": "expose_callback", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when expose slides." + }, + expose_view: { + name: "Expose view", + type: "boolean", + default: true, + description: "Clicking on an image would popup an expose view" + }, + thumb_mime_size: { + name: "Image thumbnail size", + type: "string", + default: "", + description: " Size of thumbnail in pixel for specified mime type with syntax of: mime_type(s),size (eg. image,video,128)" + } + }; + return et2_vfsMime; +}(et2_core_valueWidget_1.et2_valueWidget)); +et2_core_widget_1.et2_register_widget(et2_vfsMime, ["vfs-mime"]); /** - * vfs-size - * Human readable file sizes - * - * @augments et2_description - */ -var et2_vfsSize = (function(){ "use strict"; return et2_description.extend({ - attributes: { - "value": { - "type": "integer" - } - }, - /** - * Constructor - * - * @memberOf et2_vfsSize - */ - init: function() { - this._super.apply(this, arguments); - this.span.addClass("et2_vfs"); - }, - human_size: function(size) { - if(typeof size !== "number") - { - size = parseInt(size); - } - if(!size) - { - size = 0; - } - var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - var i = 0; - while(size >= 1024) - { - size /= 1024; - ++i; - } - return size.toFixed(i == 0 ? 0 : 1) + ' ' + units[i]; - }, - set_value: function(_value) { - if(_value.size) - { - _value = _value.size; - } - jQuery(this.node).text(this.human_size(_value)); - }, - setDetachedAttributes: function(_nodes, _values) - { - if(typeof _values["value"] !== "undefined") { - this.node = _nodes[0]; - this.set_value(_values["value"]); - delete _values["value"]; - } - this._super.apply(this, arguments); - } -});}).call(this); -et2_register_widget(et2_vfsSize, ["vfs-size"]); - - +* vfs-size +* Human readable file sizes +* +* @augments et2_description +*/ +var et2_vfsSize = /** @class */ (function (_super) { + __extends(et2_vfsSize, _super); + /** + * Constructor + * + * @memberOf et2_vfsSize + */ + function et2_vfsSize(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_vfsSize._attributes, _child || {})) || this; + _this.span.addClass("et2_vfs"); + return _this; + } + et2_vfsSize.prototype.human_size = function (size) { + if (typeof size !== "number") { + size = parseInt(size); + } + if (!size) { + size = 0; + } + var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + var i = 0; + while (size >= 1024) { + size /= 1024; + ++i; + } + return size.toFixed(i == 0 ? 0 : 1) + ' ' + units[i]; + }; + et2_vfsSize.prototype.set_value = function (_value) { + if (_value.size) { + _value = _value.size; + } + jQuery(this.node).text(this.human_size(_value)); + }; + et2_vfsSize.prototype.setDetachedAttributes = function (_nodes, _values) { + if (typeof _values["value"] !== "undefined") { + this.node = _nodes[0]; + this.set_value(_values["value"]); + delete _values["value"]; + } + _super.prototype.setDetachedAttributes.call(this, _nodes, _values); + }; + et2_vfsSize._attributes = { + "value": { + "type": "integer" + } + }; + return et2_vfsSize; +}(et2_widget_description_1.et2_description)); +et2_core_widget_1.et2_register_widget(et2_vfsSize, ["vfs-size"]); /** - * vfs-mode: textual representation of permissions + extra bits - * - * @augments et2_description - */ -var et2_vfsMode = (function(){ "use strict"; return et2_description.extend({ - // Masks for file types - types: { - 'l': 0xA000, // link - 's': 0xC000, // Socket - 'p': 0x1000, // FIFO pipe - 'c': 0x2000, // Character special - 'd': 0x4000, // Directory - 'b': 0x6000, // Block special - '-': 0x8000 // Regular - }, - // Sticky / UID / GID - sticky: [ - { mask: 0x200, "char": "T", position: 9 }, // Sticky - { mask: 0x400, "char": "S", position: 6 }, // sGID - { mask: 0x800, "char": "S", position: 3 } // SUID - ], - perms: { - 'x': 0x1, // Execute - 'w': 0x2, // Write - 'r': 0x4 // Read - }, - - /** - * Constructor - * - * @memberOf et2_vfsMode - */ - init: function() { - this._super.apply(this, arguments); - this.span.addClass("et2_vfs"); - }, - - /** - * Get text for file stuff - * Result will be like -rwxr--r--. First char is type, then read, write, execute (or other bits) for - * user, group, world - * - * @param {number} _value vfs mode - */ - text_mode: function(_value) { - var text = []; - if(typeof _value != "number") - { - _value = parseInt(_value); - } - if(!_value) return "----------"; - - // Figure out type - var type = 'u'; // unknown - for(var flag in this.types) - { - if((_value & this.types[flag]) == this.types[flag]) - { - type = flag; - break; - } - } - - // World, group, user - build string backwards - for(var i = 0; i < 3; i++) - { - for(var perm in this.perms) - { - if(_value & this.perms[perm]) - { - text.unshift(perm); - } - else - { - text.unshift("-"); - } - } - _value = _value >> 3; - } - // Sticky / UID / GID - for(var i = 0; i < this.sticky.length; i++) - { - if(this.sticky[i].mask & _value) - { - current = text[this.sticky[i].position]; - text[this.sticky[i].position] = this.sticky[i]["char"]; - if(current == 'x') text[this.sticky[i].position].toLowerCase(); - } - } - return type + text.join(''); - }, - set_value: function(_value) { - if(_value.size) - { - _value = _value.size; - } - var text = this.text_mode(_value); - jQuery(this.node).text(text); - }, - - setDetachedAttributes: function(_nodes, _values) - { - if(typeof _values["value"] !== "undefined") { - this.node = _nodes[0]; - this.set_value(_values["value"]); - delete _values["value"]; - } - this._super.apply(this, arguments); - } -});}).call(this); -et2_register_widget(et2_vfsMode, ["vfs-mode"]); - - +* vfs-mode: textual representation of permissions + extra bits +* +* @augments et2_description +*/ +var et2_vfsMode = /** @class */ (function (_super) { + __extends(et2_vfsMode, _super); + /** + * Constructor + * + * @memberOf et2_vfsMode + */ + function et2_vfsMode(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_vfsMode._attributes, _child || {})) || this; + _this.span.addClass("et2_vfs"); + return _this; + } + /** + * Get text for file stuff + * Result will be like -rwxr--r--. First char is type, then read, write, execute (or other bits) for + * user, group, world + * + * @param {number} _value vfs mode + */ + et2_vfsMode.prototype.text_mode = function (_value) { + var text = []; + if (typeof _value != "number") { + _value = parseInt(_value); + } + if (!_value) + return "----------"; + // Figure out type + var type = 'u'; // unknown + for (var flag in et2_vfsMode.types) { + if ((_value & et2_vfsMode.types[flag]) == et2_vfsMode.types[flag]) { + type = flag; + break; + } + } + // World, group, user - build string backwards + for (var i = 0; i < 3; i++) { + for (var perm in et2_vfsMode.perms) { + if (_value & et2_vfsMode.perms[perm]) { + text.unshift(perm); + } + else { + text.unshift("-"); + } + } + _value = _value >> 3; + } + // Sticky / UID / GID + for (var i = 0; i < et2_vfsMode.sticky.length; i++) { + if (et2_vfsMode.sticky[i].mask & _value) { + var current = text[et2_vfsMode.sticky[i].position]; + text[et2_vfsMode.sticky[i].position] = et2_vfsMode.sticky[i]["char"]; + if (current == 'x') + text[et2_vfsMode.sticky[i].position].toLowerCase(); + } + } + return type + text.join(''); + }; + et2_vfsMode.prototype.set_value = function (_value) { + if (_value.size) { + _value = _value.size; + } + var text = this.text_mode(_value); + jQuery(this.node).text(text); + }; + et2_vfsMode.prototype.setDetachedAttributes = function (_nodes, _values) { + if (typeof _values["value"] !== "undefined") { + this.node = _nodes[0]; + this.set_value(_values["value"]); + delete _values["value"]; + } + _super.prototype.setDetachedAttributes.call(this, _nodes, _values); + }; + // Masks for file types + et2_vfsMode.types = { + 'l': 0xA000, + 's': 0xC000, + 'p': 0x1000, + 'c': 0x2000, + 'd': 0x4000, + 'b': 0x6000, + '-': 0x8000 // Regular + }; + // Sticky / UID / GID + et2_vfsMode.sticky = [ + { mask: 0x200, "char": "T", position: 9 }, + { mask: 0x400, "char": "S", position: 6 }, + { mask: 0x800, "char": "S", position: 3 } // SUID + ]; + et2_vfsMode.perms = { + 'x': 0x1, + 'w': 0x2, + 'r': 0x4 // Read + }; + return et2_vfsMode; +}(et2_widget_description_1.et2_description)); +et2_core_widget_1.et2_register_widget(et2_vfsMode, ["vfs-mode"]); /** - * vfs-uid / vfs-gid: Displays the name for an ID. - * Same as read-only selectAccount, except if there's no user it shows "root" - * - * @augments et2_selectAccount_ro - */ -var et2_vfsUid = (function(){ "use strict"; return et2_selectAccount_ro.extend( -{ - /** - * @memberOf et2_vfsUid - * @param _node - * @param _value - */ - set_title: function(_node, _value) { - if(_value == "") - { - arguments[1] = "root"; - } - this._super.apply(this, arguments); - } -});}).call(this); -et2_register_widget(et2_vfsUid, ["vfs-uid","vfs-gid"]); - - +* vfs-uid / vfs-gid: Displays the name for an ID. +* Same as read-only selectAccount, except if there's no user it shows "root" +* +* @augments et2_selectAccount_ro +*/ +var et2_vfsUid = /** @class */ (function (_super) { + __extends(et2_vfsUid, _super); + function et2_vfsUid() { + return _super !== null && _super.apply(this, arguments) || this; + } + /** + * @memberOf et2_vfsUid + * @param _node + * @param _value + */ + et2_vfsUid.prototype.set_title = function (_node, _value) { + if (_value == "") { + arguments[1] = "root"; + } + _super.prototype.set_title.call(this, _node, _value); + }; + return et2_vfsUid; +}(et2_widget_selectAccount_1.et2_selectAccount_ro)); +et2_core_widget_1.et2_register_widget(et2_vfsUid, ["vfs-uid", "vfs-gid"]); /* vfs-upload aka VFS file: displays either download and delete (x) links or a file upload - * + ID is either a vfs path or colon separated $app:$id:$relative_path, eg: infolog:123:special/offer - * + if empty($id) / new entry, file is created in a hidden temporary directory in users home directory - * and calling app is responsible to move content of that dir to entry directory, after entry is saved - * + option: required mimetype or regular expression for mimetype to match, eg. '/^text\//i' for all text files - * + if path ends in a slash, multiple files can be uploaded, their original filename is kept then - * - * @augments et2_file - */ -var et2_vfsUpload = (function(){ "use strict"; return et2_file.extend( -{ - attributes: { - "value": { - "type": "any" // Either nothing, or an object with file info - }, - "path": { - "name": "Path", - "description": "Upload files to the specified VFS path", - "type": "string", - "default": '' - } - }, - legacyOptions: ["mime"], - - asyncOptions: { - target: egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_upload") - }, - - /** - * Constructor - * - * @param _parent - * @param attrs - * @memberof et2_vfsUpload - */ - init: function(_parent, attrs) { - this._super.apply(this, arguments); - jQuery(this.node).addClass("et2_vfs"); - - if(!this.options.path) - { - this.options.path = this.options.id; - } - // If the path is a directory, allow multiple uploads - if(this.options.path.substr(-1) == '/') - { - this.set_multiple(true); - } - this.list = jQuery(document.createElement('table')).appendTo(this.node); - }, - - /** - * If there is a file / files in the specified location, display them - * Value is the information for the file[s] in the specified location. - * - * @param {Object[]} _value - */ - set_value: function(_value) { - // Remove previous - while(this._children.length > 0) - { - var node = this._children[this._children.length-1]; - this.removeChild(node); - node.free(); - } - this.progress.empty(); - this.list.empty(); - - // Set new - if(typeof _value == 'object' && _value && _value.length) - { - for(var i = 0; i < _value.length; i++) - { - this._addFile(_value[i]); - } - } - }, - - getDOMNode: function(sender) { - if(sender !== this && sender._type.indexOf('vfs') >= 0 ) - { - var value = sender.getValue && sender.getValue() || sender.options.value || {}; - var row = jQuery("[data-path='"+(value.path.replace(/'/g, '"'))+"']",this.list); - if(sender._type === 'vfs-mime') - { - return jQuery('.icon',row).get(0) || null; - } - else - { - return jQuery('.title',row).get(0) || null; - } - } - else - { - return this._super.apply(this, arguments); - } - }, - - /** - * Add in the request id - * - * @param {type} form - */ - beforeSend: function(form) - { - var instance = this.getInstanceManager(); - - var extra = this._super.apply(this, arguments); - extra.path = this.options.path; - return extra; - }, - - /** - * A file upload is finished, update the UI - * - * @param {object} file - * @param {string|object} response - */ - finishUpload: function(file, response) { - var result = this._super.apply(this, arguments); - - if(typeof response == 'string') response = jQuery.parseJSON(response); - if(response.response[0] && typeof response.response[0].data.length == 'undefined') { - for(var key in response.response[0].data) { - var value = response.response[0].data[key]; - if(value && value.path) - { - this._addFile(value); - jQuery("[data-file='"+file.fileName.replace(/'/g, '"')+"']",this.progress).hide(); - } - } - } - return result; - }, - - _addFile: function(file_data) { - if(jQuery("[data-path='"+file_data.path.replace(/'/g, '"')+"']").remove().length) - { - for(var child_index = this._children.length-1; child_index >= 0; child_index--) - { - var child = this._children[child_index]; - if(child.options.value.path === file_data.path) - { - this.removeChild(child); - child.free(); - } - } - } - var row = jQuery(document.createElement("tr")) - .attr("data-path", file_data.path.replace(/'/g, '"')) - .attr("draggable", "true") - .appendTo(this.list); - var mime = jQuery(document.createElement("td")) - .addClass('icon') - .appendTo(row); - - var title = jQuery(document.createElement("td")) - .addClass('title') - .appendTo(row); - var mime = et2_createWidget('vfs-mime',{value: file_data},this); - var vfs = et2_createWidget('vfs', {value: file_data}, this); - - // If already attached, need to do this explicitly - if(this.isAttached()) - { - mime.set_value(file_data); - vfs.set_value(file_data); - mime.doLoadingFinished(); - vfs.doLoadingFinished(); - } - - // Add in delete button - if (!this.options.readonly) - { - var self = this; - var delete_button = jQuery(document.createElement("td")) - .appendTo(row); - jQuery("
    ") - .appendTo(delete_button) - // We don't use ui-icon because it assigns a bg image - .addClass("delete icon") - .bind( 'click', function() { - et2_createWidget("dialog", { - callback: function(button) { - if(button == et2_dialog.YES_BUTTON) - { - egw.json("filemanager_ui::ajax_action", [ - 'delete', - [row.attr('data-path').replace(/"/g, "'")], - '' - ], - function(data) { - if(data && data.errs == 0) {row.slideUp(row.remove);} - if(data && data.msg) { - self.egw().message(data.msg, data.errs == 0 ? 'success' : 'error'); - } - } - ).sendRequest(); - } - }, - message: self.egw().lang('Delete file')+'?', - title: self.egw().lang('Confirmation required'), - buttons: et2_dialog.BUTTONS_YES_NO, - dialog_type: et2_dialog.QUESTION_MESSAGE, - width: 250 - }, self); - }); - } - } -});}).call(this); -et2_register_widget(et2_vfsUpload, ["vfs-upload"]); - - -var et2_vfsSelect = (function(){ "use strict"; return et2_inputWidget.extend( -{ - // Allowed mode options - modes: ['open','open-multiple','saveas','select-dir'], - - attributes: { - "mode": { - name: "Dialog mode", - type: "string", - description: "One of {open|open-multiple|saveas|select-dir}", - default: "open-multiple" - }, - "method": { - name: "Server side callback", - type: "string", - description: "Server side callback to process selected value(s) in \n\ - app.class.method or class::method format. The first parameter will \n\ - be Method ID, the second the file list. 'download' is reserved and it \n\ - means it should use download_baseUrl instead of path in value (no method\n\ - will be actually executed)." - }, - "method_id": { - name: "Method ID", - type: "any", - description: "optional parameter passed to server side callback.\n\ - Can be a string or a function.", - default: "" - }, - "path": { - name: "Path", - type: "string", - description:"Start path in VFS. Leave unset to use the last used path." - }, - "mime": { - name: "Mime type", - type: "any", - description: "Limit display to the given mime-type" - }, - "button_label": { - name: "Button label", - description: "Set the label on the dialog's OK button.", - default: "open" - }, - "value": { - type: "any", // Object - description: "Array of paths (strings)" - }, - "button_caption":{ - name: "button caption", - type: "string", - default: "Select files from Filemanager ...", - description: "Caption for vfs-select button.", - translate:true - }, - "button_icon":{ - name: "button icon", - type: "string", - default: "check", - description: "Custom icon to show on submit button." - }, - "name": { - name:"File name", - type: "any", // Object - description: "file name", - default: "" - }, - "dialog_title":{ - name: "dialog title", - type: "string", - default: "Save as", - description: "Title of dialog", - translate:true - }, - "extra_buttons": { - name: "extra action buttons", - type: "any", - description: "Extra buttons passed to dialog. It's co-related to method." - } - }, - - /** - * Constructor - * - * @param _parent - * @param _attrs - * @memberOf et2_vfsSelect - */ - init: function(_parent, _attrs) { - // _super.apply is responsible for the actual setting of the params (some magic) - this._super.apply(this, arguments); - - // Allow no child widgets - this.supportedWidgetClasses = []; - - this.button = jQuery(document.createElement("button")) - .attr("title", this.egw().lang("Select file(s) from VFS")) - .addClass("et2_button et2_vfs_btn") - .css("background-image","url("+this.egw().image("filemanager/navbar")+")"); - - if(this.options.readonly) - { - this.button.hide(); - } - - if (this.options.button_caption != "") - { - this.button.text(this.options.button_caption); - } - this.setDOMNode(this.button[0]); - }, - - _content: function (_content, _callback) - { - egw(window).loading_prompt('vfs-select', true, '', 'body'); - var self = this; - if (typeof app.vfsSelectUI !="undefined") - { - if (this.dialog && this.dialog.div) this.dialog.div.dialog('close'); - delete app.vfsSelectUI; - } - var attrs = { - mode: this.options.mode, - label: this.options.button_label, - path: this.options.path || null, - mime: this.options.mime || null, - name: this.options.name, - method: this.options.method - }; - attrs.recentPaths = this._getRecentPaths(); - var callback = _callback || this._buildDialog; - egw(window).json( - 'EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_vfsSelect_content', - [_content, attrs], - function(_content){ - egw(window).loading_prompt('vfs-select', false); - callback.apply(self, arguments); - } - ).sendRequest(true); - }, - - /** - * Builds file navigator dialog - * - * @param {object} _data content - */ - _buildDialog: function (_data) - { - if (!_data.content.mode.match(/open|open-multiple|saveas|select-dir/)) { - egw.debug('Mode is not matched!'); - return; - } - var self = this; - var buttons = [ - { - text: egw.lang(_data.content.label), - id:"submit", - image: _data.content.mode.match(/saveas|select-dir/) ? "save" : this.options.button_icon - } - ]; - if (this.options.extra_buttons && this.options.method) - { - var extra_buttons_action = []; - for (var i=0; i < this.options.extra_buttons.length; i++) - { - delete(this.options.extra_buttons[i]['click']); - buttons.push(this.options.extra_buttons[i]); - extra_buttons_action[this.options.extra_buttons[i]['id']] = this.options.extra_buttons[i]['id']; - } - - } - buttons.push({text: egw.lang("Close"), id:"close", image:"cancel"}); - var data = jQuery.extend(_data, {'currentapp': egw(window).app_name()}); - - // define a mini app object for vfs select UI - app.vfsSelectUI = new app.classes.vfsSelectUI; - - // callback for dialog - this.submit_callback = function(submit_button_id, submit_value, savemode) - { - if ((submit_button_id == 'submit' || (extra_buttons_action && extra_buttons_action[submit_button_id])) && submit_value) - { - var files = []; - switch(_data.content.mode) - { - case 'open-multiple': - if (submit_value.dir && submit_value.dir.selected) - { - for(var key in Object.keys(submit_value.dir.selected)) - { - if (submit_value.dir.selected[key] != "") - { - files.push(submit_value.path+'/'+submit_value.dir.selected[key]); - } - } - } - break; - case 'select-dir': - files = submit_value.path; - break; - default: - if (self.options.method === 'download') submit_value.path = _data.content.download_baseUrl; - files = submit_value.path+'/'+submit_value.name; - if (self.options.mode === 'saveas' && !savemode) - { - for(var p in _data.content.dir) - { - if (_data.content.dir[p]['name'] == submit_value.name) - { - var saveModeDialogButtons = [ - {text: self.egw().lang("Yes"), id: "overwrite", class: "ui-priority-primary", "default": true, image: 'check'}, - {text: self.egw().lang("Rename"), id:"rename", image: 'edit'}, - {text: self.egw().lang("Cancel"), id:"cancel"} - ]; - return et2_dialog.show_prompt(function(_button_id, _value) { - switch (_button_id) - { - case "overwrite": - return self.submit_callback(submit_button_id, submit_value, 'overwrite'); - case "rename": - submit_value.name = _value; - return self.submit_callback(submit_button_id, submit_value, 'rename'); - } - }, - self.egw().lang('Do you want to overwrite existing file %1 in directory %2?', submit_value.name, submit_value.path), - self.egw().lang('File %1 already exists', submit_value.name), - submit_value.name, saveModeDialogButtons); - } - } - } - break; - } - self._setRecentPaths(submit_value.path); - self.value = files; - if (self.options.method && self.options.method !== 'download') - { - egw(window).json( - self.options.method, - [self.options.method_id, files, submit_button_id, savemode], - function(){ - jQuery(self.node).change(); - } - ).sendRequest(true); - } - else - { - jQuery(self.node).change(); - } - delete app.vfsSelectUI; - return true; - } - }; - - this.dialog = et2_createWidget("dialog", - { - callback: this.submit_callback, - title: this.options.dialog_title, - buttons: buttons, - minWidth: 500, - minHeight: 400, - width:400, - value: data, - template: egw.webserverUrl+'/api/templates/default/vfsSelectUI.xet?1', - resizable: false - }, et2_dialog._create_parent('api')); - this.dialog.template.uniqueId = 'api.vfsSelectUI'; - - // Don't rely only on app_name to fetch et2 object as app_name may not - // always represent current app of the window, e.g.: mail admin account. - // Try to fetch et2 from its template name. - var etemplate = jQuery('form').data('etemplate'); - var et2 = {}; - if (etemplate && etemplate.name && !app[egw(window).app_name()]) - { - et2 = etemplate2.getByTemplate(etemplate.name)[0]; - } - else - { - et2 = etemplate2.getByApplication(egw(window).app_name())[0]; - } - // we need an etemplate_exec_id for better handling serverside parts of - // widgets and since we can not have a etemplate_exec_id specifically - // for dialog template our best shot is to inherit its parent etemplate_exec_id. - this.dialog.template.etemplate_exec_id = et2.etemplate_exec_id; - - app.vfsSelectUI.et2 = this.dialog.template.widgetContainer; - // Keep the dialog always at the top - this.dialog.div.parent().css({"z-index": 100000}); - app.vfsSelectUI.vfsSelectWidget = this; - this.dialog.div.on('load', function(e) { - app.vfsSelectUI.et2_ready(app.vfsSelectUI.et2, 'api.vfsSelectUI'); - }); - }, - - /** - * Set recent path into sessionStorage - * @param {string} _path - */ - _setRecentPaths: function (_path) - { - var recentPaths = egw.getSessionItem('api', 'vfsRecentPaths') ? - egw.getSessionItem('api', 'vfsRecentPaths').split(',') : []; - if (recentPaths.indexOf(_path) == -1) recentPaths.push(_path); - egw.setSessionItem('api', 'vfsRecentPaths', recentPaths); - }, - - /** - * Get recent paths from sessionStorage - * @returns {Array} returns an array of recent paths - */ - _getRecentPaths: function () - { - return egw.getSessionItem('api', 'vfsRecentPaths') ? - egw.getSessionItem('api', 'vfsRecentPaths').split(',') : []; - }, - - /** - * click handler - * @param {event object} e - */ - click: function(e) { - this._content.call(this, null); - }, - - /** - * Set the dialog's mode. - * Valid options are in et2_vfsSelect.modes - * - * @param {string} mode 'open', 'open-multiple', 'saveas' or 'select-dir' - */ - set_mode: function(mode) { - // Check mode - if(jQuery.inArray(mode, this.modes) < 0) - { - this.egw().debug("warn", "Invalid mode for '%s': %s Valid options:", this.id,mode, this.modes); - return; - } - this.options.mode = mode; - }, - - /** - * Set the label on the dialog's OK button. - * - * @param {string} label - */ - set_button_label: function(label) - { - this.options.button_label = label; - }, - - /** - * Set the caption for vfs-select button - * - * @param {string} caption string value as a caption - */ - set_button_caption: function (caption) - { - this.options.button_caption = caption; - }, - - /** - * Set the ID passed to the server side callback - * - * @param {string} id - */ - set_method_id: function(id) { - this.options.method_id = id; - }, - - set_readonly: function(readonly) { - this.options.readonly = Boolean(readonly); - - if(this.options.readonly) - { - this.button.hide(); - } - else - { - this.button.show(); - } - }, - - set_value: function(value) { - this.value = value; - }, - - getValue: function() { - return this.value; - } -});}).call(this); -et2_register_widget(et2_vfsSelect, ["vfs-select"]); +* + ID is either a vfs path or colon separated $app:$id:$relative_path, eg: infolog:123:special/offer +* + if empty($id) / new entry, file is created in a hidden temporary directory in users home directory +* and calling app is responsible to move content of that dir to entry directory, after entry is saved +* + option: required mimetype or regular expression for mimetype to match, eg. '/^text\//i' for all text files +* + if path ends in a slash, multiple files can be uploaded, their original filename is kept then +* +* @augments et2_file +*/ +var et2_vfsUpload = /** @class */ (function (_super) { + __extends(et2_vfsUpload, _super); + /** + * Constructor + * + * @param _parent + * @param attrs + * @memberof et2_vfsUpload + */ + function et2_vfsUpload(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_vfsUpload._attributes, _child || {})) || this; + _this.legacyOptions = ["mime"]; + _this.asyncOptions = { + target: egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_upload") + }; + _this.list = null; + jQuery(_this.node).addClass("et2_vfs"); + if (!_this.options.path) { + _this.options.path = _this.options.id; + } + // If the path is a directory, allow multiple uploads + if (_this.options.path.substr(-1) == '/') { + _this.set_multiple(true); + } + _this.list = jQuery(document.createElement('table')).appendTo(_this.node); + return _this; + } + /** + * If there is a file / files in the specified location, display them + * Value is the information for the file[s] in the specified location. + * + * @param {Object[]} _value + */ + et2_vfsUpload.prototype.set_value = function (_value) { + // Remove previous + while (this._children.length > 0) { + var node = this._children[this._children.length - 1]; + this.removeChild(node); + node.free(); + } + this.progress.empty(); + this.list.empty(); + // Set new + if (typeof _value == 'object' && _value && _value.length) { + for (var i = 0; i < _value.length; i++) { + this._addFile(_value[i]); + } + } + return true; + }; + et2_vfsUpload.prototype.getDOMNode = function (sender) { + if (sender !== this && sender._type.indexOf('vfs') >= 0) { + var value = sender.getValue && sender.getValue() || sender.options.value || {}; + var row = jQuery("[data-path='" + (value.path.replace(/'/g, '"')) + "']", this.list); + if (sender._type === 'vfs-mime') { + return jQuery('.icon', row).get(0) || null; + } + else { + return jQuery('.title', row).get(0) || null; + } + } + else { + return _super.prototype.getDOMNode.call(this, sender); + } + }; + /** + * Add in the request id + * + * @param {type} form + */ + et2_vfsUpload.prototype.beforeSend = function (form) { + var extra = _super.prototype.beforeSend.call(this, form); + extra["path"] = this.options.path; + return extra; + }; + /** + * A file upload is finished, update the UI + * + * @param {object} file + * @param {string|object} response + */ + et2_vfsUpload.prototype.finishUpload = function (file, response) { + var result = _super.prototype.finishUpload.call(this, file, response); + if (typeof response == 'string') + response = jQuery.parseJSON(response); + if (response.response[0] && typeof response.response[0].data.length == 'undefined') { + for (var key in response.response[0].data) { + var value = response.response[0].data[key]; + if (value && value.path) { + this._addFile(value); + jQuery("[data-file='" + file.fileName.replace(/'/g, '"') + "']", this.progress).hide(); + } + } + } + return result; + }; + et2_vfsUpload.prototype._addFile = function (file_data) { + if (jQuery("[data-path='" + file_data.path.replace(/'/g, '"') + "']").remove().length) { + for (var child_index = this._children.length - 1; child_index >= 0; child_index--) { + var child = this._children[child_index]; + if (child.options.value.path === file_data.path) { + this.removeChild(child); + child.free(); + } + } + } + var row = jQuery(document.createElement("tr")) + .attr("data-path", file_data.path.replace(/'/g, '"')) + .attr("draggable", "true") + .appendTo(this.list); + jQuery(document.createElement("td")) + .addClass('icon') + .appendTo(row); + jQuery(document.createElement("td")) + .addClass('title') + .appendTo(row); + var mime = et2_core_widget_1.et2_createWidget('vfs-mime', { value: file_data }, this); + var vfs = et2_core_widget_1.et2_createWidget('vfs', { value: file_data }, this); + // If already attached, need to do this explicitly + if (this.isAttached()) { + mime.set_value(file_data); + vfs.set_value(file_data); + mime.doLoadingFinished(); + vfs.doLoadingFinished(); + } + // Add in delete button + if (!this.options.readonly) { + var self_3 = this; + var delete_button = jQuery(document.createElement("td")) + .appendTo(row); + jQuery("
    ") + .appendTo(delete_button) + // We don't use ui-icon because it assigns a bg image + .addClass("delete icon") + .bind('click', function () { + et2_core_widget_1.et2_createWidget("dialog", { + callback: function (button) { + if (button == et2_widget_dialog_1.et2_dialog.YES_BUTTON) { + egw.json("filemanager_ui::ajax_action", [ + 'delete', + [row.attr('data-path').replace(/"/g, "'")], + '' + ], function (data) { + if (data && data.errs == 0) { + row.slideUp(null, row.remove); + } + if (data && data.msg) { + self_3.egw().message(data.msg, data.errs == 0 ? 'success' : 'error'); + } + }).sendRequest(); + } + }, + message: self_3.egw().lang('Delete file') + '?', + title: self_3.egw().lang('Confirmation required'), + buttons: et2_widget_dialog_1.et2_dialog.BUTTONS_YES_NO, + dialog_type: et2_widget_dialog_1.et2_dialog.QUESTION_MESSAGE, + width: 250 + }, self_3); + }); + } + }; + et2_vfsUpload._attributes = { + "value": { + "type": "any" // Either nothing, or an object with file info + }, + "path": { + "name": "Path", + "description": "Upload files to the specified VFS path", + "type": "string", + "default": '' + } + }; + return et2_vfsUpload; +}(et2_widget_file_1.et2_file)); +et2_core_widget_1.et2_register_widget(et2_vfsUpload, ["vfs-upload"]); +var et2_vfsSelect = /** @class */ (function (_super) { + __extends(et2_vfsSelect, _super); + /** + * Constructor + * + * @param _parent + * @param _attrs + * @memberOf et2_vfsSelect + */ + function et2_vfsSelect(_parent, _attrs, _child) { + var _this = + // Call the inherited constructor + _super.call(this, _parent, _attrs, et2_core_inheritance_1.ClassWithAttributes.extendAttributes(et2_vfsSelect._attributes, _child || {})) || this; + // Allowed mode options + _this.modes = ['open', 'open-multiple', 'saveas', 'select-dir']; + // Allow no child widgets + _this.supportedWidgetClasses = []; + _this.button = jQuery(document.createElement("button")) + .attr("title", _this.egw().lang("Select file(s) from VFS")) + .addClass("et2_button et2_vfs_btn") + .css("background-image", "url(" + _this.egw().image("filemanager/navbar") + ")"); + if (_this.options.readonly) { + _this.button.hide(); + } + if (_this.options.button_caption != "") { + _this.button.text(_this.options.button_caption); + } + _this.setDOMNode(_this.button[0]); + return _this; + } + et2_vfsSelect.prototype._content = function (_content, _callback) { + egw(window).loading_prompt('vfs-select', true, '', 'body'); + var self = this; + if (typeof app.vfsSelectUI != "undefined") { + if (this.dialog && this.dialog.div) + this.dialog.div.dialog('close'); + delete app.vfsSelectUI; + } + var attrs = { + mode: this.options.mode, + label: this.options.button_label, + path: this.options.path || null, + mime: this.options.mime || null, + name: this.options.name, + method: this.options.method, + recentPaths: et2_vfsSelect._getRecentPaths() + }; + var callback = _callback || this._buildDialog; + egw(window).json('EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_vfsSelect_content', [_content, attrs], function (_content) { + egw(window).loading_prompt('vfs-select', false); + callback.apply(self, arguments); + }).sendRequest(true); + }; + /** + * Builds file navigator dialog + * + * @param {object} _data content + */ + et2_vfsSelect.prototype._buildDialog = function (_data) { + if (!_data.content.mode.match(/open|open-multiple|saveas|select-dir/)) { + egw.debug('warn', 'Mode is not matched!'); + return; + } + var self = this; + var buttons = [ + { + text: egw.lang(_data.content.label), + id: "submit", + image: _data.content.mode.match(/saveas|select-dir/) ? "save" : this.options.button_icon + } + ]; + var extra_buttons_action; + if (this.options.extra_buttons && this.options.method) { + for (var i = 0; i < this.options.extra_buttons.length; i++) { + delete (this.options.extra_buttons[i]['click']); + buttons.push(this.options.extra_buttons[i]); + extra_buttons_action[this.options.extra_buttons[i]['id']] = this.options.extra_buttons[i]['id']; + } + } + buttons.push({ text: egw.lang("Close"), id: "close", image: "cancel" }); + var data = jQuery.extend(_data, { 'currentapp': egw(window).app_name() }); + // define a mini app object for vfs select UI + app.vfsSelectUI = new app.classes.vfsSelectUI; + // callback for dialog + this.submit_callback = function (submit_button_id, submit_value, savemode) { + if ((submit_button_id == 'submit' || (extra_buttons_action && extra_buttons_action[submit_button_id])) && submit_value) { + var files = []; + switch (_data.content.mode) { + case 'open-multiple': + if (submit_value.dir && submit_value.dir.selected) { + for (var key in Object.keys(submit_value.dir.selected)) { + if (submit_value.dir.selected[key] != "") { + files.push(submit_value.path + '/' + submit_value.dir.selected[key]); + } + } + } + break; + case 'select-dir': + files = submit_value.path; + break; + default: + if (self.options.method === 'download') + submit_value.path = _data.content.download_baseUrl; + files = submit_value.path + '/' + submit_value.name; + if (self.options.mode === 'saveas' && !savemode) { + for (var p in _data.content.dir) { + if (_data.content.dir[p]['name'] == submit_value.name) { + var saveModeDialogButtons = [ + { text: self.egw().lang("Yes"), id: "overwrite", class: "ui-priority-primary", "default": true, image: 'check' }, + { text: self.egw().lang("Rename"), id: "rename", image: 'edit' }, + { text: self.egw().lang("Cancel"), id: "cancel" } + ]; + return et2_widget_dialog_1.et2_dialog.show_prompt(function (_button_id, _value) { + switch (_button_id) { + case "overwrite": + return self.submit_callback(submit_button_id, submit_value, 'overwrite'); + case "rename": + submit_value.name = _value; + return self.submit_callback(submit_button_id, submit_value, 'rename'); + } + }, self.egw().lang('Do you want to overwrite existing file %1 in directory %2?', submit_value.name, submit_value.path), self.egw().lang('File %1 already exists', submit_value.name), submit_value.name, saveModeDialogButtons, null); + } + } + } + break; + } + et2_vfsSelect._setRecentPaths(submit_value.path); + self.value = files; + if (self.options.method && self.options.method !== 'download') { + egw(window).json(self.options.method, [self.options.method_id, files, submit_button_id, savemode], function () { + jQuery(self.node).change(); + }).sendRequest(true); + } + else { + jQuery(self.node).change(); + } + delete app.vfsSelectUI; + return true; + } + }; + this.dialog = et2_core_widget_1.et2_createWidget("dialog", { + callback: this.submit_callback, + title: this.options.dialog_title, + buttons: buttons, + minWidth: 500, + minHeight: 400, + width: 400, + value: data, + template: egw.webserverUrl + '/api/templates/default/vfsSelectUI.xet?1', + resizable: false + }, et2_widget_dialog_1.et2_dialog._create_parent('api')); + this.dialog.template.uniqueId = 'api.vfsSelectUI'; + // Don't rely only on app_name to fetch et2 object as app_name may not + // always represent current app of the window, e.g.: mail admin account. + // Try to fetch et2 from its template name. + var etemplate = jQuery('form').data('etemplate'); + var et2; + if (etemplate && etemplate.name && !app[egw(window).app_name()]) { + et2 = etemplate2.getByTemplate(etemplate.name)[0]; + } + else { + et2 = etemplate2.getByApplication(egw(window).app_name())[0]; + } + // we need an etemplate_exec_id for better handling serverside parts of + // widgets and since we can not have a etemplate_exec_id specifically + // for dialog template our best shot is to inherit its parent etemplate_exec_id. + this.dialog.template.etemplate_exec_id = et2.etemplate_exec_id; + app.vfsSelectUI.et2 = this.dialog.template.widgetContainer; + // Keep the dialog always at the top + this.dialog.div.parent().css({ "z-index": 100000 }); + app.vfsSelectUI.vfsSelectWidget = this; + this.dialog.div.on('load', function (e) { + app.vfsSelectUI.et2_ready(app.vfsSelectUI.et2, 'api.vfsSelectUI'); + }); + }; + /** + * Set recent path into sessionStorage + * @param {string} _path + */ + et2_vfsSelect._setRecentPaths = function (_path) { + var recentPaths = egw.getSessionItem('api', 'vfsRecentPaths') ? + egw.getSessionItem('api', 'vfsRecentPaths').split(',') : []; + if (recentPaths.indexOf(_path) == -1) + recentPaths.push(_path); + egw.setSessionItem('api', 'vfsRecentPaths', recentPaths); + }; + /** + * Get recent paths from sessionStorage + * @returns {Array} returns an array of recent paths + */ + et2_vfsSelect._getRecentPaths = function () { + return egw.getSessionItem('api', 'vfsRecentPaths') ? + egw.getSessionItem('api', 'vfsRecentPaths').split(',') : []; + }; + /** + * click handler + * @param {event object} e + */ + et2_vfsSelect.prototype.click = function (e) { + this._content.call(this, null); + }; + /** + * Set the dialog's mode. + * Valid options are in et2_vfsSelect.modes + * + * @param {string} mode 'open', 'open-multiple', 'saveas' or 'select-dir' + */ + et2_vfsSelect.prototype.set_mode = function (mode) { + // Check mode + if (jQuery.inArray(mode, this.modes) < 0) { + this.egw().debug("warn", "Invalid mode for '%s': %s Valid options:", this.id, mode, this.modes); + return; + } + this.options.mode = mode; + }; + /** + * Set the label on the dialog's OK button. + * + * @param {string} label + */ + et2_vfsSelect.prototype.set_button_label = function (label) { + this.options.button_label = label; + }; + /** + * Set the caption for vfs-select button + * + * @param {string} caption string value as a caption + */ + et2_vfsSelect.prototype.set_button_caption = function (caption) { + this.options.button_caption = caption; + }; + /** + * Set the ID passed to the server side callback + * + * @param {string} id + */ + et2_vfsSelect.prototype.set_method_id = function (id) { + this.options.method_id = id; + }; + et2_vfsSelect.prototype.set_readonly = function (readonly) { + this.options.readonly = Boolean(readonly); + if (this.options.readonly) { + this.button.hide(); + } + else { + this.button.show(); + } + }; + et2_vfsSelect.prototype.set_value = function (value) { + this.value = value; + }; + et2_vfsSelect.prototype.getValue = function () { + return this.value; + }; + et2_vfsSelect._attributes = { + "mode": { + name: "Dialog mode", + type: "string", + description: "One of {open|open-multiple|saveas|select-dir}", + default: "open-multiple" + }, + "method": { + name: "Server side callback", + type: "string", + description: "Server side callback to process selected value(s) in \n\ + app.class.method or class::method format. The first parameter will \n\ + be Method ID, the second the file list. 'download' is reserved and it \n\ + means it should use download_baseUrl instead of path in value (no method\n\ + will be actually executed)." + }, + "method_id": { + name: "Method ID", + type: "any", + description: "optional parameter passed to server side callback.\n\ + Can be a string or a function.", + default: "" + }, + "path": { + name: "Path", + type: "string", + description: "Start path in VFS. Leave unset to use the last used path." + }, + "mime": { + name: "Mime type", + type: "any", + description: "Limit display to the given mime-type" + }, + "button_label": { + name: "Button label", + description: "Set the label on the dialog's OK button.", + default: "open" + }, + "value": { + type: "any", + description: "Array of paths (strings)" + }, + "button_caption": { + name: "button caption", + type: "string", + default: "Select files from Filemanager ...", + description: "Caption for vfs-select button.", + translate: true + }, + "button_icon": { + name: "button icon", + type: "string", + default: "check", + description: "Custom icon to show on submit button." + }, + "name": { + name: "File name", + type: "any", + description: "file name", + default: "" + }, + "dialog_title": { + name: "dialog title", + type: "string", + default: "Save as", + description: "Title of dialog", + translate: true + }, + "extra_buttons": { + name: "extra action buttons", + type: "any", + description: "Extra buttons passed to dialog. It's co-related to method." + } + }; + return et2_vfsSelect; +}(et2_core_inputWidget_1.et2_inputWidget)); +et2_core_widget_1.et2_register_widget(et2_vfsSelect, ["vfs-select"]); +//# sourceMappingURL=et2_widget_vfs.js.map \ No newline at end of file diff --git a/api/js/etemplate/et2_widget_vfs.ts b/api/js/etemplate/et2_widget_vfs.ts new file mode 100644 index 0000000000..4775334bbe --- /dev/null +++ b/api/js/etemplate/et2_widget_vfs.ts @@ -0,0 +1,1535 @@ +/** + * EGroupware eTemplate2 - JS VFS widgets + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link http://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2012 + */ + +/*egw:uses + /vendor/bower-asset/jquery/dist/jquery.js; + vfsSelectUI; + et2_core_inputWidget; + et2_core_valueWidget; + et2_widget_description; + et2_widget_file; + expose; +*/ + +import {et2_valueWidget} from "./et2_core_valueWidget"; +import {et2_createWidget, et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {egw_getAppObjectManager} from '../egw_action/egw_action.js'; +import {egw_keyHandler} from '../egw_action/egw_keymanager.js'; +import {et2_textbox, et2_textbox_ro} from "./et2_widget_textbox"; +import {et2_description} from "./et2_widget_description"; +import {et2_selectAccount_ro} from "./et2_widget_selectAccount"; +import {et2_file} from "./et2_widget_file"; +import {et2_dialog} from "./et2_widget_dialog"; +import {et2_inputWidget} from "./et2_core_inputWidget"; + +/** + * Class which implements the "vfs" XET-Tag + * + * @augments et2_valueWidget + */ +class et2_vfs extends et2_valueWidget implements et2_IDetachedDOM +{ + static readonly _attributes : any = { + "value": { + "type": "any", // Object + "description": "Array of (stat) information about the file" + } + }; + + /** + * Mime type of directories + */ + static readonly DIR_MIME_TYPE : string = 'httpd/unix-directory'; + + value : any; + span : JQuery = null; + + /** + * Constructor + * + * @memberOf et2_vfs + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_vfs._attributes, _child || {})); + + this.value = ""; + this.span = jQuery(document.createElement("ul")) + .addClass('et2_vfs'); + + this.setDOMNode(this.span[0]); + } + + getValue() + { + return this.value; + } + + set_value(_value) + { + if (typeof _value !== 'object') + { + // Only warn if it's an actual value, just blank for falsy values + if(_value) + { + this.egw().debug("warn", "%s only has path, needs full array", this.id, _value); + } + this.span.empty().text(_value); + return; + } + this.span.empty(); + this.value = _value; + let path = _value.path ? _value.path : '/'; + // calculate path as parent of name, which can contain slashes + // eg. _value.path=/home/ralf/sub/file, _value.name=sub/file --> path=/home/ralf + // --> generate clickable fields for sub/ + file + let sub_path = path.substring(0, _value.path.length-_value.name.length-1); + let path_offset, path_parts; + if(_value.path.indexOf(_value.name) >= 0 && sub_path[sub_path.length-1] === '/') + { + path = sub_path; + path_offset = path.split('/').length; + path_parts = _value.path.split('/'); + } + else + { + if(_value.path.indexOf(_value.name) >= 0) + { + // Remove name from end, so we can add it again later + path = sub_path; + } + path_offset = 0; + path_parts = _value.name.split('/'); + } + + let text; + for(let i = path_offset; i < path_parts.length; i++) + { + path += (path=='/'?'':'/')+path_parts[i]; + text = egw.decodePath(path_parts[i]); + + // Nice human-readable stuff for apps + if(path_parts[1] == 'apps') + { + switch(path_parts.length) + { + case 2: + if(i == 1) + { + text = this.egw().lang('applications'); + } + break; + case 3: + if( i == 2) + { + text = this.egw().lang(path_parts[2]); + } + break; + case 4: + if(!isNaN(text)) + { + let link_title = this.egw().link_title(path_parts[2],path_parts[3], + function(title) { + if(!title || this.value.name == title) return; + jQuery('li',this.span).last().text(title); + }, this + ); + if(link_title && typeof link_title !== 'undefined') text = link_title; + } + break; + } + } + let self = this; + var data = {path: path, type: i < path_parts.length-1 ? et2_vfs.DIR_MIME_TYPE : _value.mime }; + var node = jQuery(document.createElement("li")) + .addClass("vfsFilename") + .text(text + (i < path_parts.length-1 ? '/' : '')) + //.attr('title', egw.decodePath(path)) + .addClass("et2_clickable et2_link") + .click({data:data, egw: this.egw()}, function(e) { + if(!self.onclick) { + e.data.egw.open(e.data.data, "file"); + } + else if (self.click(e)) + { + e.data.egw.open(e.data.data, "file"); + } + }) + .appendTo(this.span); + } + // Last part of path do default action + this._bind_default_action(node, data); + } + + private _bind_default_action(node, data) + { + let links = []; + let widget : any = this; + let defaultAction = null; + let object = null; + let app = this.getInstanceManager().app; + while(links.length === 0 && widget.getParent()) + { + object = egw_getAppObjectManager(app).getObjectById(widget.id); + if(object && object.manager && object.manager.children) + { + links = object.manager.children; + } + widget = widget.getParent(); + } + for (let k in links) + { + if (links[k].default && links[k].enabled.exec(links[k])) + { + defaultAction = links[k]; + break; + } + } + if(defaultAction && !this.onclick) + { + node.off('click').click({data:data, egw: this.egw()}, function(e) { + // Wait until object selection happens + window.setTimeout(function() { + // execute default action + egw_keyHandler(EGW_KEY_ENTER, false, false, false); + }); + // Select row + return true; + }.bind({data: data, object: object})); + } + } + + /** + * Code for implementing et2_IDetachedDOM (data grid) + * + * @param {array} _attrs array of attribute-names to push further names onto + */ + getDetachedAttributes(_attrs) + { + _attrs.push("value"); + } + + getDetachedNodes() + { + return [this.span[0]]; + } + + setDetachedAttributes(_nodes, _values) + { + this.span = jQuery(_nodes[0]); + if(typeof _values["value"] != 'undefined') + { + this.set_value(_values["value"]); + } + } + + +} +et2_register_widget(et2_vfs, ["vfs"]); + +/** +* vfs-name +* filename automatically urlencoded on return (urldecoded on display to user) +* +* @augments et2_textbox +*/ +class et2_vfsName extends et2_textbox +{ + /** + * Constructor + * + * @memberOf et2_vfsName + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_vfsName._attributes, _child || {})); + this.input.addClass("et2_vfs"); + } + set_value(_value) + { + if(_value.path) + { + _value = _value.path; + } + try + { + _value = egw.decodePath(_value); + } catch (e) + { + _value = 'Error! ' + _value; + } + super.set_value(_value); + } + + getValue() + { + return egw.encodePath(super.getValue() || ''); + } +} +et2_register_widget(et2_vfsName, ["vfs-name"]); + +/** +* vfs-name +* filename automatically urlencoded on return (urldecoded on display to user) +* +* @augments et2_textbox +*/ +class et2_vfsPath extends et2_vfsName +{ + static readonly _attributes : any = { + noicon: { + type: "boolean", + description: "suppress folder icons", + default: true + } + }; + + private div : JQuery ; + private span : JQuery; + + /** + * Constructor + * + * @memberOf et2_vfsName + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_vfsPath._attributes, _child || {})); + } + + createInputWidget() + { + super.createInputWidget(); + this.div = jQuery(document.createElement("div")) + .addClass('et2_vfsPath'); + this.span = jQuery(document.createElement("ul")) + .appendTo(this.div); + this.div.prepend(this.input); + this.setDOMNode(this.div[0]); + this.span.on('wheel', function(e){ + var delta = e.originalEvent["deltaY"] > 0 ? 30 : -30; + this.scrollLeft = this.scrollLeft - delta; + }); + this.span.on('mouseover', function (e){ + if (this.scrollWidth > this.clientWidth) + { + jQuery(this).addClass('scrollable'); + } + else + { + jQuery(this).removeClass('scrollable'); + } + }); + this.input.on('focus', function() { + this.input.val(this.options.value); + this.span.hide(); + }.bind(this)) + .on('focusout', function() { + // Can't use show() because it uses the wrong display + this.span.css('display', 'flex'); + this.input.val(''); + }.bind(this)); + } + + change(_node?) + { + if(this.input.val()) + { + this.set_value(this.input.val()); + } + return super.change(_node); + } + + set_value(_value) + { + if(_value.path) + { + _value = _value.path; + } + if(_value === this.options.value && this._oldValue !== et2_no_init) return; + + let path_parts = _value.split('/'); + if(_value === '/') path_parts = ['']; + let path = "/"; + let text = ''; + if (this.span) this.span.empty().css('display', 'flex'); + this.input.val(''); + for(let i = 0; i < path_parts.length; i++) + { + path += (path=='/'?'':'/')+path_parts[i]; + text = egw.decodePath(path_parts[i]); + + let image = path=='/' ? this.egw().image('navbar','api') : this.egw().image(text); + + // Nice human-readable stuff for apps + if(path_parts[1] == 'apps') + { + if(i === 1) + { + text = this.egw().lang('applications'); + } + else if( i === 2) + { + text = this.egw().lang(path_parts[2]); + image = this.egw().image('navbar',path_parts[2].toLowerCase()); + } + else if(!isNaN(text)) + { + let link_title = this.egw().link_title(path_parts[2],path_parts[3], + function(title) { + if(!title) return; + jQuery('li',this.span).first().text(title); + }, this + ); + if(link_title && typeof link_title !== 'undefined') text = link_title; + } + + } + let self = this; + let node = jQuery(document.createElement("li")) + .addClass("vfsPath et2_clickable") + .text(text) + //.attr('title', egw.decodePath(path)) + .click({data:path, egw: this.egw()}, function(e) { + return self.set_value(e.data.data); + }) + .prependTo(this.span); + if(image && !this.options.noicon) + { + node.prepend(this.egw().image_element(image)); + } + jQuery(this.getDOMNode()).append(this.span); + } + + if(this.isAttached() && this.options.value !== _value) + { + this._oldValue = this.options.value; + this.options.value = _value; + this.change(); + } + } + getValue() { + return this.options ? this.options.value : null; + } +} +et2_register_widget(et2_vfsPath, ["vfs-path"]); + +/** +* vfs-name +* filename automatically urlencoded on return (urldecoded on display to user) +* +* @augments et2_textbox_ro +*/ +class et2_vfsName_ro extends et2_textbox_ro +{ + /** + * Constructor + * + * @memberOf et2_vfsName_ro + */ + /** + * Constructor + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_vfsName_ro._attributes, _child || {})); + } + + set_value(_value) + { + if(_value.path) + { + _value = _value.path; + } + try + { + _value = egw.decodePath(_value); + } catch (e) + { + _value = 'Error! ' + _value; + } + super.set_value(_value); + } + + getValue() + { + return egw.encodePath(super.getValue() || ''); + } +} +et2_register_widget(et2_vfsName_ro, ["vfs-name_ro"]); + +/** +* vfs-mime: icon for mimetype of file, or thumbnail +* incl. optional link overlay icon, if file is a symlink +* +* Creates following structure +* +* +* +* +* +* +* +* span.overlayContainer is optional and only generated for symlinks +* @todo implement mixin Expose class +* @augments et2_valueWidget +*/ +class et2_vfsMime extends et2_valueWidget implements et2_IDetachedDOM +{ + static readonly _attributes : any = { + "value": { + "type": "any", // Object + "description": "Array of (stat) information about the file" + }, + "size": { + "name": "Icon size", + "type": "integer", + "description": "Size of icon / thumbnail, in pixels", + "default": et2_no_init + }, + "expose_callback":{ + "name": "expose_callback", + "type": "js", + "default": et2_no_init, + "description": "JS code which is executed when expose slides." + }, + expose_view: { + name: "Expose view", + type: "boolean", + default: true, + description: "Clicking on an image would popup an expose view" + }, + thumb_mime_size:{ + name: "Image thumbnail size", + type: "string", + default:"", + description:" Size of thumbnail in pixel for specified mime type with syntax of: mime_type(s),size (eg. image,video,128)" + } + + }; + + legacyOptions : string[] = ["size"]; + iconOverlayContainer : JQuery = null; + overlayContainer : JQuery; + image : JQuery = null; + + /** + * Constructor + * + * @memberOf et2_vfsMime + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_vfsMime._attributes, _child || {})); + this.iconOverlayContainer = jQuery(document.createElement('span')).addClass('iconOverlayContainer'); + this.image = jQuery(document.createElement("img")); + this.image.addClass("et2_vfs vfsMimeIcon"); + this.iconOverlayContainer.append(this.image); + this.setDOMNode(this.iconOverlayContainer[0]); + } + + /** + * Handler for expose slide action, from expose + * Returns data needed for the given index, or false to let expose handle it + * + * @param {Gallery} gallery + * @param {integer} index + * @param {DOMNode} slide + * @return {Array} array of objects consist of media contnet + */ + expose_onslide(gallery, index, slide) + { + var content = false; + if (this.options.expose_callback && typeof this.options.expose_callback == 'function') + { + //Call the callback to load more items + content = this.options.expose_callback.call(this,[gallery, index]); + if (content) this.add(content); + } + return content; + } + + /** + * Function to get media content to feed the expose + * + * @param {type} _value + * @returns {Array} return an array of object consists of media content + */ + getMedia(_value) + { + let base_url = egw.webserverUrl.match(/^\/ig/)?egw(window).window.location.origin + egw.webserverUrl:egw.webserverUrl; + let mediaContent = [{ + title: _value.name, + type: _value.mime, + href: _value.download_url + }]; + // check if download_url is not already an url (some stream-wrappers allow to specify that!) + if (_value.download_url && (_value.download_url[0] == '/' || _value.download_url.substr(0, 4) != 'http')) + { + mediaContent[0].href = base_url + _value.download_url; + + if (mediaContent[0].href && mediaContent[0].href.match(/\/webdav.php/,'ig')) + { + mediaContent[0]["download_href"] = mediaContent[0].href + '?download'; + } + } + if (_value && _value.mime && _value.mime.match(/video\//,'ig')) + { + mediaContent[0]["thumbnail"] = this.egw().mime_icon(_value.mime, _value.path, undefined, _value.mtime); + } + else + { + mediaContent[0]["thumbnail"] = _value.path && _value.mime ? + this.egw().mime_icon(_value.mime, _value.path, undefined, _value.mtime) : + this.image.attr('src')+ '&thheight=128'; + } + return mediaContent; + } + + set_value(_value) + { + if (typeof _value !== 'object') + { + this.egw().debug("warn", "%s only has path, needs array with path & mime", this.id, _value); + // Keep going, will be 'unknown type' + } + let src = this.egw().mime_icon(_value.mime, _value.path, undefined, _value.mtime); + if(src) + { + // Set size of thumbnail + if(src.indexOf("thumbnail.php") > -1) + { + if(this.options.size) + { + src += "&thsize="+this.options.size; + } + else if (this.options.thumb_mime_size) + { + let mime_size = this.options.thumb_mime_size.split(','); + let mime_regex = RegExp(_value.mime.split('/')[0]); + if (typeof mime_size != 'undefined' && jQuery.isArray(mime_size) + && !isNaN(mime_size[mime_size.length-1]) && isNaN(mime_size[0]) && this.options.thumb_mime_size.match(mime_regex[0], 'ig')) + { + src += "&thsize=" + mime_size[mime_size.length-1]; + } + } + this.image.css("max-width", "100%"); + } + this.image.attr("src", src); + // tooltip for mimetypes with available detailed thumbnail + if (_value.mime && _value.mime.match(/application\/vnd\.oasis\.opendocument\.(text|presentation|spreadsheet|chart)/)) + { + let tooltip_target = this.image.parent().parent().parent().length > 0 ? + // Nextmatch row + this.image.parent().parent().parent() : + // Not in nextmatch + this.image.parent(); + tooltip_target.tooltip({ + items:"img", + position: {my:"right top", at:"left top", collision:"flipfit"}, + content: function(){ + return ''; + } + }); + } + } + // add/remove link icon, if file is (not) a symlink + if ((_value.mode & et2_vfsMode.types.l) == et2_vfsMode.types.l) + { + if (typeof this.overlayContainer == 'undefined') + { + + this.overlayContainer = jQuery(document.createElement('span')).addClass('overlayContainer'); + this.overlayContainer.append(jQuery(document.createElement('img')) + .addClass('overlay').attr('src', this.egw().image('link', 'etemplate'))); + this.iconOverlayContainer.append(this.overlayContainer); + } + } + else if (typeof this.overlayContainer != 'undefined') + { + this.overlayContainer.remove(); + delete this.overlayContainer; + } + } + + /** + * Implementation of "et2_IDetachedDOM" for fast viewing in gridview + * Override to add needed attributes + * + * @param {array} _attrs array of attribute-names to push further names onto + */ + getDetachedAttributes(_attrs) + { + _attrs.push("value", "class"); + } + + getDetachedNodes() + { + return [this.node, this.iconOverlayContainer[0], this.image[0]]; + } + + setDetachedAttributes(_nodes, _values) + { + this.iconOverlayContainer = jQuery(_nodes[1]); + this.image = jQuery(_nodes[2]); + this.node = _nodes[0]; + this.overlayContainer = _nodes[0].children[1]; + if(typeof _values['class'] != "undefined") { + this.image.addClass(_values['class']); + } + if(typeof _values['value'] != "undefined") { + this.set_value(_values['value']); + } + } +} +et2_register_widget(et2_vfsMime, ["vfs-mime"]); + +/** +* vfs-size +* Human readable file sizes +* +* @augments et2_description +*/ +class et2_vfsSize extends et2_description +{ + static readonly _attributes : any = { + "value": { + "type": "integer" + } + }; + /** + * Constructor + * + * @memberOf et2_vfsSize + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_vfsSize._attributes, _child || {})); + this.span.addClass("et2_vfs"); + } + + human_size(size) + { + if(typeof size !== "number") + { + size = parseInt(size); + } + if(!size) + { + size = 0; + } + const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + let i = 0; + while(size >= 1024) + { + size /= 1024; + ++i; + } + return size.toFixed(i == 0 ? 0 : 1) + ' ' + units[i]; + } + + set_value(_value) + { + if(_value.size) + { + _value = _value.size; + } + jQuery(this.node).text(this.human_size(_value)); + } + + setDetachedAttributes(_nodes, _values) + { + if(typeof _values["value"] !== "undefined") { + this.node = _nodes[0]; + this.set_value(_values["value"]); + delete _values["value"]; + } + super.setDetachedAttributes(_nodes, _values); + } +} +et2_register_widget(et2_vfsSize, ["vfs-size"]); + + +/** +* vfs-mode: textual representation of permissions + extra bits +* +* @augments et2_description +*/ +class et2_vfsMode extends et2_description +{ + // Masks for file types + static readonly types : {l : number, s : number, p : number, c : number, d : number, b: number, '-' : number} = { + 'l': 0xA000, // link + 's': 0xC000, // Socket + 'p': 0x1000, // FIFO pipe + 'c': 0x2000, // Character special + 'd': 0x4000, // Directory + 'b': 0x6000, // Block special + '-': 0x8000 // Regular + }; + + // Sticky / UID / GID + static readonly sticky : {mask : number, char : string, position : number}[] = [ + { mask: 0x200, "char": "T", position: 9 }, // Sticky + { mask: 0x400, "char": "S", position: 6 }, // sGID + { mask: 0x800, "char": "S", position: 3 } // SUID + ]; + + static readonly perms : {x : number, w : number, r : number} = { + 'x': 0x1, // Execute + 'w': 0x2, // Write + 'r': 0x4 // Read + }; + + /** + * Constructor + * + * @memberOf et2_vfsMode + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_vfsMode._attributes, _child || {})); + this.span.addClass("et2_vfs"); + } + + /** + * Get text for file stuff + * Result will be like -rwxr--r--. First char is type, then read, write, execute (or other bits) for + * user, group, world + * + * @param {number} _value vfs mode + */ + text_mode(_value) + { + let text = []; + if(typeof _value != "number") + { + _value = parseInt(_value); + } + if(!_value) return "----------"; + + // Figure out type + let type = 'u'; // unknown + for(let flag in et2_vfsMode.types) + { + if((_value & et2_vfsMode.types[flag]) == et2_vfsMode.types[flag]) + { + type = flag; + break; + } + } + + // World, group, user - build string backwards + for(let i = 0; i < 3; i++) + { + for(let perm in et2_vfsMode.perms) + { + if(_value & et2_vfsMode.perms[perm]) + { + text.unshift(perm); + } + else + { + text.unshift("-"); + } + } + _value = _value >> 3; + } + // Sticky / UID / GID + for(let i = 0; i < et2_vfsMode.sticky.length; i++) + { + if(et2_vfsMode.sticky[i].mask & _value) + { + let current = text[et2_vfsMode.sticky[i].position]; + text[et2_vfsMode.sticky[i].position] = et2_vfsMode.sticky[i]["char"]; + if(current == 'x') text[et2_vfsMode.sticky[i].position].toLowerCase(); + } + } + return type + text.join(''); + } + + set_value(_value) + { + if(_value.size) + { + _value = _value.size; + } + let text = this.text_mode(_value); + jQuery(this.node).text(text); + } + + setDetachedAttributes(_nodes, _values) + { + if(typeof _values["value"] !== "undefined") { + this.node = _nodes[0]; + this.set_value(_values["value"]); + delete _values["value"]; + } + super.setDetachedAttributes(_nodes, _values); + } +} +et2_register_widget(et2_vfsMode, ["vfs-mode"]); + + +/** +* vfs-uid / vfs-gid: Displays the name for an ID. +* Same as read-only selectAccount, except if there's no user it shows "root" +* +* @augments et2_selectAccount_ro +*/ +class et2_vfsUid extends et2_selectAccount_ro +{ + /** + * @memberOf et2_vfsUid + * @param _node + * @param _value + */ + set_title(_node, _value) + { + if(_value == "") + { + arguments[1] = "root"; + } + super.set_title(_node, _value); + } +} +et2_register_widget(et2_vfsUid, ["vfs-uid","vfs-gid"]); + + +/* vfs-upload aka VFS file: displays either download and delete (x) links or a file upload +* + ID is either a vfs path or colon separated $app:$id:$relative_path, eg: infolog:123:special/offer +* + if empty($id) / new entry, file is created in a hidden temporary directory in users home directory +* and calling app is responsible to move content of that dir to entry directory, after entry is saved +* + option: required mimetype or regular expression for mimetype to match, eg. '/^text\//i' for all text files +* + if path ends in a slash, multiple files can be uploaded, their original filename is kept then +* +* @augments et2_file +*/ +class et2_vfsUpload extends et2_file +{ + static readonly _attributes : any = { + "value": { + "type": "any" // Either nothing, or an object with file info + }, + "path": { + "name": "Path", + "description": "Upload files to the specified VFS path", + "type": "string", + "default": '' + } + }; + + legacyOptions : string[] = ["mime"]; + + asyncOptions : {target : any} = { + target: egw.ajaxUrl("EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_upload") + }; + + list : JQuery = null; + + /** + * Constructor + * + * @param _parent + * @param attrs + * @memberof et2_vfsUpload + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_vfsUpload._attributes, _child || {})); + + jQuery(this.node).addClass("et2_vfs"); + + if(!this.options.path) + { + this.options.path = this.options.id; + } + // If the path is a directory, allow multiple uploads + if(this.options.path.substr(-1) == '/') + { + this.set_multiple(true); + } + this.list = jQuery(document.createElement('table')).appendTo(this.node); + } + + /** + * If there is a file / files in the specified location, display them + * Value is the information for the file[s] in the specified location. + * + * @param {Object[]} _value + */ + set_value(_value) { + // Remove previous + while(this._children.length > 0) + { + var node = this._children[this._children.length-1]; + this.removeChild(node); + node.free(); + } + this.progress.empty(); + this.list.empty(); + + // Set new + if(typeof _value == 'object' && _value && _value.length) + { + for(var i = 0; i < _value.length; i++) + { + this._addFile(_value[i]); + } + } + return true; + } + + getDOMNode(sender) { + if(sender !== this && sender._type.indexOf('vfs') >= 0 ) + { + var value = sender.getValue && sender.getValue() || sender.options.value || {}; + var row = jQuery("[data-path='"+(value.path.replace(/'/g, '"'))+"']",this.list); + if(sender._type === 'vfs-mime') + { + return jQuery('.icon',row).get(0) || null; + } + else + { + return jQuery('.title',row).get(0) || null; + } + } + else + { + return super.getDOMNode(sender); + } + } + + /** + * Add in the request id + * + * @param {type} form + */ + beforeSend(form) + { + let extra = super.beforeSend(form); + extra["path"] = this.options.path; + return extra; + } + + /** + * A file upload is finished, update the UI + * + * @param {object} file + * @param {string|object} response + */ + finishUpload(file, response) { + let result = super.finishUpload(file, response); + + if(typeof response == 'string') response = jQuery.parseJSON(response); + if(response.response[0] && typeof response.response[0].data.length == 'undefined') { + for(let key in response.response[0].data) { + let value = response.response[0].data[key]; + if(value && value.path) + { + this._addFile(value); + jQuery("[data-file='"+file.fileName.replace(/'/g, '"')+"']",this.progress).hide(); + } + } + } + return result; + } + + private _addFile(file_data) { + if(jQuery("[data-path='"+file_data.path.replace(/'/g, '"')+"']").remove().length) + { + for(var child_index = this._children.length-1; child_index >= 0; child_index--) + { + var child = this._children[child_index]; + if(child.options.value.path === file_data.path) + { + this.removeChild(child); + child.free(); + } + } + } + let row = jQuery(document.createElement("tr")) + .attr("data-path", file_data.path.replace(/'/g, '"')) + .attr("draggable", "true") + .appendTo(this.list); + jQuery(document.createElement("td")) + .addClass('icon') + .appendTo(row); + + jQuery(document.createElement("td")) + .addClass('title') + .appendTo(row); + let mime = et2_createWidget('vfs-mime',{value: file_data}, this); + let vfs = et2_createWidget('vfs', {value: file_data}, this); + + // If already attached, need to do this explicitly + if(this.isAttached()) + { + mime.set_value(file_data); + vfs.set_value(file_data); + mime.doLoadingFinished(); + vfs.doLoadingFinished(); + } + + // Add in delete button + if (!this.options.readonly) + { + let self = this; + let delete_button = jQuery(document.createElement("td")) + .appendTo(row); + jQuery("
    ") + .appendTo(delete_button) + // We don't use ui-icon because it assigns a bg image + .addClass("delete icon") + .bind( 'click', function() { + et2_createWidget("dialog", { + callback: function(button) { + if(button == et2_dialog.YES_BUTTON) + { + egw.json("filemanager_ui::ajax_action", [ + 'delete', + [row.attr('data-path').replace(/"/g, "'")], + '' + ], + function(data) { + if(data && data.errs == 0) {row.slideUp(null, row.remove);} + if(data && data.msg) { + self.egw().message(data.msg, data.errs == 0 ? 'success' : 'error'); + } + } + ).sendRequest(); + } + }, + message: self.egw().lang('Delete file')+'?', + title: self.egw().lang('Confirmation required'), + buttons: et2_dialog.BUTTONS_YES_NO, + dialog_type: et2_dialog.QUESTION_MESSAGE, + width: 250 + }, self); + }); + } + } +} +et2_register_widget(et2_vfsUpload, ["vfs-upload"]); + + +class et2_vfsSelect extends et2_inputWidget +{ + // Allowed mode options + modes : string[] = ['open','open-multiple','saveas','select-dir']; + + static readonly _attributes : any = { + "mode": { + name: "Dialog mode", + type: "string", + description: "One of {open|open-multiple|saveas|select-dir}", + default: "open-multiple" + }, + "method": { + name: "Server side callback", + type: "string", + description: "Server side callback to process selected value(s) in \n\ + app.class.method or class::method format. The first parameter will \n\ + be Method ID, the second the file list. 'download' is reserved and it \n\ + means it should use download_baseUrl instead of path in value (no method\n\ + will be actually executed)." + }, + "method_id": { + name: "Method ID", + type: "any", + description: "optional parameter passed to server side callback.\n\ + Can be a string or a function.", + default: "" + }, + "path": { + name: "Path", + type: "string", + description:"Start path in VFS. Leave unset to use the last used path." + }, + "mime": { + name: "Mime type", + type: "any", + description: "Limit display to the given mime-type" + }, + "button_label": { + name: "Button label", + description: "Set the label on the dialog's OK button.", + default: "open" + }, + "value": { + type: "any", // Object + description: "Array of paths (strings)" + }, + "button_caption":{ + name: "button caption", + type: "string", + default: "Select files from Filemanager ...", + description: "Caption for vfs-select button.", + translate:true + }, + "button_icon":{ + name: "button icon", + type: "string", + default: "check", + description: "Custom icon to show on submit button." + }, + "name": { + name:"File name", + type: "any", // Object + description: "file name", + default: "" + }, + "dialog_title":{ + name: "dialog title", + type: "string", + default: "Save as", + description: "Title of dialog", + translate:true + }, + "extra_buttons": { + name: "extra action buttons", + type: "any", + description: "Extra buttons passed to dialog. It's co-related to method." + } + }; + + button : JQuery; + submit_callback : any; + dialog : et2_dialog; + private value : any; + /** + * Constructor + * + * @param _parent + * @param _attrs + * @memberOf et2_vfsSelect + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_vfsSelect._attributes, _child || {})); + + // Allow no child widgets + this.supportedWidgetClasses = []; + + this.button = jQuery(document.createElement("button")) + .attr("title", this.egw().lang("Select file(s) from VFS")) + .addClass("et2_button et2_vfs_btn") + .css("background-image","url("+this.egw().image("filemanager/navbar")+")"); + + if(this.options.readonly) + { + this.button.hide(); + } + + if (this.options.button_caption != "") + { + this.button.text(this.options.button_caption); + } + this.setDOMNode(this.button[0]); + } + + private _content(_content, _callback) + { + egw(window).loading_prompt('vfs-select', true, '', 'body'); + let self = this; + if (typeof app.vfsSelectUI !="undefined") + { + if (this.dialog && this.dialog.div) this.dialog.div.dialog('close'); + delete app.vfsSelectUI; + } + let attrs = { + mode: this.options.mode, + label: this.options.button_label, + path: this.options.path || null, + mime: this.options.mime || null, + name: this.options.name, + method: this.options.method, + recentPaths: et2_vfsSelect._getRecentPaths() + }; + var callback = _callback || this._buildDialog; + egw(window).json( + 'EGroupware\\Api\\Etemplate\\Widget\\Vfs::ajax_vfsSelect_content', + [_content, attrs], + function(_content){ + egw(window).loading_prompt('vfs-select', false); + callback.apply(self, arguments); + } + ).sendRequest(true); + } + + /** + * Builds file navigator dialog + * + * @param {object} _data content + */ + private _buildDialog(_data) + { + if (!_data.content.mode.match(/open|open-multiple|saveas|select-dir/)) { + egw.debug('warn', 'Mode is not matched!'); + return; + } + let self = this; + let buttons = [ + { + text: egw.lang(_data.content.label), + id:"submit", + image: _data.content.mode.match(/saveas|select-dir/) ? "save" : this.options.button_icon + } + ]; + let extra_buttons_action; + + if (this.options.extra_buttons && this.options.method) + { + for (let i=0; i < this.options.extra_buttons.length; i++) + { + delete(this.options.extra_buttons[i]['click']); + buttons.push(this.options.extra_buttons[i]); + extra_buttons_action[this.options.extra_buttons[i]['id']] = this.options.extra_buttons[i]['id']; + } + + } + buttons.push({text: egw.lang("Close"), id:"close", image:"cancel"}); + let data = jQuery.extend(_data, {'currentapp': egw(window).app_name()}); + + // define a mini app object for vfs select UI + app.vfsSelectUI = new app.classes.vfsSelectUI; + + // callback for dialog + this.submit_callback = function(submit_button_id, submit_value, savemode) + { + if ((submit_button_id == 'submit' || (extra_buttons_action && extra_buttons_action[submit_button_id])) && submit_value) + { + let files : any = []; + switch(_data.content.mode) + { + case 'open-multiple': + if (submit_value.dir && submit_value.dir.selected) + { + for(var key in Object.keys(submit_value.dir.selected)) + { + if (submit_value.dir.selected[key] != "") + { + files.push(submit_value.path+'/'+submit_value.dir.selected[key]); + } + } + } + break; + case 'select-dir': + files = submit_value.path; + break; + default: + if (self.options.method === 'download') submit_value.path = _data.content.download_baseUrl; + files = submit_value.path+'/'+submit_value.name; + if (self.options.mode === 'saveas' && !savemode) + { + for(var p in _data.content.dir) + { + if (_data.content.dir[p]['name'] == submit_value.name) + { + var saveModeDialogButtons = [ + {text: self.egw().lang("Yes"), id: "overwrite", class: "ui-priority-primary", "default": true, image: 'check'}, + {text: self.egw().lang("Rename"), id:"rename", image: 'edit'}, + {text: self.egw().lang("Cancel"), id:"cancel"} + ]; + return et2_dialog.show_prompt(function(_button_id, _value) { + switch (_button_id) + { + case "overwrite": + return self.submit_callback(submit_button_id, submit_value, 'overwrite'); + case "rename": + submit_value.name = _value; + return self.submit_callback(submit_button_id, submit_value, 'rename'); + } + }, + self.egw().lang('Do you want to overwrite existing file %1 in directory %2?', submit_value.name, submit_value.path), + self.egw().lang('File %1 already exists', submit_value.name), + submit_value.name, saveModeDialogButtons, null); + } + } + } + break; + } + et2_vfsSelect._setRecentPaths(submit_value.path); + self.value = files; + if (self.options.method && self.options.method !== 'download') + { + egw(window).json( + self.options.method, + [self.options.method_id, files, submit_button_id, savemode], + function(){ + jQuery(self.node).change(); + } + ).sendRequest(true); + } + else + { + jQuery(self.node).change(); + } + delete app.vfsSelectUI; + return true; + } + }; + + this.dialog = et2_createWidget("dialog", + { + callback: this.submit_callback, + title: this.options.dialog_title, + buttons: buttons, + minWidth: 500, + minHeight: 400, + width:400, + value: data, + template: egw.webserverUrl+'/api/templates/default/vfsSelectUI.xet?1', + resizable: false + }, et2_dialog._create_parent('api')); + this.dialog.template.uniqueId = 'api.vfsSelectUI'; + + // Don't rely only on app_name to fetch et2 object as app_name may not + // always represent current app of the window, e.g.: mail admin account. + // Try to fetch et2 from its template name. + let etemplate = jQuery('form').data('etemplate'); + let et2; + if (etemplate && etemplate.name && !app[egw(window).app_name()]) + { + et2 = etemplate2.getByTemplate(etemplate.name)[0]; + } + else + { + et2 = etemplate2.getByApplication(egw(window).app_name())[0]; + } + // we need an etemplate_exec_id for better handling serverside parts of + // widgets and since we can not have a etemplate_exec_id specifically + // for dialog template our best shot is to inherit its parent etemplate_exec_id. + this.dialog.template.etemplate_exec_id = et2.etemplate_exec_id; + + app.vfsSelectUI.et2 = this.dialog.template.widgetContainer; + // Keep the dialog always at the top + this.dialog.div.parent().css({"z-index": 100000}); + app.vfsSelectUI.vfsSelectWidget = this; + this.dialog.div.on('load', function(e) { + app.vfsSelectUI.et2_ready(app.vfsSelectUI.et2, 'api.vfsSelectUI'); + }); + } + + /** + * Set recent path into sessionStorage + * @param {string} _path + */ + private static _setRecentPaths(_path) + { + let recentPaths = egw.getSessionItem('api', 'vfsRecentPaths') ? + egw.getSessionItem('api', 'vfsRecentPaths').split(',') : []; + if (recentPaths.indexOf(_path) == -1) recentPaths.push(_path); + egw.setSessionItem('api', 'vfsRecentPaths', recentPaths); + } + + /** + * Get recent paths from sessionStorage + * @returns {Array} returns an array of recent paths + */ + private static _getRecentPaths() + { + return egw.getSessionItem('api', 'vfsRecentPaths') ? + egw.getSessionItem('api', 'vfsRecentPaths').split(',') : []; + } + + /** + * click handler + * @param {event object} e + */ + click(e) + { + this._content.call(this, null); + } + + /** + * Set the dialog's mode. + * Valid options are in et2_vfsSelect.modes + * + * @param {string} mode 'open', 'open-multiple', 'saveas' or 'select-dir' + */ + set_mode(mode) + { + // Check mode + if(jQuery.inArray(mode, this.modes) < 0) + { + this.egw().debug("warn", "Invalid mode for '%s': %s Valid options:", this.id,mode, this.modes); + return; + } + this.options.mode = mode; + } + + /** + * Set the label on the dialog's OK button. + * + * @param {string} label + */ + set_button_label(label) + { + this.options.button_label = label; + } + + /** + * Set the caption for vfs-select button + * + * @param {string} caption string value as a caption + */ + set_button_caption(caption) + { + this.options.button_caption = caption; + } + + /** + * Set the ID passed to the server side callback + * + * @param {string} id + */ + set_method_id(id) + { + this.options.method_id = id; + } + + set_readonly(readonly) + { + this.options.readonly = Boolean(readonly); + + if(this.options.readonly) + { + this.button.hide(); + } + else + { + this.button.show(); + } + } + + set_value(value) + { + this.value = value; + } + + getValue() + { + return this.value; + } +} +et2_register_widget(et2_vfsSelect, ["vfs-select"]); + diff --git a/api/js/jsapi/egw_global.d.ts b/api/js/jsapi/egw_global.d.ts index ed80601723..e7ccbd01ac 100644 --- a/api/js/jsapi/egw_global.d.ts +++ b/api/js/jsapi/egw_global.d.ts @@ -459,9 +459,9 @@ declare interface IegwGlobal * * @param {string} application Name of application, or common * @param {string} key - * @param {string} value + * @param {string | array} value */ - setSessionItem(application : string, key : string, value : string) : void; + setSessionItem(application : string, key : string, value : string[]) : void; /** * Remove a value from session storage * @param {string} application