diff --git a/api/js/etemplate/et2_widget_placeholder.ts b/api/js/etemplate/et2_widget_placeholder.ts new file mode 100644 index 0000000000..189a827d10 --- /dev/null +++ b/api/js/etemplate/et2_widget_placeholder.ts @@ -0,0 +1,271 @@ +/** + * EGroupware eTemplate2 - JS Placeholder widgets + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package etemplate + * @subpackage api + * @link https://www.egroupware.org + * @author Nathan Gray + * @copyright Nathan Gray 2021 + */ + +/*egw:uses + et2_core_inputWidget; + et2_core_valueWidget; + et2_widget_description; +*/ + +import {et2_valueWidget} from "./et2_core_valueWidget"; +import {et2_createWidget, et2_register_widget, WidgetConfig} from "./et2_core_widget"; +import {ClassWithAttributes} from "./et2_core_inheritance"; +import {et2_dialog} from "./et2_widget_dialog"; +import {et2_inputWidget} from "./et2_core_inputWidget"; +import type {egw} from "../jsapi/egw_global"; +import {et2_selectbox} from "./et2_widget_selectbox"; +import {et2_description} from "./et2_widget_description"; +import {et2_link_entry} from "./et2_widget_link"; + + +/** + * Display a dialog to choose a placeholder + */ +export class et2_placeholder_select extends et2_inputWidget +{ + static readonly _attributes : any = { + insert_callback: { + "name": "Insert callback", + "description": "Method called with the selected placeholder text", + "type": "js" + } + }; + + static placeholders : Object | null = null; + + button : JQuery; + submit_callback : any; + dialog : et2_dialog; + protected value : any; + + /** + * Constructor + * + * @param _parent + * @param _attrs + * @memberOf et2_vfsSelect + */ + constructor(_parent, _attrs? : WidgetConfig, _child? : object) + { + // Call the inherited constructor + super(_parent, _attrs, ClassWithAttributes.extendAttributes(et2_placeholder_select._attributes, _child || {})); + + // Allow no child widgets + this.supportedWidgetClasses = []; + } + + _content(_content, _callback) + { + let self = this; + if(this.dialog && this.dialog.div) + { + this.dialog.div.dialog('close'); + } + + var callback = _callback || this._buildDialog; + if(et2_placeholder_select.placeholders === null) + { + this.egw().loading_prompt('placeholder_select', true, '', 'body'); + this.egw().json( + 'EGroupware\\Api\\Etemplate\\Widget\\Placeholder::ajax_get_placeholders', + [], + function(_content) + { + this.egw().loading_prompt('placeholder_select', false); + et2_placeholder_select.placeholders = _content; + callback.apply(self, arguments); + }.bind(this) + ).sendRequest(true); + } + else + { + this._buildDialog(et2_placeholder_select.placeholders); + } + } + + /** + * Builds file navigator dialog + * + * @param {object} _data content + */ + private _buildDialog(_data) + { + + let self = this; + let buttons = [ + { + text: this.egw().lang("Insert"), + id: "submit", + } + ]; + let extra_buttons_action = {}; + + if(this.options.extra_buttons && this.options.method) + { + for(let i = 0; i < this.options.extra_buttons.length; i++) + { + delete (this.options.extra_buttons[i]['click']); + buttons.push(this.options.extra_buttons[i]); + extra_buttons_action[this.options.extra_buttons[i]['id']] = this.options.extra_buttons[i]['id']; + } + + } + buttons.push({text: this.egw().lang("Cancel"), id: "cancel", image: "cancel"}); + + let data = { + content: {app: '', group: ''}, + sel_options: {app: [], group: []} + }; + + Object.keys(_data).map((key) => + { + data.sel_options.app.push( + { + value: key, + label: this.egw().lang(key) + }); + }); + data.sel_options.group = this._get_group_options(Object.keys(_data)[0]); + data.content.app = data.sel_options.app[0].value; + data.content.group = data.sel_options.group[0].value; + + // callback for dialog + this.submit_callback = function(submit_button_id, submit_value, savemode) + { + if((submit_button_id == 'submit' || (extra_buttons_action && extra_buttons_action[submit_button_id])) && submit_value) + { + this.options.insert_callback(submit_value.placeholder_list); + return true; + } + }.bind(this); + + this.dialog = et2_createWidget("dialog", + { + callback: this.submit_callback, + title: this.options.dialog_title || this.egw().lang("Insert Placeholder"), + buttons: buttons, + minWidth: 500, + minHeight: 400, + width: 400, + value: data, + template: this.egw().webserverUrl + '/api/templates/default/insert_merge_placeholder.xet?1', + resizable: true + }, et2_dialog._create_parent('api')); + this.dialog.template.uniqueId = 'api.insert_merge_placeholder'; + + // Keep the dialog always at the top + this.dialog.div.parent().css({"z-index": 100000}); + + this.dialog.div.on('load', function(e) + { + console.log(this); + + let app = this.dialog.template.widgetContainer.getDOMWidgetById("app"); + let group = this.dialog.template.widgetContainer.getDOMWidgetById("group"); + let placeholder_list = this.dialog.template.widgetContainer.getDOMWidgetById("placeholder_list"); + let preview = this.dialog.template.widgetContainer.getDOMWidgetById("preview_placeholder"); + let entry = this.dialog.template.widgetContainer.getDOMWidgetById("entry"); + + + placeholder_list.set_select_options(this._get_placeholders(app.get_value(), group.get_value())); + + // Further setup / styling that can't be done in etemplate + this.dialog.template.DOMContainer.style.display = "flex"; + this.dialog.template.DOMContainer.firstChild.style.display = "flex"; + group.getDOMNode().size = 5; + placeholder_list.getDOMNode().size = 5; + + // Bind some handlers + group.onchange = (select_node, select_widget) => + { + console.log(this, arguments); + placeholder_list.set_select_options(this._get_placeholders(app.get_value(), group.get_value())); + preview.set_value(""); + } + placeholder_list.onchange = this._on_placeholder_select.bind(this); + entry.onchange = this._on_placeholder_select.bind(this); + }.bind(this)); + } + + doLoadingFinished() + { + this._content.call(this, null); + return true; + } + + _on_placeholder_select(node, widget : et2_selectbox | et2_link_entry) + { + let app = this.dialog.template.widgetContainer.getDOMWidgetById("app"); + let entry = this.dialog.template.widgetContainer.getDOMWidgetById("entry"); + let placeholder_list = this.dialog.template.widgetContainer.getDOMWidgetById("placeholder_list"); + let preview = this.dialog.template.widgetContainer.getDOMWidgetById("preview_placeholder"); + let preview_content = this.dialog.template.widgetContainer.getDOMWidgetById("preview_content"); + console.log(this, arguments); + preview.set_value(placeholder_list.get_value()); + if(placeholder_list.get_value() && entry.get_value() && entry.get_value().app && entry.get_value().id) + { + // Show the selected placeholder replaced with value from the selected entry + this.egw().json( + 'EGroupware\\Api\\Etemplate\\Widget\\Placeholder::ajax_fill_placeholders', + [entry.get_value().app, placeholder_list.get_value(), entry.get_value().id], + function(_content) + { + preview_content.set_value(_content); + preview_content.getDOMNode().parentNode.style.visibility = _content.trim() ? null : 'hidden'; + }.bind(this) + ).sendRequest(true); + } + else + { + preview_content.getDOMNode().parentNode.style.visibility = 'hidden'; + } + } + + _get_group_options(appname) + { + let options = []; + Object.keys(et2_placeholder_select.placeholders[appname]).map((key) => + { + options.push( + { + value: key, + label: this.egw().lang(key) + }); + }); + return options; + } + + _get_placeholders(appname, group) + { + let options = []; + Object.keys(et2_placeholder_select.placeholders[appname][group]).map((key) => + { + options.push( + { + value: key, + label: et2_placeholder_select.placeholders[appname][group][key] + }); + }); + return options; + } + + set_value(value) + { + this.value = value; + } + + getValue() + { + return this.value; + } +}; +et2_register_widget(et2_placeholder_select, ["placeholder-select"]); + diff --git a/api/js/etemplate/etemplate2.ts b/api/js/etemplate/etemplate2.ts index 3ec44666a9..43b417a31a 100644 --- a/api/js/etemplate/etemplate2.ts +++ b/api/js/etemplate/etemplate2.ts @@ -62,6 +62,7 @@ import './et2_widget_image'; import './et2_widget_iframe'; import './et2_widget_file'; import './et2_widget_link'; +import './et2_widget_placeholder'; import './et2_widget_progress'; import './et2_widget_portlet'; import './et2_widget_selectAccount'; diff --git a/api/src/Contacts/Merge.php b/api/src/Contacts/Merge.php index 796e767135..4681067995 100644 --- a/api/src/Contacts/Merge.php +++ b/api/src/Contacts/Merge.php @@ -246,14 +246,21 @@ class Merge extends Api\Storage\Merge 'owner' => lang('Owner'), ) as $name => $label) { - if (in_array($name,array('start','end')) && $n&1) // main values, which should be in the first column + if(in_array($name, array('start', + 'end')) && $n & 1) // main values, which should be in the first column { echo "\n"; $n++; } - if (!($n&1)) echo ''; - echo '{{calendar/#/'.$name.'}}'.$label.''; - if ($n&1) echo "\n"; + if(!($n & 1)) + { + echo ''; + } + echo '{{calendar/#/' . $name . '}}' . $label . ''; + if($n & 1) + { + echo "\n"; + } $n++; } echo "\n"; @@ -261,6 +268,55 @@ class Merge extends Api\Storage\Merge $GLOBALS['egw']->framework->render(ob_get_clean()); } + /** + * Get a list of placeholders provided. + * + * Placeholders are grouped logically. Group key should have a user-friendly translation. + */ + public function get_placeholder_list() + { + $placeholders = []; + $group = 'contact'; + foreach($this->contacts->contact_fields as $name => $label) + { + if(in_array($name, array('tid', 'label', 'geo'))) + { + continue; + } // dont show them, as they are not used in the UI atm. + + switch($name) + { + case 'adr_one_street': + $group = 'business'; + break; + case 'adr_two_street': + $group = 'private'; + break; + case 'tel_work': + $group = 'phone'; + break; + case 'email': + case 'email_home': + $group = 'email'; + break; + case 'url': + $group = 'details'; + } + $placeholders[$group]["{{" . $name . "}}"] = $label; + if($name == 'cat_id') + { + $placeholders[$group]["{{" . $name . "}}"] = lang('Category path'); + } + } + + $group = 'customfields'; + foreach($this->contacts->customfields as $name => $field) + { + $placeholders[$group]["{{" . $name . "}}"] = $field['label']; + } + return $placeholders; + } + /** * Get insert-in-document action with optional default document on top * diff --git a/api/src/Etemplate/Widget/Placeholder.php b/api/src/Etemplate/Widget/Placeholder.php new file mode 100644 index 0000000000..6fc67d2144 --- /dev/null +++ b/api/src/Etemplate/Widget/Placeholder.php @@ -0,0 +1,207 @@ + true, + 'ajax_fill_placeholder' => true + ); + + /** + * Constructor + * + * @param string|\XMLReader $xml string with xml or XMLReader positioned on the element to construct + * @throws Api\Exception\WrongParameter + */ + public function __construct($xml = '') + { + if($xml) + { + parent::__construct($xml); + } + } + + /** + * Set up what we know on the server side. + * + * Set the options for the application select. + * + * @param string $cname + * @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont' + */ + public function beforeSendToClient($cname, array $expand = null) + { + } + + /** + * Get the placeholders that match the given parameters. + * Default options will get all placeholders in a single request. + */ + public static function ajax_get_placeholders($apps = null, $group = null) + { + $placeholders = []; + + if(is_null($apps)) + { + $apps = ['addressbook']; + } + + foreach($apps as $appname) + { + $merge = Api\Storage\Merge::get_app_class($appname); + switch($appname) + { + case 'user': + $list = $merge->get_user_replacement_list(); + break; + default: + $list = $merge->get_placeholder_list(); + break; + } + if(!is_null($group)) + { + $list = array_intersect_key($list, $group); + } + $placeholders[$appname] = $list; + } + + $response = Api\Json\Response::get(); + $response->data($placeholders); + } + + public function ajax_fill_placeholders($app, $content, $entry) + { + $merge = Api\Storage\Merge::get_app_class($app); + $err = ""; + + switch($app) + { + case 'addressbook': + default: + $merged = $merge->merge_string($content, [$entry['id']], $err, 'text/plain'); + } + $response = Api\Json\Response::get(); + $response->data($merged); + } + + /** + * Validate input + * + * Following attributes get checked: + * - needed: value must NOT be empty + * - min, max: int and float widget only + * - maxlength: maximum length of string (longer strings get truncated to allowed size) + * - preg: perl regular expression incl. delimiters (set by default for int, float and colorpicker) + * - int and float get casted to their type + * + * @param string $cname current namespace + * @param array $expand values for keys 'c', 'row', 'c_', 'row_', 'cont' + * @param array $content + * @param array &$validated =array() validated content + */ + public function validate($cname, array $expand, array $content, &$validated = array()) + { + $form_name = self::form_name($cname, $this->id, $expand); + + if(!$this->is_readonly($cname, $form_name)) + { + $value = $value_in =& self::get_array($content, $form_name); + + // keep values added into request by other ajax-functions, eg. files draged into htmlarea (Vfs) + if((!$value || is_array($value) && !$value['to_id']) && is_array($expand['cont'][$this->id]) && !empty($expand['cont'][$this->id]['to_id'])) + { + if(!is_array($value)) + { + $value = array( + 'to_app' => $expand['cont'][$this->id]['to_app'], + ); + } + $value['to_id'] = $expand['cont'][$this->id]['to_id']; + } + + // Link widgets can share IDs, make sure to preserve values from others + $already = self::get_array($validated, $form_name); + if($already != null) + { + $value = array_merge($value, $already); + } + // Automatically do link if user selected entry but didn't click 'Link' button + $link = self::get_array($content, self::form_name($cname, $this->id . '_link_entry')); + if($this->type == 'link-to' && is_array($link) && $link['app'] && $link['id']) + { + // Do we have enough information to link automatically? + if(is_array($value) && $value['to_id']) + { + Api\Link::link($value['to_app'], $value['to_id'], $link['app'], $link['id']); + } + else + { + // Not enough information, leave it to the application + if(!is_array($value['to_id'])) + { + $value['to_id'] = array(); + } + $value['to_id'][] = $link; + } + } + + // Look for files - normally handled by ajax + $files = self::get_array($content, self::form_name($cname, $this->id . '_file')); + if(is_array($files) && !(is_array($value) && $value['to_id'])) + { + $value = array(); + if(is_dir($GLOBALS['egw_info']['server']['temp_dir']) && is_writable($GLOBALS['egw_info']['server']['temp_dir'])) + { + $path = $GLOBALS['egw_info']['server']['temp_dir'] . '/'; + } + else + { + $path = ''; + } + foreach($files as $name => $attrs) + { + if(!is_array($value['to_id'])) + { + $value['to_id'] = array(); + } + $value['to_id'][] = array( + 'app' => Api\Link::VFS_APPNAME, + 'id' => array( + 'name' => $attrs['name'], + 'type' => $attrs['type'], + 'tmp_name' => $path . $name + ) + ); + } + } + $valid =& self::get_array($validated, $form_name, true); + if(true) + { + $valid = $value; + } + //error_log($this); + //error_log(" " . array2string($valid)); + } + } +} diff --git a/api/src/Storage/Merge.php b/api/src/Storage/Merge.php index b7bd67868a..97e51e18d1 100644 --- a/api/src/Storage/Merge.php +++ b/api/src/Storage/Merge.php @@ -1572,6 +1572,25 @@ abstract class Merge return $app; } + /** + * Get the correct class for the given app + * + * @param $appname + */ + public static function get_app_class($appname) + { + if(class_exists($appname) && is_subclass_of($appname, 'EGroupware\\Api\\Storage\\Merge')) + { + $classname = "{$appname}_merge"; + $document_merge = new $classname(); + } + else + { + $document_merge = new Api\Contacts\Merge(); + } + return $document_merge; + } + /** * Get the replacements for any entry specified by app & id * @@ -1580,7 +1599,7 @@ abstract class Merge * @param string $content * @return array */ - public function get_app_replacements($app, $id, $content, $prefix='') + public function get_app_replacements($app, $id, $content, $prefix = '') { $replacements = array(); if($app == 'addressbook') diff --git a/api/templates/default/insert_merge_placeholder.xet b/api/templates/default/insert_merge_placeholder.xet new file mode 100644 index 0000000000..1dbdd7f5b4 --- /dev/null +++ b/api/templates/default/insert_merge_placeholder.xet @@ -0,0 +1,68 @@ + + + + + +