Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions assets/src/legacy/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -900,15 +900,28 @@ window.lizMap = function() {
lizMap.mainLizmap.popup.mapPopup.setPosition([eventLonLatInfo.lon, eventLonLatInfo.lat]);
}

// Activate Boostrap 2 tabs here as they are not
// automatically activated when created in popup anchored
$('#' + popupContainerId + ' a[data-toggle="tab"]').on( 'click',function (e) {
e.preventDefault();
$(this).tab('show');
});
document.getElementById('liz_layer_popup_contentDiv')
.querySelectorAll('a[data-toggle="tab"]')
.forEach(el => {
el.addEventListener('click', e => {
e.preventDefault();
bootstrap.Tab.getOrCreateInstance(el).show();
});
});
}
lastLonLatInfo = eventLonLatInfo;

// Activate tabs in dock popup (popupcontent)
if (popupContainerId) {
document.querySelectorAll('#' + popupContainerId + ' a[data-toggle="tab"]')
.forEach(el => {
el.addEventListener('click', e => {
e.preventDefault();
bootstrap.Tab.getOrCreateInstance(el).show();
});
});
}

// Display related children objects
addChildrenFeatureInfo( popup, popupContainerId );
// Display geometries
Expand Down
141 changes: 140 additions & 1 deletion lizmap/modules/lizmap/lib/Request/WMSRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use Lizmap\App\WktTools;
use Lizmap\Project\Project;
use Lizmap\Project\Qgis;
use Lizmap\Project\UnknownLizmapProjectException;

/**
Expand Down Expand Up @@ -695,6 +696,10 @@ protected function gfiVectorXmlToHtml($layerId, $layerName, $layerTitle, $layer,

$remoteStorageProfile = RemoteStorageRequest::getProfile('webdav');

// Fields configured in QGIS with a CheckBox edit widget: rendered as
// actual checkboxes in the auto popup, mirroring the editing form.
$checkBoxFields = $this->getCheckBoxFieldsForLayer($layerId);

// Get the template for the popup content
$templateConfigured = false;
$popupTemplate = '';
Expand Down Expand Up @@ -754,6 +759,7 @@ protected function gfiVectorXmlToHtml($layerId, $layerName, $layerTitle, $layer,
'featureId' => $id,
'attributes' => $feature->Attribute,
'remoteStorageProfile' => $remoteStorageProfile,
'checkBoxFields' => $checkBoxFields,
));
$autoContent = $popupFeatureContent;
// Get specific template for the layer has been configured
Expand Down Expand Up @@ -852,7 +858,7 @@ protected function gfiVectorXmlToHtml($layerId, $layerName, $layerTitle, $layer,
$finalContent = $autoContent;
if (property_exists($configLayer, 'popupSource')) {
if (in_array($configLayer->popupSource, array('qgis', 'form')) && $maptipValue) {
$finalContent = $maptipValue;
$finalContent = $this->applyCheckBoxesToFormPopup($maptipValue, $checkBoxFields);
}
if ($configLayer->popupSource == 'lizmap' && $templateConfigured) {
$finalContent = $lizmapContent;
Expand All @@ -875,6 +881,7 @@ protected function gfiVectorXmlToHtml($layerId, $layerName, $layerTitle, $layer,
'allFeatureAttributes' => array_reverse($allFeatureAttributes),
'remoteStorageProfile' => $remoteStorageProfile,
'allFeatureToolbars' => array_reverse($allFeatureToolbars),
'checkBoxFields' => $checkBoxFields,
));
}

Expand Down Expand Up @@ -1369,4 +1376,136 @@ protected function getMapData($project, $params, $forced = false)

return new OGCResponse($code, $mime, $data, $cached);
}

/**
* Build a map of fields configured with a CheckBox edit widget for the
* given layer, using the QGIS project's typed XML info (cached per request
* by ProjectInfo::fromQgisPath).
*
* @param string $layerId
*
* @return array<string, array{CheckedState: string, UncheckedState: string}>
*/
private function getCheckBoxFieldsForLayer($layerId)
{
$checkBoxFields = array();

$qgisPath = $this->project->getQgisPath();
if (!$qgisPath || !file_exists($qgisPath)) {
return $checkBoxFields;
}

try {
$projectInfo = Qgis\ProjectInfo::fromQgisPath($qgisPath);
} catch (\Exception $e) {
return $checkBoxFields;
}

$xmlLayer = $projectInfo->getLayerById($layerId);
if (!$xmlLayer instanceof Qgis\Layer\VectorLayer) {
return $checkBoxFields;
}

foreach ($xmlLayer->fieldConfiguration as $field) {
$editWidget = $field->editWidget;
if (strtolower($editWidget->type) !== 'checkbox') {
continue;
}
$config = $editWidget->config;
if (!$config instanceof Qgis\BaseQgisObject) {
continue;
}
$data = $config->getData();
$checked = array_key_exists('CheckedState', $data) ? (string) $data['CheckedState'] : '';
$unchecked = array_key_exists('UncheckedState', $data) ? (string) $data['UncheckedState'] : '';
// Fall back to QGIS defaults (see QgisFormControl::fillCheckboxValues)
$checkBoxFields[(string) $field->name] = array(
'CheckedState' => $checked === '' ? 't' : $checked,
'UncheckedState' => $unchecked === '' ? 'f' : $unchecked,
);
}

return $checkBoxFields;
}

