*/
class BsSimpleFieldRenderer extends BsFieldRenderer
{
/**
* @inheritDoc
*/
public function Render() {
$oOutput = parent::Render();
$sFieldClass = get_class($this->oField);
$sFieldMandatoryClass = ($this->oField->GetMandatory()) ? 'form_mandatory' : '';
$sFieldDescriptionForHTMLTag = ($this->oField->HasDescription()) ? 'data-tooltip-content="'.utils::HtmlEntities($this->oField->GetDescription()).'"' : '';
// Prepare input validations tags
$sInputTags = $this->ComputeInputValidationTags($this->oField);
// Rendering field in edition mode
if (!$this->oField->GetReadOnly() && !$this->oField->GetHidden()) {
// HTML content
switch ($sFieldClass) {
case 'Combodo\\iTop\\Form\\Field\\DateTimeField':
case 'Combodo\\iTop\\Form\\Field\\PasswordField':
case 'Combodo\\iTop\\Form\\Field\\StringField':
case 'Combodo\\iTop\\Form\\Field\\UrlField':
case 'Combodo\\iTop\\Form\\Field\\EmailField':
case 'Combodo\\iTop\\Form\\Field\\PhoneField':
case 'Combodo\\iTop\\Form\\Field\\SelectField':
case 'Combodo\\iTop\\Form\\Field\\MultipleSelectField':
// Opening container
$oOutput->AddHtml('
');
// Label
$oOutput->AddHtml('
');
if ($this->oField->GetLabel() !== '') {
$oOutput->AddHtml('');
}
$oOutput->AddHtml('
');
// Value
$oOutput->AddHtml('
');
// - Help block
$oOutput->AddHtml('');
// - Value regarding the field type
switch ($sFieldClass) {
case 'Combodo\\iTop\\Form\\Field\\DateTimeField':
/* @see N°803 - Allow display & edition of attributes on n:n relations on Portal
* LinkedSetFieldRenderer allow modification of link attributes, the default widget positioning truncates the popup.
*/
$sParent = '';
if ($this->oField->GetDateTimePickerWidgetParent() != null) {
$sParent = ", widgetParent: '{$this->oField->GetDateTimePickerWidgetParent()}'";
}
$oOutput->AddHtml('
');
// Then the previous entries if necessary
if ($sFieldClass === 'Combodo\\iTop\\Form\\Field\\CaseLogField') {
$this->PreparingCaseLogEntries($oOutput);
}
$oOutput->AddHtml('
');
// Closing container
$oOutput->AddHtml('
');
// Some additional stuff if we are displaying it with a rich editor
if ($bRichEditor) {
// Enable CKEditor
CKEditorHelper::ConfigureCKEditorElementForRenderingOutput($oOutput, $this->oField->GetGlobalId(), $this->oField->GetCurrentValue(), false, false, ['maximize' => []]);
if (($this->oField->GetObject() !== null) && ($this->oField->GetTransactionId() !== null)) {
$oOutput->AddJs(InlineImage::EnableCKEditorImageUpload($this->oField->GetObject(), utils::GetUploadTempId($this->oField->GetTransactionId())));
}
}
break;
case 'Combodo\\iTop\\Form\\Field\\RadioField':
case 'Combodo\\iTop\\Form\\Field\\CheckboxField':
$sFieldType = ($sFieldClass === 'Combodo\\iTop\\Form\\Field\\RadioField') ? 'radio' : 'checkbox';
// Opening container
$oOutput->AddHtml('
');
// Label
$oOutput->AddHtml('
');
if ($this->oField->GetLabel() !== '') {
$oOutput->AddHtml('');
}
$oOutput->AddHtml('
');
// Value
$oOutput->AddHtml('
');
// - Help block
$oOutput->AddHtml('');
$oOutput->AddHtml('
');
$i = 0;
foreach ($this->oField->GetChoices() as $sChoice => $sLabel) {
// Note : The test is a double equal on purpose as the type of the value received from the XHR is not always the same as the type of the allowed values. (eg : string vs int)
$sCheckedAtt = ($this->oField->IsAmongValues($sChoice)) ? 'checked' : '';
$sCheckedClass = ($this->oField->IsAmongValues($sChoice)) ? 'active' : '';
$oOutput->AddHtml('');
$i++;
}
$oOutput->AddHtml('
');
$oOutput->AddHtml('
');
// Closing container
$oOutput->AddHtml('
');
break;
case 'Combodo\\iTop\\Form\\Field\\HiddenField':
$oOutput->AddHtml('');
break;
}
// JS FieldChange trigger (:input are not always at the same depth)
switch ($sFieldClass) {
case 'Combodo\\iTop\\Form\\Field\\PasswordField':
case 'Combodo\\iTop\\Form\\Field\\StringField':
case 'Combodo\\iTop\\Form\\Field\\UrlField':
case 'Combodo\\iTop\\Form\\Field\\EmailField':
case 'Combodo\\iTop\\Form\\Field\\PhoneField':
case 'Combodo\\iTop\\Form\\Field\\SelectField':
case 'Combodo\\iTop\\Form\\Field\\MultipleSelectField':
case 'Combodo\\iTop\\Form\\Field\\HiddenField':
$oOutput->AddJs(<<oField->GetGlobalId()}").off("change keyup").on("change keyup", function(){
var me = this;
$(this).closest(".field_set").trigger("field_change", {
id: $(me).attr("id"),
name: $(me).closest(".form_field").attr("data-field-id"),
value: $(me).val()
});
}).on("mouseup", function(){this.focus();});
JS
);
break;
case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
case 'Combodo\\iTop\\Form\\Field\\CaseLogField':
if ($this->oField->GetFormat() === TextAreaField::ENUM_FORMAT_HTML) {
$oOutput->AddJs(<<oField->GetGlobalId()}")
.then((oCKEditor) => {
oCKEditor.model.document.on("change:data", () => {
const oFieldElem = $("#{$this->oField->GetGlobalId()}");
oFieldElem.val(oCKEditor.getData());
oFieldElem.closest(".field_set").trigger("field_change", {
id: oFieldElem.attr("id"),
name: oFieldElem.closest(".form_field").attr("data-field-id"),
value: oCKEditor.getData()
});
});
});
JS
);
} else {
$oOutput->AddJs(<<oField->GetGlobalId()}").off("change keyup").on("change keyup", function(){
var me = this;
$(this).closest(".field_set").trigger("field_change", {
id: $(me).attr("id"),
name: $(me).closest(".form_field").attr("data-field-id"),
value: $(me).val()
});
}).on("mouseup", function(){this.focus();});
JS
);
}
break;
case 'Combodo\\iTop\\Form\\Field\\DateTimeField':
// We need the focusout event has the datepicker widget seems to override the change event
$oOutput->AddJs(
<<oField->GetGlobalId()}").off("change keyup focusout").on("change keyup focusout", function(){
var me = this;
$(this).closest(".field_set").trigger("field_change", {
id: $(me).attr("id"),
name: $(me).closest(".form_field").attr("data-field-id"),
value: $(me).val()
});
});
EOF
);
break;
case 'Combodo\\iTop\\Form\\Field\\RadioField':
case 'Combodo\\iTop\\Form\\Field\\CheckboxField':
$oOutput->AddJs(
<<oField->GetGlobalId()} input").off("change").on("change", function(){
var me = this;
$(this).closest(".field_set").trigger("field_change", {
id: $(me).closest("#{$this->oField->GetGlobalId()}").attr("id"),
name: $(me).attr("name"),
value: $(me).val()
});
});
EOF
);
break;
}
}
// ... and in read-only mode (or hidden)
else {
// ... specific rendering for fields with multiple values
if (($this->oField instanceof MultipleChoicesField) && ($this->oField->GetMultipleValuesEnabled())) {
// TODO
}
// ... classic rendering for fields with only one value
else {
switch ($sFieldClass) {
case 'Combodo\\iTop\\Form\\Field\\LabelField':
case 'Combodo\\iTop\\Form\\Field\\StringField':
case 'Combodo\\iTop\\Form\\Field\\UrlField':
case 'Combodo\\iTop\\Form\\Field\\EmailField':
case 'Combodo\\iTop\\Form\\Field\\PhoneField':
case 'Combodo\\iTop\\Form\\Field\\DateTimeField':
case 'Combodo\\iTop\\Form\\Field\\DurationField':
// Opening container
$oOutput->AddHtml('
');
// Showing label / value only if read-only but not hidden
if (!$this->oField->GetHidden()) {
// Label
$oOutput->AddHtml('
');
if ($this->oField->GetLabel() !== '') {
$oOutput->AddHtml('');
}
$oOutput->AddHtml('
');
break;
case 'Combodo\\iTop\\Form\\Field\\HiddenField':
$oOutput->AddHtml('');
break;
}
}
}
// Attaching JS widget only if field is hidden or NOT read only
if($this->oField->GetHidden() || !$this->oField->GetReadOnly()) {
// JS Form field widget construct
$aValidators = array();
foreach ($this->oField->GetValidators() as $oValidator) {
if (false === ($oValidator instanceof AbstractRegexpValidator)) {
// no JS counterpart, so skipping !
continue;
}
$aValidators[$oValidator::GetName()] = array(
'reg_exp' => $oValidator->GetRegExp(),
'message' => Dict::S($oValidator->GetErrorMessage()),
);
}
$sFormFieldOptions = json_encode(array(
'validators' => $aValidators
));
switch ($sFieldClass) {
case 'Combodo\\iTop\\Form\\Field\\PasswordField':
case 'Combodo\\iTop\\Form\\Field\\StringField':
case 'Combodo\\iTop\\Form\\Field\\UrlField':
case 'Combodo\\iTop\\Form\\Field\\EmailField':
case 'Combodo\\iTop\\Form\\Field\\PhoneField':
case 'Combodo\\iTop\\Form\\Field\\SelectField':
case 'Combodo\\iTop\\Form\\Field\\MultipleSelectField':
case 'Combodo\\iTop\\Form\\Field\\HiddenField':
case 'Combodo\\iTop\\Form\\Field\\RadioField':
case 'Combodo\\iTop\\Form\\Field\\CheckboxField':
case 'Combodo\\iTop\\Form\\Field\\DateTimeField':
$oOutput->AddJs(
<<oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").portal_form_field($sFormFieldOptions);
EOF
);
break;
case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
case 'Combodo\\iTop\\Form\\Field\\CaseLogField':
$bRichEditor = ($this->oField->GetFormat() === TextAreaField::ENUM_FORMAT_HTML);
if($bRichEditor) {
// Overloading $sFormFieldOptions to include the set_current_value_callback. It would have been nicer to refactor the variable for all field types, but as this is a fix for a maintenance release, we rather be safe.
$sValidators = json_encode($aValidators);
$oOutput->AddJs(
<<oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").portal_form_field_html({
validators: $sValidators,
set_current_value_callback: function(me, oEvent, oData){ $(me.element).find('textarea').val(oData); }
});
EOF
);
}
else {
$oOutput->AddJs(
<<oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}']").portal_form_field($sFormFieldOptions);
EOF
);
}
break;
}
}
// Finally, no matter the field mode
switch ($sFieldClass) {
case 'Combodo\\iTop\\Form\\Field\\TextAreaField':
case 'Combodo\\iTop\\Form\\Field\\CaseLogField':
$bRichEditor = ($this->oField->GetFormat() === TextAreaField::ENUM_FORMAT_HTML);
if($bRichEditor) {
// MagnificPopup on images
$oOutput->AddJs(InlineImage::FixImagesWidth());
// Trigger highlighter for all code blocks in this caselog
$oOutput->AddJs(<<oField->GetId()}'][data-form-path='{$this->oField->GetFormPath()}'] pre").each(function(i, block) {
hljs.highlightBlock(block);
});
JS
);
}
break;
}
return $oOutput;
}
/**
* Note: Since 3.0.0 this is highly inspired from an extension of the community (see https://github.com/Molkobain/itop-bubble-caselogs)
*
* @param RenderingOutput $oOutput
*
* @throws \Exception
*/
protected function PreparingCaseLogEntries(RenderingOutput &$oOutput) {
$aEntries = $this->oField->GetEntries();
$iNbEntries = count($aEntries);
if ($iNbEntries > 0) {
// Dict entries
$sOpenAllEntriesTooltip = utils::HtmlEntities(Dict::S('UI:Layout:ActivityPanel:Tab:Toolbar:Action:OpenAll:Tooltip'));
$sCloseAllEntriesTooltip = utils::HtmlEntities(Dict::S('UI:Layout:ActivityPanel:Tab:Toolbar:Action:CloseAll:Tooltip'));
$sUsersCountTooltip = utils::HtmlEntities(Dict::S('UI:Layout:ActivityPanel:Tab:Toolbar:Info:AuthorsCount:Tooltip'));
$sEntriesCountTooltip = utils::HtmlEntities(Dict::S('UI:Layout:ActivityPanel:Tab:Toolbar:Info:MessagesCount:Tooltip'));
$sCloseEntryTooltip = utils::HtmlEntities(Dict::S('Portal:Form:Caselog:Entry:Close:Tooltip'));
// First pass to retrieve number of users
$aUserIds = array();
for ($i = 0; $i < $iNbEntries; $i++) {
$iEntryUserId = $aEntries[$i]['user_id'];
if (!in_array($iEntryUserId, $aUserIds)) {
$aUserIds[] = $iEntryUserId;
}
}
$iNbUsers = count($aUserIds);
// Opening thread
$oOutput->AddHtml(<<
HTML
);
// - Header
$oOutput->AddHtml(<<
{$iNbUsers}{$iNbEntries}
HTML
);
// - Content
$oOutput->AddHtml(<<
HTML
);
$sThreadUniqueId = uniqid();
$sLastDate = null;
$sLastUserId = null;
$iLastLoopIndex = $iNbEntries - 1;
// Caching profile picture url as it is resource consuming
$aContactPicturesCache = array();
$aPeerColorClassCache = array();
// Note: Yes, the config. param. is named after the backoffice element but we hope that we will "soon" have some kind of "light" activity panel in the portal too, so we keep this name.
$bHideContactPicture = false;
if (defined('PORTAL_ID'))
{
$bHideContactPicture= in_array(PORTAL_ID, utils::GetConfig()->Get('activity_panel.hide_avatars'));
}
// Current user
$iCurrentUserId = UserRights::GetUserId();
for ($i = 0; $i < $iNbEntries; $i++) {
$sEntryDatetime = AttributeDateTime::GetFormat()->Format($aEntries[$i]['date']);
$sEntryDate = AttributeDate::GetFormat()->Format($aEntries[$i]['date']);
$sEntryUserLogin = $aEntries[$i]['user_login'];
$iEntryUserId = $aEntries[$i]['user_id'];
// - Friendlyname
if (false === empty($iEntryUserId)) {
$oEntryUser = MetaModel::GetObject('User', $iEntryUserId, false /* Necessary in case user has been deleted */, true);
if(!is_null($oEntryUser)) {
$sEntryUserLogin = UserRights::GetUserFriendlyName($oEntryUser->Get('login'));
}
// Retrieve (and cache) profile picture if available (standard datamodel)
// Note: Here the cache is more about nor retrieving the User object several times rather than computing the picture URL
if (!array_key_exists($iEntryUserId, $aContactPicturesCache)) {
// First, check if we should display the picture
if ($bHideContactPicture === true) {
$sEntryContactPictureAbsoluteUrl = null;
}
// Otherwise try to retrieve one for the current contact
else {
if(is_null($oEntryUser)) {
$sEntryContactPictureAbsoluteUrl = null;
}
else {
$sEntryContactPictureAbsoluteUrl = UserRights::GetUserPictureAbsUrl($oEntryUser->Get('login'), false);
}
}
$aContactPicturesCache[$iEntryUserId] = $sEntryContactPictureAbsoluteUrl;
}
}
// Open user block if previous user was different or if previous date was different
if (($iEntryUserId !== $sLastUserId) || ($sEntryDate !== $sLastDate)) {
if ($sEntryDate !== $sLastDate) {
$oOutput->AddHtml(<<{$sEntryDate}
HTML
);
}
// Open block
if ($iEntryUserId === $iCurrentUserId) {
$sEntryBlockClass = 'ipb-caselog-thread--block-me';
}
else {
if (!array_key_exists($iEntryUserId, $aPeerColorClassCache)) {
$iPeerClassNumber = (count($aPeerColorClassCache) % 5) + 1;
$aPeerColorClassCache[$iEntryUserId] = 'ipb-caselog-thread--block-color-'.$iPeerClassNumber;
}
$sEntryBlockClass = $aPeerColorClassCache[$iEntryUserId];
}
$oOutput->AddHtml(<<
HTML
);
// Open medallion from profile picture or first name letter
$bEntryHasMedallionPicture = (empty($aContactPicturesCache[$iEntryUserId]) === false);
$sEntryMedallionStyle = $bEntryHasMedallionPicture ? ' background-image: url(\''.$aContactPicturesCache[$iEntryUserId].'\');' : '';
$sEntryMedallionContent = $bEntryHasMedallionPicture ? '' : utils::FormatInitialsForMedallion(UserRights::GetUserInitials($sEntryUserLogin));
// - Entry tooltip
$sEntryMedallionTooltip = utils::HtmlEntities($sEntryUserLogin);
$sEntryMedallionTooltipPlacement = ($iEntryUserId === $iCurrentUserId) ? 'left' : 'right';
$oOutput->AddHtml(<<
$sEntryMedallionContent
{$sEntryMedallionTooltip}
HTML
);
// Open entries
$oOutput->AddHtml(<<
HTML
);
}
// Prepare entry content
$sEntryId = 'ipb-caselog-thread--block-entry-'.$sThreadUniqueId.'-'.$i;
$sEntryHtml = AttributeText::RenderWikiHtml($aEntries[$i]['message_html'], true /* wiki only */);
$sEntryHtml = InlineImage::FixUrls($sEntryHtml);
// Add entry
$oOutput->AddHtml(<<
{$sEntryHtml}
{$sEntryDatetime}
HTML
);
// Close user block if next user is different or if last entry or if next entry is for another date
if (($i === $iLastLoopIndex)
|| ($i < $iLastLoopIndex && $iEntryUserId !== $aEntries[$i + 1]['user_id'])
|| ($i < $iLastLoopIndex && $sEntryDate !== AttributeDate::GetFormat()->Format($aEntries[$i + 1]['date']))) {
// Close entries and block
$oOutput->AddHtml(<<
HTML
);
}
// Update current loop informations
$sLastDate = $sEntryDate;
$sLastUserId = $iEntryUserId;
}
// Close thread content and thread
$oOutput->AddHtml(<<
HTML
);
// Add JS handlers
$oOutput->AddJs(<<oField->GetId()}"][data-form-path="{$this->oField->GetFormPath()}"]')
.on('click', '.ipb-caselog-thread--block-entry-toggler, .ipb-caselog-thread--block-entry.closed', function(){
$(this).closest('.ipb-caselog-thread--block-entry').toggleClass('closed');
})
.on('click', '.ipb-caselog-thread--open-all-toggler', function(oEvent){
oEvent.preventDefault()
$('[data-field-id="{$this->oField->GetId()}"][data-form-path="{$this->oField->GetFormPath()}"]').find('.ipb-caselog-thread--block-entry').removeClass('closed');
})
.on('click', '.ipb-caselog-thread--close-all-toggler', function(oEvent){
oEvent.preventDefault()
$('[data-field-id="{$this->oField->GetId()}"][data-form-path="{$this->oField->GetFormPath()}"]').find('.ipb-caselog-thread--block-entry').addClass('closed');
});
JS
);
}
}
/**
* @param \Combodo\iTop\Form\Field\Field $oField
*
* @return string
*/
private function ComputeInputValidationTags(Field $oField): string
{
// Result tags
$sTags = '';
// Iterate throw validators...
foreach ($oField->GetValidators() as $oValidator) {
// Validator
if ($oValidator instanceof AbstractRegexpValidator) {
if (!($oField instanceof DateField || $oField instanceof DateTimeField)) { // unrecognized regular expression
$sTags .= ' pattern="'.$oValidator->GetRegExp().'" ';
}
}
// Mandatory validator
if ($oValidator instanceof MandatoryValidator) {
$sTags .= ' required ';
}
}
return $sTags;
}
}