This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git

commit cd6f542d2144f93ce45db7d60c9ea416e49de815
Author: Emil Huseynov <[email protected]>
AuthorDate: Thu Oct 29 16:44:25 2026 +0400

    WW-5774 Move combobox change handling into a static script with a delegated 
listener
    
    The combobox templates emitted two inline script blocks per instance: an
    autoPopulate_<id> function and a block that bound every select marked with
    data-struts-combobox. A CSP built on script hashes or static scripts only 
still
    blocked the combobox, the block was duplicated in the simple and html5 
themes,
    and N comboboxes in an iterator emitted N blocks.
    
    combobox.js registers one delegated change listener per page, in the capture
    phase, matching the transfer selects after WW-5772. The select now carries 
the
    target field name, the header key and the empty-option flag in data-*
    attributes, and the templates emit no per-instance inline script.
---
 .../org/apache/struts2/static/combobox.js          | 56 ++++++++++++++++++++++
 .../src/main/resources/template/html5/combobox.ftl | 33 ++-----------
 .../main/resources/template/simple/combobox.ftl    | 33 ++-----------
 .../org/apache/struts2/views/jsp/ui/ComboBox-1.txt | 21 +-------
 .../org/apache/struts2/views/jsp/ui/ComboBox-2.txt | 27 +----------
 .../org/apache/struts2/views/jsp/ui/ComboBox-3.txt | 27 +----------
 .../org/apache/struts2/views/jsp/ui/ComboBox-4.txt | 21 +-------
 7 files changed, 72 insertions(+), 146 deletions(-)

diff --git a/core/src/main/resources/org/apache/struts2/static/combobox.js 
b/core/src/main/resources/org/apache/struts2/static/combobox.js
new file mode 100644
index 000000000..94a570fda
--- /dev/null
+++ b/core/src/main/resources/org/apache/struts2/static/combobox.js
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * Copies the selected option of a combobox select into its text field. The 
select carries its settings in data-*
+ * attributes:
+ *     data-struts-combobox-target   name of the text field in the same form
+ *     data-struts-header-key        value of the header option, which is not 
copied
+ *     data-struts-empty-option      "true" when the empty option is rendered, 
which is not copied
+ * The listener is registered once per page, in the capture phase, so it also 
covers comboboxes rendered later and
+ * is not stopped by an ancestor's stopPropagation().
+ */
+(function () {
+    if (window.strutsComboboxListener) {
+        return;
+    }
+    window.strutsComboboxListener = true;
+
+    document.addEventListener('change', function (event) {
+        var select = event.target;
+        if (!select || !select.getAttribute) {
+            return;
+        }
+        var target = select.getAttribute('data-struts-combobox-target');
+        if (target === null || !select.form || select.selectedIndex < 0) {
+            return;
+        }
+        var value = select.options[select.selectedIndex].value;
+        if (select.hasAttribute('data-struts-header-key') && value === 
select.getAttribute('data-struts-header-key')) {
+            return;
+        }
+        if (select.getAttribute('data-struts-empty-option') === 'true' && 
value === '') {
+            return;
+        }
+        var field = select.form.elements[target];
+        if (field) {
+            field.value = value;
+        }
+    }, true);
+})();
diff --git a/core/src/main/resources/template/html5/combobox.ftl 
b/core/src/main/resources/template/html5/combobox.ftl
index dedda39d6..9f5c1b617 100644
--- a/core/src/main/resources/template/html5/combobox.ftl
+++ b/core/src/main/resources/template/html5/combobox.ftl
@@ -18,26 +18,14 @@
  * under the License.
  */
 -->
-<@s.script>
-function autoPopulate_${attributes.escapedId}(targetElement) {
-<#if attributes.headerKey?? && attributes.headerValue??>
-  if (targetElement.options[targetElement.selectedIndex].value == 
'${attributes.headerKey?js_string}') {
-    return;
-  }
-</#if>
-<#if attributes.emptyOption!false>
-  if (targetElement.options[targetElement.selectedIndex].value == '') {
-    return;
-  }
-</#if>
-  
targetElement.form.elements['${attributes.name?js_string}'].value=targetElement.options[targetElement.selectedIndex].value;
-}
-</@s.script>
+<@s.script src="${base}${attributes.staticContentPath}/combobox.js" />
 <#include "/${attributes.templateDir}/html5/text.ftl" /><#rt/>
 
 <#include 