/**
* Replace raw CheckBox-widget values in the QGIS drag-and-drop form popup
* HTML with disabled <input type="checkbox"> elements, mirroring the
* editing form. Leaves non-matching values untouched.
*
* QGIS Server emits each field in the form popup as:
* <span id="dd_jforms_view_edition_FIELDNAME" class="jforms-control-input">VALUE</span>
*
* @param string $maptipHtml form popup HTML returned by QGIS Server
* @param array $checkBoxFields map fieldName => ['CheckedState' => string, 'UncheckedState' => string]
*
* @return string
*/
private function applyCheckBoxesToFormPopup($maptipHtml, $checkBoxFields)
{
if (!is_string($maptipHtml) || $maptipHtml === '' || !is_array($checkBoxFields) || count($checkBoxFields) === 0) {
return $maptipHtml;
}

return preg_replace_callback(
'/(<span\s+id="dd_jforms_view_edition_([^"]+)"\s+class="jforms-control-input"\s*>)(.*?)(<\/span>)/us',
function ($m) use ($checkBoxFields) {
$fieldName = html_entity_decode($m[2], ENT_QUOTES, 'UTF-8');
if (!isset($checkBoxFields[$fieldName])) {
return $m[0];
}
$value = trim(html_entity_decode($m[3], ENT_QUOTES, 'UTF-8'));
$cfg = $checkBoxFields[$fieldName];
$state = self::matchCheckBoxState(
$value,
isset($cfg['CheckedState']) ? (string) $cfg['CheckedState'] : '',
isset($cfg['UncheckedState']) ? (string) $cfg['UncheckedState'] : ''
);
if ($state === 'checked') {
return $m[1].'<input type="checkbox" disabled="disabled" checked="checked" class="lizmap-popup-checkbox-widget">'.$m[4];
}
if ($state === 'unchecked') {
return $m[1].'<input type="checkbox" disabled="disabled" class="lizmap-popup-checkbox-widget">'.$m[4];
}

return $m[0];
},
$maptipHtml
);
}

/**
* Match a raw attribute value against CheckBox widget states. Tries the
* configured CheckedState/UncheckedState first, then falls back to common
* boolean representations so that fields stored as boolean (which come
* through WMS/WFS as 'true'/'false') still render as checkboxes.
*
* @param string $value raw attribute value
* @param string $checkedExpected CheckedState configured in QGIS
* @param string $uncheckedExpected UncheckedState configured in QGIS
*
* @return null|string 'checked', 'unchecked', or null for no match
*/
private static function matchCheckBoxState($value, $checkedExpected, $uncheckedExpected)
{
if ($checkedExpected !== '' && $value === $checkedExpected) {
return 'checked';
}
if ($uncheckedExpected !== '' && $value === $uncheckedExpected) {
return 'unchecked';
}
$normalized = strtolower(trim($value));
if (in_array($normalized, array('true', 't', '1', 'yes', 'on'), true)) {
return 'checked';
}
if (in_array($normalized, array('false', 'f', '0', 'no', 'off'), true)) {
return 'unchecked';
}
// Treat null-like values (NULL, empty, QGIS's "()" for NULL boolean) as unchecked
if (in_array($normalized, array('', 'null', '()'), true)) {
return 'unchecked';
}

return null;
}
}
2 changes: 1 addition & 1 deletion lizmap/modules/view/templates/popupDefaultContent.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
{if $attribute['name'] != 'geometry' && $attribute['name'] != 'maptip'}
<tr data-field-name="{$attribute['name']}" {if $attribute['value']=='' || $attribute['value']=='NULL' } class="empty-data" {/if}>
<th>{$attribute['name']}</th>
<td>{$attribute['name']|featurepopup:$attribute['value'],$repository,$project,$remoteStorageProfile}</td>
<td>{$attribute['name']|popupcheckbox:$attribute['value'],$repository,$project,$checkBoxFields,$remoteStorageProfile}</td>
</tr>
{/if}
{/foreach}
Expand Down
2 changes: 1 addition & 1 deletion lizmap/modules/view/templates/popup_all_features_table.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<td>{$allFeatureToolbars[$key]}</td>
{foreach $featureAttributes as $attribute}
{if $attribute['name'] != 'geometry' && $attribute['name'] != 'maptip' && $attribute['value'] != ''}
<td>{$attribute['name']|featurepopup:$attribute['value'],$repository,$project,$remoteStorageProfile}</td>
<td>{$attribute['name']|popupcheckbox:$attribute['value'],$repository,$project,$checkBoxFields,$remoteStorageProfile}</td>
{/if}
{/foreach}
</tr>
Expand Down
86 changes: 86 additions & 0 deletions lizmap/plugins/tpl/common/modifier.popupcheckbox.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

