2020-02-05 21:48:50 +01:00
/ * *
* EGroupware eTemplate2 - JS Link 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 2011 Nathan Gray
* @version $Id $
* /
/ * e g w : u s e s
/ v e n d o r / b o w e r - a s s e t / j q u e r y / d i s t / j q u e r y . j s ;
/ v e n d o r / b o w e r - a s s e t / j q u e r y - u i / j q u e r y - u i . j s ;
et2_core_inputWidget ;
et2_core_valueWidget ;
// Include menu system for list context menu
egw_action . egw_menu_dhtmlx ;
* /
import { WidgetConfig , et2_widget , et2_register_widget } from "./et2_core_widget" ;
import { ClassWithAttributes } from "./et2_core_inheritance" ;
import { et2_valueWidget } from "./et2_core_valueWidget" ;
import { et2_inputWidget } from "./et2_core_inputWidget" ;
import { et2_selectbox } from "./et2_widget_selectbox" ;
import { et2_button } from "./et2_widget_button" ;
import { et2_vfs_select } from "./et2_widget_vfs" ;
2020-02-05 23:27:24 +01:00
/ * *
2020-02-05 21:48:50 +01:00
* UI widgets for Egroupware linking system
* /
export class et2_link_to extends et2_inputWidget
{
static readonly _attributes : any = {
"only_app" : {
"name" : "Application" ,
"type" : "string" ,
"default" : "" ,
"description" : "Limit to just this one application - hides app selection"
} ,
"application_list" : {
"name" : "Application list" ,
"type" : "any" ,
"default" : "" ,
"description" : "Limit to the listed application or applications (comma seperated)"
} ,
"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." ,
translate :true
} ,
"no_files" : {
"name" : "No files" ,
"type" : "boolean" ,
"default" : false ,
"description" : "Suppress attach-files"
} ,
"search_label" : {
"name" : "Search label" ,
"type" : "string" ,
"default" : "" ,
"description" : "Label to use for search"
} ,
"link_label" : {
"name" : "Link label" ,
"type" : "string" ,
"default" : "Link" ,
"description" : "Label for the link button"
} ,
"value" : {
// Could be string or int if application is provided, or an Object
"type" : "any"
}
} ;
private div : JQuery ;
private link_button : JQuery ;
private link_div : JQuery ;
private filemanager_button : JQuery ;
private file_div : JQuery ;
private status_span : JQuery ;
private link_entry : et2_link_entry ;
private vfs_select : et2_vfs_select ;
private file_upload : et2_file ;
/ * *
* Constructor
*
* @memberOf et2_link_to
* /
2020-02-05 23:27:24 +01:00
constructor ( _parent : et2_widget , _attrs? : WidgetConfig , _child? : object ) {
super ( _parent , _attrs , ClassWithAttributes . extendAttributes ( et2_link_to . _attributes , _child || { } ) ) ;
2020-02-05 21:48:50 +01:00
this . div = jQuery ( document . createElement ( "div" ) ) . addClass ( "et2_link_to et2_toolbar" ) ;
this . link_button = null ;
this . status_span = null ;
this . link_entry = null ;
this . file_upload = null ;
this . createInputWidget ( ) ;
}
destroy ( )
{
this . link_button = null ;
this . status_span = null ;
if ( this . link_entry )
{
this . link_entry . destroy ( ) ;
this . link_entry = null ;
}
if ( this . file_upload )
{
this . file_upload . destroy ( ) ;
this . file_upload = null ;
}
this . div = null ;
super . destroy . apply ( this , arguments ) ;
}
/ * *
* Override to provide proper node for sub widgets to go in
*
* @param { Object } _sender
* /
getDOMNode ( _sender )
{
if ( _sender == this ) {
return this . div [ 0 ] ;
} else if ( _sender . _type == 'link-entry' ) {
return this . link_div [ 0 ] ;
} else if ( _sender . _type == 'file' ) {
return this . file_div [ 0 ] ;
} else if ( _sender . _type == 'vfs-select' ) {
return this . filemanager_button [ 0 ] ;
}
}
createInputWidget ( )
{
// Need a div for file upload widget
this . file_div = jQuery ( document . createElement ( "div" ) ) . css ( { display : 'inline-block' } ) . appendTo ( this . div ) ;
// Filemanager link popup
this . filemanager_button = jQuery ( document . createElement ( "div" ) ) . css ( { display : 'inline-block' } ) . appendTo ( this . div ) ;
// Need a div for link-to widget
this . link_div = jQuery ( document . createElement ( "div" ) )
. addClass ( 'div_link' )
// Leave room for link button
. appendTo ( this . div ) ;
if ( ! this . options . readonly )
{
// One common link button
this . link_button = jQuery ( document . createElement ( "button" ) )
. text ( this . egw ( ) . lang ( this . options . link_label ) )
. appendTo ( this . div ) . hide ( )
. addClass ( 'link' )
. click ( this , this . createLink ) ;
// Span for indicating status
this . status_span = jQuery ( document . createElement ( "span" ) )
. appendTo ( this . div ) . addClass ( "status" ) . hide ( ) ;
}
this . setDOMNode ( this . div [ 0 ] ) ;
}
doLoadingFinished ( )
{
super . doLoadingFinished . apply ( this , arguments ) ;
var self = this ;
if ( this . link_entry && this . vfs_select && this . file_upload )
{
// Already done
return false ;
}
// Link-to
var link_entry_attrs = {
id : this.id + '_link_entry' ,
only_app : this.options.only_app ,
application_list : this.options.application_list ,
blur : this.options.search_label ? this . options.search_label : this.egw ( ) . lang ( 'Search...' ) ,
query : function ( ) { self . link_button . hide ( ) ; return true ; } ,
select : function ( ) { self . link_button . show ( ) ; return true ; } ,
readonly : this . options . readonly
} ;
this . link_entry = et2_createWidget ( "link-entry" , link_entry_attrs , this ) ;
// Filemanager select
var select_attrs : any = {
button_label : egw.lang ( 'Link' ) ,
button_caption : '' ,
button_icon : 'link' ,
readonly : this . options . readonly ,
dialog_title : egw.lang ( 'Link' ) ,
extra_buttons : [ { text : egw.lang ( "copy" ) , id : "copy" , image : "copy" } ,
{ text : egw.lang ( "move" ) , id : "move" , image : "move" } ] ,
onchange : function ( )
{
var values = true ;
// If entry not yet saved, store for linking on server
if ( ! self . options . value . to_id || typeof self . options . value . to_id == 'object' )
{
values = self . options . value . to_id || { } ;
var files = self . vfs_select . getValue ( ) ;
if ( typeof files !== 'undefined' )
{
for ( var i = 0 ; i < files . length ; i ++ )
{
values [ 'link:' + files [ i ] ] = {
app : 'link' ,
id : files [ i ] ,
type : 'unknown' ,
icon : 'link' ,
remark : '' ,
title : files [ i ]
} ;
}
}
}
self . _link_result ( values ) ;
}
} ;
// only set server-side callback, if we have a real application-id (not null or array)
// otherwise it only gives an error on server-side
if ( self . options . value && self . options . value . to_id && typeof self . options . value . to_id != 'object' ) {
select_attrs . method = 'EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_existing' ;
select_attrs . method_id = self . options . value . to_app + ':' + self . options . value . to_id ;
}
this . vfs_select = et2_createWidget ( "vfs-select" , select_attrs , this ) ;
this . vfs_select . set_readonly ( this . options . readonly ) ;
// File upload
var file_attrs = {
multiple : true ,
id : this.id + '_file' ,
label : '' ,
// Make the whole template a drop target
drop_target : this.getInstanceManager ( ) . DOMContainer . getAttribute ( "id" ) ,
readonly : this . options . readonly ,
// Change to this tab when they drop
onStart : function ( event , file_count )
{
// Find the tab widget, if there is one
var tabs : et2_widget = self ;
do {
tabs = tabs . getParent ( ) ;
} while ( tabs != self . getRoot ( ) && tabs . getType ( ) != 'tabbox' ) ;
if ( tabs != self . getRoot ( ) )
{
( < et2_tabbox > < unknown > tabs ) . activateTab ( self ) ;
}
return true ;
} ,
onFinish : function ( event , file_count )
{
event . data = self ;
self . filesUploaded ( event ) ;
// Auto-link uploaded files
self . createLink ( event ) ;
}
} ;
this . file_upload = et2_createWidget ( "file" , file_attrs , this ) ;
this . file_upload . set_readonly ( this . options . readonly ) ;
return true ;
}
getValue ( )
{
return this . options . value ;
}
filesUploaded ( event )
{
var self = this ;
this . link_button . show ( ) ;
}
/ * *
* Create a link using the current internal values
*
* @param { Object } event
* /
createLink ( event )
{
// Disable link button
event . data . link_button . attr ( "disabled" , true ) ;
var values = event . data . options . value ;
var self = event . data ;
var links = [ ] ;
// Links to other entries
event . data = self . link_entry ;
self . link_entry . createLink ( event , links ) ;
// Files
if ( ! self . options . no_files )
{
for ( var file in self . file_upload . options . value ) {
links . push ( {
app : 'file' ,
id : file ,
name : self.file_upload.options.value [ file ] . name ,
type : self . file_upload . options . value [ file ] . type ,
remark : jQuery ( "li[file='" + self . file_upload . options . value [ file ] . name . replace ( /'/g , '"' ) + "'] > input" , self . file_upload . progress )
. filter ( function ( ) { return jQuery ( this ) . attr ( "placeholder" ) != jQuery ( this ) . val ( ) ; } ) . val ( )
} ) ;
}
}
if ( links . length == 0 )
{
return ;
}
var request = egw . json ( "EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link" ,
[ values . to_app , values . to_id , links ] ,
self . _link_result ,
self ,
true ,
self
) ;
request . sendRequest ( ) ;
}
/ * *
* Sent some links , server has a result
*
* @param { Object } success
* /
_link_result ( success )
{
if ( success )
{
this . link_button . hide ( ) . attr ( "disabled" , false ) ;
this . status_span . removeClass ( "error" ) . addClass ( "success" ) ;
this . status_span . fadeIn ( ) . delay ( 1000 ) . fadeOut ( ) ;
delete this . options . value . app ;
delete this . options . value . id ;
for ( var file in this . file_upload . options . value ) {
delete this . file_upload . options . value [ file ] ;
}
this . file_upload . progress . empty ( ) ;
// Server says it's OK, but didn't store - we'll send this again on submit
// This happens if you link to something before it's saved to the DB
if ( typeof success == "object" )
{
// Save as appropriate in value
if ( typeof this . options . value != "object" )
{
this . options . value = { } ;
}
this . options . value . to_id = success ;
for ( let link in success )
{
// Icon should be in registry
if ( typeof success [ link ] . icon == 'undefined' )
{
success [ link ] . icon = egw . link_get_registry ( success [ link ] . app , 'icon' ) ;
// No icon, try by mime type - different place for un-saved entries
if ( success [ link ] . icon == false && success [ link ] . id . type )
{
// Triggers icon by mime type, not thumbnail or app
success [ link ] . type = success [ link ] . id . type ;
success [ link ] . icon = true ;
}
}
// Special handling for file - if not existing, we can't ask for title
if ( success [ link ] . app == 'file' && typeof success [ link ] . title == 'undefined' )
{
success [ link ] . title = success [ link ] . id . name || '' ;
}
}
}
// Look for a link-list with the same ID, refresh it
var self = this ;
var list_widget = null ;
this . getRoot ( ) . iterateOver (
function ( widget ) {
if ( widget . id == self . id ) {
list_widget = widget ;
if ( success === true )
{
widget . _get_links ( ) ;
}
}
} ,
this , et2_link_list
) ;
// If there's an array of data (entry is not yet saved), updating the list will
// not work, so add them in explicitly.
if ( list_widget && success )
{
// Clear list
list_widget . set_value ( null ) ;
// Add temp links in
for ( var link_id in success )
{
let link = success [ link_id ] ;
if ( typeof link . title == 'undefined' )
{
// Callback to server for title
egw . link_title ( link . app , link . id , function ( title ) {
link . title = title ;
list_widget . _add_link ( link ) ;
} ) ;
}
else
{
// Add direct
list_widget . _add_link ( link ) ;
}
}
}
}
else
{
this . status_span . removeClass ( "success" ) . addClass ( "error" )
. fadeIn ( ) ;
}
this . div . trigger ( 'link.et2_link_to' , success ) ;
}
set_no_files ( no_files )
{
if ( this . options . readonly ) return ;
if ( no_files )
{
this . file_div . hide ( ) ;
this . filemanager_button . hide ( ) ;
}
else
{
this . file_div . show ( ) ;
this . filemanager_button . show ( ) ;
}
this . options . no_files = no_files ;
}
}
et2_register_widget ( et2_link_to , [ "link-to" ] ) ;
/ * *
* List of applications that support link
* /
export class et2_link_apps extends et2_selectbox
{
2020-02-07 17:31:10 +01:00
static readonly _attributes : any = {
2020-02-05 21:48:50 +01:00
"only_app" : {
"name" : "Application" ,
"type" : "string" ,
"default" : "" ,
"description" : "Limit to just this one application - hides app selection"
} ,
"application_list" : {
"name" : "Application list" ,
"type" : "any" ,
"default" : "" ,
"description" : "Limit to the listed application or applications (comma seperated)"
}
} ;
/ * *
* Constructor
*
* /
2020-02-05 23:27:24 +01:00
constructor ( _parent : et2_widget , _attrs? : WidgetConfig , _child? : object )
2020-02-05 21:48:50 +01:00
{
2020-02-05 23:27:24 +01:00
super ( _parent , _attrs , ClassWithAttributes . extendAttributes ( et2_link_apps . _attributes , _child || { } ) ) ;
2020-02-05 21:48:50 +01:00
if ( this . options . select_options != null )
{
// Preset to last application
if ( ! this . options . value )
{
this . set_value ( egw . preference ( 'link_app' , this . egw ( ) . getAppName ( ) ) ) ;
}
// Register to update preference
var self = this ;
this . input . bind ( "click" , function ( ) {
if ( typeof self . options . value != 'undefined' ) var appname = self . options . value . to_app ;
egw . set_preference ( appname || self . egw ( ) . getAppName ( ) , 'link_app' , self . getValue ( ) ) ;
} ) ;
}
}
/ * *
* We get some minor speedups by overriding parent searching and directly setting select options
*
* @param { Array } _attrs an array of attributes
* /
transformAttributes ( _attrs )
{
var select_options = { } ;
// Limit to one app
if ( _attrs . only_app )
{
select_options [ _attrs . only_app ] = this . egw ( ) . lang ( _attrs . only_app ) ;
}
else if ( _attrs . application_list )
{
select_options = _attrs . application_list ;
}
else
{
select_options = egw . link_app_list ( 'query' ) ;
if ( typeof select_options [ 'addressbook-email' ] !== 'undefined' )
{
delete select_options [ 'addressbook-email' ] ;
}
}
_attrs . select_options = select_options ;
super . transformAttributes ( _attrs ) ;
}
}
et2_register_widget ( et2_link_apps , [ "link-apps" ] ) ;
/ * *
* Search and select an entry for linking
* /
export class et2_link_entry extends et2_inputWidget
{
2020-02-07 17:31:10 +01:00
static readonly _attributes : any = {
2020-02-05 21:48:50 +01:00
"value" : {
"type" : "any" ,
"default" : { }
} ,
"only_app" : {
"name" : "Application" ,
"type" : "string" ,
"default" : "" ,
"description" : "Limit to just this one application - hides app selection"
} ,
"application_list" : {
"name" : "Application list" ,
"type" : "any" ,
"default" : "" ,
"description" : "Limit to the listed applications (comma seperated)"
} ,
"app_icons" : {
"name" : "Application icons" ,
"type" : "boolean" ,
"default" : false ,
"description" : "Show application icons instead of names"
} ,
"blur" : {
"name" : "Placeholder" ,
"type" : "string" ,
"default" : et2_no_init ,
"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." ,
translate :true
} ,
"query" : {
"name" : "Query callback" ,
"type" : "js" ,
"default" : et2_no_init ,
"description" : "Callback before query to server. It will be passed the request & et2_link_entry objects. Must return true, or false to abort query."
} ,
"select" : {
"name" : "Select callback" ,
"type" : "js" ,
"default" : et2_no_init ,
"description" : "Callback when user selects an option. Must return true, or false to abort normal action."
}
} ;
protected static readonly legacyOptions = [ "only_app" , "application_list" ] ;
protected static readonly search_timeout = 500 ; //ms after change to send query
protected static readonly minimum_characters = 4 ; // Don't send query unless there's at least this many chars
private cache : any = { } ;
private div : JQuery ;
private app_select : JQuery ;
private search : JQuery ;
private clear : JQuery ;
private link_button : JQuery ;
private response : any ;
private request : any ;
private last_search : string ;
processing : boolean = false ;
/ * *
* Constructor
*
* @memberOf et2_link_entry
* /
2020-02-05 23:27:24 +01:00
constructor ( _parent : et2_widget , _attrs? : WidgetConfig , _child? : object )
{
super ( _parent , _attrs , ClassWithAttributes . extendAttributes ( et2_link_entry . _attributes , _child || { } ) ) ;
2020-02-05 21:48:50 +01:00
this . search = null ;
this . clear = null ;
this . app_select = null ;
this . _oldValue = {
id : null ,
app : this.options.value && this . options . value . app ? this . options.value.app : this.options.only_app
} ;
if ( typeof this . options . value == 'undefined' || this . options . value == null )
{
this . options . value = { } ;
}
this . cache = { } ;
this . request = null ;
this . createInputWidget ( ) ;
var self = this ;
jQuery ( this . getInstanceManager ( ) . DOMContainer ) . on ( 'clear' , function ( ) {
// We need to unbind events to prevent a second triggerd event handler
// (eg. setting a project in infolog edit dialog) when the widget gets cleared.
jQuery ( self . getDOMNode ( ) ) . off ( ) ;
} ) ;
}
destroy ( )
{
super . destroy . apply ( this , arguments ) ;
this . div = null ;
if ( this . search . data ( "ui-autocomplete" ) )
{
this . search . autocomplete ( "destroy" ) ;
}
this . search = null ;
this . clear = null ;
this . app_select = null ;
this . request = null ;
}
createInputWidget ( )
{
var self = this ;
this . div = jQuery ( document . createElement ( "div" ) ) . addClass ( "et2_link_entry" ) ;
// Application selection
jQuery . widget ( "custom.iconselectmenu" , jQuery . ui . selectmenu , {
2020-02-07 17:31:10 +01:00
_setText : function ( element , value )
{
if ( element === this . buttonText )
{
2020-02-05 21:48:50 +01:00
this . _setButtonText ( value ) ;
2020-02-07 17:31:10 +01:00
}
else
{
2020-02-05 21:48:50 +01:00
this . _superApply ( element , value ) ;
}
} ,
_setButtonText : function ( value )
{
var _value = this . focusIndex ;
if ( typeof this . focusIndex === 'undefined' )
{
_value = this . element . find ( "option:selected" ) . val ( ) ;
}
else
{
var selected = this . items [ _value ] || { } ;
_value = selected . value ;
}
var url = self . egw ( ) . image ( 'navbar' , _value ) ;
var buttonItem = jQuery ( "<span>" , {
"class" : "ui-selectmenu-text" ,
title : value
} ) ;
jQuery ( '.ui-selectmenu-text' , this . button ) . replaceWith ( buttonItem ) ;
buttonItem . css ( 'background-image' , 'url(' + url + ')' ) ;
} ,
_renderItem : function ( ul , item )
{
var li = jQuery ( "<li>" , { class : "et2_link_entry_app_option" } ) ,
wrapper = jQuery ( "<div>" , { text : item.label } ) ;
2020-02-07 17:31:10 +01:00
if ( item . disabled )
{
2020-02-05 21:48:50 +01:00
li . addClass ( "ui-state-disabled" ) ;
}
ul . addClass ( self . div . attr ( "class" ) ) ;
var url = self . egw ( ) . image ( 'navbar' , item . value ) ;
jQuery ( "<span>" , {
style : 'background-image: url("' + url + '");' ,
"class" : "ui-icon " + item . element . attr ( "data-class" ) ,
title : item.label
} )
. appendTo ( wrapper ) ;
return li . append ( wrapper ) . appendTo ( ul ) ;
}
} ) ;
this . app_select = jQuery ( document . createElement ( "select" ) ) . appendTo ( this . div )
. change ( function ( e )
{
// Clear cache when app changes
self . cache = { } ;
// Update preference with new value
egw . set_preference ( self . options . value . to_app || self . egw ( ) . getAppName ( ) , 'link_app' , self . app_select . val ( ) ) ;
if ( typeof self . options . value != 'object' ) self . options . value = { } ;
self . options . value . app = self . app_select . val ( ) ;
} ) ;
var opt_count = 0 ;
2020-02-07 17:31:10 +01:00
for ( var key in this . options . select_options )
{
2020-02-05 21:48:50 +01:00
opt_count ++ ;
var option = jQuery ( document . createElement ( "option" ) )
. attr ( "value" , key )
. text ( this . options . select_options [ key ] ) ;
option . appendTo ( this . app_select ) ;
}
if ( this . options . only_app )
{
this . app_select . val ( this . options . only_app ) ;
this . app_select . hide ( ) ;
if ( this . options . app_icons && this . app_select . iconselectmenu ( 'instance' ) )
{
this . app_select . iconselectmenu ( 'widget' ) . hide ( ) ;
}
this . div . addClass ( "no_app" ) ;
}
else
{
// Now that options are in, set to last used app
this . app_select . val ( this . options . value . app || '' ) ;
if ( this . app_select . iconselectmenu ( 'instance' ) )
{
this . app_select . iconselectmenu ( 'update' ) ;
}
}
// Search input
this . search = jQuery ( document . createElement ( "input" ) )
// .attr("type", "search") // Fake it for all browsers below
. focus ( function ( ) { if ( ! self . options . only_app ) {
// Adjust width, leave room for app select & link button
self . div . removeClass ( "no_app" ) ;
if ( self . options . app_icons )
{
self . app_select . iconselectmenu ( 'widget' ) . show ( ) ;
}
else
{
self . app_select . show ( ) ;
}
} } )
. blur ( function ( e ) {
if ( self . div . has ( e . relatedTarget ) . length ) return ;
if ( self . options . app_icons )
{
// Adjust width, leave room for app select & link button
self . div . addClass ( "no_app" ) ;
self . app_select . iconselectmenu ( 'widget' ) . hide ( ) ;
}
else if ( self . search . val ( ) )
{
if ( self . options . only_app ) {
// Adjust width, leave room for app select & link button
self . div . addClass ( "no_app" ) ;
}
}
} )
. appendTo ( this . div ) ;
this . set_blur ( this . options . blur ? this . options.blur : this.egw ( ) . lang ( "search" ) , this . search ) ;
// Autocomplete
this . search . autocomplete ( {
source : function ( request , response )
{
return self . query ( request , response ) ;
} ,
select : function ( event , item )
{
event . data = self ;
// Correct changed value from server
item . item . value = item . item . value . trim ( ) ;
self . select ( event , item ) ;
return false ;
} ,
focus : function ( event , item )
{
event . stopPropagation ( ) ;
self . search . val ( item . item . label ) ;
return false ;
} ,
minLength : et2_link_entry.minimum_characters ,
delay : et2_link_entry.search_timeout ,
disabled : self.options.disabled ,
appendTo : self.div
} ) ;
// Custom display (colors)
2020-02-07 17:31:10 +01:00
this . search . data ( "uiAutocomplete" ) . _renderItem = function ( ul , item )
{
2020-02-05 21:48:50 +01:00
var li = jQuery ( document . createElement ( 'li' ) )
. data ( "item.autocomplete" , item ) ;
var extra : any = { } ;
// Extra stuff
if ( typeof item . label == 'object' ) {
extra = item . label ;
item . label = extra . label ? extra.label : extra ;
if ( extra [ 'style.backgroundColor' ] || extra . color )
{
li . css ( { 'border-left' : '5px solid ' + ( extra . color ? extra.color : extra [ 'style.backgroundColor' ] ) } ) ;
}
// Careful with this, some browsers may have trouble loading all at once, which can slow display
if ( extra . icon )
{
var img = self . egw ( ) . image ( extra . icon ) ;
if ( img )
{
jQuery ( document . createElement ( "img" ) )
. attr ( "src" , img )
. css ( "float" , "right" )
. appendTo ( li ) ;
}
}
}
// Normal stuff
li . append ( jQuery ( "<a></a>" ) . text ( item . label ) )
. appendTo ( ul ) ;
window . setTimeout ( function ( ) { ul . css ( 'max-width' , jQuery ( '.et2_container' ) . width ( ) - ul . offset ( ) . left ) ; } , 300 ) ;
return li ;
} ;
// Bind to enter key to start search early
2020-02-07 17:31:10 +01:00
this . search . keydown ( function ( e )
{
2020-02-05 21:48:50 +01:00
var keycode = ( e . keyCode ? e.keyCode : e.which ) ;
if ( keycode == 13 && ! self . processing )
{
self . search . autocomplete ( "option" , "minLength" , 0 ) ;
self . search . autocomplete ( "search" ) ;
self . search . autocomplete ( "option" , "minLength" , self . minimum_characters ) ;
return false ;
}
} ) ;
// Clear / last button
this . clear = jQuery ( document . createElement ( "span" ) )
. addClass ( "ui-icon ui-icon-close" )
2020-02-07 17:31:10 +01:00
. click ( function ( e )
{
2020-02-05 21:48:50 +01:00
if ( ! self . search ) return ; // only gives an error, we should never get into that situation
// No way to tell if the results is open, so if they click the button while open, it clears
if ( self . last_search && self . last_search != self . search . val ( ) )
{
// Repeat last search (should be cached)
self . search . val ( self . last_search ) ;
self . last_search = "" ;
self . search . autocomplete ( "search" ) ;
}
else
{
// Clear
self . search . autocomplete ( "close" ) ;
self . set_value ( null ) ;
self . search . val ( "" ) ;
// call trigger, after finishing this handler, not in the middle of it
window . setTimeout ( function ( )
{
self . search . trigger ( "change" ) ;
} , 0 ) ;
}
self . search . focus ( ) ;
} )
. appendTo ( this . div )
. hide ( ) ;
this . setDOMNode ( this . div [ 0 ] ) ;
}
getDOMNode ( )
{
return this . div ? this . div [ 0 ] : null ;
}
transformAttributes ( _attrs )
{
super . transformAttributes . apply ( this , arguments ) ;
_attrs [ "select_options" ] = { } ;
if ( _attrs [ "application_list" ] )
{
var apps = ( typeof _attrs [ "application_list" ] == "string" ) ? et2_csvSplit ( _attrs [ "application_list" ] , null , "," ) : _attrs [ "application_list" ] ;
for ( var i = 0 ; i < apps . length ; i ++ )
{
_attrs [ "select_options" ] [ apps [ i ] ] = this . egw ( ) . lang ( apps [ i ] ) ;
}
}
else
{
_attrs [ "select_options" ] = this . egw ( ) . link_app_list ( 'query' ) ;
if ( typeof _attrs [ "select_options" ] [ "addressbook-email" ] != 'undefined' ) delete _attrs [ "select_options" ] [ "addressbook-email" ] ;
}
// Check whether the options entry was found, if not read it from the
// content array.
if ( _attrs [ "select_options" ] == null )
{
_attrs [ "select_options" ] = this . getArrayMgr ( 'content' )
. getEntry ( "options-" + this . id ) ;
}
// Default to an empty object
if ( _attrs [ "select_options" ] == null )
{
_attrs [ "select_options" ] = { } ;
}
}
doLoadingFinished ( )
{
if ( typeof this . options . value == 'object' && ! this . options . value . app )
{
this . options . value . app = egw . preference ( 'link_app' , this . options . value . to_app || this . egw ( ) . getAppName ( ) ) ;
// If there's no value set for app, then take the first one from the selectbox
if ( typeof this . options . value . app == 'undefined' || ! this . options . value . app )
{
this . options . value . app = Object . keys ( this . options . select_options ) [ 0 ] ;
}
this . app_select . val ( this . options . value . app ) ;
}
if ( this . options . app_icons )
{
var self = this ;
this . div . addClass ( 'app_icons' ) ;
this . app_select . iconselectmenu ( {
width : 50 ,
change : function ( )
{
window . setTimeout ( function ( )
{
self . app_select . trigger ( "change" ) ;
} , 0 ) ;
}
} )
. iconselectmenu ( "menuWidget" ) ;
this . app_select . iconselectmenu ( 'widget' ) . hide ( ) ;
}
return super . doLoadingFinished . apply ( this , arguments ) ;
}
getValue ( )
{
var value = this . options && this . options . only_app ? this . options.value.id : this.options? this . options.value : null ;
if ( this . options && ! this . options . only_app && this . search )
{
value . search = this . search . val ( ) ;
}
return value ;
}
set_value ( _value )
{
if ( typeof _value == 'string' || typeof _value == 'number' )
{
if ( typeof _value == 'string' && _value . indexOf ( "," ) > 0 ) _value = _value . replace ( "," , ":" ) ;
if ( typeof _value == 'string' && _value . indexOf ( ":" ) >= 0 )
{
var split = _value . split ( ":" ) ;
_value = {
app : split.shift ( ) ,
id : split.length == 1 ? split [ 0 ] : split
} ;
}
else if ( _value && this . options . only_app )
{
_value = {
app : this.options.only_app ,
id : _value
} ;
}
}
this . _oldValue = this . options . value ;
if ( ! _value || _value . length == 0 || _value == null || jQuery . isEmptyObject ( _value ) )
{
this . search . val ( "" ) ;
this . clear . hide ( ) ;
this . options . value = _value = { 'id' : null } ;
}
if ( ! _value . app ) _value . app = this . options . only_app || this . app_select . val ( ) ;
if ( _value . id ) {
// Remove specific display and revert to CSS file
// show() would use inline, should be inline-block
this . clear . css ( 'display' , '' ) ;
} else {
this . clear . hide ( ) ;
return ;
}
if ( typeof _value != 'object' || ( ! _value . app && ! _value . id ) )
{
console . warn ( "Bad value for link widget. Need an object with keys 'app', 'id', and optionally 'title'" , _value ) ;
return ;
}
if ( ! _value . title ) {
var title = this . egw ( ) . link_title ( _value . app , _value . id ) ;
if ( title != null )
{
_value . title = title ;
}
else
{
// Title will be fetched from server and then set
var title = this . egw ( ) . link_title ( _value . app , _value . id , function ( title )
{
this . search . removeClass ( "loading" ) . val ( title + "" ) ;
// Remove specific display and revert to CSS file
// show() would use inline, should be inline-block
this . clear . css ( 'display' , '' ) ;
} , this ) ;
this . search . addClass ( "loading" ) ;
}
}
if ( _value . title )
{
this . search . val ( _value . title + "" ) ;
}
this . options . value = _value ;
jQuery ( "option[value='" + _value . app + "']" , this . app_select ) . prop ( "selected" , true ) ;
this . app_select . hide ( ) ;
if ( this . options . app_icons && this . app_select . iconselectmenu ( 'instance' ) )
{
this . app_select . iconselectmenu ( 'widget' ) . hide ( ) ;
}
this . div . addClass ( "no_app" ) ;
}
set_blur ( _value , input )
{
if ( typeof input == 'undefined' ) input = this . search ;
if ( _value )
{
input . attr ( "placeholder" , _value ) ; // HTML5
if ( ! input [ 0 ] . placeholder )
{
// Not HTML5
if ( input . val ( ) == "" ) input . val ( _value ) ;
input . focus ( input , function ( e )
{
var placeholder = _value ;
if ( e . data . val ( ) == placeholder ) e . data . val ( "" ) ;
} ) . blur ( input , function ( e )
{
var placeholder = _value ;
if ( e . data . val ( ) == "" ) e . data . val ( placeholder ) ;
} ) ;
if ( input . val ( ) == "" )
{
input . val ( _value ) ;
}
}
}
else
{
this . search . removeAttr ( "placeholder" ) ;
}
}
/ * *
* Set the query callback
*
* @param { function } f
* /
set_query ( f )
{
this . options . query = f ;
}
/ * *
* Set the select callback
*
* @param { function } f
* /
set_select ( f )
{
this . options . select = f ;
}
/ * *
* Ask server for entries matching selected app / type and filtered by search string
*
* @param { Object } request
* @param { Object } response
* /
query ( request , response )
{
// If there is a pending request, abort it
if ( this . request )
{
this . request . abort ( ) ;
this . request = null ;
}
// Remember last search
this . last_search = this . search . val ( ) ;
// Allow hook / tie in
if ( this . options . query && typeof this . options . query == 'function' )
{
if ( ! this . options . query ( request , this ) ) return false ;
}
if ( ( typeof request . no_cache == 'undefined' && ! request . no_cache ) && request . term in this . cache ) {
return response ( this . cache [ request . term ] ) ;
}
// Remember callback
this . response = response ;
this . search . addClass ( "loading" ) ;
// Remove specific display and revert to CSS file
// show() would use inline, should be inline-block
this . clear . css ( 'display' , '' ) ;
this . request = egw . json ( "EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_search" ,
[ this . app_select . val ( ) , '' , request . term , request . options ] ,
this . _results ,
this , true , this
) . sendRequest ( ) ;
}
/ * *
* User selected a value
*
* @param { Object } event
* @param { Object } selected
*
* /
select ( event , selected )
{
if ( selected . item . value !== null && typeof selected . item . value == "string" )
{
// Correct changed value from server
selected . item . value = selected . item . value . trim ( ) ;
}
if ( this . options . select && typeof this . options . select == 'function' )
{
if ( ! this . options . select ( event , selected ) ) return false ;
}
if ( typeof event . data . options . value != 'object' || event . data . options . value == null )
{
event . data . options . value = { } ;
}
event . data . options . value . id = selected . item . value ;
// Set a processing flag to filter some events
event . data . processing = true ;
// Remove specific display and revert to CSS file
// show() would use inline, should be inline-block
this . clear . css ( 'display' , '' ) ;
event . data . search . val ( selected . item . label ) ;
// Fire change event
this . search . change ( ) ;
// Turn off processing flag when done
window . setTimeout ( jQuery . proxy ( function ( ) { delete this . processing ; } , event . data ) ) ;
}
/ * *
* Server found some results
*
* @param { Array } data
* /
_results ( data )
{
if ( this . request )
{
this . request = null ;
}
this . search . removeClass ( "loading" ) ;
var result = [ ] ;
for ( var id in data ) {
result . push ( { "value" : id , "label" : data [ id ] } ) ;
}
this . cache [ this . search . val ( ) ] = result ;
this . response ( result ) ;
}
/ * *
* Create a link using the current internal values
*
* @param { Object } event
* @param { Object } _links
* /
createLink ( event , _links )
{
var values = event . data . options . value ;
var self = event . data ;
var links = [ ] ;
if ( typeof _links == 'undefined' )
{
links = [ ] ;
}
else
{
links = _links ;
}
// Links to other entries
if ( values . id ) {
links . push ( {
app : values.app ,
id : values.id
} ) ;
self . search . val ( "" ) ;
}
// If a link array was passed in, don't make the ajax call
if ( typeof _links == 'undefined' )
{
var request = egw . json ( "EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link" ,
[ values . to_app , values . to_id , links ] ,
self . _link_result ,
this ,
true
) ;
request . sendRequest ( ) ;
}
}
/ * *
* Sent some links , server has a result
*
* @param { Object } success
*
* /
_link_result ( success )
{
if ( success ) {
this . status_span . fadeIn ( ) . delay ( 1000 ) . fadeOut ( ) ;
delete this . options . value . app ;
delete this . options . value . id ;
}
}
}
et2_register_widget ( et2_link_entry , [ "link-entry" ] ) ;
/ * *
* UI widget for a single ( read - only ) link
*
* /
export class et2_link extends et2_valueWidget implements et2_IDetachedDOM
{
static readonly _attributes : any = {
"only_app" : {
"name" : "Application" ,
"type" : "string" ,
"default" : "" ,
"description" : "Use the given application, so you can pass just the ID for value"
} ,
"value" : {
description : "Array with keys app, id, and optionally title" ,
type : "any"
} ,
"needed" : {
"ignore" : true
} ,
"link_hook" : {
"name" : "Type" ,
"type" : "string" ,
"default" : "view" ,
"description" : "Hook used for displaying link (view/edit/add)"
} ,
"target_app" : {
"name" : "Target application" ,
"type" : "string" ,
"default" : "" ,
"description" : "Optional parameter to be passed to egw().open in order to open links in specified application"
}
} ;
legacyOptions = [ "only_app" ] ;
private label_span : JQuery ;
private link : JQuery ;
/ * *
* Constructor
*
* @memberOf et2_link
* /
2020-02-05 23:27:24 +01:00
constructor ( _parent : et2_widget , _attrs? : WidgetConfig , _child? : object )
2020-02-05 21:48:50 +01:00
{
2020-02-05 23:27:24 +01:00
super ( _parent , _attrs , ClassWithAttributes . extendAttributes ( et2_link . _attributes , _child || { } ) ) ;
2020-02-05 21:48:50 +01:00
this . label_span = jQuery ( document . createElement ( "label" ) )
. addClass ( "et2_label" ) ;
this . link = jQuery ( document . createElement ( "span" ) )
. addClass ( "et2_link" )
. appendTo ( this . label_span ) ;
if ( this . options [ 'class' ] ) this . label_span . addClass ( this . options [ 'class' ] ) ;
this . setDOMNode ( this . label_span [ 0 ] ) ;
}
destroy ( )
{
if ( this . link ) this . link . unbind ( ) ;
this . link = null ;
super . destroy . apply ( this , arguments ) ;
}
set_label ( label )
{
// Remove current label
this . label_span . contents ( )
. filter ( function ( ) { return this . nodeType == 3 ; } ) . remove ( ) ;
var parts = et2_csvSplit ( label , 2 , "%s" ) ;
this . label_span . prepend ( parts [ 0 ] ) ;
this . label_span . append ( parts [ 1 ] ) ;
this . label = label ;
// add class if label is empty
this . label_span . toggleClass ( 'et2_label_empty' , ! label || ! parts [ 0 ] ) ;
}
set_value ( _value )
{
if ( typeof _value != 'object' && _value && ! this . options . only_app )
{
if ( _value . indexOf ( ':' ) >= 0 )
{
var app = _value . split ( ':' , 1 ) ;
var id = _value . substr ( app [ 0 ] . length + 1 ) ;
_value = { 'app' : app [ 0 ] , 'id' : id } ;
}
else
{
console . warn ( "Bad value for link widget. Need an object with keys 'app', 'id', and optionally 'title'" , _value ) ;
return ;
}
}
// Application set, just passed ID
else if ( typeof _value != "object" )
{
_value = {
app : this.options.only_app ,
id : _value
} ;
}
if ( ! _value || jQuery . isEmptyObject ( _value ) )
{
this . link . text ( "" ) . unbind ( ) ;
return ;
}
var self = this ;
this . link . unbind ( ) ;
if ( _value . id && _value . app )
{
this . link . addClass ( "et2_link" ) ;
this . link . click ( function ( e )
{
if ( ! self . options . target_app ) {
self . options . target_app = _value . app ;
}
self . egw ( ) . open ( _value , "" , self . options . link_hook , _value . extra_args , _value . app , self . options . target_app ) ;
e . stopImmediatePropagation ( ) ;
} ) ;
}
else
{
this . link . removeClass ( "et2_link" ) ;
}
if ( ! _value . title )
{
var self = this ;
var node = this . link [ 0 ] ;
if ( _value . app && _value . id )
{
var title = this . egw ( ) . link_title ( _value . app , _value . id , function ( title ) { self . set_title ( node , title ) ; } , this ) ;
if ( title != null )
{
_value . title = title ;
}
else
{
// Title will be fetched from server and then set
return ;
}
}
else
{
_value . title = "" ;
}
}
this . set_title ( this . link , _value . title ) ;
}
/ * *
* Sets the text to be displayed .
* Used as a callback , so node is provided to make sure we get the right one
*
* @param { Object } node
* @param { String } _value description
* /
set_title ( node , _value )
{
if ( _value === false || _value === null ) _value = "" ;
jQuery ( node ) . text ( _value + "" ) ;
}
/ * *
* Creates a list of attributes which can be set when working in the
* "detached" mode . The result is stored in the _attrs array which is provided
* by the calling code .
*
* @param { Array } _attrs an array of attributes
* /
getDetachedAttributes ( _attrs )
{
_attrs . push ( "label" , "value" ) ;
}
/ * *
* Returns an array of DOM nodes . The ( relatively ) same DOM - Nodes have to be
* passed to the "setDetachedAttributes" function in the same order .
* /
getDetachedNodes ( )
{
return [ this . node , this . link [ 0 ] ] ;
}
/ * *
* Sets the given associative attribute - > value array and applies the
* attributes to the given DOM - Node .
*
* @param _nodes is an array of nodes which have to be in the same order as
* the nodes returned by "getDetachedNodes"
* @param _values is an associative array which contains a subset of attributes
* returned by the "getDetachedAttributes" function and sets them to the
* given values .
* /
setDetachedAttributes ( _nodes , _values )
{
this . node = _nodes [ 0 ] ;
this . label_span = jQuery ( _nodes [ 0 ] ) ;
this . link = jQuery ( _nodes [ 1 ] ) ;
if ( typeof _values [ "id" ] !== "undefined" ) this . set_id ( _values [ 'id' ] ) ;
if ( typeof _values [ "label" ] !== "undefined" ) this . set_label ( _values [ 'label' ] ) ;
if ( typeof _values [ "value" ] !== "undefined" ) this . set_value ( _values [ "value" ] ) ;
}
}
et2_register_widget ( et2_link , [ "link" , "link-entry_ro" ] ) ;
/ * *
* UI widget for one or more links , comma separated
*
* TODO : This one used to have expose
* /
export class et2_link_string extends et2_valueWidget implements et2_IDetachedDOM
{
2020-02-07 17:31:10 +01:00
static readonly _attributes : any = {
2020-02-05 21:48:50 +01:00
"application" : {
"name" : "Application" ,
"type" : "string" ,
"default" : "" ,
"description" : "Use the given application, so you can pass just the ID for value"
} ,
"value" : {
"description" : "Either an array of link information (see egw_link::link()) or array with keys to_app and to_id" ,
"type" : "any"
} ,
"only_app" : {
"name" : "Application filter" ,
"type" : "string" ,
"default" : "" ,
"description" : "Appname, eg. 'projectmananager' to list only linked projects"
} ,
"link_type" : {
"name" : "Type filter" ,
"type" : "string" ,
"default" : "" ,
"description" : "Sub-type key to list only entries of that type"
} ,
"expose_view" : {
name : "Expose view" ,
type : "boolean" ,
default : true ,
description : "Clicking on description with href value would popup an expose view, and will show content referenced by href."
}
} ;
protected list : JQuery ;
protected value : any ;
/ * *
* Constructor
*
* @memberOf et2_link_string
* /
2020-02-05 23:27:24 +01:00
constructor ( _parent : et2_widget , _attrs? : WidgetConfig , _child? : object )
2020-02-05 21:48:50 +01:00
{
2020-02-05 23:27:24 +01:00
super ( _parent , _attrs , ClassWithAttributes . extendAttributes ( et2_link_string . _attributes , _child || { } ) ) ;
2020-02-05 21:48:50 +01:00
this . list = jQuery ( document . createElement ( "ul" ) )
. addClass ( "et2_link_string" ) ;
if ( this . options [ 'class' ] ) this . list . addClass ( this . options [ 'class' ] ) ;
this . setDOMNode ( this . list [ 0 ] ) ;
}
destroy ( )
{
super . destroy . apply ( this , arguments ) ;
if ( this . node != null ) {
jQuery ( this . node ) . children ( ) . unbind ( ) ;
}
}
set_value ( _value )
{
// Get data
if ( ! _value || _value == null )
{
this . list . empty ( ) ;
return ;
}
if ( typeof _value == "string" && _value . indexOf ( ',' ) > 0 )
{
_value = _value . split ( ',' ) ;
}
if ( ! _value . to_app && typeof _value == "object" && this . options . application )
{
_value . to_app = this . options . application ;
}
if ( typeof _value == 'object' && _value . to_app && _value . to_id )
{
this . value = _value ;
this . _get_links ( ) ;
return ;
}
this . list . empty ( ) ;
if ( typeof _value == 'object' && _value . length > 0 ) {
// Have full info
// Don't store new value, just update display
// Make new links
for ( var i = 0 ; i < _value . length ; i ++ )
{
if ( ! this . options . only_app || this . options . only_app && _value [ i ] . app == this . options . only_app )
{
this . _add_link ( _value [ i ] . id ? _value [ i ] : { id :_value [ i ] , app : _value.to_app } ) ;
}
}
}
else if ( this . options . application )
{
this . _add_link ( { id :_value , app : this.options.application } ) ;
}
}
_get_links ( )
{
var _value = this . value ;
// Just IDs - get from server
if ( this . options . only_app )
{
_value . only_app = this . options . only_app ;
}
this . egw ( ) . jsonq ( 'EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_list' , [ _value ] , this . set_value , this ) ;
return ;
}
/ * *
* Function to get media content to feed the expose
* @param { type } _value
* @returns { Array | Array . getMedia . mediaContent }
* /
getMedia ( _value )
{
var base_url = egw . webserverUrl . match ( /^\// , 'ig' ) ? egw ( window ) . window . location . origin + egw.webserverUrl : egw.webserverUrl ;
var mediaContent = [ ] ;
if ( _value && typeof _value . type != 'undefined' && _value . type . match ( /video\// , 'ig' ) )
{
mediaContent = [ {
title : _value.id ,
type : _value . type ,
poster : '' , // TODO: Should be changed by correct video thumbnail later
href : base_url + egw ( ) . mime_open ( _value ) ,
download_href : base_url + egw ( ) . mime_open ( _value ) + '?download'
} ] ;
}
else if ( _value )
{
mediaContent = [ {
title : _value.id ,
href : base_url + egw ( ) . mime_open ( _value ) . url ,
download_href : base_url + egw ( ) . mime_open ( _value ) . url + '?download' ,
type : _value . type
} ] ;
}
if ( mediaContent [ 0 ] . href && mediaContent [ 0 ] . href . match ( /\/webdav.php/ , 'ig' ) ) mediaContent [ 0 ] [ "download_href" ] = mediaContent [ 0 ] . href + '?download' ;
return mediaContent ;
}
_add_link ( _link_data )
{
var self = this ;
var link = jQuery ( document . createElement ( "li" ) )
. appendTo ( this . list )
. addClass ( "et2_link loading" )
. click ( function ( e ) {
var fe = egw_get_file_editor_prefered_mimes ( _link_data . type ) ;
if ( self . options . expose_view && typeof _link_data . type != 'undefined'
&& _link_data . type . match ( self . mime_regexp , 'ig' ) )
{
self . _init_blueimp_gallery ( e , _link_data ) ;
}
else if ( typeof _link_data . type != 'undefined' && fe && fe . mime && fe . mime [ _link_data . type ] )
{
egw . open_link ( egw . link ( '/index.php' , {
menuaction : fe.edit.menuaction ,
path : egw ( ) . mime_open ( _link_data ) . url . replace ( '/webdav.php' , '' )
} ) , '' , fe . edit_popup ) ;
}
else
{
self . egw ( ) . open ( _link_data , "" , "view" , null , _link_data . app , _link_data . app ) ;
}
e . stopImmediatePropagation ( ) ;
} ) ;
if ( _link_data . title ) link . text ( _link_data . title ) ;
// Now that link is created, get title from server & update
if ( ! _link_data . title ) {
this . egw ( ) . link_title ( _link_data . app , _link_data . id , function ( title ) {
if ( title )
this . removeClass ( "loading" ) . text ( title ) ;
else
this . remove ( ) ; // no rights or not found
} , link ) ;
}
}
/ * *
* Creates a list of attributes which can be set when working in the
* "detached" mode . The result is stored in the _attrs array which is provided
* by the calling code .
*
* @param { Array } _attrs an array of attributes
* /
getDetachedAttributes ( _attrs )
{
// Create the label container if it didn't exist yet
if ( this . _labelContainer == null )
{
this . _labelContainer = jQuery ( document . createElement ( "label" ) )
. addClass ( "et2_label" ) ;
this . getSurroundings ( ) . insertDOMNode ( this . _labelContainer [ 0 ] ) ;
this . getSurroundings ( ) . update ( ) ;
}
_attrs . push ( "value" , "label" ) ;
}
/ * *
* Returns an array of DOM nodes . The ( relatively ) same DOM - Nodes have to be
* passed to the "setDetachedAttributes" function in the same order .
* /
getDetachedNodes ( )
{
// Create the label container if it didn't exist yet
if ( this . _labelContainer == null )
{
this . _labelContainer = jQuery ( document . createElement ( "label" ) )
. addClass ( "et2_label" ) ;
this . getSurroundings ( ) . insertDOMNode ( this . _labelContainer [ 0 ] ) ;
}
return [ this . list [ 0 ] , this . _labelContainer [ 0 ] ] ;
}
/ * *
* Sets the given associative attribute - > value array and applies the
* attributes to the given DOM - Node .
*
* @param _nodes is an array of nodes which have to be in the same order as
* the nodes returned by "getDetachedNodes"
* @param _values is an associative array which contains a subset of attributes
* returned by the "getDetachedAttributes" function and sets them to the
* given values .
* /
setDetachedAttributes ( _nodes , _values )
{
this . list = jQuery ( _nodes [ 0 ] ) ;
this . set_value ( _values [ "value" ] ) ;
// Special detached, to prevent DOM node modification of the normal method
this . _labelContainer = _nodes . length > 1 ? jQuery ( _nodes [ 1 ] ) : null ;
if ( _values [ 'label' ] )
{
this . set_label ( _values [ 'label' ] ) ;
}
else if ( this . _labelContainer )
{
this . _labelContainer . contents ( ) . not ( this . list ) . remove ( ) ;
}
}
}
et2_register_widget ( et2_link_string , [ "link-string" ] ) ;
/ * *
* UI widget for one or more links in a list ( table )
* /
export class et2_link_list extends et2_link_string
{
2020-02-07 17:31:10 +01:00
static readonly _attributes : any = {
2020-02-05 21:48:50 +01:00
"show_deleted" : {
"name" : "Show deleted" ,
"type" : "boolean" ,
"default" : false ,
"description" : "Show links that are marked as deleted, being held for purge"
} ,
"onchange" : {
"name" : "onchange" ,
"type" : "js" ,
"default" : et2_no_init ,
"description" : "JS code which is executed when the links change."
} ,
readonly : {
name : "readonly" ,
type : "boolean" ,
"default" : false ,
description : "Does NOT allow user to enter data, just displays existing data"
} ,
"target_app" : {
"name" : "Target application" ,
"type" : "string" ,
"default" : "" ,
"description" : "Optional parameter to be passed to egw().open in order to open links in specified application "
}
} ;
private context : egwMenu ;
/ * *
* Constructor
*
* /
2020-02-05 23:27:24 +01:00
constructor ( _parent : et2_widget , _attrs? : WidgetConfig , _child? : object )
2020-02-05 21:48:50 +01:00
{
2020-02-05 23:27:24 +01:00
super ( _parent , _attrs , ClassWithAttributes . extendAttributes ( et2_link_list . _attributes , _child || { } ) ) ;
2020-02-05 21:48:50 +01:00
this . list = jQuery ( document . createElement ( "table" ) )
. addClass ( "et2_link_list" ) ;
if ( this . options [ 'class' ] ) this . list . addClass ( this . options [ 'class' ] ) ;
this . setDOMNode ( this . list [ 0 ] ) ;
// Set up context menu
var self = this ;
this . context = new egwMenu ( ) ;
this . context . addItem ( "comment" , this . egw ( ) . lang ( "Comment" ) , "" , function ( ) {
var link_id = typeof self . context . data . link_id == 'number' ? self.context.data.link_id : self.context.data.link_id.replace ( /[:\.]/g , '_' ) ;
et2_dialog . show_prompt (
function ( button , comment ) {
if ( button != et2_dialog . OK_BUTTON ) return ;
var remark = jQuery ( '#link_' + ( self . context . data . dom_id ? self.context.data.dom_id : link_id ) , self . list ) . children ( '.remark' ) ;
if ( isNaN ( self . context . data . link_id ) ) // new entry, not yet stored
{
remark . text ( comment ) ;
// Look for a link-to with the same ID, refresh it
if ( self . context . data . link_id )
{
var _widget = link_id . widget || null ;
self . getRoot ( ) . iterateOver (
function ( widget ) {
if ( widget . id == self . id ) {
_widget = widget ;
}
} ,
self , et2_link_to
) ;
var value = _widget != null ? _widget . getValue ( ) : false ;
if ( _widget && value && value . to_id )
{
value . to_id [ self . context . data . link_id ] . remark = comment ;
}
}
return ;
}
remark . addClass ( "loading" ) ;
var request = egw . json ( "EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_comment" ,
[ link_id , comment ] ,
function ( ) {
if ( remark )
{
// Append "" to make sure it's a string, not undefined
remark . removeClass ( "loading" ) . text ( comment + "" ) ;
// Update internal data
self . context . data . remark = comment + "" ;
}
} ,
this , true
) . sendRequest ( ) ;
} ,
'' , self . egw ( ) . lang ( "Comment" ) , self . context . data . remark || ''
) ;
} ) ;
this . context . addItem ( "file_info" , this . egw ( ) . lang ( "File information" ) , this . egw ( ) . image ( "edit" ) , function ( menu_item ) {
var link_data = self . context . data ;
if ( link_data . app == 'file' )
{
// File info is always the same
var url = '/apps/' + link_data . app2 + '/' + link_data . id2 + '/' + decodeURIComponent ( link_data . id ) ;
if ( typeof url == 'string' && url . indexOf ( 'webdav.php' ) )
{
// URL is url to file in webdav, so get rid of that part
url = url . replace ( '/webdav.php' , '' ) ;
}
self . egw ( ) . open ( url , "filemanager" , "edit" ) ;
}
} ) ;
this . context . addItem ( "-" , "-" ) ;
this . context . addItem ( "save" , this . egw ( ) . lang ( "Save as" ) , this . egw ( ) . image ( 'save' ) , function ( menu_item ) {
var link_data = self . context . data ;
// Download file
if ( link_data . download_url )
{
var url = link_data . download_url ;
if ( url [ 0 ] == '/' ) url = egw . link ( url ) ;
let a = document . createElement ( 'a' ) ;
if ( typeof a . download == "undefined" )
{
window . location . href = url + "?download" ;
return false ;
}
// Multiple file download for those that support it
a = jQuery ( a )
. prop ( 'href' , url )
. prop ( 'download' , link_data . title || "" )
. appendTo ( self . getInstanceManager ( ) . DOMContainer ) ;
var evt = document . createEvent ( 'MouseEvent' ) ;
evt . initMouseEvent ( 'click' , true , true , window , 1 , 0 , 0 , 0 , 0 , false , false , false , false , 0 , null ) ;
a [ 0 ] . dispatchEvent ( evt ) ;
a . remove ( ) ;
return false ;
}
self . egw ( ) . open ( link_data , "" , "view" , 'download' , link_data . target ? link_data.target : link_data.app , link_data . app ) ;
} ) ;
this . context . addItem ( "zip" , this . egw ( ) . lang ( "Save as Zip" ) , this . egw ( ) . image ( 'save_zip' ) , function ( menu_item ) {
// Highlight files for nice UI indicating what will be in the zip.
// Files have negative IDs.
jQuery ( '[id^="link_-"]' , this . list ) . effect ( 'highlight' , { } , 2000 ) ;
// Download ZIP
window . location = self . egw ( ) . link ( '/index.php' , {
menuaction : 'api.EGroupware\\Api\\Etemplate\\Widget\\Link.download_zip' ,
app : self.value.to_app ,
id : self.value.to_id
} ) ;
} ) ;
this . context . addItem ( "-" , "-" ) ;
this . context . addItem ( "delete" , this . egw ( ) . lang ( "Delete link" ) , this . egw ( ) . image ( "delete" ) , function ( menu_item ) {
var link_id = isNaN ( self . context . data . link_id ) ? self.context.data : self.context.data.link_id ;
var row = jQuery ( '#link_' + ( self . context . data . dom_id ? self.context.data.dom_id : self.context.data.link_id ) , self . list ) ;
et2_dialog . show_dialog (
function ( button ) { if ( button == et2_dialog . YES_BUTTON ) self . _delete_link ( link_id , row ) ; } ,
egw . lang ( 'Delete link?' )
) ;
} ) ;
// Native DnD - Doesn't play nice with jQueryUI Sortable
// Tell jQuery to include this property
jQuery . event . props . push ( 'dataTransfer' ) ;
}
destroy ( )
{
super . destroy . apply ( this , arguments ) ;
if ( this . context )
{
this . context . clear ( ) ;
delete this . context ;
}
}
set_value ( _value )
{
this . list . empty ( ) ;
// Handle server passed a list of links that aren't ready yet
if ( _value && typeof _value == "object" )
{
var list = [ ] ;
if ( _value . to_id && typeof _value . to_id == "object" )
{
list = _value . to_id ;
}
else if ( _value . length )
{
list = _value ;
}
if ( list . length > 0 )
{
for ( var id in list )
{
var link = list [ id ] ;
if ( link . app )
{
// Temp IDs can cause problems since the ID includes the file name or :
if ( link . link_id && typeof link . link_id != 'number' )
{
link . dom_id = 'temp_' + egw . uid ( ) ;
}
// Icon should be in registry
if ( ! link . icon )
{
link . icon = egw . link_get_registry ( link . app , 'icon' ) ;
// No icon, try by mime type - different place for un-saved entries
if ( link . icon == false && link . id . type )
{
// Triggers icon by mime type, not thumbnail or app
link . type = link . id . type ;
link . icon = true ;
}
}
// Special handling for file - if not existing, we can't ask for title
if ( typeof link . id == 'object' && ! link . title )
{
link . title = link . id . name || '' ;
}
this . _add_link ( link ) ;
}
}
}
else
{
super . set_value ( _value ) ;
}
}
}
_add_link ( _link_data )
{
var row = jQuery ( document . createElement ( "tr" ) )
. attr ( "id" , "link_" + ( _link_data . dom_id ? _link_data . dom_id : ( typeof _link_data . link_id == "string" ? _link_data . link_id . replace ( /[:\.]/g , '_' ) : _link_data . link_id || _link_data . id ) ) )
. attr ( "draggable" , _link_data . app == 'file' ? "true" : "" )
. appendTo ( this . list ) ;
if ( ! _link_data . link_id )
{
for ( var k in _link_data )
{
row [ 0 ] . dataset [ k ] = _link_data [ k ] ;
}
}
// Icon
var icon = jQuery ( document . createElement ( "td" ) )
. appendTo ( row )
. addClass ( "icon" ) ;
if ( _link_data . icon )
{
var icon_widget = et2_createWidget ( "image" , { } ) ;
var src = '' ;
// Creat a mime widget if the link has type
if ( _link_data . type )
{
// VFS - file
var vfs_widget = et2_createWidget ( 'vfs-mime' , { } ) ;
vfs_widget . set_value ( {
download_url :_link_data.download_url ,
name :_link_data.title ,
mime :_link_data.type ,
path :_link_data.icon
} ) ;
icon . append ( vfs_widget . getDOMNode ( ) ) ;
}
else
{
src = this . egw ( ) . image ( _link_data . icon ) ;
if ( src ) icon_widget . set_src ( src ) ;
icon . append ( icon_widget . getDOMNode ( ) ) ;
}
}
var columns = [ 'title' , 'remark' ] ;
var self = this ;
for ( var i = 0 ; i < columns . length ; i ++ ) {
var $td = jQuery ( document . createElement ( "td" ) )
. appendTo ( row )
. addClass ( columns [ i ] )
. text ( _link_data [ columns [ i ] ] ? _link_data [ columns [ i ] ] + "" : "" ) ;
var dirs = _link_data [ columns [ i ] ] ? _link_data [ columns [ i ] ] . split ( '/' ) : [ ] ;
if ( columns [ i ] == 'title' && _link_data . type && dirs . length > 1 )
{
this . _format_vfs ( $td , dirs , _link_data ) ;
}
//Bind the click handler if there is download_url
if ( _link_data && ( typeof _link_data . download_url != 'undefined' || _link_data . app != 'egw-data' ) )
{
$td . click ( function ( ) {
var fe_mime = egw_get_file_editor_prefered_mimes ( _link_data . type ) ;
// Check if the link entry is mime with media type, in order to open it in expose view
if ( typeof _link_data . type != 'undefined' &&
( _link_data . type . match ( self . mime_regexp , 'ig' ) || ( fe_mime && fe_mime . mime [ _link_data . type ] ) ) )
{
var $vfs_img_node = jQuery ( this ) . parent ( ) . find ( '.vfsMimeIcon' ) ;
if ( $vfs_img_node . length > 0 ) $vfs_img_node . click ( ) ;
}
else
{
if ( ! self . options . target_app ) {
self . options . target_app = _link_data . app ;
}
self . egw ( ) . open ( _link_data , "" , "view" , null , _link_data . target ? _link_data.target : _link_data.app , self . options . target_app ) ;
}
} ) ;
}
}
if ( typeof _link_data . title == 'undefined' )
{
// Title will be fetched from server and then set
jQuery ( 'td.title' , row ) . addClass ( "loading" ) ;
var title = this . egw ( ) . link_title ( _link_data . app , _link_data . id , function ( title ) {
jQuery ( 'td.title' , this ) . removeClass ( "loading" ) . text ( title + "" ) ;
} , row ) ;
}
// Date
/ *
var date_row = jQuery ( document . createElement ( "td" ) )
. appendTo ( row ) ;
if ( _link_data . lastmod )
{
var date_widget = et2_createWidget ( "date-since" ) ;
date_widget . set_value ( _link_data . lastmod ) ;
date_row . append ( date_widget . getDOMNode ( ) ) ;
}
* /
// Delete
// build delete button if the link is not readonly
if ( ! this . options . readonly )
{
var delete_button = jQuery ( document . createElement ( "td" ) )
. appendTo ( row ) ;
jQuery ( "<div />" )
. appendTo ( delete_button )
// We don't use ui-icon because it assigns a bg image
. addClass ( "delete icon" )
. bind ( 'click' , function ( ) {
et2_dialog . show_dialog (
function ( button ) {
if ( button == et2_dialog . YES_BUTTON )
{
self . _delete_link (
self . value && typeof self . value . to_id != 'object' && _link_data . link_id ? _link_data.link_id :_link_data ,
row
) ;
}
} ,
egw . lang ( 'Delete link?' )
) ;
} ) ;
}
// Context menu
row . bind ( "contextmenu" , function ( e ) {
// Comment only available if link_id is there and not readonly
self . context . getItem ( "comment" ) . set_enabled ( typeof _link_data . link_id != 'undefined' && ! self . options . readonly ) ;
// File info only available for existing files
self . context . getItem ( "file_info" ) . set_enabled ( typeof _link_data . id != 'object' && _link_data . app == 'file' ) ;
self . context . getItem ( "save" ) . set_enabled ( typeof _link_data . id != 'object' && _link_data . app == 'file' ) ;
// Zip download only offered if there are at least 2 files
self . context . getItem ( "zip" ) . set_enabled ( jQuery ( '[id^="link_-"]' , this . list ) . length >= 2 ) ;
// Show delete item only if the widget is not readonly
self . context . getItem ( "delete" ) . set_enabled ( ! self . options . readonly ) ;
self . context . data = _link_data ;
self . context . showAt ( e . pageX , e . pageY , true ) ;
e . preventDefault ( ) ;
} ) ;
// Drag - adapted from egw_action_dragdrop, sidestepping action system
// so all linked files get it
// // Unfortunately, dragging files is currently only supported by Chrome
if ( navigator && navigator . userAgent . indexOf ( 'Chrome' ) >= 0 )
{
row . on ( "dragstart" , _link_data , function ( event ) {
if ( event . dataTransfer == null ) {
return ;
}
var data = event . data || { } ;
if ( data && data . type && data . download_url )
{
event . dataTransfer . dropEffect = "copy" ;
event . dataTransfer . effectAllowed = "copy" ;
var url = data . download_url ;
// NEED an absolute URL
if ( url [ 0 ] == '/' ) url = egw . link ( url ) ;
// egw.link adds the webserver, but that might not be an absolute URL - try again
if ( url [ 0 ] == '/' ) url = window . location . origin + url ;
// Unfortunately, dragging files is currently only supported by Chrome
if ( navigator && navigator . userAgent . indexOf ( 'Chrome' ) )
{
event . dataTransfer . setData ( "DownloadURL" , data . type + ':' + data . title + ':' + url ) ;
}
// Include URL as a fallback
event . dataTransfer . setData ( "text/uri-list" , url ) ;
}
if ( event . dataTransfer . types . length == 0 )
{
// No file data? Abort: drag does nothing
event . preventDefault ( ) ;
return ;
}
//event.dataTransfer.setDragImage(event.delegate.target,0,0);
var div = jQuery ( document . createElement ( "div" ) )
. attr ( 'id' , 'drag_helper' )
. css ( {
position : 'absolute' ,
top : '0px' ,
left : '0px' ,
width : '300px'
} ) ;
div . append ( event . target . cloneNode ( true ) ) ;
self . list . append ( div ) ;
event . dataTransfer . setDragImage ( div . get ( 0 ) , 0 , 0 ) ;
} )
. on ( 'drag' , function ( ) {
jQuery ( '#drag_helper' , self . list ) . remove ( ) ;
} ) ;
}
}
_delete_link ( link_id , row )
{
if ( row )
{
var delete_button = jQuery ( '.delete' , row ) ;
delete_button . removeClass ( "delete" ) . addClass ( "loading" ) ;
row . off ( ) ;
}
if ( this . onchange )
{
this . onchange ( this , link_id , row ) ;
}
if ( typeof link_id != "object" )
{
egw . json ( "EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_delete" , [ link_id ] ,
function ( data ) { if ( data ) { row . slideUp ( row . remove ) ; } }
) . sendRequest ( ) ;
}
else if ( row )
{
// No link ID means a link on an unsaved entry.
// Just remove the row, but need to adjust the link_to value also
row . slideUp ( row . remove ) ;
// Look for a link-to with the same ID, refresh it
if ( link_id . link_id )
{
var self = this ;
var _widget = link_id . widget || null ;
this . getRoot ( ) . iterateOver (
function ( widget ) {
if ( widget . id == self . id ) {
_widget = widget ;
}
} ,
this , et2_link_to
) ;
var value = _widget != null ? _widget . getValue ( ) : false ;
if ( _widget && value && value . to_id )
{
delete value . to_id [ link_id . link_id ] ;
_widget . set_value ( value ) ;
}
}
}
}
/ * *
* When the link is to a VFS file , we do some special formatting .
*
* Instead of listing the full path , we use
* Path : - filename
* When multiple files from the same directory are linked , we exclude
* the directory name from all but the first link to that directory
*
* @param { JQuery } $td Current table data cell for the title
* @param { String [ ] } dirs List of directories in the linked file ' s path
* @param { String [ ] } _link_data Data for the egw_link
* @returns { undefined }
* /
_format_vfs ( $td , dirs , _link_data )
{
// Keep it here for matching next row
$td . attr ( 'data-title' , _link_data [ 'title' ] ) ;
// VFS link - check for same dir as above, and hide dir
var reformat = false ;
var span_size = 1 ;
var prev = jQuery ( 'td.title' , $td . parent ( ) . prev ( 'tr' ) ) ;
if ( prev . length === 1 )
{
var prev_dirs = ( prev . attr ( 'data-title' ) || '' ) . split ( '/' ) ;
if ( prev_dirs . length > 1 && prev_dirs . length == dirs . length )
{
for ( var i = 0 ; i < dirs . length ; i ++ )
{
// Current is same as prev, blank it
if ( dirs [ i ] === prev_dirs [ i ] )
{
reformat = true ;
span_size += dirs [ i ] . length + 1 ;
dirs [ i ] = '' ;
}
else
{
break ;
}
}
}
}
var filename = dirs . pop ( ) ;
if ( reformat && ( dirs . length - i ) === 0 )
{
$td . html ( '<span style="display: inline-block; width:' + span_size + 'ex;"></span> - ' + filename ) ;
}
else
{
// Different format for directory
span_size += dirs . join ( '/' ) . length + 1 ;
$td . html ( '<span style="display: inline-block; text-align: right; width:' + span_size + 'ex;">' + dirs . join ( '/' ) + ':</span> - ' + filename ) ;
}
}
}
et2_register_widget ( et2_link_list , [ "link-list" ] ) ;
/ * *
*
*
* /
export class et2_link_add extends et2_inputWidget
{
static readonly _attributes : any = {
"value" : {
"description" : "Either an array of link information (see egw_link::link()) or array with keys to_app and to_id" ,
"type" : "any"
} ,
"application" : {
"name" : "Application" ,
"type" : "string" ,
"default" : "" ,
"description" : "Limit to the listed application or applications (comma seperated)"
}
} ;
private span : JQuery ;
private div : JQuery ;
private app_select : et2_link_apps ;
private button : et2_button ;
/ * *
* Constructor
* /
2020-02-05 23:27:24 +01:00
constructor ( _parent : et2_widget , _attrs? : WidgetConfig , _child? : object )
2020-02-05 21:48:50 +01:00
{
2020-02-05 23:27:24 +01:00
super ( _parent , _attrs , ClassWithAttributes . extendAttributes ( et2_link_add . _attributes , _child || { } ) ) ;
2020-02-05 21:48:50 +01:00
this . span = jQuery ( document . createElement ( "span" ) )
. text ( this . egw ( ) . lang ( "Add new" ) )
. addClass ( 'et2_link_add_span' ) ;
this . div = jQuery ( document . createElement ( "div" ) ) . append ( this . span ) ;
this . setDOMNode ( this . div [ 0 ] ) ;
}
doLoadingFinished ( )
{
super . doLoadingFinished . apply ( this , arguments ) ;
if ( this . app_select && this . button )
{
// Already done
return false ;
}
this . app_select = et2_createWidget ( "link-apps" , jQuery . extend ( { } , this . options , {
'id' : this . options . id + 'app' ,
value : this.options.application ? this . options.application : this.options.value && this . options . value . add_app ? this . options.value.add_app : null ,
application_list : this.options.application ? this . options.application : null
} ) , this ) ;
this . div . append ( this . app_select . getDOMNode ( ) ) ;
this . button = et2_createWidget ( "button" , { id :this.options.id + "_add" , label : this.egw ( ) . lang ( "add" ) } , this ) ;
this . button . set_label ( this . egw ( ) . lang ( "add" ) ) ;
var self = this ;
this . button . click = function ( )
{
self . egw ( ) . open ( self . options . value . to_app + ":" + self . options . value . to_id , self . app_select . get_value ( ) , 'add' ) ;
return false ;
} ;
this . div . append ( this . button . getDOMNode ( ) ) ;
return true ;
}
/ * *
* Should be handled client side .
* Return null to avoid overwriting other link values , in case designer used the same ID for multiple widgets
* /
getValue ( )
{
return null ;
}
}
et2_register_widget ( et2_link_add , [ "link-add" ] ) ;