"/${attributes.templateDir}/${attributes.expandTheme}/controlheader.ftl" />
 <#if attributes.list??>
-<select data-struts-combobox="${attributes.escapedId}"<#rt/>
+<select data-struts-combobox="${attributes.escapedId}" 
data-struts-combobox-target="${attributes.name}"<#rt/>
+<#if attributes.headerKey?? && attributes.headerValue??> 
data-struts-header-key="${attributes.headerKey}"</#if><#t/>
+<#if attributes.emptyOption!false> data-struts-empty-option="true"</#if><#t/>
  <#include "/${attributes.templateDir}/${attributes.expandTheme}/css.ftl" 
/><#rt/>
 <#if attributes.disabled!false>
  disabled="disabled"<#rt/>
@@ -97,18 +85,5 @@ function autoPopulate_${attributes.escapedId}(targetElement) 
{
 >${tmpListValue}</option><#rt/>
 </@s.iterator>
 </select><#rt/>
-<@s.script>
-       (function () {
-               var selects = 
document.querySelectorAll('select[data-struts-combobox="${attributes.escapedId}"]');
-               for (var i = 0; i < selects.length; i++) {
-                       if (!selects[i].strutsComboboxBound) {
-                               selects[i].strutsComboboxBound = true;
-                               selects[i].addEventListener('change', function 
() {
-                                       
autoPopulate_${attributes.escapedId}(this);
-                               });
-                       }
-               }
-       })();
-</@s.script>
 </#if>
 <#include 
"/${attributes.templateDir}/${attributes.expandTheme}/controlfooter.ftl" 
/><#rt/>
diff --git a/core/src/main/resources/template/simple/combobox.ftl 
b/core/src/main/resources/template/simple/combobox.ftl
index ca17e4a05..13dd95851 100644
--- a/core/src/main/resources/template/simple/combobox.ftl
+++ b/core/src/main/resources/template/simple/combobox.ftl
@@ -18,25 +18,13 @@
  * under the License.
  */
 -->
-<@s.script>
-       function autoPopulate_${attributes.escapedId}(targetElement) {
-       <#if attributes.headerKey?? && attributes.headerValue??>
-               if (targetElement.options[targetElement.selectedIndex].value == 
'${attributes.headerKey?js_string}') {
-                       return;
-               }
-       </#if>
-       <#if attributes.emptyOption!false>
-               if (targetElement.options[targetElement.selectedIndex].value == 
'') {
-                       return;
-               }
-       </#if>
-               
targetElement.form.elements['${attributes.name?js_string}'].value=targetElement.options[targetElement.selectedIndex].value;
-       }
-</@s.script>
+<@s.script src="${base}${attributes.staticContentPath}/combobox.js" />
 <#include "/${attributes.templateDir}/simple/text.ftl" />
 <br />
 <#if attributes.list??>
-<select data-struts-combobox="${attributes.escapedId}"<#rt/>
+<select data-struts-combobox="${attributes.escapedId}" 
data-struts-combobox-target="${attributes.name}"<#rt/>
+<#if attributes.headerKey?? && attributes.headerValue??> 
data-struts-header-key="${attributes.headerKey}"</#if><#t/>
+<#if attributes.emptyOption!false> data-struts-empty-option="true"</#if><#t/>
 <#include "/${attributes.templateDir}/${attributes.expandTheme}/css.ftl" />
     <#if attributes.disabled!false>
  disabled="disabled"<#rt/>
@@ -98,17 +86,4 @@
     </option><#lt/>
     </@s.iterator>
 </select>
-<@s.script>
-       (function () {
-               var selects = 
document.querySelectorAll('select[data-struts-combobox="${attributes.escapedId}"]');
-               for (var i = 0; i < selects.length; i++) {
-                       if (!selects[i].strutsComboboxBound) {
-                               selects[i].strutsComboboxBound = true;
-                               selects[i].addEventListener('change', function 
() {
-                                       
autoPopulate_${attributes.escapedId}(this);
-                               });
-                       }
-               }
-       })();
-</@s.script>
 </#if>
diff --git 
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/ComboBox-1.txt 
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/ComboBox-1.txt
index b3c6f098a..8c91cbd7f 100644
--- a/core/src/test/resources/org/apache/struts2/views/jsp/ui/ComboBox-1.txt
+++ b/core/src/test/resources/org/apache/struts2/views/jsp/ui/ComboBox-1.txt
@@ -1,29 +1,12 @@
 <tr>
     <td class="tdLabel"><label for="cb" class="label">mylabel:</label></td>
     <td class="tdInput">
-<script nonce="r4nd0m">
-       function autoPopulate_cb(targetElement) {
-               
targetElement.form.elements['foo'].value=targetElement.options[targetElement.selectedIndex].value;
-       }
-</script>
+<script src="/static/combobox.js" nonce="r4nd0m"></script>
 <input type="text" name="foo" value="hello" id="cb"/><br/>
-<select data-struts-combobox="cb">
+<select data-struts-combobox="cb" data-struts-combobox-target="foo">
     <option value="foo">foo</option>
     <option value="bar">bar</option>
     <option value="baz">baz</option>
 </select>
-<script nonce="r4nd0m">
-    (function () {
-        var selects = 
document.querySelectorAll('select[data-struts-combobox="cb"]');
-        for (var i = 0; i < selects.length; i++) {
-            if (!selects[i].strutsComboboxBound) {
-                selects[i].strutsComboboxBound = true;
-                selects[i].addEventListener('change', function () {
-                    autoPopulate_cb(this);
-                });
-            }
-        }
-    })();
-</script>
     </td>
 </tr>
diff --git 
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/ComboBox-2.txt 
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/ComboBox-2.txt
index 7bea2770f..2f39ed24d 100644
--- a/core/src/test/resources/org/apache/struts2/views/jsp/ui/ComboBox-2.txt
+++ b/core/src/test/resources/org/apache/struts2/views/jsp/ui/ComboBox-2.txt
@@ -1,19 +1,9 @@
 <tr>
     <td class="tdLabel"><label for="myFavouriteFruit" class="label">My 
Favourite Fruit:</label></td>
     <td class="tdInput">
-<script>
-       function autoPopulate_myFavouriteFruit(targetElement) {
-               if (targetElement.options[targetElement.selectedIndex].value == 
'-1') {
-                       return;
-               }
-               if (targetElement.options[targetElement.selectedIndex].value == 
'') {
-                   return;
-               }
-               
targetElement.form.elements['myFavouriteFruit'].value=targetElement.options[targetElement.selectedIndex].value;
-       }
-</script>
+<script src="/static/combobox.js"></script>
 <input type="text" name="myFavouriteFruit" value="banana" 
id="myFavouriteFruit"/><br/>
-<select data-struts-combobox="myFavouriteFruit">
+<select data-struts-combobox="myFavouriteFruit" 
data-struts-combobox-target="myFavouriteFruit" data-struts-header-key="-1" 
data-struts-empty-option="true">
        <option value="-1">--- Please Select ---</option>
        <option value=""></option>
     <option value="apple">apple</option>
@@ -21,18 +11,5 @@
     <option value="pineaple">pineaple</option>
     <option value="grapes">grapes</option>
 </select>
-<script>
-    (function () {
-        var selects = 
document.querySelectorAll('select[data-struts-combobox="myFavouriteFruit"]');
-        for (var i = 0; i < selects.length; i++) {
-            if (!selects[i].strutsComboboxBound) {
-                selects[i].strutsComboboxBound = true;
-                selects[i].addEventListener('change', function () {
-                    autoPopulate_myFavouriteFruit(this);
-                });
-            }
-        }
-    })();
-</script>
     </td>
 </tr>
\ No newline at end of file
diff --git 
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/ComboBox-3.txt 
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/ComboBox-3.txt
index 7bea2770f..2f39ed24d 100644
--- a/core/src/test/resources/org/apache/struts2/views/jsp/ui/ComboBox-3.txt
+++ b/core/src/test/resources/org/apache/struts2/views/jsp/ui/ComboBox-3.txt
@@ -1,19 +1,9 @@
 <tr>
     <td class="tdLabel"><label for="myFavouriteFruit" class="label">My 
Favourite Fruit:</label></td>
     <td class="tdInput">
-<script>
-       function autoPopulate_myFavouriteFruit(targetElement) {
-               if (targetElement.options[targetElement.selectedIndex].value == 
'-1') {
-                       return;
-               }
-               if (targetElement.options[targetElement.selectedIndex].value == 
'') {
-                   return;
-               }
-               
targetElement.form.elements['myFavouriteFruit'].value=targetElement.options[targetElement.selectedIndex].value;
-       }
-</script>
+<script src="/static/combobox.js"></script>
 <input type="text" name="myFavouriteFruit" value="banana" 
id="myFavouriteFruit"/><br/>
-<select data-struts-combobox="myFavouriteFruit">
+<select data-struts-combobox="myFavouriteFruit" 
data-struts-combobox-target="myFavouriteFruit" data-struts-header-key="-1" 
data-struts-empty-option="true">
        <option value="-1">--- Please Select ---</option>
        <option value=""></option>
     <option value="apple">apple</option>
@@ -21,18 +11,5 @@
     <option value="pineaple">pineaple</option>
     <option value="grapes">grapes</option>
 </select>
-<script>
-    (function () {
-        var selects = 
document.querySelectorAll('select[data-struts-combobox="myFavouriteFruit"]');
-        for (var i = 0; i < selects.length; i++) {
-            if (!selects[i].strutsComboboxBound) {
-                selects[i].strutsComboboxBound = true;
-                selects[i].addEventListener('change', function () {
-                    autoPopulate_myFavouriteFruit(this);
-                });
-            }
-        }
-    })();
-</script>
     </td>
 </tr>
\ No newline at end of file
diff --git 
a/core/src/test/resources/org/apache/struts2/views/jsp/ui/ComboBox-4.txt 
b/core/src/test/resources/org/apache/struts2/views/jsp/ui/ComboBox-4.txt
index 71063a4ab..1e6830100 100644
--- a/core/src/test/resources/org/apache/struts2/views/jsp/ui/ComboBox-4.txt
+++ b/core/src/test/resources/org/apache/struts2/views/jsp/ui/ComboBox-4.txt
@@ -1,27 +1,10 @@
  <tr>
      <td class="tdLabel"><label for="cb[&#39;&quot;.&quot;&#39;]=bc(){};//" 
class="label">mylabel:</label></td>
      <td class="tdInput">
- <script>
-    function autoPopulate_cb__________bc_______(targetElement) {
-        
targetElement.form.elements['foo'].value=targetElement.options[targetElement.selectedIndex].value;
-    }
- </script>
+ <script src="/static/combobox.js"></script>
  <input type="text" name="foo" value="hello" 
id="cb[&#39;&quot;.&quot;&#39;]=bc(){};//"/><br/>
- <select data-struts-combobox="cb__________bc_______">
+ <select data-struts-combobox="cb__________bc_______" 
data-struts-combobox-target="foo">
      <option value="foo">foo</option>
  </select>
-<script>
-    (function () {
-        var selects = 
document.querySelectorAll('select[data-struts-combobox="cb__________bc_______"]');
-        for (var i = 0; i < selects.length; i++) {
-            if (!selects[i].strutsComboboxBound) {
-                selects[i].strutsComboboxBound = true;
-                selects[i].addEventListener('change', function () {
-                    autoPopulate_cb__________bc_______(this);
-                });
-            }
-        }
-    })();
-</script>
      </td>
  </tr>

Reply via email to