*
* @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('
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;
}
}