* * @property \Combodo\iTop\Form\Field\LinkedSetField $oField * */ class BsLinkedSetFieldRenderer extends BsFieldRenderer { /** * @inheritDoc */ public function Render() { $oOutput = parent::Render(); $sFieldMandatoryClass = ($this->oField->GetMandatory()) ? 'form_mandatory' : ''; $sFieldDescriptionForHTMLTag = ($this->oField->HasDescription()) ? 'data-tooltip-content="'.utils::HtmlEntities($this->oField->GetDescription()).'"' : ''; // Retrieve link and remote attributes $aAttributesToDisplay = $this->oField->GetAttributesToDisplay(); $aLnkAttributesToDisplay = $this->oField->GetLnkAttributesToDisplay(); // we sort the table on the first non link column $iSortColumnIndex = count($this->oField->GetLnkAttributesToDisplay()); // if we are in edition mode, we skip the first column (selection checkbox column) if(!$this->oField->GetReadOnly()){ $iSortColumnIndex++; } // Vars to build the table $sAttributesToDisplayAsJson = json_encode($aAttributesToDisplay); $sLnkAttributesToDisplayAsJson = json_encode($aLnkAttributesToDisplay); $sAttCodesToDisplayAsJson = json_encode($this->oField->GetAttributesToDisplay(true)); $sLnkAttCodesToDisplayAsJson = json_encode($this->oField->GetLnkAttributesToDisplay(true)); $aItems = array(); $aItemIds = array(); $aAddedItemIds = array(); $aAddedTargetIds = array(); $this->InjectRendererFileAssets($this->oField->GetLinkedClass(), $this->oField->GetLnkAttributesToDisplay(true), $oOutput); $this->PrepareItems($aItems, $aItemIds, $oOutput, $aAddedItemIds, $aAddedTargetIds); $sItemsAsJson = json_encode($aItems); $sItemIdsAsJson = utils::EscapeHtml(json_encode(array('current' => $aItemIds, 'add' => $aAddedItemIds))); foreach ($aAddedTargetIds as $sId) { $aItemIds[$sId] = array(); } if (!$this->oField->GetHidden()) { // Rendering field $sIsEditable = ($this->oField->GetReadOnly()) ? 'false' : 'true'; $sCollapseTogglerIconVisibleClass = 'glyphicon-menu-down'; $sCollapseTogglerIconHiddenClass = 'glyphicon-menu-down collapsed'; $sCollapseTogglerClass = 'form_linkedset_toggler'; $sCollapseTogglerId = $sCollapseTogglerClass.'_'.$this->oField->GetGlobalId(); $sFieldWrapperId = 'form_linkedset_wrapper_'.$this->oField->GetGlobalId(); // Preparing collapsed state if ($this->oField->GetDisplayOpened()) { $sCollapseTogglerExpanded = 'true'; $sCollapseTogglerIconClass = $sCollapseTogglerIconVisibleClass; $sCollapseJSInitState = 'true'; } else { $sCollapseTogglerClass .= ' collapsed'; $sCollapseTogglerExpanded = 'false'; $sCollapseTogglerIconClass = $sCollapseTogglerIconHiddenClass; $sCollapseJSInitState = 'false'; } $oOutput->AddHtml('
'); if ($this->oField->GetLabel() !== '') { $oOutput->AddHtml(''); } $oOutput->AddHtml('
'); // Rendering table // - Vars $sTableId = 'table_' . $this->oField->GetGlobalId(); // - Output $oOutput->AddHtml( <<
EOF ); // Rendering table widget // - Vars $sEmptyTableLabel = utils::EscapeHtml(Dict::S(($this->oField->GetReadOnly()) ? 'Portal:Datatables:Language:EmptyTable' : 'UI:Message:EmptyList:UseAdd')); $sLabelGeneralCheckbox = utils::EscapeHtml(Dict::S('Core:BulkExport:CheckAll').' / '.Dict::S('Core:BulkExport:UncheckAll')); $sSelectionOptionHtml = ($this->oField->GetReadOnly()) ? 'false' : '{"style": "multi"}'; $sSelectionInputGlobalHtml = ($this->oField->GetReadOnly()) ? '' : ''; $sSelectionInputHtml = ($this->oField->GetReadOnly()) ? '' : ''; // - Output $oOutput->AddJs( <<oField->GetGlobalId()} === undefined) { buildTable_{$this->oField->GetGlobalId()}(); } }) .on('show.bs.collapse', function(){ $('#{$sCollapseTogglerId} > span.glyphicon').removeClass('{$sCollapseTogglerIconHiddenClass}').addClass('{$sCollapseTogglerIconVisibleClass}'); }) .on('hide.bs.collapse', function(){ $('#{$sCollapseTogglerId} > span.glyphicon').removeClass('{$sCollapseTogglerIconVisibleClass}').addClass('{$sCollapseTogglerIconHiddenClass}'); }); // Places a loader in the empty datatables $('#{$sTableId} > tbody').html('' + $('#page_overlay').html() + ''); // Prepares data for datatables var oLnkColumnProperties_{$this->oField->GetGlobalId()} = {$sLnkAttributesToDisplayAsJson}; var oColumnProperties_{$this->oField->GetGlobalId()} = {$sAttributesToDisplayAsJson}; var oRawDatas_{$this->oField->GetGlobalId()} = {$sItemsAsJson}; var oTable_{$this->oField->GetGlobalId()}; var oSelectedItems_{$this->oField->GetGlobalId()} = {}; var oRenderersJs_{$this->oField->GetGlobalId()} = ''; var getColumnsDefinition_{$this->oField->GetGlobalId()} = function() { var aColumnsDefinition = []; if({$sIsEditable}) { aColumnsDefinition.push({ "width": "auto", "searchable": false, "sortable": false, "title": '{$sSelectionInputGlobalHtml}', "type": "html", "data": "id", "render": function(data, type, row) { var oCheckboxElem = $('{$sSelectionInputHtml}'); if(row.limited_access) { oCheckboxElem.html('-'); } else { oCheckboxElem.find(':input').attr('data-object-id', row.id).attr('data-target-object-id', row.target_id); } return oCheckboxElem.prop('outerHTML'); } }); } for(sKey in oLnkColumnProperties_{$this->oField->GetGlobalId()}) { aColumnProperties = oLnkColumnProperties_{$this->oField->GetGlobalId()}[sKey]; // Level main column aColumnsDefinition.push({ "width": "auto", "searchable": true, "sortable": false, "title": aColumnProperties.label, "defaultContent": "", "type": "html", "data": "attributes.lnk__" + sKey, "className": {$sIsEditable} && aColumnProperties.mandatory ? 'mandatory' : '', "render": function(data, type, row){ var cellElem; var metadataNames = ['object_class', 'object_id', 'attribute_code', 'attribute_type', 'value_raw']; // Preparing the cell data if(data.url !== undefined) { cellElem = $(''); cellElem.attr('href', data.url); } else { cellElem = $(''); } for(var sPropName in row.attributes[data.prefix+data.attribute_code]) { var propValue = row.attributes[data.prefix+data.attribute_code][sPropName]; if(sPropName === 'value_html') { cellElem.html(propValue); } else if(metadataNames.indexOf(sPropName) > -1) { cellElem.attr('data-'+sPropName.replace('_', '-'), propValue) } } return cellElem.prop('outerHTML'); }, }); } for(sKey in oColumnProperties_{$this->oField->GetGlobalId()}) { aColumnProperties = oColumnProperties_{$this->oField->GetGlobalId()}[sKey]; // Level main column aColumnsDefinition.push({ "width": "auto", "searchable": true, "sortable": true, "title": aColumnProperties.label, "defaultContent": "", "type": "html", "data": "attributes." + sKey, "className": aColumnProperties.mandatory ? 'mandatory' : '', "render": function(data, type, row){ var cellElem; var metadataNames = ['object_class', 'object_id', 'attribute_code', 'attribute_type', 'value_raw']; // Preparing the cell data if(data.url !== undefined) { cellElem = $(''); cellElem.attr('href', data.url); } else { cellElem = $(''); } for(var sPropName in row.attributes[data.attribute_code]) { var propValue = row.attributes[data.attribute_code][sPropName]; if(sPropName === 'value_html') { cellElem.html(propValue); } else if(metadataNames.indexOf(sPropName) > -1) { cellElem.attr('data-'+sPropName.replace('_', '-'), propValue) } } return cellElem.prop('outerHTML'); }, }); } return aColumnsDefinition; }; // Helper to build the datatable // Note : Those options should be externalized in an library so we can use them on any DataTables for the portal. // We would just have to override / complete the necessary elements var buildTable_{$this->oField->GetGlobalId()} = function() { var iDefaultOrderColumnIndex = {$iSortColumnIndex}; // Instantiates datatables oTable_{$this->oField->GetGlobalId()} = $('#{$sTableId}').DataTable({ "language": { "emptyTable": "{$sEmptyTableLabel}" }, "displayLength": -1, "scrollY": "300px", "scrollCollapse": true, "retrieve": true, "order": [[iDefaultOrderColumnIndex, "asc"]], "dom": 't', "columns": getColumnsDefinition_{$this->oField->GetGlobalId()}(), "select": {$sSelectionOptionHtml}, "rowId": "id", "data": oRawDatas_{$this->oField->GetGlobalId()}, "rowCallback": function(oRow, oData){ if(oData.limited_access) { $(oRow).addClass('limited_access'); } // Opening in a new modal on click $(oRow).find('a').off('click').on('click', function(oEvent){ // Prevents link opening. oEvent.preventDefault(); // Prevents row selection oEvent.stopPropagation(); // Note : This could be better if we check for an existing modal first instead of always creating a new one CombodoModal.OpenModal({ content: { endpoint: $(this).attr('href'), }, }); }); // Prevent row selection on input click $('input,select,textarea,.input-group-addon', oRow).on('click', function(oEvent){ if($(this).data('type') !== 'row-selection'){ // Prevents row selection oEvent.stopPropagation(); } }); // Store attributes inline css and js for (var key in oData.attributes) { const aElement = oData.attributes[key]; if(aElement.css_inline !== undefined){ $('td:first-child', oRow).append($('')); } if(aElement.js_inline !== undefined){ oRenderersJs_{$this->oField->GetGlobalId()} += aElement.js_inline; } } }, "initComplete": function(){ // Execute inline js provided by attributes renderers eval(oRenderersJs_{$this->oField->GetGlobalId()}); }, }); // Handles items selection/deselection // - Preventing limited access rows to be selected on click oTable_{$this->oField->GetGlobalId()}.off('user-select').on('user-select', function(oEvent, dt, type, cell, originalEvent){ if($(originalEvent.target).closest('tr[id]').hasClass('limited_access')) { oEvent.preventDefault(); } }); // - Selecting when clicking on the rows (instead of the global checkbox) oTable_{$this->oField->GetGlobalId()}.off('select').on('select', function(oEvent, dt, type, indexes){ var aData = oTable_{$this->oField->GetGlobalId()}.rows(indexes).data().toArray(); // Checking input $('#{$sTableId} tbody tr[id].selected td:first-child input').prop('checked', true); // Saving values in temp array for(var i in aData) { var iItemId = aData[i].id; if(!(iItemId in oSelectedItems_{$this->oField->GetGlobalId()})) { oSelectedItems_{$this->oField->GetGlobalId()}[iItemId] = aData[i].name; } } // Updating remove button updateRemoveButtonState_{$this->oField->GetGlobalId()}(); }); // - Deselecting when clicking on the rows (instead of the global checkbox) oTable_{$this->oField->GetGlobalId()}.off('deselect').on('deselect', function(oEvent, dt, type, indexes){ var aData = oTable_{$this->oField->GetGlobalId()}.rows(indexes).data().toArray(); // Checking input $('#{$sTableId} tbody tr[id]:not(.selected) td:first-child input').prop('checked', false); // Saving values in temp array for(var i in aData) { var iItemId = aData[i].id; if(iItemId in oSelectedItems_{$this->oField->GetGlobalId()}) { delete oSelectedItems_{$this->oField->GetGlobalId()}[iItemId]; } } // Unchecking global checkbox $('#{$this->oField->GetGlobalId()}_check_all').prop('checked', false); // Updating remove button updateRemoveButtonState_{$this->oField->GetGlobalId()}(); }); // - From the global button $('#{$this->oField->GetGlobalId()}_check_all').off('click').on('click', function(oEvent){ if($(this).prop('checked')) { oTable_{$this->oField->GetGlobalId()}.rows(':not(.limited_access)').select(); } else { oTable_{$this->oField->GetGlobalId()}.rows(':not(.limited_access)').deselect(); } updateRemoveButtonState_{$this->oField->GetGlobalId()}(); }); }; JS ); // Additional features if in edition mode if (!$this->oField->GetReadOnly()) { $aErrorMessagesMandatory = Dict::S('Core:Validator:Mandatory'); $aErrorMessagesDefault = Dict::S('Core:Validator:Default'); // Attaching JS widget $sObjectInformationsUrl = $this->oField->GetInformationEndpoint(); $oOutput->AddJs( <<GetValidatorsAsJson()}, 'on_validation_callback': function(oFormField){ const aLinkedSetInputs = $('#{$sFieldWrapperId} input,select,textarea', oFormField.element); aLinkedSetInputs.each(function(e){ const oInput = $(this); const aInputValidity = oInput[0].validity; const oFormFieldControl = oInput.closest('.form_field_control'); if(aInputValidity.valueMissing){ oFormFieldControl.toggleClass('has-error', true); $('.help-block', oFormFieldControl).html('$aErrorMessagesMandatory'); } else if(aInputValidity.patternMismatch){ oFormFieldControl.toggleClass('has-error', true); $('.help-block', oFormFieldControl).html('$aErrorMessagesDefault'); } else{ oFormFieldControl.toggleClass('has-error', false); $('.help-block', oFormFieldControl).empty(); } }); }, 'get_current_value_callback': function(me, oEvent, oData){ // Read linked set value as array var aValue = JSON.parse(me.element.find('#{$this->oField->GetGlobalId()}').val()); // Iterate throw table rows and extract link attributes input values... $('tbody tr', me.element).each(function(){ // Extract link id const sId = $(this).attr('id'); // Security if(sId !== undefined){ // Prepare link attributes values const aValues = {}; // Extract inputs values... $('input,select,textarea', $(this)).each(function(){ if($(this).attr('id') !== undefined){ aValues[$(this).attr('name')] = $(this).val(); } }); // Set values if(aValue.current !== undefined && aValue.current[sId] !== undefined){ aValue.current[sId] = aValues; } const iAddId = -parseInt(sId); if(aValue.add !== undefined && aValue.add[iAddId] !== undefined){ aValue.add[iAddId] = aValues; } } }); return JSON.stringify(aValue); }, 'set_current_value_callback': function(me, oEvent, oData){ // When we have data (meaning that we picked objects from search) if(oData !== undefined && Object.keys(oData.values).length > 0) { // Showing loader while retrieving informations $('#page_overlay').fadeIn(200); // Retrieving new rows ids var aObjectIds = Object.keys(oData.values); // Retrieving rows informations so we can add them $.post( '{$sObjectInformationsUrl}', { sObjectClass: '{$this->oField->GetTargetClass()}', sLinkClass: '{$this->oField->GetLinkedClass()}', aObjectIds: aObjectIds, aObjectAttCodes: $sAttCodesToDisplayAsJson, aLinkAttCodes: $sLnkAttCodesToDisplayAsJson, sDateTimePickerWidgetParent: '#table_{$this->oField->GetGlobalId()}_wrapper' }, function(oData){ // Updating datatables if(oData.items !== undefined) { for(var i in oData.items) { // Adding target item id information oData.items[i].target_id = oData.items[i].id; // Adding item to table only if it's not already there if($('#{$sTableId} tr[id] > td input[data-target-object-id="' + oData.items[i].target_id + '"], #{$sTableId} tr[id] > td input[data-target-object-id="' + (oData.items[i].target_id*-1) + '"]').length === 0) { // Making id negative in order to recognize it when persisting oData.items[i].id = -1 * parseInt(oData.items[i].id); oTable_{$this->oField->GetGlobalId()}.row.add(oData.items[i]); } } oTable_{$this->oField->GetGlobalId()}.draw(); // Execute inline js for each attributes renderers for(let i in oData.items) { for(let key in oData.items[i].attributes){ eval(oData.items[i].attributes[key].js_inline) } } // Updating input updateInputValue_{$this->oField->GetGlobalId()}(); } } ) .done(function(oData){ // Updating items count updateItemCount_{$this->oField->GetGlobalId()}(); // Updating global checkbox $('#{$this->oField->GetGlobalId()}_check_all').prop('checked', false); }) .always(function(oData){ // Hiding loader $('#page_overlay').fadeOut(200); }); } // We come from a button else { // Updating input updateInputValue_{$this->oField->GetGlobalId()}(); // Updating items count updateItemCount_{$this->oField->GetGlobalId()}(); // Updating global checkbox $('#{$this->oField->GetGlobalId()}_check_all').prop('checked', false); } } }); JS ); // Rendering table // - Vars $sButtonRemoveId = 'btn_remove_' . $this->oField->GetGlobalId(); $sButtonAddId = 'btn_add_' . $this->oField->GetGlobalId(); $sLabelRemove = Dict::S('UI:Button:Remove'); $sLabelAdd = Dict::S('UI:Button:AddObject'); // - Output $oOutput->AddHtml( <<
EOF ); // Rendering table widget // - Vars $sAddButtonEndpoint = str_replace('-sMode-', 'from-attribute', $this->oField->GetSearchEndpoint()); // - Output $oOutput->AddJs( <<oField->GetGlobalId()} = function() { var bIsDisabled = (Object.keys(oSelectedItems_{$this->oField->GetGlobalId()}).length == 0); $('#{$sButtonRemoveId}').prop('disabled', bIsDisabled); }; // - Item count state handler var updateItemCount_{$this->oField->GetGlobalId()} = function() { $('#{$sCollapseTogglerId} > .text').text( oTable_{$this->oField->GetGlobalId()}.rows().count() ); }; // - Field input handler var updateInputValue_{$this->oField->GetGlobalId()} = function() { // Retrieving table rows var aData = oTable_{$this->oField->GetGlobalId()}.rows().data().toArray(); // Retrieving input values var oValues = JSON.parse($('#{$this->oField->GetGlobalId()}').val()); oValues.add = {}; oValues.remove = {}; // Checking removed objects for(var i in oValues.current) { if($('#{$sTableId} tr[id="'+i+'"]').length === 0) { oValues.remove[i] = {}; } } // Checking added objects for(var i in aData) { if(oValues.current[aData[i].id] === undefined) { oValues.add[aData[i].target_id] = {}; } } // Setting input values $('#{$this->oField->GetGlobalId()}').val(JSON.stringify(oValues)); }; // Handles items remove/add $('#{$sButtonRemoveId}').off('click').on('click', function(){ // Removing items from table oTable_{$this->oField->GetGlobalId()}.rows({selected: true}).remove().draw(); // Resetting selected items oSelectedItems_{$this->oField->GetGlobalId()} = {}; // Updating form value $("[data-field-id='{$this->oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").triggerHandler('set_current_value'); // Updating global checkbox state $('#{$this->oField->GetGlobalId()}_check_all').prop('checked', false); // Updating remove button updateRemoveButtonState_{$this->oField->GetGlobalId()}(); }); $('#{$sButtonAddId}').off('click').on('click', function(){ // Preparing current values var aObjectIdsToIgnore = []; $('#{$sTableId} tr[id] > td input[data-target-object-id]').each(function(iIndex, oElem){ aObjectIdsToIgnore.push( $(oElem).attr('data-target-object-id') ); }); // Creating a new modal var oOptions = { content: { endpoint: '{$sAddButtonEndpoint}', data: { sFormPath: '{$this->oField->GetFormPath()}', sFieldId: '{$this->oField->GetId()}', aObjectIdsToIgnore : aObjectIdsToIgnore }, }, }; if($('.modal[data-source-element="{$sButtonAddId}"]').length === 0) { oOptions['attributes'] = {'data-source-element': '{$sButtonAddId}'}; } else { oOptions['base_modal'] = { 'usage': 'replace', 'selector': '.modal[data-source-element="{$sButtonAddId}"]:first' }; } CombodoModal.OpenModal(oOptions); }); JS ); } } // ... and in hidden mode else { $oOutput->AddHtml(''); } // End of table rendering $oOutput->AddHtml(''); $oOutput->AddHtml(''); return $oOutput; } /** * @param $aItems * @param $aItemIds * * @throws \Exception * @throws \CoreException */ protected function PrepareItems(&$aItems, &$aItemIds, $oOutput, &$aAddedItemIds, &$aAddedTargetIds) { /** @var \ormLinkSet $oValueSet */ $oValueSet = $this->oField->GetCurrentValue(); $oValueSet->OptimizeColumnLoad(array($this->oField->GetTargetClass() => $this->oField->GetAttributesToDisplay(true))); while ($oItem = $oValueSet->Fetch()) { // In case of indirect linked set, we must retrieve the remote object if ($this->oField->IsIndirect()) { try { // Note : AllowAllData set to true here instead of checking scope's flag because we are displaying a value that has been set and validated $oRemoteItem = MetaModel::GetObject($this->oField->GetTargetClass(), $oItem->Get($this->oField->GetExtKeyToRemote()), true, true); } catch (Exception $e) { // In some cases we can't retrieve an object from a linkedset, eg. when the extkey to remote is 0 due to a database corruption. // Rather than crashing we rather just skip the object like in the administration console IssueLog::Error('Could not retrieve object of linkedset in form #'.$this->oField->GetFormPath().' for field #'.$this->oField->GetId().'. Message: '.$e->getMessage()); continue; } } else { $oRemoteItem = $oItem; } // Skip item if not supposed to be displayed $bLimitedAccessItem = $this->oField->IsLimitedAccessItem($oRemoteItem->GetKey()); if ($bLimitedAccessItem && !$this->oField->GetDisplayLimitedAccessItems()) { continue; } $aItemProperties = array( 'id' => ($this->oField->IsIndirect() && $oItem->IsNew()) ? -1 * $oRemoteItem->GetKey() : $oItem->GetKey(), 'target_id' => $oRemoteItem->GetKey(), 'name' => $oItem->GetName(), 'attributes' => array(), 'limited_access' => $bLimitedAccessItem, 'disabled' => true, 'active' => false, 'inactive' => true, 'not-selectable' => true, ); // Link attributes to display $this->PrepareItem($oItem, $this->oField->GetLinkedClass(), $this->oField->GetLnkAttributesToDisplay(true), !$this->oField->GetReadOnly(), $aItemProperties, 'lnk__'); // Remote attributes to display $this->PrepareItem($oRemoteItem, $this->oField->GetTargetClass(), $this->oField->GetAttributesToDisplay(true), false, $aItemProperties); // Remap objects to avoid added item to be considered as current item when form validation isn't valid // and form reconstruct $aItems[] = $aItemProperties; if ($oItem->IsNew()) { $aAddedItemIds[-1 * $aItemProperties['id']] = array(); $aAddedTargetIds[] = $oRemoteItem->GetKey(); } else { $aItemIds[$aItemProperties['id']] = array(); } } $oValueSet->rewind(); } /** * @param string $sClass * @param array $aAttributesCodesToDisplay * @param $oOutput * * @return void * @throws \CoreException */ protected function InjectRendererFileAssets(string $sClass, array $aAttributesCodesToDisplay, $oOutput) { // handle abstract class while(MetaModel::IsAbstract($sClass)){ $aChildClasses = MetaModel::EnumChildClasses($sClass); if(count($aChildClasses) > 0){ $sClass = $aChildClasses[0]; } } // create a fake object to pass to renderers for retrieving global assets $oItem = MetaModel::NewObject($sClass); // Iterate throw attributes... foreach ($aAttributesCodesToDisplay as $sAttCode) { // Retrieve attribute definition $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); // make form field from attribute $oField = $oAttDef->MakeFormField($oItem); // retrieve the form field renderer $sFieldRendererClass = static::GetFieldRendererClass($oField); // retrieve renderer global assets if ($sFieldRendererClass !== null) { /** @var FieldRenderer $oFieldRenderer */ $oFieldRenderer = new $sFieldRendererClass($oField); $oFieldOutput = $oFieldRenderer->Render(); static::TransferFieldRendererGlobalOutput($oFieldOutput, $oOutput); } } } /** * @param \DBObject $oItem * @param string $sClass * @param array $aAttributesCodesToDisplay * @param bool $bIsEditable * @param array $aItemProperties * @param $oOutput * * @return void * @throws \ArchivedObjectException * @throws \CoreException */ protected function PrepareItem(DBObject $oItem, string $sClass, array $aAttributesCodesToDisplay, bool $bIsEditable, array &$aItemProperties, string $sAttribueKeyPrefix = '') { // Iterate throw attributes... foreach ($aAttributesCodesToDisplay as $sAttCode) { if ($sAttCode !== 'id') { // Retrieve attribute definition $oAttDef = MetaModel::GetAttributeDef($sClass, $sAttCode); // Prepare attribute properties $aAttProperties = [ 'prefix'=> $sAttribueKeyPrefix, 'object_class' => $sClass, 'object_id' => $oItem->GetKey(), 'attribute_code' => $sAttCode, 'attribute_type' => get_class($oAttDef), ]; // - Value raw // For simple fields, we get the raw (stored) value as well $bExcludeRawValue = false; foreach (ApplicationHelper::GetAttDefClassesToExcludeFromMarkupMetadataRawValue() as $sAttDefClassToExclude) { if (is_a($oAttDef, $sAttDefClassToExclude, true)) { $bExcludeRawValue = true; break; } } $aAttProperties['value_raw'] = ($bExcludeRawValue === false) ? $oItem->Get($sAttCode) : null; // External key specific if ($bIsEditable) { $oField = $oAttDef->MakeFormField($oItem); // Prevent datetimepicker popup to be truncated if ($oField instanceof DateTimeField) { $oField->SetDateTimePickerWidgetParent('#table_'.$this->oField->GetGlobalId().'_wrapper'); } $sFieldRendererClass = static::GetFieldRendererClass($oField); if ($sFieldRendererClass !== null) { /** @var FieldRenderer $oFieldRenderer */ $oFieldRenderer = new $sFieldRendererClass($oField); $oFieldOutput = $oFieldRenderer->Render(); $aAttProperties['js_inline'] = $oFieldOutput->GetJs(); $aAttProperties['css_inline'] = $oFieldOutput->GetCss(); $aAttProperties['value_html'] = $oFieldOutput->GetHtml(); } } else if ($oAttDef->IsExternalKey()) { /** @var \AttributeExternalKey $oAttDef */ $aAttProperties['value_html'] = $oItem->Get($sAttCode.'_friendlyname'); // Checking if user can access object's external key $sObjectUrl = ApplicationContext::MakeObjectUrl($oAttDef->GetTargetClass(), $oItem->Get($sAttCode)); if (!empty($sObjectUrl)) { $aAttProperties['url'] = $sObjectUrl; } } else { // Others attributes $aAttProperties['value_html'] = $oAttDef->GetAsHTML($oItem->Get($sAttCode)); if ($oAttDef instanceof AttributeFriendlyName) { // Checking if user can access object $sObjectUrl = ApplicationContext::MakeObjectUrl($sClass, $oItem->GetKey()); if (!empty($sObjectUrl)) { $aAttProperties['url'] = $sObjectUrl; } } } $aItemProperties['attributes'][$sAttribueKeyPrefix.$sAttCode] = $aAttProperties; } } } /** * Transfer field renderer output to page output. * * @param \Combodo\iTop\Renderer\RenderingOutput $oFieldOutput * @param \Combodo\iTop\Renderer\RenderingOutput $oPageOutput * * @return void */ public static function TransferFieldRendererGlobalOutput(RenderingOutput $oFieldOutput, RenderingOutput $oPageOutput) { foreach ($oFieldOutput->GetJsFiles() as $sJsFile) { $oPageOutput->AddJsFile($sJsFile); } foreach ($oFieldOutput->GetCssFiles() as $sCssFile) { $oPageOutput->AddCssFile($sCssFile); } } /** * Retrieve a field renderer class. * * @param \Combodo\iTop\Form\Field\Field $oField * * @return string|null */ public static function GetFieldRendererClass(Field $oField): ?string { $aRegisteredFields = BsFieldRendererMappings::RegisterSupportedFields(); $sFieldClass = get_class($oField); foreach ($aRegisteredFields as $aRegisteredField) { if ($aRegisteredField['field'] === $sFieldClass) { return $aRegisteredField['field_renderer']; } } return null; } }