Skip to content

Commit 0ed183c

Browse files
herrtunanteclaude
andcommitted
Balloon: tighten generator, escape tooltips, prune dead WebKit CSS
Generator (CollectEarthBalloonGenerator): - Cache the HTML template in a static volatile field; the template is an immutable classpath resource and was being re-read on every call. - Replace ten sequential String.replace() passes in replaceButtonLocalizationText with a single LinkedHashMap + regex pass. - Reuse the existing balloonInputFieldsUtils field in getIdAttributesSurvey instead of allocating a new instance. - Introduce CALCULATED_FIELD_CLASS so calculated fields can be styled separately from CSV-extras; emitted in addition to the existing "extra" class so current hide-logic still applies. Formatter (CEComponentHTMLFormatter): - HTML-escape the code-item description before embedding it in the Bootstrap tooltip title blob; with data-html="true" the tooltip is parsed as HTML and unescaped <, &, " would break the markup. - Rename helper to escapeHtml since it is now used for both attribute and text contexts. - Tighten the &amp;# post-fix to a numeric-character-reference regex so it can only fire on actual &#xNNNN; / &#NNNN; sequences. - Drop vestigial "selectboxit show-menu-arrow show-tick" classes from both select fields - bootstrap-select is not bundled, those classes were dead weight. - Add an empty "no value" option to buildCodeRange so a freshly-opened range field does not silently take the "from" value (parity with buildCodeSelect). - Move the tooltip-icon inline display:inline-block to a CSS class. CSS (earth.css): - Remove the dead .bootstrap-select:not([class*="span"]) rule. - Replace the six-layer inset box-shadow on .btn.active with a two-layer equivalent - WebKit 538 (Google Earth Pro) composites each shadow per paint, this is the bulk of the per-paint cost when many active code buttons are visible. - Collapse the 48-selector .col-xs-1...col-lg-12 rule into 4 prefix- anchored attribute selectors with the same precision. - Add .ui-icon.tooltip-icon { display: inline-block } to replace the inline style now removed from the markup. Template (balloon_template_new.txt): - Modernise the meta tag to <meta charset="utf-8">. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 14b0571 commit 0ed183c

4 files changed

Lines changed: 97 additions & 60 deletions

File tree