/**
* Plugin modifier for the popup templates.
*
* Renders a field value as a disabled checkbox when the field is configured
* with a QGIS CheckBox edit widget. Falls back to the standard featurepopup
* modifier for anything that doesn't match a recognised checked/unchecked
* state.
*
* @author 3liz
* @copyright 2026 3liz
*
* @see http://3liz.com
*
* @license Mozilla Public License : http://www.mozilla.org/MPL/
*
* @param mixed $attributeName feature Attribute name
* @param mixed $attributeValue feature Attribute value
* @param string $repository lizmap Repository
* @param string $project name of the project
* @param array $checkBoxFields map fieldName => ['CheckedState' => string, 'UncheckedState' => string]
* @param null|array $remoteStorageProfile webDav configuration
*
* @return string
*/
function jtpl_modifier_common_popupcheckbox($attributeName, $attributeValue, $repository, $project, $checkBoxFields, $remoteStorageProfile = null)
{
$name = (string) $attributeName;

if (is_array($checkBoxFields) && isset($checkBoxFields[$name])) {
$cfg = $checkBoxFields[$name];
$state = lizmap_popup_checkbox_match_state(
(string) $attributeValue,
isset($cfg['CheckedState']) ? (string) $cfg['CheckedState'] : '',
isset($cfg['UncheckedState']) ? (string) $cfg['UncheckedState'] : ''
);
if ($state === 'checked') {
return '<input type="checkbox" disabled="disabled" checked="checked" class="lizmap-popup-checkbox-widget">';
}
if ($state === 'unchecked') {
return '<input type="checkbox" disabled="disabled" class="lizmap-popup-checkbox-widget">';
}
}

$popupClass = jClasses::getService('view~popup');

return $popupClass->getHtmlFeatureAttribute($attributeName, $attributeValue, $repository, $project, null, $remoteStorageProfile);
}

/**
* Decide whether the given raw value represents a checked, unchecked, or
* unrecognised state for a QGIS CheckBox-widget field. Matches the
* configured CheckedState/UncheckedState first, then falls back to common
* boolean representations (so fields typed as boolean, which come through
* WMS/WFS as 'true'/'false' regardless of the widget's labels, also render
* as checkboxes). Null-like values render as unchecked.
*
* @param string $value raw attribute value
* @param string $checkedExpected CheckedState configured in QGIS
* @param string $uncheckedExpected UncheckedState configured in QGIS
*
* @return null|string 'checked', 'unchecked', or null for no match
*/
function lizmap_popup_checkbox_match_state($value, $checkedExpected, $uncheckedExpected)
{
if ($checkedExpected !== '' && $value === $checkedExpected) {
return 'checked';
}
if ($uncheckedExpected !== '' && $value === $uncheckedExpected) {
return 'unchecked';
}
$normalized = strtolower(trim($value));
if (in_array($normalized, array('true', 't', '1', 'yes', 'on'), true)) {
return 'checked';
}
if (in_array($normalized, array('false', 'f', '0', 'no', 'off'), true)) {
return 'unchecked';
}
// QGIS's "()" represents a NULL boolean in some popup renderings
if (in_array($normalized, array('', 'null', '()'), true)) {
return 'unchecked';
}

return null;
}
3 changes: 1 addition & 2 deletions tests/end2end/playwright/popup.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ test.describe('Popup @readonly', () => {
await expect(page.locator('#popup_dd_1_tab1 a img')).toHaveAttribute('src', mediaLink);
});

test.fail('changes popup tab', async ({ page }) => {
test('changes popup tab', async ({ page }) => {
const project = new ProjectPage(page, 'popup');
await project.open();

Expand All @@ -332,7 +332,6 @@ test.describe('Popup @readonly', () => {
responseExpect(getFeatureInfoResponse).toBeHtml();

await page.getByRole('link', { name: 'tab2' }).click({ force: true });
// This expect failed because of BS5, the click does not open the tab
await expect(page.locator('#popup_dd_1_tab2')).toHaveClass(/active/);
});

Expand Down
Loading