2020-02-16 11:31:32 +01:00
/ * *
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
2021-06-07 17:33:53 +02:00
* @link https : //www.egroupware.org
2020-02-05 21:48:50 +01:00
* @author Nathan Gray
* @copyright 2011 Nathan Gray
* /
2020-03-30 18:28:48 +02:00
/ * 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 ;
2020-09-11 00:59:34 +02:00
et2_widget_selectbox ;
2020-03-30 18:28:48 +02:00
// Include menu system for list context menu
egw_action . egw_menu_dhtmlx ;
* /
2020-12-21 21:53:24 +01:00
import { et2_createWidget , et2_register_widget , et2_widget , WidgetConfig } from "./et2_core_widget" ;
2020-03-30 18:28:48 +02:00
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" ;
2020-12-21 21:53:24 +01:00
import { et2_file } from "./et2_widget_file" ;
import { et2_vfsSelect } from "./et2_widget_vfs" ;
2022-05-13 11:59:13 +02:00
import { egw } from "../jsapi/egw_global" ;
2021-06-07 17:33:53 +02:00
import { et2_tabbox } from "./et2_widget_tabs" ;
import { et2_csvSplit , et2_no_init } from "./et2_core_common" ;
2022-05-26 22:47:52 +02:00
import { et2_IDetachedDOM } from "./et2_core_interfaces" ;
2022-05-11 22:00:23 +02:00
import { et2_DOMWidget } from "./et2_core_DOMWidget" ;
import { Et2LinkList } from "./Et2Link/Et2LinkList" ;
2022-05-13 11:59:13 +02:00
import type { Et2LinkString } from "./Et2Link/Et2LinkString" ;
import { Et2Link } from "./Et2Link/Et2Link" ;
2020-02-05 21:48:50 +01:00
2020-03-30 18:28:48 +02: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." ,
2021-08-12 19:21:49 +02:00
translate : true
2020-02-05 21:48:50 +01:00
} ,
2021-08-12 19:21:49 +02:00
"no_files" : {
2020-02-05 21:48:50 +01:00
"name" : "No files" ,
"type" : "boolean" ,
"default" : false ,
"description" : "Suppress attach-files"
} ,
2021-08-12 19:21:49 +02:00
"search_label" : {
2020-02-05 21:48:50 +01:00
"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"
}
} ;
2021-08-12 19:21:49 +02:00
2020-02-05 21:48:50 +01:00
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 ;
2021-06-07 17:33:53 +02:00
private vfs_select : et2_vfsSelect ;
2020-02-05 21:48:50 +01:00
private file_upload : et2_file ;
/ * *
* Constructor
*
* @memberOf et2_link_to
* /
2021-08-12 19:21:49 +02:00
constructor ( _parent : et2_widget , _attrs? : WidgetConfig , _child? : object )
{
2020-02-05 23:27:24 +01:00
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 ( ) ;
}
2021-08-12 19:21:49 +02:00
destroy ( )
2020-02-05 21:48:50 +01:00
{
this . link_button = null ;
this . status_span = null ;
2021-08-12 19:21:49 +02:00
if ( this . link_entry )
2020-02-05 21:48:50 +01:00
{
this . link_entry . destroy ( ) ;
this . link_entry = null ;
}
2021-08-12 19:21:49 +02:00
if ( this . file_upload )
2020-02-05 21:48:50 +01:00
{
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
* /
2021-08-12 19:21:49 +02:00
getDOMNode ( _sender )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
if ( _sender == this )
{
2020-02-05 21:48:50 +01:00
return this . div [ 0 ] ;
2021-08-12 19:21:49 +02:00
}
else if ( _sender . _type == 'link-entry' )
{
2020-02-05 21:48:50 +01:00
return this . link_div [ 0 ] ;
2021-08-12 19:21:49 +02:00
}
else if ( _sender . _type == 'file' )
{
2020-02-05 21:48:50 +01:00
return this . file_div [ 0 ] ;
2021-08-12 19:21:49 +02:00
}
else if ( _sender . _type == 'vfs-select' )
{
2020-02-05 21:48:50 +01:00
return this . filemanager_button [ 0 ] ;
}
}
2021-08-12 19:21:49 +02:00
createInputWidget ( )
2020-02-05 21:48:50 +01:00
{
// Need a div for file upload widget
2021-08-12 19:21:49 +02:00
this . file_div = jQuery ( document . createElement ( "div" ) ) . css ( { display : 'inline-block' } ) . appendTo ( this . div ) ;
2020-02-05 21:48:50 +01:00
// Filemanager link popup
2021-08-12 19:21:49 +02:00
this . filemanager_button = jQuery ( document . createElement ( "div" ) ) . css ( { display : 'inline-block' } ) . appendTo ( this . div ) ;
2020-02-05 21:48:50 +01:00
// 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 ] ) ;
}
2021-08-12 19:21:49 +02:00
doLoadingFinished ( )
2020-02-05 21:48:50 +01:00
{
super . doLoadingFinished . apply ( this , arguments ) ;
var self = this ;
2021-08-12 19:21:49 +02:00
if ( this . link_entry && this . vfs_select && this . file_upload )
2020-02-05 21:48:50 +01:00
{
// 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...' ) ,
2021-08-12 19:21:49 +02:00
query : function ( )
{
self . link_button . hide ( ) ;
return true ;
} ,
select : function ( )
{
self . link_button . show ( ) ;
return true ;
} ,
2020-02-05 21:48:50 +01:00
readonly : this . options . readonly
} ;
2021-08-12 19:21:49 +02:00
this . link_entry = < et2_link_entry > et2_createWidget ( "link-entry" , link_entry_attrs , this ) ;
2020-02-05 21:48:50 +01:00
// Filemanager select
2021-08-12 19:21:49 +02:00
var select_attrs : any = {
2020-02-05 21:48:50 +01:00
button_label : egw.lang ( 'Link' ) ,
button_caption : '' ,
2021-08-12 19:21:49 +02:00
button_icon : 'link' ,
2020-02-05 21:48:50 +01:00
readonly : this . options . readonly ,
dialog_title : egw.lang ( 'Link' ) ,
2021-08-12 19:21:49 +02:00
extra_buttons : [ { text : egw.lang ( "copy" ) , id : "copy" , image : "copy" } ,
{ text : egw.lang ( "move" ) , id : "move" , image : "move" } ] ,
onchange : function ( )
2020-02-05 21:48:50 +01:00
{
var values = true ;
// If entry not yet saved, store for linking on server
2021-08-12 19:21:49 +02:00
if ( ! self . options . value . to_id || typeof self . options . value . to_id == 'object' )
2020-02-05 21:48:50 +01:00
{
values = self . options . value . to_id || { } ;
var files = self . vfs_select . getValue ( ) ;
2021-08-12 19:21:49 +02:00
if ( typeof files !== 'undefined' )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
for ( var i = 0 ; i < files . length ; i ++ )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
values [ 'link:' + files [ i ] ] = {
2020-02-05 21:48:50 +01:00
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
2021-08-12 19:21:49 +02:00
if ( self . options . value && self . options . value . to_id && typeof self . options . value . to_id != 'object' )
{
2020-02-05 21:48:50 +01:00
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 ;
}
2021-08-12 19:21:49 +02:00
this . vfs_select = < et2_vfsSelect > et2_createWidget ( "vfs-select" , select_attrs , this ) ;
2020-02-05 21:48:50 +01:00
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
2021-08-12 19:21:49 +02:00
onStart : function ( event , file_count )
2020-02-05 21:48:50 +01:00
{
// Find the tab widget, if there is one
2021-08-12 19:21:49 +02:00
var tabs : et2_widget = self ;
do
{
2020-02-05 21:48:50 +01:00
tabs = tabs . getParent ( ) ;
2021-08-12 19:21:49 +02:00
}
while ( tabs != self . getRoot ( ) && tabs . getType ( ) != 'tabbox' ) ;
if ( tabs != self . getRoot ( ) )
2020-02-05 21:48:50 +01:00
{
( < et2_tabbox > < unknown > tabs ) . activateTab ( self ) ;
}
return true ;
} ,
2021-08-12 19:21:49 +02:00
onFinish : function ( event , file_count )
2020-02-05 21:48:50 +01:00
{
event . data = self ;
self . filesUploaded ( event ) ;
// Auto-link uploaded files
self . createLink ( event ) ;
}
} ;
2021-08-12 19:21:49 +02:00
this . file_upload = < et2_file > et2_createWidget ( "file" , file_attrs , this ) ;
2020-02-05 21:48:50 +01:00
this . file_upload . set_readonly ( this . options . readonly ) ;
return true ;
}
2021-08-12 19:21:49 +02:00
getValue ( )
2020-02-05 21:48:50 +01:00
{
return this . options . value ;
}
2021-08-12 19:21:49 +02:00
filesUploaded ( event )
2020-02-05 21:48:50 +01:00
{
var self = this ;
this . link_button . show ( ) ;
}
/ * *
* Create a link using the current internal values
*
* @param { Object } event
* /
2021-08-12 19:21:49 +02:00
createLink ( event )
2020-02-05 21:48:50 +01:00
{
// 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 ;
2021-08-12 19:21:49 +02:00
self . link_entry . createLink ( event , links ) ;
2020-02-05 21:48:50 +01:00
// Files
2021-08-12 19:21:49 +02:00
if ( ! self . options . no_files )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
for ( var file in self . file_upload . options . value )
{
2020-02-05 21:48:50 +01:00
links . push ( {
app : 'file' ,
id : file ,
name : self.file_upload.options.value [ file ] . name ,
type : self . file_upload . options . value [ file ] . type ,
2021-08-12 19:21:49 +02:00
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 ( )
2020-02-05 21:48:50 +01:00
} ) ;
}
}
2021-08-12 19:21:49 +02:00
if ( links . length == 0 )
2020-02-05 21:48:50 +01:00
{
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
* /
2021-08-12 19:21:49 +02:00
_link_result ( success )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
if ( success )
2020-02-05 21:48:50 +01:00
{
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 ;
2021-08-12 19:21:49 +02:00
for ( var file in this . file_upload . options . value )
{
2020-02-05 21:48:50 +01:00
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
2021-08-12 19:21:49 +02:00
if ( typeof success == "object" )
2020-02-05 21:48:50 +01:00
{
// Save as appropriate in value
2021-08-12 19:21:49 +02:00
if ( typeof this . options . value != "object" )
2020-02-05 21:48:50 +01:00
{
this . options . value = { } ;
}
this . options . value . to_id = success ;
2021-08-12 19:21:49 +02:00
for ( let link in success )
2020-02-05 21:48:50 +01:00
{
// Icon should be in registry
2021-08-12 19:21:49 +02:00
if ( typeof success [ link ] . icon == 'undefined' )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
success [ link ] . icon = egw . link_get_registry ( success [ link ] . app , 'icon' ) ;
2020-02-05 21:48:50 +01:00
// No icon, try by mime type - different place for un-saved entries
2021-08-12 19:21:49 +02:00
if ( success [ link ] . icon == false && success [ link ] . id . type )
2020-02-05 21:48:50 +01:00
{
// 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
2021-08-12 19:21:49 +02:00
if ( success [ link ] . app == 'file' && typeof success [ link ] . title == 'undefined' )
2020-02-05 21:48:50 +01:00
{
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 (
2021-08-12 19:21:49 +02:00
function ( widget )
{
if ( widget . id == self . id )
{
2020-02-05 21:48:50 +01:00
list_widget = widget ;
2021-08-12 19:21:49 +02:00
if ( success === true )
2020-02-05 21:48:50 +01:00
{
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.
2021-08-12 19:21:49 +02:00
if ( list_widget && success )
2020-02-05 21:48:50 +01:00
{
// Clear list
list_widget . set_value ( null ) ;
// Add temp links in
2021-08-12 19:21:49 +02:00
for ( var link_id in success )
2020-02-05 21:48:50 +01:00
{
let link = success [ link_id ] ;
2021-08-12 19:21:49 +02:00
if ( typeof link . title == 'undefined' )
2020-02-05 21:48:50 +01:00
{
// Callback to server for title
change egw.jsonq() and egw.link_title() to return promises
* egw.jsonq() now always returns a promise like egw.request(), still supporting old callback syntax
* egw.link_title(_app, _id, _callback, _context, _force_reload) supports boolean values for _callback (to not break existing code)
- false: just a cache lookup (like current call with just 2 parameters), returning null, if no title is cached, or the title
- true: always return a promise, which might already be resolved, if title was cached
- function: also returns a promise and calls the callback
--> calling egw.link_title(_app, _id) without 3rd parameter is deprecated now (gives a console.trace), to be changed in future to always return a promise, unless called with false, to just return a cache-lookup
* Et2SelectAccountReadonly and et2_link_widget is changed to use the new/updated syntax with promises
2022-05-02 11:27:33 +02:00
egw . link_title ( link . app , link . id , true ) . then ( title = >
2021-08-12 19:21:49 +02:00
{
2020-02-05 21:48:50 +01:00
link . title = title ;
list_widget . _add_link ( link ) ;
} ) ;
}
else
{
// Add direct
list_widget . _add_link ( link ) ;
}
}
}
2022-05-12 18:54:29 +02:00
// Update any neighbouring link lists
( < Et2LinkList > < unknown > ( < et2_DOMWidget > this . getParent ( ) ) . getDOMNode ( ) . querySelector ( 'et2-link-list' ) ) ? . get_links ( Object . values ( success ) ) ;
2020-02-05 21:48:50 +01:00
}
else
{
this . status_span . removeClass ( "success" ) . addClass ( "error" )
. fadeIn ( ) ;
}
2021-08-12 19:21:49 +02:00
this . div . trigger ( 'link.et2_link_to' , success ) ;
2020-02-05 21:48:50 +01:00
}
set_no_files ( no_files )
{
2021-08-12 19:21:49 +02:00
if ( this . options . readonly ) return ;
if ( no_files )
2020-02-05 21:48:50 +01:00
{
this . file_div . hide ( ) ;
this . filemanager_button . hide ( ) ;
}
else
{
this . file_div . show ( ) ;
this . filemanager_button . show ( ) ;
}
this . options . no_files = no_files ;
}
}
2021-08-12 19:21:49 +02:00
2020-02-05 21:48:50 +01:00
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
*
* /
2021-08-12 19:21:49 +02: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
2021-08-12 19:21:49 +02:00
if ( ! this . options . value )
2020-02-05 21:48:50 +01:00
{
this . set_value ( egw . preference ( 'link_app' , this . egw ( ) . getAppName ( ) ) ) ;
}
// Register to update preference
var self = this ;
2021-08-12 19:21:49 +02:00
this . input . bind ( "click" , function ( )
{
2020-02-05 21:48:50 +01:00
if ( typeof self . options . value != 'undefined' ) var appname = self . options . value . to_app ;
2021-08-12 19:21:49 +02:00
egw . set_preference ( appname || self . egw ( ) . getAppName ( ) , 'link_app' , self . getValue ( ) ) ;
2020-02-05 21:48:50 +01:00
} ) ;
}
}
/ * *
* We get some minor speedups by overriding parent searching and directly setting select options
*
* @param { Array } _attrs an array of attributes
* /
2021-08-12 19:21:49 +02:00
transformAttributes ( _attrs )
2020-02-05 21:48:50 +01:00
{
var select_options = { } ;
// Limit to one app
2021-08-12 19:21:49 +02:00
if ( _attrs . only_app )
2020-02-05 21:48:50 +01:00
{
select_options [ _attrs . only_app ] = this . egw ( ) . lang ( _attrs . only_app ) ;
}
else if ( _attrs . application_list )
{
select_options = _attrs . application_list ;
}
else
2021-08-12 19:21:49 +02:00
{
2020-02-05 21:48:50 +01:00
select_options = egw . link_app_list ( 'query' ) ;
2021-08-12 19:21:49 +02:00
if ( typeof select_options [ 'addressbook-email' ] !== 'undefined' )
2020-02-05 21:48:50 +01:00
{
delete select_options [ 'addressbook-email' ] ;
}
}
_attrs . select_options = select_options ;
super . transformAttributes ( _attrs ) ;
}
}
2021-08-12 19:21:49 +02:00
2020-02-05 21:48:50 +01:00
et2_register_widget ( et2_link_apps , [ "link-apps" ] ) ;
/ * *
* Search and select an entry for linking
* /
export class et2_link_entry extends et2_inputWidget
{
2021-08-12 19:21:49 +02: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." ,
2021-08-12 19:21:49 +02:00
translate : true
2020-02-05 21:48:50 +01:00
} ,
"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."
}
} ;
2020-08-07 00:39:59 +02:00
public static readonly legacyOptions = [ "only_app" , "application_list" ] ;
2020-02-05 21:48:50 +01:00
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 = { } ;
2020-09-28 18:05:32 +02:00
protected div : JQuery ;
protected app_select : JQuery ;
protected search : JQuery ;
protected clear : JQuery ;
protected link_button : JQuery ;
2020-02-05 21:48:50 +01:00
private request : any ;
private last_search : string ;
processing : boolean = false ;
/ * *
* Constructor
*
* @memberOf et2_link_entry
* /
2021-08-12 19:21:49 +02:00
constructor ( _parent : et2_widget , _attrs? : WidgetConfig , _child? : object )
2020-02-05 23:27:24 +01:00
{
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
} ;
2021-08-12 19:21:49 +02:00
if ( typeof this . options . value == 'undefined' || this . options . value == null )
2020-02-05 21:48:50 +01:00
{
this . options . value = { } ;
}
this . cache = { } ;
this . request = null ;
this . createInputWidget ( ) ;
var self = this ;
2021-08-12 19:21:49 +02:00
jQuery ( this . getInstanceManager ( ) . DOMContainer ) . on ( 'clear' , function ( )
{
2020-02-05 21:48:50 +01:00
// 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 ( ) ;
} ) ;
}
2021-08-12 19:21:49 +02:00
destroy ( )
2020-02-05 21:48:50 +01:00
{
super . destroy . apply ( this , arguments ) ;
this . div = null ;
2021-08-12 19:21:49 +02:00
if ( this . search . data ( "ui-autocomplete" ) )
2020-02-05 21:48:50 +01:00
{
this . search . autocomplete ( "destroy" ) ;
}
this . search = null ;
this . clear = null ;
this . app_select = null ;
this . request = null ;
}
2021-08-12 19:21:49 +02:00
createInputWidget ( )
2020-02-05 21:48:50 +01:00
{
var self = this ;
this . div = jQuery ( document . createElement ( "div" ) ) . addClass ( "et2_link_entry" ) ;
2022-05-26 22:47:52 +02:00
this . app_select = jQuery ( document . createElement ( "et2-link-apps" ) ) . appendTo ( this . div )
. on ( "change" , function ( e )
2020-02-05 21:48:50 +01:00
{
// Clear cache when app changes
self . cache = { } ;
// Update preference with new value
2021-08-12 19:21:49 +02:00
egw . set_preference ( self . options . value . to_app || self . egw ( ) . getAppName ( ) , 'link_app' , self . app_select . val ( ) ) ;
2020-02-05 21:48:50 +01:00
2022-05-26 22:47:52 +02:00
if ( typeof self . options . value != 'object' )
{
self . options . value = { } ;
}
2020-02-05 21:48:50 +01:00
self . options . value . app = self . app_select . val ( ) ;
2021-05-13 18:01:38 +02:00
} )
. attr ( "aria-label" , egw . lang ( "linkapps" ) ) ;
2021-08-12 19:21:49 +02:00
if ( this . options . only_app )
2020-02-05 21:48:50 +01:00
{
this . app_select . val ( this . options . only_app ) ;
this . app_select . hide ( ) ;
this . div . addClass ( "no_app" ) ;
}
else
{
// Now that options are in, set to last used app
2021-08-12 19:21:49 +02:00
this . app_select . val ( this . options . value . app || '' ) ;
2020-02-05 21:48:50 +01:00
}
// Search input
this . search = jQuery ( document . createElement ( "input" ) )
// .attr("type", "search") // Fake it for all browsers below
2021-08-12 19:21:49 +02:00
. focus ( function ( )
{
if ( ! self . options . only_app )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
// Adjust width, leave room for app select & link button
self . div . removeClass ( "no_app" ) ;
2022-05-26 22:47:52 +02:00
self . app_select . show ( ) ;
2020-02-05 21:48:50 +01:00
}
2021-08-12 19:21:49 +02:00
} )
. blur ( function ( e )
{
if ( self . div . has ( e . relatedTarget ) . length ) return ;
if ( self . options . app_icons )
2020-02-05 21:48:50 +01:00
{
// Adjust width, leave room for app select & link button
self . div . addClass ( "no_app" ) ;
}
else if ( self . search . val ( ) )
{
2021-08-12 19:21:49 +02:00
if ( self . options . only_app )
{
2020-02-05 21:48:50 +01:00
// Adjust width, leave room for app select & link button
self . div . addClass ( "no_app" ) ;
}
}
} )
2021-08-12 19:21:49 +02:00
. attr ( "aria-label" , egw . lang ( "Link search" ) )
. attr ( "role" , "searchbox" )
2020-02-05 21:48:50 +01:00
. appendTo ( this . div ) ;
this . set_blur ( this . options . blur ? this . options.blur : this.egw ( ) . lang ( "search" ) , this . search ) ;
// Autocomplete
2022-05-26 22:47:52 +02:00
/ *
2020-02-05 21:48:50 +01:00
this . search . autocomplete ( {
2021-08-12 19:21:49 +02:00
source : function ( request , response )
2020-02-05 21:48:50 +01:00
{
return self . query ( request , response ) ;
} ,
2021-08-12 19:21:49 +02:00
select : function ( event , item )
2020-02-05 21:48:50 +01:00
{
event . data = self ;
// Correct changed value from server
2021-08-12 19:21:49 +02:00
if ( item . item . value )
2020-09-15 19:23:02 +02:00
{
2021-08-12 19:21:49 +02:00
item . item . value = ( "" + item . item . value ) . trim ( ) ;
2020-09-15 19:23:02 +02:00
}
2021-08-12 19:21:49 +02:00
self . select ( event , item ) ;
2020-02-05 21:48:50 +01:00
return false ;
} ,
2021-08-12 19:21:49 +02:00
focus : function ( event , item )
2020-02-05 21:48:50 +01:00
{
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
} ) ;
2022-05-26 22:47:52 +02:00
* /
2020-02-05 21:48:50 +01:00
// Custom display (colors)
2022-05-26 22:47:52 +02:00
/ *
2021-08-12 19:21:49 +02: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 ) ;
2021-08-12 19:21:49 +02:00
var extra : any = { } ;
2020-02-05 21:48:50 +01:00
// Extra stuff
2021-08-12 19:21:49 +02:00
if ( typeof item . label == 'object' )
{
2020-02-05 21:48:50 +01:00
extra = item . label ;
item . label = extra . label ? extra.label : extra ;
2021-08-12 19:21:49 +02:00
if ( extra [ 'style.backgroundColor' ] || extra . color )
2020-02-05 21:48:50 +01:00
{
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
2021-08-12 19:21:49 +02:00
if ( extra . icon )
2020-02-05 21:48:50 +01:00
{
var img = self . egw ( ) . image ( extra . icon ) ;
2021-08-12 19:21:49 +02:00
if ( img )
2020-02-05 21:48:50 +01:00
{
jQuery ( document . createElement ( "img" ) )
. attr ( "src" , img )
. css ( "float" , "right" )
. appendTo ( li ) ;
}
}
}
// Normal stuff
2021-08-12 19:21:49 +02:00
li . append ( jQuery ( "<a></a>" ) . text ( item . label ) )
2020-02-05 21:48:50 +01:00
. appendTo ( ul ) ;
2021-08-12 19:21:49 +02:00
window . setTimeout ( function ( )
{
ul . css ( 'max-width' , jQuery ( '.et2_container' ) . width ( ) - ul . offset ( ) . left ) ;
} , 300 ) ;
2020-02-05 21:48:50 +01:00
return li ;
} ;
2022-05-26 22:47:52 +02:00
* /
2020-02-05 21:48:50 +01:00
// Bind to enter key to start search early
2022-05-26 22:47:52 +02:00
this . search . keydown ( function ( e )
2021-08-12 19:21:49 +02:00
{
2020-02-05 21:48:50 +01:00
var keycode = ( e . keyCode ? e.keyCode : e.which ) ;
2022-05-26 22:47:52 +02:00
if ( keycode == 13 && ! self . processing )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
self . search . autocomplete ( "option" , "minLength" , 0 ) ;
2020-02-05 21:48:50 +01:00
self . search . autocomplete ( "search" ) ;
2021-08-12 19:21:49 +02:00
self . search . autocomplete ( "option" , "minLength" , self . minimum_characters ) ;
2020-02-05 21:48:50 +01:00
return false ;
}
} ) ;
// Clear / last button
this . clear = jQuery ( document . createElement ( "span" ) )
. addClass ( "ui-icon ui-icon-close" )
2021-08-12 19:21:49 +02: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
2021-08-12 19:21:49 +02:00
if ( self . last_search && self . last_search != self . search . val ( ) )
2020-02-05 21:48:50 +01:00
{
// 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
2021-08-12 19:21:49 +02:00
window . setTimeout ( function ( )
2020-02-05 21:48:50 +01:00
{
self . search . trigger ( "change" ) ;
} , 0 ) ;
}
self . search . focus ( ) ;
} )
. appendTo ( this . div )
. hide ( ) ;
this . setDOMNode ( this . div [ 0 ] ) ;
}
2021-08-12 19:21:49 +02:00
getDOMNode ( )
2020-02-05 21:48:50 +01:00
{
return this . div ? this . div [ 0 ] : null ;
}
2021-08-12 19:21:49 +02:00
transformAttributes ( _attrs )
2020-02-05 21:48:50 +01:00
{
super . transformAttributes . apply ( this , arguments ) ;
_attrs [ "select_options" ] = { } ;
2021-08-12 19:21:49 +02:00
if ( _attrs [ "application_list" ] )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
var apps = ( typeof _attrs [ "application_list" ] == "string" ) ? et2_csvSplit ( _attrs [ "application_list" ] , null , "," ) : _attrs [ "application_list" ] ;
for ( var i = 0 ; i < apps . length ; i ++ )
2020-02-05 21:48:50 +01:00
{
_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" ] = { } ;
}
}
2021-08-12 19:21:49 +02:00
doLoadingFinished ( )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
if ( typeof this . options . value == 'object' && ! this . options . value . app )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
this . options . value . app = egw . preference ( 'link_app' , this . options . value . to_app || this . egw ( ) . getAppName ( ) ) ;
2020-02-05 21:48:50 +01:00
// 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 ) ;
}
2021-08-12 19:21:49 +02:00
return super . doLoadingFinished . apply ( this , arguments ) ;
2020-02-05 21:48:50 +01:00
}
2021-08-12 19:21:49 +02:00
getValue ( )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
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 )
2020-02-05 21:48:50 +01:00
{
value . search = this . search . val ( ) ;
}
return value ;
}
2021-08-12 19:21:49 +02:00
set_value ( _value )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
if ( typeof _value == 'string' || typeof _value == 'number' )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
if ( typeof _value == 'string' && _value . indexOf ( "," ) > 0 ) _value = _value . replace ( "," , ":" ) ;
if ( typeof _value == 'string' && _value . indexOf ( ":" ) >= 0 )
2020-02-05 21:48:50 +01:00
{
var split = _value . split ( ":" ) ;
_value = {
app : split.shift ( ) ,
id : split.length == 1 ? split [ 0 ] : split
} ;
}
2021-08-12 19:21:49 +02:00
else if ( _value && this . options . only_app )
2020-02-05 21:48:50 +01:00
{
_value = {
app : this.options.only_app ,
id : _value
} ;
}
}
2021-08-19 10:46:34 +02:00
// display a search query, not a selected entry
else if ( _value !== null && typeof _value === 'object' && typeof _value . query === 'string' )
{
this . options . value = { app : _value.app || this . options . only_app , id : null } ;
this . search . val ( _value . query ) ;
return ;
}
2020-02-05 21:48:50 +01:00
this . _oldValue = this . options . value ;
2021-08-12 19:21:49 +02:00
if ( ! _value || _value . length == 0 || _value == null || jQuery . isEmptyObject ( _value ) )
2020-02-05 21:48:50 +01:00
{
this . search . val ( "" ) ;
this . clear . hide ( ) ;
2021-08-12 19:21:49 +02:00
this . options . value = _value = { 'id' : null } ;
2020-02-05 21:48:50 +01:00
}
2021-08-12 19:21:49 +02:00
if ( ! _value . app ) _value . app = this . options . only_app || this . app_select . val ( ) ;
2020-02-05 21:48:50 +01:00
2021-08-12 19:21:49 +02:00
if ( _value . id )
{
2020-02-05 21:48:50 +01:00
// Remove specific display and revert to CSS file
// show() would use inline, should be inline-block
2021-08-12 19:21:49 +02:00
this . clear . css ( 'display' , '' ) ;
}
else
{
2020-02-05 21:48:50 +01:00
this . clear . hide ( ) ;
return ;
}
2021-08-12 19:21:49 +02:00
if ( typeof _value != 'object' || ( ! _value . app && ! _value . id ) )
2020-02-05 21:48:50 +01:00
{
console . warn ( "Bad value for link widget. Need an object with keys 'app', 'id', and optionally 'title'" , _value ) ;
return ;
}
2021-08-12 19:21:49 +02:00
if ( ! _value . title )
{
change egw.jsonq() and egw.link_title() to return promises
* egw.jsonq() now always returns a promise like egw.request(), still supporting old callback syntax
* egw.link_title(_app, _id, _callback, _context, _force_reload) supports boolean values for _callback (to not break existing code)
- false: just a cache lookup (like current call with just 2 parameters), returning null, if no title is cached, or the title
- true: always return a promise, which might already be resolved, if title was cached
- function: also returns a promise and calls the callback
--> calling egw.link_title(_app, _id) without 3rd parameter is deprecated now (gives a console.trace), to be changed in future to always return a promise, unless called with false, to just return a cache-lookup
* Et2SelectAccountReadonly and et2_link_widget is changed to use the new/updated syntax with promises
2022-05-02 11:27:33 +02:00
var title = this . egw ( ) . link_title ( _value . app , _value . id , false ) ;
2021-08-12 19:21:49 +02:00
if ( title != null )
2020-02-05 21:48:50 +01:00
{
_value . title = title ;
}
else
{
// Title will be fetched from server and then set
change egw.jsonq() and egw.link_title() to return promises
* egw.jsonq() now always returns a promise like egw.request(), still supporting old callback syntax
* egw.link_title(_app, _id, _callback, _context, _force_reload) supports boolean values for _callback (to not break existing code)
- false: just a cache lookup (like current call with just 2 parameters), returning null, if no title is cached, or the title
- true: always return a promise, which might already be resolved, if title was cached
- function: also returns a promise and calls the callback
--> calling egw.link_title(_app, _id) without 3rd parameter is deprecated now (gives a console.trace), to be changed in future to always return a promise, unless called with false, to just return a cache-lookup
* Et2SelectAccountReadonly and et2_link_widget is changed to use the new/updated syntax with promises
2022-05-02 11:27:33 +02:00
var title = this . egw ( ) . link_title ( _value . app , _value . id , true ) . then ( title = >
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
this . search . removeClass ( "loading" ) . val ( title + "" ) ;
2020-02-05 21:48:50 +01:00
// Remove specific display and revert to CSS file
// show() would use inline, should be inline-block
2021-08-12 19:21:49 +02:00
this . clear . css ( 'display' , '' ) ;
change egw.jsonq() and egw.link_title() to return promises
* egw.jsonq() now always returns a promise like egw.request(), still supporting old callback syntax
* egw.link_title(_app, _id, _callback, _context, _force_reload) supports boolean values for _callback (to not break existing code)
- false: just a cache lookup (like current call with just 2 parameters), returning null, if no title is cached, or the title
- true: always return a promise, which might already be resolved, if title was cached
- function: also returns a promise and calls the callback
--> calling egw.link_title(_app, _id) without 3rd parameter is deprecated now (gives a console.trace), to be changed in future to always return a promise, unless called with false, to just return a cache-lookup
* Et2SelectAccountReadonly and et2_link_widget is changed to use the new/updated syntax with promises
2022-05-02 11:27:33 +02:00
} ) ;
2020-02-05 21:48:50 +01:00
this . search . addClass ( "loading" ) ;
}
}
2021-08-12 19:21:49 +02:00
if ( _value . title )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
this . search . val ( _value . title + "" ) ;
2020-02-05 21:48:50 +01:00
}
this . options . value = _value ;
2021-08-12 19:21:49 +02:00
jQuery ( "option[value='" + _value . app + "']" , this . app_select ) . prop ( "selected" , true ) ;
2020-02-05 21:48:50 +01:00
this . app_select . hide ( ) ;
2022-05-26 22:47:52 +02:00
2020-02-05 21:48:50 +01:00
this . div . addClass ( "no_app" ) ;
}
2021-08-12 19:21:49 +02:00
set_blur ( _value , input )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
if ( typeof input == 'undefined' ) input = this . search ;
2020-02-05 21:48:50 +01:00
2021-08-12 19:21:49 +02:00
if ( _value )
2020-02-05 21:48:50 +01:00
{
input . attr ( "placeholder" , _value ) ; // HTML5
2021-08-12 19:21:49 +02:00
if ( ! input [ 0 ] . placeholder )
2020-02-05 21:48:50 +01:00
{
// Not HTML5
2021-08-12 19:21:49 +02:00
if ( input . val ( ) == "" ) input . val ( _value ) ;
input . focus ( input , function ( e )
2020-02-05 21:48:50 +01:00
{
var placeholder = _value ;
2021-08-12 19:21:49 +02:00
if ( e . data . val ( ) == placeholder ) e . data . val ( "" ) ;
} ) . blur ( input , function ( e )
2020-02-05 21:48:50 +01:00
{
var placeholder = _value ;
2021-08-12 19:21:49 +02:00
if ( e . data . val ( ) == "" ) e . data . val ( placeholder ) ;
2020-02-05 21:48:50 +01:00
} ) ;
2021-08-12 19:21:49 +02:00
if ( input . val ( ) == "" )
2020-02-05 21:48:50 +01:00
{
input . val ( _value ) ;
}
}
}
else
2021-08-12 19:21:49 +02:00
{
2020-02-05 21:48:50 +01:00
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
* /
2021-08-12 19:21:49 +02:00
query ( request , response )
2020-02-05 21:48:50 +01:00
{
// If there is a pending request, abort it
2021-08-12 19:21:49 +02:00
if ( this . request )
2020-02-05 21:48:50 +01:00
{
this . request . abort ( ) ;
this . request = null ;
}
// Remember last search
this . last_search = this . search . val ( ) ;
// Allow hook / tie in
2021-08-12 19:21:49 +02:00
if ( this . options . query && typeof this . options . query == 'function' )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
if ( ! this . options . query ( request , this ) ) return false ;
2020-02-05 21:48:50 +01:00
}
2021-08-12 19:21:49 +02:00
if ( ( typeof request . no_cache == 'undefined' && ! request . no_cache ) && request . term in this . cache )
{
2020-02-05 21:48:50 +01:00
return response ( this . cache [ request . term ] ) ;
}
this . search . addClass ( "loading" ) ;
// Remove specific display and revert to CSS file
// show() would use inline, should be inline-block
2021-08-12 19:21:49 +02:00
this . clear . css ( 'display' , '' ) ;
2021-08-23 09:37:18 +02:00
this . request = egw . request ( "EGroupware\\Api\\Etemplate\\Widget\\Link::ajax_link_search" ,
[ this . app_select . val ( ) , '' , request . term , request . options ] ) ;
this . request . then ( ( data ) = >
{
if ( this . request )
{
this . request = null ;
}
this . search . removeClass ( "loading" ) ;
let result = [ ] ;
for ( var id in data )
{
result . push ( { "value" : id , "label" : data [ id ] } ) ;
}
this . cache [ this . search . val ( ) ] = result ;
response ( result ) ;
} ) ;
2020-02-05 21:48:50 +01:00
}
/ * *
* User selected a value
*
* @param { Object } event
* @param { Object } selected
*
* /
2021-08-12 19:21:49 +02:00
select ( event , selected )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
if ( selected . item . value !== null && typeof selected . item . value == "string" )
2020-02-05 21:48:50 +01:00
{
// Correct changed value from server
selected . item . value = selected . item . value . trim ( ) ;
}
2021-08-12 19:21:49 +02:00
if ( this . options . select && typeof this . options . select == 'function' )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
if ( ! this . options . select ( event , selected ) ) return false ;
2020-02-05 21:48:50 +01:00
}
2021-08-12 19:21:49 +02:00
if ( typeof event . data . options . value != 'object' || event . data . options . value == null )
2020-02-05 21:48:50 +01:00
{
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
2021-08-12 19:21:49 +02:00
this . clear . css ( 'display' , '' ) ;
2020-02-05 21:48:50 +01:00
event . data . search . val ( selected . item . label ) ;
// Fire change event
this . search . change ( ) ;
// Turn off processing flag when done
2021-08-12 19:21:49 +02:00
window . setTimeout ( jQuery . proxy ( function ( )
{
delete this . processing ;
} , event . data ) ) ;
2020-02-05 21:48:50 +01:00
}
/ * *
* Create a link using the current internal values
*
* @param { Object } event
* @param { Object } _links
* /
2021-08-12 19:21:49 +02:00
createLink ( event , _links )
2020-02-05 21:48:50 +01:00
{
var values = event . data . options . value ;
var self = event . data ;
var links = [ ] ;
2021-08-12 19:21:49 +02:00
if ( typeof _links == 'undefined' )
2020-02-05 21:48:50 +01:00
{
links = [ ] ;
}
else
{
links = _links ;
}
// Links to other entries
2021-08-12 19:21:49 +02:00
if ( values . id )
{
2020-02-05 21:48:50 +01:00
links . push ( {
app : values.app ,
id : values.id
} ) ;
self . search . val ( "" ) ;
}
// If a link array was passed in, don't make the ajax call
2021-08-12 19:21:49 +02:00
if ( typeof _links == 'undefined' )
2020-02-05 21:48:50 +01:00
{
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
*
* /
2021-08-12 19:21:49 +02:00
_link_result ( success )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
if ( success )
{
2020-02-05 21:48:50 +01:00
this . status_span . fadeIn ( ) . delay ( 1000 ) . fadeOut ( ) ;
delete this . options . value . app ;
delete this . options . value . id ;
}
}
}
2021-08-12 19:21:49 +02:00
2020-02-05 21:48:50 +01:00
et2_register_widget ( et2_link_entry , [ "link-entry" ] ) ;
2022-05-13 11:59:13 +02:00
/ * *
* @deprecated use Et2Link
* /
export type et2_link = Et2Link ;
2020-02-05 21:48:50 +01:00
/ * *
* UI widget for a single ( read - only ) link
*
* /
2022-05-13 11:59:13 +02:00
export class et2_link_entry_ro extends et2_valueWidget implements et2_IDetachedDOM
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
static readonly _attributes : any = {
2020-02-05 21:48:50 +01:00
"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"
2020-02-16 11:31:32 +01:00
} ,
"extra_link_target" : {
"name" : "Link target" ,
"type" : "string" ,
"default" : null ,
"description" : "Optional parameter to be passed to egw().open in order to open links in specified target eg. _blank"
2021-02-03 14:14:22 +01:00
} ,
"break_title" : {
"name" : "break title" ,
"type" : "string" ,
"default" : null ,
"description" : "Breaks title into multiple lines based on selected delimiter by replacing it with '\r\n'"
2020-02-05 21:48:50 +01:00
}
} ;
2020-03-30 18:28:48 +02:00
public static readonly legacyOptions = [ "only_app" ] ;
2020-02-05 21:48:50 +01:00
private label_span : JQuery ;
private link : JQuery ;
/ * *
* Constructor
*
* @memberOf et2_link
* /
2021-08-12 19:21:49 +02:00
constructor ( _parent : et2_widget , _attrs? : WidgetConfig , _child? : object )
2020-02-05 21:48:50 +01:00
{
2022-05-13 11:59:13 +02:00
super ( _parent , _attrs , ClassWithAttributes . extendAttributes ( et2_valueWidget . _attributes , _child || { } ) ) ;
2020-02-05 23:27:24 +01:00
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 ) ;
2021-08-12 19:21:49 +02:00
if ( this . options [ 'class' ] ) this . label_span . addClass ( this . options [ 'class' ] ) ;
2020-02-05 21:48:50 +01:00
this . setDOMNode ( this . label_span [ 0 ] ) ;
}
2021-08-12 19:21:49 +02:00
destroy ( )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
if ( this . link ) this . link . unbind ( ) ;
2020-02-05 21:48:50 +01:00
this . link = null ;
super . destroy . apply ( this , arguments ) ;
}
2021-08-12 19:21:49 +02:00
set_label ( label )
2020-02-05 21:48:50 +01:00
{
// Remove current label
this . label_span . contents ( )
2021-08-12 19:21:49 +02:00
. filter ( function ( )
{
return this . nodeType == 3 ;
} ) . remove ( ) ;
2020-02-05 21:48:50 +01:00
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 ] ) ;
}
2021-08-12 19:21:49 +02:00
set_value ( _value )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
if ( typeof _value != 'object' && _value && ! this . options . only_app )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
if ( _value . indexOf ( ':' ) >= 0 )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
var app = _value . split ( ':' , 1 ) ;
var id = _value . substr ( app [ 0 ] . length + 1 ) ;
2020-02-05 21:48:50 +01:00
_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 = {
2021-08-12 19:21:49 +02:00
app : this.options.only_app ,
id : _value
2020-02-05 21:48:50 +01:00
} ;
}
2021-08-12 19:21:49 +02:00
if ( ! _value || jQuery . isEmptyObject ( _value ) )
2020-02-05 21:48:50 +01:00
{
this . link . text ( "" ) . unbind ( ) ;
return ;
}
var self = this ;
this . link . unbind ( ) ;
2021-08-12 19:21:49 +02:00
if ( _value . id && _value . app )
2020-02-05 21:48:50 +01:00
{
this . link . addClass ( "et2_link" ) ;
2021-08-12 19:21:49 +02:00
this . link . click ( function ( e )
2020-02-05 21:48:50 +01:00
{
2020-12-03 18:19:25 +01:00
// try to fetch value.title if it wasn't fetched during initiation.
change egw.jsonq() and egw.link_title() to return promises
* egw.jsonq() now always returns a promise like egw.request(), still supporting old callback syntax
* egw.link_title(_app, _id, _callback, _context, _force_reload) supports boolean values for _callback (to not break existing code)
- false: just a cache lookup (like current call with just 2 parameters), returning null, if no title is cached, or the title
- true: always return a promise, which might already be resolved, if title was cached
- function: also returns a promise and calls the callback
--> calling egw.link_title(_app, _id) without 3rd parameter is deprecated now (gives a console.trace), to be changed in future to always return a promise, unless called with false, to just return a cache-lookup
* Et2SelectAccountReadonly and et2_link_widget is changed to use the new/updated syntax with promises
2022-05-02 11:27:33 +02:00
if ( ! _value . title ) _value . title = self . egw ( ) . link_title ( _value . app , _value . id , false ) ;
2021-08-12 19:21:49 +02:00
if ( ! self . options . target_app )
{
2020-02-05 21:48:50 +01:00
self . options . target_app = _value . app ;
}
2020-02-16 11:31:32 +01:00
const target = self . options . extra_link_target || _value . app ;
self . egw ( ) . open ( _value , "" , self . options . link_hook , _value . extra_args , target , self . options . target_app ) ;
2020-02-05 21:48:50 +01:00
e . stopImmediatePropagation ( ) ;
} ) ;
}
else
{
this . link . removeClass ( "et2_link" ) ;
}
2021-08-12 19:21:49 +02:00
if ( ! _value . title )
2020-02-05 21:48:50 +01:00
{
change egw.jsonq() and egw.link_title() to return promises
* egw.jsonq() now always returns a promise like egw.request(), still supporting old callback syntax
* egw.link_title(_app, _id, _callback, _context, _force_reload) supports boolean values for _callback (to not break existing code)
- false: just a cache lookup (like current call with just 2 parameters), returning null, if no title is cached, or the title
- true: always return a promise, which might already be resolved, if title was cached
- function: also returns a promise and calls the callback
--> calling egw.link_title(_app, _id) without 3rd parameter is deprecated now (gives a console.trace), to be changed in future to always return a promise, unless called with false, to just return a cache-lookup
* Et2SelectAccountReadonly and et2_link_widget is changed to use the new/updated syntax with promises
2022-05-02 11:27:33 +02:00
const node = this . link [ 0 ] ;
2021-08-12 19:21:49 +02:00
if ( _value . app && _value . id )
2020-02-05 21:48:50 +01:00
{
change egw.jsonq() and egw.link_title() to return promises
* egw.jsonq() now always returns a promise like egw.request(), still supporting old callback syntax
* egw.link_title(_app, _id, _callback, _context, _force_reload) supports boolean values for _callback (to not break existing code)
- false: just a cache lookup (like current call with just 2 parameters), returning null, if no title is cached, or the title
- true: always return a promise, which might already be resolved, if title was cached
- function: also returns a promise and calls the callback
--> calling egw.link_title(_app, _id) without 3rd parameter is deprecated now (gives a console.trace), to be changed in future to always return a promise, unless called with false, to just return a cache-lookup
* Et2SelectAccountReadonly and et2_link_widget is changed to use the new/updated syntax with promises
2022-05-02 11:27:33 +02:00
this . egw ( ) . link_title ( _value . app , _value . id , true ) . then ( title = >
2020-02-05 21:48:50 +01:00
{
change egw.jsonq() and egw.link_title() to return promises
* egw.jsonq() now always returns a promise like egw.request(), still supporting old callback syntax
* egw.link_title(_app, _id, _callback, _context, _force_reload) supports boolean values for _callback (to not break existing code)
- false: just a cache lookup (like current call with just 2 parameters), returning null, if no title is cached, or the title
- true: always return a promise, which might already be resolved, if title was cached
- function: also returns a promise and calls the callback
--> calling egw.link_title(_app, _id) without 3rd parameter is deprecated now (gives a console.trace), to be changed in future to always return a promise, unless called with false, to just return a cache-lookup
* Et2SelectAccountReadonly and et2_link_widget is changed to use the new/updated syntax with promises
2022-05-02 11:27:33 +02:00
this . set_title ( node , title ) ;
} ) ;
// Title will be fetched from server and then set
return ;
2020-02-05 21:48:50 +01:00
}
change egw.jsonq() and egw.link_title() to return promises
* egw.jsonq() now always returns a promise like egw.request(), still supporting old callback syntax
* egw.link_title(_app, _id, _callback, _context, _force_reload) supports boolean values for _callback (to not break existing code)
- false: just a cache lookup (like current call with just 2 parameters), returning null, if no title is cached, or the title
- true: always return a promise, which might already be resolved, if title was cached
- function: also returns a promise and calls the callback
--> calling egw.link_title(_app, _id) without 3rd parameter is deprecated now (gives a console.trace), to be changed in future to always return a promise, unless called with false, to just return a cache-lookup
* Et2SelectAccountReadonly and et2_link_widget is changed to use the new/updated syntax with promises
2022-05-02 11:27:33 +02:00
_value . title = "" ;
2020-02-05 21:48:50 +01:00
}
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
* /
2021-08-12 19:21:49 +02:00
set_title ( node , _value )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
if ( _value === false || _value === null ) _value = "" ;
2021-05-10 19:55:22 +02:00
if ( this . options . break_title )
{
// Set up title to optionally break on the provided character - replace all space with nbsp, add a
// zero-width space after the break string
_value = _value
2021-08-12 19:21:49 +02:00
. replace ( this . options . break_title , this . options . break_title . trimEnd ( ) + "\u200B" )
2021-05-10 19:55:22 +02:00
. replace ( / /g , '\u00a0' ) ;
}
2021-08-12 19:21:49 +02:00
jQuery ( node ) . text ( _value + "" ) ;
2020-02-05 21:48:50 +01:00
}
/ * *
* 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
* /
2021-08-12 19:21:49 +02:00
getDetachedAttributes ( _attrs )
2020-02-05 21:48:50 +01:00
{
2021-08-12 19:21:49 +02:00
_attrs . push ( "label" , "value" ) ;
2020-02-05 21:48:50 +01:00
}
/ * *
* Returns an array of DOM nodes . The ( relatively ) same DOM - Nodes have to be
* passed to the "setDetachedAttributes" function in the same order .
* /
2021-08-12 19:21:49 +02:00
getDetachedNodes ( )
2020-02-05 21:48:50 +01:00
{
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 .
* /
2021-08-12 19:21:49 +02:00
setDetachedAttributes ( _nodes , _values )
2020-02-05 21:48:50 +01:00
{
this . node = _nodes [ 0 ] ;
this . label_span = jQuery ( _nodes [ 0 ] ) ;
this . link = jQuery ( _nodes [ 1 ] ) ;
2021-08-12 19:21:49 +02:00
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" ] ) ;
2020-02-05 21:48:50 +01:00
}
}
2021-08-12 19:21:49 +02:00
2022-05-13 11:59:13 +02:00
et2_register_widget ( et2_link_entry_ro , [ "link-entry_ro" ] ) ;
2020-02-05 21:48:50 +01:00
/ * *
* UI widget for one or more links , comma separated
*
2022-05-13 11:59:13 +02:00
* @deprecated use Et2LinkString
2020-02-05 21:48:50 +01:00
* /
2022-05-13 11:59:13 +02:00
export type et2_link_string = Et2LinkString ;
2020-02-05 21:48:50 +01:00
/ * *
* UI widget for one or more links in a list ( table )
2022-05-13 11:59:13 +02:00
*
* @deprecated use Et2LinkList
2020-02-05 21:48:50 +01:00
* /
2022-05-13 11:59:13 +02:00
// can't just define as type, as tracker/app.ts uses it with iterateOver()!
// export type et2_link_list = Et2LinkList;
export class et2_link_list extends Et2LinkList { }
2020-02-05 21:48:50 +01:00
/ * *
*
*
* /
export class et2_link_add extends et2_inputWidget
{
2021-08-12 19:21:49 +02:00
static readonly _attributes : any = {
2020-02-05 21:48:50 +01:00
"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 ;
2021-08-12 19:21:49 +02:00
2020-02-05 21:48:50 +01:00
/ * *
* Constructor
* /
2021-08-12 19:21:49 +02: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" ) )
2021-08-12 19:21:49 +02:00
. text ( this . egw ( ) . lang ( "Add new" ) )
. addClass ( 'et2_link_add_span' ) ;
2020-02-05 21:48:50 +01:00
this . div = jQuery ( document . createElement ( "div" ) ) . append ( this . span ) ;
this . setDOMNode ( this . div [ 0 ] ) ;
}
2021-08-12 19:21:49 +02:00
doLoadingFinished ( )
2020-02-05 21:48:50 +01:00
{
super . doLoadingFinished . apply ( this , arguments ) ;
2021-08-12 19:21:49 +02:00
if ( this . app_select && this . button )
2020-02-05 21:48:50 +01:00
{
// Already done
return false ;
}
2021-08-12 19:21:49 +02:00
this . app_select = < et2_link_apps > et2_createWidget ( "link-apps" , jQuery . extend ( { } , this . options , {
2020-02-05 21:48:50 +01:00
'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
2021-08-12 19:21:49 +02:00
} ) , this ) ;
2020-02-05 21:48:50 +01:00
this . div . append ( this . app_select . getDOMNode ( ) ) ;
2021-08-12 19:21:49 +02:00
this . button = < et2_button > et2_createWidget ( "button" , {
id : this.options.id + "_add" ,
label : this.egw ( ) . lang ( "add" )
} , this ) ;
2020-02-05 21:48:50 +01:00
this . button . set_label ( this . egw ( ) . lang ( "add" ) ) ;
var self = this ;
2021-08-12 19:21:49 +02:00
this . button . click = function ( )
2020-02-05 21:48:50 +01:00
{
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 ;
}
2021-08-12 19:21:49 +02:00
2020-02-05 21:48:50 +01:00
/ * *
* Should be handled client side .
* Return null to avoid overwriting other link values , in case designer used the same ID for multiple widgets
* /
2021-08-12 19:21:49 +02:00
getValue ( )
2020-02-05 21:48:50 +01:00
{
return null ;
}
}
2021-08-12 19:21:49 +02:00
change egw.jsonq() and egw.link_title() to return promises
* egw.jsonq() now always returns a promise like egw.request(), still supporting old callback syntax
* egw.link_title(_app, _id, _callback, _context, _force_reload) supports boolean values for _callback (to not break existing code)
- false: just a cache lookup (like current call with just 2 parameters), returning null, if no title is cached, or the title
- true: always return a promise, which might already be resolved, if title was cached
- function: also returns a promise and calls the callback
--> calling egw.link_title(_app, _id) without 3rd parameter is deprecated now (gives a console.trace), to be changed in future to always return a promise, unless called with false, to just return a cache-lookup
* Et2SelectAccountReadonly and et2_link_widget is changed to use the new/updated syntax with promises
2022-05-02 11:27:33 +02:00
et2_register_widget ( et2_link_add , [ "link-add" ] ) ;