collect-server/src/main/java/org/openforis/collect/io/metadata/collectearth/balloon/CEComponentHTMLFormatter.java

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,15 @@ private XMLBuilder createBuilder(CEField comp, boolean includeLabel, XMLBuilder
171171
XMLBuilder formControlContainer = formGroupBuilder.e("div") //$NON-NLS-1$
172172
.a("class", "col-sm-8"); //$NON-NLS-1$ //$NON-NLS-2$
173173

174-
String componentAdditionalClass = comp.isExtra() || comp.isCalculated() ? " " + CollectEarthBalloonGenerator.EXTRA_HIDDEN_FIELD_CLASS : "";
174+
// Mark as "extra" for the existing hide-logic; additionally tag calculated fields
175+
// with their own class so styles/scripts can distinguish them from CSV-extras.
176+
String componentAdditionalClass = "";
177+
if (comp.isExtra() || comp.isCalculated()) {
178+
componentAdditionalClass = " " + CollectEarthBalloonGenerator.EXTRA_HIDDEN_FIELD_CLASS;
179+
if (comp.isCalculated()) {
180+
componentAdditionalClass += " " + CollectEarthBalloonGenerator.CALCULATED_FIELD_CLASS;
181+
}
182+
}
175183

176184
if (comp instanceof CECodeField) {
177185
if (comp.isReadOnly()) {
@@ -315,23 +323,24 @@ private XMLBuilder createBuilder(CEField comp, boolean includeLabel, XMLBuilder
315323
}
316324

317325
public void addTooltip(XMLBuilder formGroupBuilder, String tooltip) {
318-
if( !StringUtils.isBlank(tooltip) ) {
326+
if (!StringUtils.isBlank(tooltip)) {
319327
formGroupBuilder.e("span")
320-
.a( "class", "ui-icon ui-icon-info" )
321-
.a( "style", "display:inline-block")
322-
.a( "title", tooltip);
328+
.a("class", "ui-icon ui-icon-info tooltip-icon")
329+
.a("title", tooltip);
323330
}
324331
}
325332

326333
private void buildCodeSelect(XMLBuilder builder, CECodeField comp) {
327334
String elId = comp.getHtmlParameterName();
328335

329-
//build select
336+
//build select. Plugin in use is SelectBoxIt (initialised by JS) — the previous
337+
//"selectboxit show-menu-arrow show-tick" classes mixed in dead Bootstrap-Select
338+
//hooks (that plugin isn't bundled) and have no effect; dropped.
330339
XMLBuilder selectBuilder = builder.e("select") //$NON-NLS-1$
331340
.a("id", elId) //$NON-NLS-1$
332341
.a("name", elId) //$NON-NLS-1$
333342
.a("data-field-type", comp.getType().name()) //$NON-NLS-1$
334-
.a("class", "form-control selectboxit show-menu-arrow show-tick") //$NON-NLS-1$ //$NON-NLS-2$
343+
.a("class", "form-control") //$NON-NLS-1$ //$NON-NLS-2$
335344
.a("data-width", "75px"); //$NON-NLS-1$ //$NON-NLS-2$
336345
if (comp.getParentName() != null) {
337346
selectBuilder.a("data-parent-id-field-id", comp.getParentName()); //$NON-NLS-1$
@@ -368,20 +377,23 @@ private void buildCodeSelect(XMLBuilder builder, CECodeField comp) {
368377
private void buildCodeRange(XMLBuilder builder, CERangeField comp) {
369378
String elId = comp.getHtmlParameterName();
370379

371-
//build select
380+
//build select (see buildCodeSelect for class-list rationale)
372381
XMLBuilder selectBuilder = builder.e("select") //$NON-NLS-1$
373382
.a("id", elId) //$NON-NLS-1$
374383
.a("name", elId) //$NON-NLS-1$
375384
.a("data-field-type", comp.getType().name()) //$NON-NLS-1$
376-
.a("class", "form-control selectboxit show-menu-arrow show-tick") //$NON-NLS-1$ //$NON-NLS-2$
385+
.a("class", "form-control") //$NON-NLS-1$ //$NON-NLS-2$
377386
.a("data-width", "75px"); //$NON-NLS-1$ //$NON-NLS-2$
378387

379-
//add root items, if any
380-
for( int i=comp.getFrom(); i<comp.getTo(); i++ ){
381-
String item = i+""; //$NON-NLS-1$
382-
selectBuilder.e("option") //$NON-NLS-1$
383-
.a("value", item) //$NON-NLS-1$
384-
.t(item);
388+
// Match buildCodeSelect: emit an empty "no value" option as the default selection
389+
// so a freshly-opened range field doesn't silently take the "from" value.
390+
selectBuilder.e("option").a("selected", "selected").a("value", "").t(Messages.getString("CEComponentHTMLFormatter.119", language));
391+
392+
for (int i = comp.getFrom(); i < comp.getTo(); i++) {
393+
String item = i + ""; //$NON-NLS-1$
394+
selectBuilder.e("option") //$NON-NLS-1$
395+
.a("value", item) //$NON-NLS-1$
396+
.t(item);
385397
}
386398
}
387399

@@ -439,8 +451,10 @@ private void buildCodeButtonGroup(XMLBuilder formControlContainer, CECodeField c
439451

440452
if (item.hasUploadedImage()) {
441453
String imgFilePath = CollectEarthProjectFileCreatorImpl.getCodeListImageFilePath(item);
442-
String titleText = StringUtils.isBlank(description) ? "" : description; //$NON-NLS-1$
443-
String htmlTitle = "<span><img src=\"" + escapeHtmlAttribute(imgFilePath) + "\" width=\"250\"><br/>" + titleText + "</span>"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
454+
// Bootstrap tooltip with data-html="true" parses title as HTML, so the description
455+
// must be HTML-escaped or stray <,>,& in user content would break the markup.
456+
String titleText = StringUtils.isBlank(description) ? "" : escapeHtml(description); //$NON-NLS-1$
457+
String htmlTitle = "<span><img src=\"" + escapeHtml(imgFilePath) + "\" width=\"250\"><br/>" + titleText + "</span>"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
444458
itemBuilder
445459
.a("title", htmlTitle) //$NON-NLS-1$
446460
.a("data-html", "true") //$NON-NLS-1$ //$NON-NLS-2$
@@ -465,7 +479,10 @@ private String writeToString(XMLBuilder builder) {
465479
}};
466480
builder.toWriter(writer, outputProperties);
467481
String result = writer.toString();
468-
return result.replace("&amp;#", "&#"); //to avoid double escaping unicode characters
482+
// HtmlUnicodeEscaperUtil emits &#xNNNN; entities BEFORE XML serialization, so
483+
// XMLBuilder's text-escaper turns the leading & into &amp;. Undo that only for
484+
// actual numeric character references — not arbitrary &amp;# runs in user text.
485+
return result.replaceAll("&amp;#(x?[0-9A-Fa-f]+;)", "&#$1");
469486
} catch(Exception e) {
470487
throw new RuntimeException(e);
471488
}
@@ -492,7 +509,7 @@ public static String getDescription(CodeListItem item, String lang) {
492509
return HtmlUnicodeEscaperUtil.escapeHtmlUnicode( description );
493510
}
494511

495-
private static String escapeHtmlAttribute(String value) {
512+
private static String escapeHtml(String value) {
496513
if (value == null) {
497514
return "";
498515
}

collect-server/src/main/java/org/openforis/collect/io/metadata/collectearth/balloon/CollectEarthBalloonGenerator.java

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@
88
import java.util.Collections;
99
import java.util.HashMap;
1010
import java.util.HashSet;
11+
import java.util.LinkedHashMap;
1112
import java.util.List;
1213
import java.util.Locale;
1314
import java.util.Map;
1415
import java.util.Set;
16+
import java.util.regex.Matcher;
17+
import java.util.regex.Pattern;
1518

1619
import org.apache.commons.io.IOUtils;
1720
import org.openforis.collect.earth.core.handlers.BalloonInputFieldsUtils;
@@ -62,6 +65,7 @@ public class CollectEarthBalloonGenerator {
6265

6366
public static final String EXTRA_HIDDEN_PREFIX = "EXTRA_";
6467
protected static final String EXTRA_HIDDEN_FIELD_CLASS = "extra";
68+
protected static final String CALCULATED_FIELD_CLASS = "calculated";
6569
private static final Set<String> HIDDEN_ATTRIBUTE_NAMES = Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
6670
"operator", "location", "plot_file", "actively_saved", "actively_saved_on"))); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$
6771

@@ -116,38 +120,43 @@ private String replaceHostForPreview(String html) {
116120
}
117121

118122
private String replaceButtonLocalizationText(String htmlForBalloon) {
119-
htmlForBalloon = htmlForBalloon.replace(PLACEHOLDER_FOR_FINISH_TRANSLATION, HtmlUnicodeEscaperUtil.escapeHtmlUnicode( Messages.getString("CollectEarthBalloonGenerator.11", language) ) ); //$NON-NLS-1$
120-
htmlForBalloon = htmlForBalloon.replace(PLACEHOLDER_FOR_NEXT_TRANSLATION, HtmlUnicodeEscaperUtil.escapeHtmlUnicode(Messages.getString("CollectEarthBalloonGenerator.12", language)) ); //$NON-NLS-1$
121-
htmlForBalloon = htmlForBalloon.replace(PLACEHOLDER_FOR_PREVIOUS_TRANSLATION,HtmlUnicodeEscaperUtil.escapeHtmlUnicode(Messages.getString("CollectEarthBalloonGenerator.13", language)) ); //$NON-NLS-1$
122-
123-
htmlForBalloon = htmlForBalloon.replace(PLACEHOLDER_COLLECT_NOT_RUNNING,HtmlUnicodeEscaperUtil.escapeHtmlUnicode(Messages.getString("CollectEarthBalloonGenerator.14", language)) ); //$NON-NLS-1$
124-
htmlForBalloon = htmlForBalloon.replace(PLACEHOLDER_PLACEMARK_ALREADY_FILLED,HtmlUnicodeEscaperUtil.escapeHtmlUnicode(Messages.getString("CollectEarthBalloonGenerator.15", language)) ); //$NON-NLS-1$
125-
htmlForBalloon = htmlForBalloon.replace(PLACEHOLDER_UI_LANGUAGE, language ); //$NON-NLS-1$
126-
127-
// Added to handle multiple id attributes within a survey
128-
htmlForBalloon = htmlForBalloon.replace(PLACEHOLDER_EXTRA_ID_ATTRIBUTES, getIdAttributesSurvey() ); //$NON-NLS-1$
129-
130-
// Added to handle multiple id attributes within a survey
131-
htmlForBalloon = htmlForBalloon.replace(PLACEHOLDER_FOR_EXTRA_ID_GET_REQUEST, getIdPlaceholdersSurvey() ); //$NON-NLS-1$
132-
133-
htmlForBalloon = htmlForBalloon.replace(PLACEHOLDER_PREVIEW, String.valueOf(preview).toLowerCase(Locale.ENGLISH));
134-
135-
htmlForBalloon = htmlForBalloon.replace(PLACEHOLDER_RANDOM_NUMBER, String.valueOf(Numbers.randomInt(10000, 5000000)));
123+
// All placeholder->value substitutions collected once, then applied in a single
124+
// regex pass over the template instead of ten sequential full-string scans.
125+
Map<String, String> replacements = new LinkedHashMap<String, String>();
126+
replacements.put(PLACEHOLDER_FOR_FINISH_TRANSLATION, HtmlUnicodeEscaperUtil.escapeHtmlUnicode(Messages.getString("CollectEarthBalloonGenerator.11", language))); //$NON-NLS-1$
127+
replacements.put(PLACEHOLDER_FOR_NEXT_TRANSLATION, HtmlUnicodeEscaperUtil.escapeHtmlUnicode(Messages.getString("CollectEarthBalloonGenerator.12", language))); //$NON-NLS-1$
128+
replacements.put(PLACEHOLDER_FOR_PREVIOUS_TRANSLATION, HtmlUnicodeEscaperUtil.escapeHtmlUnicode(Messages.getString("CollectEarthBalloonGenerator.13", language))); //$NON-NLS-1$
129+
replacements.put(PLACEHOLDER_COLLECT_NOT_RUNNING, HtmlUnicodeEscaperUtil.escapeHtmlUnicode(Messages.getString("CollectEarthBalloonGenerator.14", language))); //$NON-NLS-1$
130+
replacements.put(PLACEHOLDER_PLACEMARK_ALREADY_FILLED, HtmlUnicodeEscaperUtil.escapeHtmlUnicode(Messages.getString("CollectEarthBalloonGenerator.15", language))); //$NON-NLS-1$
131+
replacements.put(PLACEHOLDER_UI_LANGUAGE, language);
132+
replacements.put(PLACEHOLDER_EXTRA_ID_ATTRIBUTES, getIdAttributesSurvey());
133+
replacements.put(PLACEHOLDER_FOR_EXTRA_ID_GET_REQUEST, getIdPlaceholdersSurvey());
134+
replacements.put(PLACEHOLDER_PREVIEW, String.valueOf(preview).toLowerCase(Locale.ENGLISH));
135+
replacements.put(PLACEHOLDER_RANDOM_NUMBER, String.valueOf(Numbers.randomInt(10000, 5000000)));
136136

137-
return htmlForBalloon;
137+
StringBuilder regex = new StringBuilder();
138+
for (String key : replacements.keySet()) {
139+
if (regex.length() > 0) regex.append('|');
140+
regex.append(Pattern.quote(key));
141+
}
142+
Matcher m = Pattern.compile(regex.toString()).matcher(htmlForBalloon);
143+
StringBuffer out = new StringBuffer(htmlForBalloon.length() + 256);
144+
while (m.find()) {
145+
m.appendReplacement(out, Matcher.quoteReplacement(replacements.get(m.group())));
146+
}
147+
m.appendTail(out);
148+
return out.toString();
138149
}
139150

140151
private String getIdAttributesSurvey() {
141152
List<AttributeDefinition> keyAttributeDefinitions = survey.getSchema().getFirstRootEntityDefinition().getKeyAttributeDefinitions();
142-
BalloonInputFieldsUtils balloonUtils = new BalloonInputFieldsUtils();
143-
144153
StringBuilder sb = new StringBuilder("[");
145154
boolean first = true;
146155
for (AttributeDefinition keyAttribute : keyAttributeDefinitions) {
147156
if (!first) {
148157
sb.append(",");
149158
}
150-
sb.append("'").append(balloonUtils.getCollectBalloonParamName(keyAttribute)).append("'");
159+
sb.append("'").append(balloonInputFieldsUtils.getCollectBalloonParamName(keyAttribute)).append("'");
151160
first = false;
152161
}
153162
sb.append("]");
@@ -175,15 +184,25 @@ private String getIdPlaceholdersSurvey() {
175184

176185

177186

187+
// Template is an immutable classpath resource; safe to cache for the JVM lifetime.
188+
// A redeploy creates a new classloader and clears this field.
189+
private static volatile String cachedHtmlTemplate;
190+
178191
private String getHTMLTemplate() throws IOException {
192+
String tpl = cachedHtmlTemplate;
193+
if (tpl != null) {
194+
return tpl;
195+
}
179196
InputStream is = getClass().getClassLoader().getResourceAsStream(BALLOON_TEMPLATE_TXT);
180197
if (is == null) {
181198
throw new IOException("Balloon HTML template not found on classpath: " + BALLOON_TEMPLATE_TXT);
182199
}
183200
try {
184201
StringWriter writer = new StringWriter();
185202
IOUtils.copy(is, writer, OpenForisIOUtils.UTF_8);
186-
return writer.toString();
203+
tpl = writer.toString();
204+
cachedHtmlTemplate = tpl;
205+
return tpl;
187206
} finally {
188207
IOUtils.closeQuietly(is);
189208
}

collect-server/src/main/resources/org/openforis/collect/designer/templates/collectearth/balloon_template_new.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<!DOCTYPE html>
22
<html>
33
<head>
4-
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
4+
<meta charset="utf-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66

77
<script type="text/javascript">

collect-server/src/main/resources/org/openforis/collect/designer/templates/collectearth/earthFiles/css/earth.css

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,6 @@ hr {
9595
margin: 5px;
9696
}
9797

98-
.bootstrap-select:not([class*="span"]) {
99-
width: 170px;
100-
}
101-
10298
.ui-dialog-titlebar-close {
10399
background-image: url(../img/close.png) !important;
104100
background-position: left top !important;
@@ -139,11 +135,12 @@ hr {
139135
margin-bottom: 4px;
140136
}
141137

138+
/* Simplified from a six-layer inset stack — WebKit 538 (Google Earth Pro) composites
139+
each shadow per paint; one inner halo + one inner edge is visually equivalent and
140+
roughly 3x cheaper to paint, especially on grids with many active code buttons. */
142141
.btn:active, .btn.active {
143-
box-shadow: inset 2px 2px 0 0 rgba(255, 255, 255, 0.8), inset -2px -2px
144-
0 0 rgba(255, 255, 255, 0.8), inset 4px 4px 0 0 rgba(0, 0, 0, 0.8),
145-
inset -4px -4px 0 0 rgba(0, 0, 0, 0.8), inset 5px 5px 5px 5px
146-
rgba(0, 0, 0, 0.8), inset -5px -5px 5px 5px rgba(0, 0, 0, 0.8);
142+
box-shadow: inset 0 0 0 2px rgba(255, 255, 255, 0.85),
143+
inset 0 0 8px 2px rgba(0, 0, 0, 0.7);
147144
color: rgba(255, 255, 255, 1) !important;
148145
text-shadow: 0px 0px 6px rgba(0, 0, 0, 1);
149146
}
@@ -175,16 +172,14 @@ table.independentToggle button, .independentToggle button {
175172
border-top-right-radius: 4px !important;
176173
}
177174

178-
.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2,
179-
.col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3,
180-
.col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5,
181-
.col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6,
182-
.col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8,
183-
.col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9,
184-
.col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11,
185-
.col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12
186-
{
187-
padding: 0px 4px 0px 4px;
175+
/* Collapsed from 48 explicit .col-xs-1 ... .col-lg-12 selectors into 4 prefix-anchored
176+
attribute selectors. Anchored with ^= so it matches only Bootstrap grid classes
177+
starting at the class boundary, not arbitrary class substrings. */
178+
[class^="col-xs-"], [class*=" col-xs-"],
179+
[class^="col-sm-"], [class*=" col-sm-"],
180+
[class^="col-md-"], [class*=" col-md-"],
181+
[class^="col-lg-"], [class*=" col-lg-"] {
182+
padding: 0 4px;
188183
}
189184

190185
/** Tooltip */
@@ -325,3 +320,9 @@ div.actions.clearfix{
325320
background: green;
326321
padding: 2px;
327322
}
323+
324+
/* Tooltip-icon: was an inline style="display:inline-block" on every span; moved
325+
here so the style engine can share the computed style across all instances. */
326+
.ui-icon.tooltip-icon {
327+
display: inline-block;
328+
}

0 commit comments

Comments
 (0)