/**
* Copyright (C) 2010 Orbeon, Inc.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation; either version
* 2.1 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
*/
package org.orbeon.oxf.xforms.processor.handlers.xhtml;
import org.orbeon.dom.QName;
import org.orbeon.oxf.xforms.XFormsConstants;
import org.orbeon.oxf.xforms.XFormsContainingDocument;
import org.orbeon.oxf.xforms.XFormsUtils;
import org.orbeon.oxf.xforms.analysis.controls.SelectAppearanceTrait;
import org.orbeon.oxf.xforms.analysis.controls.SelectionControlTrait;
import org.orbeon.oxf.xforms.control.LHHAValue;
import org.orbeon.oxf.xforms.control.XFormsControl;
import org.orbeon.oxf.xforms.control.XFormsSingleNodeControl;
import org.orbeon.oxf.xforms.control.XFormsValueControl;
import org.orbeon.oxf.xforms.control.controls.XFormsSelect1Control;
import org.orbeon.oxf.xforms.control.controls.XFormsSelect1Control$;
import org.orbeon.oxf.xforms.itemset.Item;
import org.orbeon.oxf.xforms.itemset.Itemset;
import org.orbeon.oxf.xforms.itemset.ItemsetListener;
import org.orbeon.oxf.xforms.itemset.XFormsItemUtils;
import org.orbeon.oxf.xforms.processor.handlers.HandlerContext;
import org.orbeon.oxf.xml.*;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import scala.Option;
import scala.Tuple2;
import scala.collection.immutable.List$;
import java.util.Iterator;
import java.util.List;
/**
* Handle xf:select and xf:select1.
*
* TODO: Subclasses per appearance.
*/
public class XFormsSelect1Handler extends XFormsControlLifecyleHandler {
public XFormsSelect1Handler() {
super(false);
}
@Override
protected boolean isDefaultIncremental() {
// Incremental mode is the default
return true;
}
protected SelectAppearanceTrait getAppearanceTrait() {
if (elementAnalysis instanceof SelectAppearanceTrait) {
return (SelectAppearanceTrait) elementAnalysis;
} else {
return null;
}
}
protected void handleControlStart(
String uri,
String localname,
String qName,
Attributes attributes,
String effectiveId,
XFormsControl control
) throws SAXException {
// Get items, dynamic or static, if possible
final XFormsSelect1Control xformsSelect1Control = (XFormsSelect1Control) control;
// Get items if:
// 1. The itemset is static
// 2. The control exists and is relevant
final Itemset itemset = XFormsSelect1Control.getInitialItemset(containingDocument, xformsSelect1Control, getPrefixedId());
final SelectAppearanceTrait appearanceTrait = getAppearanceTrait();
outputContent(
uri,
localname,
attributes,
effectiveId,
xformsSelect1Control,
itemset,
appearanceTrait != null && appearanceTrait.isMultiple(),
appearanceTrait != null && appearanceTrait.isFull(),
false
);
}
public void outputContent(
String uri,
String localname,
Attributes attributes,
String effectiveId,
final XFormsValueControl control,
Itemset itemset,
final boolean isMultiple,
final boolean isFull,
boolean isBooleanInput
) throws SAXException {
final XMLReceiver xmlReceiver = handlerContext.getController().getOutput();
final XFormsControl xformsControl = (XFormsControl) control; // cast because Java is not aware that XFormsValueControl extends XFormsControl
final XFormsSingleNodeControl singleNodeControl = (XFormsSingleNodeControl) control; // same as above
final AttributesImpl containerAttributes = getEmptyNestedControlAttributesMaybeWithId(uri, localname, attributes, effectiveId, xformsControl, !isFull);
final String xhtmlPrefix = handlerContext.findXHTMLPrefix();
final SelectAppearanceTrait appearanceTrait = getAppearanceTrait();
final boolean isStaticReadonly = isStaticReadonly(xformsControl);
final boolean allowFullStaticReadonly =
isMultiple && containingDocument.isReadonlyAppearanceStaticSelectFull() ||
! isMultiple && containingDocument.isReadonlyAppearanceStaticSelect1Full();
final boolean mustOutputFull = isBooleanInput || (isFull && (allowFullStaticReadonly || ! isStaticReadonly));
final boolean encode =
(elementAnalysis instanceof SelectionControlTrait)
? XFormsSelect1Control$.MODULE$.mustEncodeValues(containingDocument, (SelectionControlTrait) elementAnalysis)
: false; // case of boolean input
if (mustOutputFull) {
// Full appearance, also in static readonly mode
outputFull(uri, localname, attributes, effectiveId, control, itemset, isMultiple, isBooleanInput, isStaticReadonly, encode);
} else if (! isStaticReadonly) {
// Create xhtml:select
final String selectQName = XMLUtils.buildQName(xhtmlPrefix, "select");
containerAttributes.addAttribute("", "name", "name", XMLReceiverHelper.CDATA, effectiveId);// necessary for noscript mode
if (appearanceTrait != null && appearanceTrait.isCompact())
containerAttributes.addAttribute("", "multiple", "multiple", XMLReceiverHelper.CDATA, "multiple");
// Handle accessibility attributes
handleAccessibilityAttributes(attributes, containerAttributes);
if (xformsControl != null)
xformsControl.addExtensionAttributesExceptClassAndAcceptForHandler
(containerAttributes, XFormsConstants.XXFORMS_NAMESPACE_URI);
if (isHTMLDisabled(xformsControl))
outputDisabledAttribute(containerAttributes);
if (singleNodeControl != null)
handleAriaAttributes(singleNodeControl.isRequired(), singleNodeControl.isValid(), containerAttributes);
xmlReceiver.startElement(XMLConstants.XHTML_NAMESPACE_URI, "select", selectQName, containerAttributes);
{
final String optionQName = XMLUtils.buildQName(xhtmlPrefix, "option");
final String optGroupQName = XMLUtils.buildQName(xhtmlPrefix, "optgroup");
if (itemset != null) {
itemset.visit(xmlReceiver, new ItemsetListener<ContentHandler>() {
private boolean inOptgroup = false; // nesting opgroups is not allowed, avoid it
private boolean gotSelected = false;
public void startLevel(ContentHandler contentHandler, Item item) {}
public void endLevel(ContentHandler contentHandler) throws SAXException {
if (inOptgroup) {
// End xhtml:optgroup
contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "optgroup", optGroupQName);
inOptgroup = false;
}
}
public void startItem(ContentHandler contentHandler, Item item, boolean first) throws SAXException {
assert !item.label().isHTML();
final String label = item.label().label();
final String value = item.value();
if (value == null) {
assert item.hasChildren();
final String itemClasses = getItemClasses(item, null);
final AttributesImpl optGroupAttributes = getIdClassXHTMLAttributes(SAXUtils.EMPTY_ATTRIBUTES, itemClasses, null);
if (label != null)
optGroupAttributes.addAttribute("", "label", "label", XMLReceiverHelper.CDATA, label);
// If another optgroup is open, close it - nested optgroups are not allowed. Of course this results in an
// incorrect structure for tree-like itemsets, there is no way around that. If the user however does
// the indentation himself, it will still look right.
if (inOptgroup)
contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "optgroup", optGroupQName);
// Start xhtml:optgroup
contentHandler.startElement(XMLConstants.XHTML_NAMESPACE_URI, "optgroup", optGroupQName, optGroupAttributes);
inOptgroup = true;
} else {
gotSelected |= handleItemCompact(contentHandler, optionQName, control, isMultiple, item, encode, gotSelected);
}
}
public void endItem(ContentHandler contentHandler, Item item) {}
});
}
}
xmlReceiver.endElement(XMLConstants.XHTML_NAMESPACE_URI, "select", selectQName);
} else {
// Output static read-only value
final String spanQName = XMLUtils.buildQName(xhtmlPrefix, "span");
containerAttributes.addAttribute("", "class", "class", "CDATA", "xforms-field");
xmlReceiver.startElement(XMLConstants.XHTML_NAMESPACE_URI, "span", spanQName, containerAttributes);
if (!handlerContext.isTemplate()) {
final String value = (control == null || control.getValue() == null) ? "" : control.getValue();
if (itemset != null) {
boolean selectedFound = false;
final XMLReceiverHelper ch = new XMLReceiverHelper(xmlReceiver);
for (final Item currentItem : itemset.jSelectedItems(value)) {
if (selectedFound)
ch.text(" - ");
currentItem.label().streamAsHTML(ch, xformsControl.getLocationData());
selectedFound = true;
}
}
}
xmlReceiver.endElement(XMLConstants.XHTML_NAMESPACE_URI, "span", spanQName);
}
}
private void outputFull(
String uri,
String localname,
Attributes attributes,
String effectiveId,
XFormsValueControl control,
Itemset itemset,
boolean isMultiple,
boolean isBooleanInput,
boolean isStaticReadonly,
boolean encode
) throws SAXException {
final XMLReceiver xmlReceiver = handlerContext.getController().getOutput();
final SelectAppearanceTrait appearanceTrait = getAppearanceTrait();
final XFormsControl xformsControl = (XFormsControl) control; // cast because Java is not aware that XFormsValueControl extends XFormsControl
final AttributesImpl containerAttributes =
getEmptyNestedControlAttributesMaybeWithId(
uri,
localname,
attributes,
effectiveId,
xformsControl,
!(appearanceTrait != null && appearanceTrait.isFull())
);
// To help with styling
containerAttributes.addAttribute("", "class", "class", XMLReceiverHelper.CDATA, "xforms-items");
// For accessibility, label the group, since the control label doesn't apply to a single input
final String role = isMultiple ? "group" : "radiogroup";
containerAttributes.addAttribute("", "role", "role", XMLReceiverHelper.CDATA, role);
final String labelId = getLHHACId(containingDocument, effectiveId, LHHAC_CODES.get(LHHAC.LABEL));
containerAttributes.addAttribute("", "aria-labelledby", "aria-labelledby", XMLReceiverHelper.CDATA, labelId);
final String xhtmlPrefix = handlerContext.findXHTMLPrefix();
final String fullItemType = isMultiple ? "checkbox" : "radio";
// In noscript mode, use <fieldset>
// TODO: This really hasn't much to do with noscript; should we always use fieldset, or make this an
// option? Benefit of limiting to noscript is that then no JS change is needed
final String containingElementName = handlerContext.isNoScript() ? "fieldset" : "span";
final String containingElementQName = XMLUtils.buildQName(xhtmlPrefix, containingElementName);
final String spanQName = XMLUtils.buildQName(xhtmlPrefix, "span");
{
// Output container <span>/<fieldset> for select/select1
final boolean outputContainerElement = !isBooleanInput;
if (outputContainerElement)
xmlReceiver.startElement(XMLConstants.XHTML_NAMESPACE_URI, containingElementName, containingElementQName, containerAttributes);
{
if (handlerContext.isNoScript()) {
// Output <legend>
final String legendName = "legend";
final String legendQName = XMLUtils.buildQName(xhtmlPrefix, legendName);
reusableAttributes.clear();
// TODO: handle other attributes? xforms-disabled?
reusableAttributes.addAttribute("", "class", "class", XMLReceiverHelper.CDATA, "xforms-label");
xmlReceiver.startElement(XMLConstants.XHTML_NAMESPACE_URI, legendName, legendQName, reusableAttributes);
if (control != null) {
final boolean mustOutputHTMLFragment = xformsControl.isHTMLLabel();
outputLabelText(xmlReceiver, xformsControl, xformsControl.getLabel(), xhtmlPrefix, mustOutputHTMLFragment);
}
xmlReceiver.endElement(XMLConstants.XHTML_NAMESPACE_URI, legendName, legendQName);
}
if (itemset != null) {
int itemIndex = 0;
for (Iterator<Item> i = itemset.jAllItemsIterator(); i.hasNext(); itemIndex++) {
handleItemFull(
this,
xmlReceiver,
reusableAttributes,
attributes,
xhtmlPrefix,
spanQName,
containingDocument,
control,
effectiveId,
getItemId(effectiveId, Integer.toString(itemIndex)),
isMultiple,
fullItemType,
i.next(),
itemIndex == 0,
isBooleanInput,
isStaticReadonly,
encode
);
}
}
}
if (outputContainerElement)
xmlReceiver.endElement(XMLConstants.XHTML_NAMESPACE_URI, containingElementName, containingElementQName);
}
// NOTE: Templates for full items are output globally in XHTMLBodyHandler
}
public static void outputItemFullTemplate(
XFormsBaseHandlerXHTML baseHandler,
ContentHandler contentHandler,
String xhtmlPrefix,
String spanQName,
XFormsContainingDocument containingDocument,
AttributesImpl reusableAttributes,
Attributes attributes,
String templateId,
String itemName,
boolean isMultiple,
String fullItemType
) throws SAXException {
reusableAttributes.clear();
// reusableAttributes.addAttribute("", "id", "id", ContentHandlerHelper.CDATA, XFormsUtils.namespaceId(handlerContext.containingDocument(), templateId));
// Client queries template by id without namespace, so output that. Not ideal as all ids should be namespaced.
reusableAttributes.addAttribute("", "id", "id", XMLReceiverHelper.CDATA, templateId);
reusableAttributes.addAttribute("", "class", "class", XMLReceiverHelper.CDATA, "xforms-template");
contentHandler.startElement(XMLConstants.XHTML_NAMESPACE_URI, "span", spanQName, reusableAttributes);
handleItemFull(
baseHandler,
contentHandler,
reusableAttributes,
attributes,
xhtmlPrefix,
spanQName,
containingDocument,
null,
itemName,
"$xforms-item-id-select" + (isMultiple? "" : "1") + "$", // create separate id for select/select1
isMultiple,
fullItemType,
Item.apply(
0,
isMultiple,
List$.MODULE$.<Tuple2<QName, String>>empty(), // make sure the value "$xforms-template-value$" is not encrypted
new LHHAValue("$xforms-template-label$", false),
Option.apply(new LHHAValue("$xforms-template-help$", false)),
Option.apply(new LHHAValue("$xforms-template-hint$", false)),
"$xforms-template-value$"
),
true,
false,
false,
false
);
contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "span", spanQName);
}
private void outputJSONTreeInfo(
XFormsValueControl control,
Itemset itemset,
boolean encode,
ContentHandler contentHandler
) throws SAXException {
if (control != null && ! handlerContext.isTemplate()) {
// Produce a JSON fragment with hierarchical information
final String result = itemset.asJSON(control.getValue(), encode, handlerContext.getLocationData());
contentHandler.characters(result.toCharArray(), 0, result.length());
} else {
// Don't produce any content when generating a template
}
}
public static void handleItemFull(
XFormsBaseHandlerXHTML baseHandler,
ContentHandler contentHandler,
AttributesImpl reusableAttributes,
Attributes attributes,
String xhtmlPrefix,
String spanQName,
XFormsContainingDocument containingDocument,
XFormsValueControl control,
String itemName,
String itemEffectiveId,
boolean isMultiple,
String type,
Item item,
boolean isFirst,
boolean isBooleanInput,
boolean isStaticReadonly,
boolean encode
) throws SAXException {
final HandlerContext handlerContext = baseHandler.getHandlerContext();
// Whether this is selected
boolean isSelected = isSelected(handlerContext, control, isMultiple, item);
// xhtml:span enclosing input and label
final String itemClasses = getItemClasses(item, isSelected ? "xforms-selected" : "xforms-deselected");
final AttributesImpl spanAttributes = getIdClassXHTMLAttributes(containingDocument, reusableAttributes, SAXUtils.EMPTY_ATTRIBUTES, itemClasses, null);
// Add item attributes to span
addItemAttributes(item, spanAttributes);
contentHandler.startElement(XMLConstants.XHTML_NAMESPACE_URI, "span", spanQName, spanAttributes);
{
final LHHAValue itemLabel = item.label();
final String itemNamespacedId = XFormsUtils.namespaceId(handlerContext.getContainingDocument(), itemEffectiveId);
final String labelName = ! isStaticReadonly ? "label" : "span";
if (! isBooleanInput) {
reusableAttributes.clear();
// Add Bootstrap classes
reusableAttributes.addAttribute("", "class", "class", XMLReceiverHelper.CDATA, isMultiple ? "checkbox" : "radio");
// No need for @for as the input, if any, is nested
outputLabelForStart(handlerContext, reusableAttributes, null, null, LHHAC.LABEL, labelName, false);
}
{
// xhtml:input
if (! isStaticReadonly) {
final String elementName = "input";
final String elementQName = XMLUtils.buildQName(xhtmlPrefix, elementName);
reusableAttributes.clear();
reusableAttributes.addAttribute("", "id", "id", XMLReceiverHelper.CDATA, itemNamespacedId);
reusableAttributes.addAttribute("", "type", "type", XMLReceiverHelper.CDATA, type);
// Get group name from selection control if possible, otherwise use effective id
final String name = (!isMultiple && control instanceof XFormsSelect1Control) ? ((XFormsSelect1Control) control).getGroupName() : itemName;
reusableAttributes.addAttribute("", "name", "name", XMLReceiverHelper.CDATA, name);
reusableAttributes.addAttribute("", "value", "value", XMLReceiverHelper.CDATA, item.externalValue(encode));
if (!handlerContext.isTemplate() && control != null) {
if (isSelected)
reusableAttributes.addAttribute("", "checked", "checked", XMLReceiverHelper.CDATA, "checked");
if (isFirst)
handleAccessibilityAttributes(attributes, reusableAttributes);
}
if (baseHandler.isHTMLDisabled((XFormsControl) control))// cast because Java is not aware that XFormsValueControl extends XFormsControl
outputDisabledAttribute(reusableAttributes);
contentHandler.startElement(XMLConstants.XHTML_NAMESPACE_URI, elementName, elementQName, reusableAttributes);
contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, elementName, elementQName);
}
if (! isBooleanInput) {
// <span class="xforms-hint-region"> or plain <span>
reusableAttributes.clear();
if (item.hint().isDefined())
reusableAttributes.addAttribute("", "class", "class", XMLReceiverHelper.CDATA, "xforms-hint-region");
contentHandler.startElement(XMLConstants.XHTML_NAMESPACE_URI, "span", spanQName, reusableAttributes);
outputLabelText(handlerContext.getController().getOutput(), null, itemLabel.label(), xhtmlPrefix, itemLabel.isHTML());
contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "span", spanQName);
// <span class="xforms-help">
{
final Option<LHHAValue> helpOpt = item.help();
if (helpOpt.isDefined()) {
final LHHAValue help = helpOpt.get();
reusableAttributes.clear();
reusableAttributes.addAttribute("", "class", "class", XMLReceiverHelper.CDATA, "xforms-help");
outputLabelFor(handlerContext, reusableAttributes, null, null, LHHAC.HELP, "span", help.label(), help.isHTML(), false);
}
}
// <span class="xforms-hint">
{
final Option<LHHAValue> hintOpt = item.hint();
if (hintOpt.isDefined()) {
final LHHAValue hint = hintOpt.get();
reusableAttributes.clear();
reusableAttributes.addAttribute("", "class", "class", XMLReceiverHelper.CDATA, "xforms-hint");
outputLabelFor(handlerContext, reusableAttributes, null, null, LHHAC.HINT, "span", hint.label(), hint.isHTML(), false);
}
}
}
}
if (! isBooleanInput)
outputLabelForEnd(handlerContext, labelName);
}
contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "span", spanQName);
}
private static boolean isSelected(HandlerContext handlerContext, XFormsValueControl xformsControl, boolean isMultiple, Item item) {
boolean isSelected;
if (!handlerContext.isTemplate() && xformsControl != null) {
final String itemValue = (item.value() == null) ? "" : item.value();
final String controlValue = (xformsControl.getValue() == null) ? "" : xformsControl.getValue();
isSelected = XFormsItemUtils.isSelected(isMultiple, controlValue, itemValue);
} else {
isSelected = false;
}
return isSelected;
}
private boolean handleItemCompact(
ContentHandler contentHandler,
String optionQName,
XFormsValueControl xformsControl,
boolean isMultiple,
Item item,
boolean encode,
boolean gotSelected
) throws SAXException {
final String itemClasses = getItemClasses(item, null);
final AttributesImpl optionAttributes = getIdClassXHTMLAttributes(SAXUtils.EMPTY_ATTRIBUTES, itemClasses, null);
// Add item attributes to option
addItemAttributes(item, optionAttributes);
optionAttributes.addAttribute("", "value", "value", XMLReceiverHelper.CDATA, item.externalValue(encode));
// Figure out whether what items are selected
// Don't output more than one `selected` in the case of single-selection, see:
// https://github.com/orbeon/orbeon-forms/issues/2901
boolean mustSelect = (isMultiple || ! gotSelected) && isSelected(handlerContext, xformsControl, isMultiple, item);
if (mustSelect)
optionAttributes.addAttribute("", "selected", "selected", XMLReceiverHelper.CDATA, "selected");
// xhtml:option
contentHandler.startElement(XMLConstants.XHTML_NAMESPACE_URI, "option", optionQName, optionAttributes);
assert !item.label().isHTML();
final String label = item.label().label();
if (label != null)
contentHandler.characters(label.toCharArray(), 0, label.length());
contentHandler.endElement(XMLConstants.XHTML_NAMESPACE_URI, "option", optionQName);
return mustSelect;
}
private static void addItemAttributes(Item item, AttributesImpl spanAttributes) {
final List<Tuple2<QName, String>> itemAttributes = item.jAttributes();
if (! itemAttributes.isEmpty()) {
for (final Tuple2<QName, String> nameValue: itemAttributes) {
final QName attributeQName = nameValue._1();
if (!attributeQName.equals(XFormsConstants.CLASS_QNAME)) { // class is handled separately
final String attributeName = Itemset.getAttributeName(attributeQName);
spanAttributes.addAttribute("", attributeName, attributeName, XMLReceiverHelper.CDATA, nameValue._2());
}
}
}
}
private static String getItemClasses(Item item, String initialClasses) {
final Option<String> classOpt = item.classAttribute();
final StringBuilder sb = (initialClasses != null) ? new StringBuilder(initialClasses) : new StringBuilder();
if (classOpt.isDefined()) {
if (sb.length() > 0)
sb.append(' ');
sb.append(classOpt.get());
}
return sb.toString();
}
public static String getItemId(String effectiveId, String itemIndex) {
return XFormsUtils.appendToEffectiveId(
effectiveId,
"" + XFormsConstants.COMPONENT_SEPARATOR + XFormsConstants.COMPONENT_SEPARATOR + "e" + itemIndex
);
}
@Override
public String getForEffectiveId(String effectiveId) {
// For full appearance we don't put a @for attribute so that selecting the main label doesn't select the item
final SelectAppearanceTrait appearanceTrait = getAppearanceTrait();
return appearanceTrait != null && appearanceTrait.isFull() ? null : super.getForEffectiveId(effectiveId);
}
@Override
protected void handleLabel() throws SAXException {
final SelectAppearanceTrait appearanceTrait = getAppearanceTrait();
final boolean isFull = appearanceTrait != null && appearanceTrait.isFull();
if (!isStaticReadonly(currentControlOrNull()) && handlerContext.isNoScript() && isFull) {
// NOP: in noscript mode for full items, this is handled by fieldset/legend
} else if (isFull) {
// For radio and checkboxes, produce span with an id
handleLabelHintHelpAlert(
getStaticLHHA(getPrefixedId(), LHHAC.LABEL),
getEffectiveId(),
null,
LHHAC.LABEL,
"span", // Make element name a span, as a label would need a `for`
currentControlOrNull(),
isTemplate(),
true // Pretend we're "external", so the element gets an id
);
} else {
super.handleLabel();
}
}
}