org.broadleafcommerce.openadmin.public.sc.client.widgets.form.FilterBuilder.js Maven / Gradle / Ivy
Show all versions of broadleaf-open-admin-platform Show documentation
/*
* Isomorphic SmartClient
* Version SC_SNAPSHOT-2011-01-06 (2011-01-06)
* Copyright(c) 1998 and beyond Isomorphic Software, Inc. All rights reserved.
* "SmartClient" is a trademark of Isomorphic Software, Inc.
*
* [email protected]
*
* http://smartclient.com/license
*/
// FilterBuilder requires DynamicForm but is loaded in a separate module.
// Ensure DF is present before attempting to initialize
if (isc.DynamicForm) {
// We need a new class for the clause forms that make up a FilterBuilder because we need to
// override a DynamicForm static method (there are instance overrides as well that we were
// previously handling with a defaults block)
isc.defineClass("DynamicFilterForm", "DynamicForm").addClassMethods({
canEditField : function (field, widget) {
// Whether an item is editable in a FilterBuilder is determined by canFilter, not canEdit
return (field.canFilter != false);
}
});
isc.DynamicFilterForm.addProperties({
_$Enter:"Enter",
handleKeyPress : function (event, eventInfo) {
// We need to suppress normal DynamicForm saveOnEnter behavior; we also need to let
// the FilterBuilder that will eventually see this event know whether or not the field
// triggering it was a TextItem
var item = this.getFocusSubItem();
if (isc.isA.TextItem(item)) eventInfo.firedOnTextItem = true;
// But we need normal key handling for everything except Enter!
if (event.keyName != this._$Enter) {
return this.Super("handleKeyPress", [event, eventInfo]);
}
},
itemChanged : function (item, newValue, oldValue) {
if (this.creator.itemChanged) this.creator.itemChanged();
}
});
//> @class FilterClause
// An HStack-based widget that allows a user to input a single criterion based on one field and
// one operator.
//
//
// @treeLocation Client Reference/Forms
// @visibility external
//<
isc.defineClass("FilterClause", "HStack").addProperties({
height: 20,
//> @attr filterClause.criterion (Criteria : null : IRW)
// Initial criterion for this FilterClause.
//
// When initialized with a criterion, the clause will be automatically set up for editing
// the supplied criterion.
//
// Note that an empty or partial criterion is allowed, for example, it may specify
// +link{criterion.fieldName} only and will generate an expression with the operator not chosen.
// @visibility external
//<
//> @attr filterClause.showFieldTitles (boolean : true : IR)
// If true (the default), show field titles in the drop-down box used to select a field for querying.
// If false, show actual field names instead.
// @visibility external
//<
showFieldTitles: true,
//> @attr filterClause.validateOnChange (boolean : true : IR)
// If true (the default), validates the entered value when it changes, to make sure it is a
// a valid value of its type (valid string, number, and so on). No other validation is
// carried out. If you switch this property off, it is still possible to validate the
// FilterClause
by calling +link{filterClause.validate()} from your own code.
// @visibility external
//<
validateOnChange: true,
// Clause creation
// ---------------------------------------------------------------------------------------
fieldPickerWidth: 150,
operatorPickerWidth: 150,
valueItemWidth: 150,
fieldPickerDefaults: {
type: "SelectItem",
name: "fieldName",
showTitle: false,
textMatchStyle: "startsWith",
changed : function () { this.form.creator.fieldNameChanged(this.form); }
},
operatorPickerDefaults : {
// list of operators
name:"operator",
type:"select",
showTitle:false,
addUnknownValues:false,
defaultToFirstOption:true,
changed : function () { this.form.creator.operatorChanged(this.form); }
},
//> @attr filterClause.clause (AutoChild : null : IR)
// AutoChild containing the UI for the filter-properties in this FilterClause.
// @visibility external
//<
clauseConstructor: isc.DynamicFilterForm,
//> @attr filterClause.showRemoveButton (boolean : true : IR)
// If set, show a button for this clause allowing it to be removed.
// @visibility external
//<
showRemoveButton:true,
//> @attr filterClause.removeButtonPrompt (string : "Remove" : IR)
// The hover prompt text for the remove button.
//
// @group i18nMessages
// @visibility external
//<
removeButtonPrompt: "Remove",
// set this flag to prevent non-filterable fields from being excluded - such exclusion makes
// sense in a FilterBuilder, but we don't want it when we're using a FilterClause widget simply
// as a UI - for instance, from the HiliteEditor.
excludeNonFilterableFields: true,
//> @attr filterClause.removeButton (AutoChild : null : IR)
// The clause removal ImgButton that appears before this clause if
// +link{showRemoveButton} is set.
// @visibility external
//<
removeButtonDefaults : {
_constructor:isc.ImgButton,
width:18, height:18, layoutAlign:"center",
src:"[SKIN]/actions/remove.png",
showRollOver:false, showDown:false,
showDisabled:false, // XXX
click: function () { this.creator.remove(); }
},
flattenItems: true
});
isc.FilterClause.addMethods({
getPrimaryDS : function () {
if (this.dataSource) return this.getDataSource();
else if (this.fieldDataSource) return this.fieldDataSource;
},
initWidget : function () {
if (this.dataSource && !isc.isA.DataSource(this.dataSource))
this.dataSource = isc.DataSource.get(this.dataSource);
if (this.fieldDataSource && !isc.isA.DataSource(this.fieldDataSource))
this.fieldDataSource = isc.DataSource.get(this.fieldDataSource);
this.setupClause();
},
getField : function (fieldName) {
var field;
if (this.dataSource) {
field = this.getDataSource().getField(fieldName);
} else {
if (this.clause) {
field = this.fieldData ? this.fieldData[fieldName] : null;
if (!field) field = this.clause.getField("fieldName").getSelectedRecord();
if (!field) field = this.field;
else this.field = field;
}
}
return field;
},
getFieldNames : function () {
if (this.dataSource) return this.getDataSource().getFieldNames(true);
},
getFieldOperatorMap : function (field, includeHidden, valueType, omitValueType) {
return this.getPrimaryDS().getFieldOperatorMap(field, includeHidden, valueType, omitValueType);
},
getSearchOperator : function (operatorName) {
return this.getPrimaryDS().getSearchOperator(operatorName);
},
combineFieldData : function (field, targetField) {
var ds = this.getPrimaryDS(),
dsField = ds.getField(targetField);
if (dsField)
return ds.combineFieldData(field, targetField);
else return field;
},
setupClause : function () {
if (this.showRemoveButton) this.addAutoChild("removeButton");
var fieldMap = {};
if (this.showClause != false) {
var items = [
isc.addProperties(isc.clone(this.fieldPickerDefaults),
{ width: this.fieldPickerWidth}, this.fieldPickerProperties,
{name:"fieldName"}
),
isc.addProperties(isc.clone(this.operatorPickerDefaults),
{ width: this.operatorPickerWidth }, this.operatorPickerProperties,
{ name:"operator" }
)
],
criterion = this.criterion,
fieldNames = this.getFieldNames(),
selectedFieldName
;
if (this.fieldName && this.dataSource) {
// fieldName provided - change the type of the first DF field and set it's value -
// this behavior is only supported when this.dataSource is present
var specificFieldName = this.fieldName;
var field = this.getField(specificFieldName),
fieldTitle
;
isc.addProperties(items[0], { type: "staticText", clipValue: true, wrap: false });
if (!field || (this.excludeNonFilterableFields && field.canFilter == false)) specificFieldName = fieldNames[0];
else if (this.showFieldTitles) {
fieldTitle = field.title ? field.title : specificFieldName;
}
items[0].defaultValue = fieldTitle || specificFieldName;
selectedFieldName = specificFieldName;
} else {
if (this.fieldDataSource) {
// change the fieldPicker to be a ComboBoxItem, setup the fieldDataSource as
// it's optionDataSource and provide type-ahead auto-completion
isc.addProperties(items[0], {
type: "ComboBoxItem",
completeOnTab: true,
optionDataSource: this.fieldDataSource,
valueField: "name",
displayField: this.showFieldTitles ? "title" : "name",
pickListProperties: { reusePickList : function () { return false;} }
});
if (this.field) items[0].defaultValue = this.field.name;
} else {
// build and assign a valueMap to the fieldPicker item
for (var i = 0; i < fieldNames.length; i++) {
var fieldName = fieldNames[i],
field = this.getField(fieldName);
if (field.canFilter == false) continue;
if (this.showFieldTitles) {
var fieldTitle = field.title;
fieldTitle = fieldTitle ? fieldTitle : fieldName;
fieldMap[fieldName] = fieldTitle;
} else {
fieldMap[fieldName] = fieldName;
}
}
items[0].valueMap = fieldMap;
items[0].defaultValue = isc.firstKey(fieldMap);
}
}
this.fieldPicker = items[0];
var fieldItem = items[0],
operatorItem = items[1];
if (!this.fieldName) {
if (criterion && criterion.fieldName) {
if (this.fieldDataSource) {
fieldItem.defaultValue = criterion.fieldName;
} else {
if (fieldNames.contains(criterion.fieldName)) {
fieldItem.defaultValue = criterion.fieldName;
} else {
isc.logWarn("Criterion specified field " + criterion.fieldName + ", which is not" +
" in the record. Using the first record field (" +
fieldMap ? isc.firstKey(fieldMap) : fieldNames[0] +
") instead");
fieldItem.defaultValue = fieldMap ? isc.firstKey(fieldMap) : fieldNames[0];
}
}
}
selectedFieldName = fieldItem.defaultValue;
}
if (selectedFieldName) {
//var field = this.field || this.getField(selectedFieldName);
var field = this.getField(selectedFieldName);
var valueMap = field ? this.getFieldOperatorMap(field, false, "criteria", true) : null;
operatorItem.valueMap = valueMap;
if (valueMap) {
if (criterion && criterion.operator) {
operatorItem.defaultValue = criterion.operator;
} else {
operatorItem.defaultValue = isc.firstKey(valueMap);
}
}
this._lastFieldName = selectedFieldName;
var operator = this.getSearchOperator(operatorItem.defaultValue);
if (!operator && valueMap.length > 0) {
isc.logWarn("Criterion specified unknown operator " +
(criterion ? criterion.operator : "[null criterion]") +
". Using the first valid operator (" + isc.firstKey(valueMap) + ") instead");
operatorItem.defaultValue = isc.firstKey(valueMap);
operator = this.getSearchOperator(operatorItem.defaultValue);
}
var valueItems = this.buildValueItemList(field, operator);
if (criterion) {
if (criterion.value != null && valueItems.containsProperty("name", "value")) {
valueItems.find("name", "value").defaultValue = criterion.value;
}
if (criterion.start != null && valueItems.containsProperty("name", "start")) {
valueItems.find("name", "start").defaultValue = criterion.start;
}
if (criterion.end != null && valueItems.containsProperty("name", "end")) {
valueItems.find("name", "end").defaultValue = criterion.end;
}
}
if (valueItems) items.addList(valueItems);
} else {
operatorItem.disabled = true;
}
this.addAutoChild("clause", {
flattenItems: this.flattenItems,
items: items
});
this.fieldPicker = this.clause.getItem("fieldName");
this.operatorPicker = this.clause.getItem("operator");
}
this.addMembers([this.removeButton, this.clause]);
if (this.fieldPicker && this.fieldPicker.type == "staticText") {
this.fieldPicker.prompt = this.fieldPicker.getValue();
}
},
// create the form items that constitute the clause, based on the DataSource field involved and
// the chosen operator.
buildValueItemList : function (field, operator) {
// Sanity check only - we don't expect the operator to be unset but if it is log a warning
if (operator == null) this.logWarn("buildValueItemList passed null operator");
if (field == null) return;
var fieldName = field.name,
valueType = operator ? operator.valueType : "text",
baseFieldType = isc.SimpleType.getType(field.type) || isc.SimpleType.getType("text"),
items = [],
props,
editorType = null
;
// In case this is a user-defined type, derive the built-in base type it
// ultimately inherits from, so we know which is the best FormItem to use
while (baseFieldType.inheritsFrom) {
baseFieldType = isc.SimpleType.getType(baseFieldType.inheritsFrom);
}
// We're not interested in the object, just the name
baseFieldType = baseFieldType.name;
if (isc.isA.FilterBuilder(this.creator)) {
editorType = this.creator.getEditorType(fieldName, operator.ID);
if (editorType != null) baseFieldType = editorType;
}
if (valueType == "valueSet") {
return; // XXX - For now, we can't cope with these
// a value of the same type as the field
} else if (valueType == "fieldType" || valueType == "custom") {
editorType = null;
if (valueType == "custom" && operator && operator.editorType) {
editorType = operator.editorType;
}
var fieldDef = isc.addProperties({
type: baseFieldType, name: field.name, showTitle: false,
width: this.valueItemWidth, editorType: editorType,
changed : function () {
this.form.creator.valueChanged(this, this.form);
}
}, this.getValueFieldProperties(field.type, fieldName));
// Pick up DataSource presentation hints
fieldDef = this.combineFieldData(fieldDef, field);
fieldDef.name = "value";
if (field.type == "enum") {
fieldDef = isc.addProperties(fieldDef, {
valueMap: field.valueMap
});
}
if (baseFieldType == "boolean") {
fieldDef = isc.addProperties(fieldDef, {
defaultValue: false
});
}
if (field.editorType == "SelectItem" || field.editorType == "ComboBoxItem") {
if (field.editorProperties != null) {
props = field.editorProperties;
fieldDef = isc.addProperties(fieldDef, {
optionDataSource: props.optionDataSource ? props.optionDataSource : this.getDataSource(),
valueField: props.valueField ? props.valueField : field.name,
displayField : props.displayField ? props.displayField : field.name
});
}
} else if (field.editorProperties) {
fieldDef = isc.addProperties({}, fieldDef, field.editorProperties);
}
items.add(fieldDef);
} else if (valueType == "fieldName") {
// another field in the same DataSource
props = {
type: "select", name: "value", showTitle: false,
width: this.valueItemWidth,
textMatchStyle: this.fieldPicker.textMatchStyle,
changed : function () {
this.form.creator.valueChanged(this, this.form);
}
};
if (this.fieldDataSource) {
// using a fieldDataSource - apply this as the optionDataSource
props = isc.addProperties(props, {
type: "ComboBoxItem",
completeOnTab: true,
optionDataSource: this.fieldDataSource,
valueField: "name",
displayField: this.showFieldTitles ? "title" : "name",
pickListProperties: { reusePickList : function () { return false; } }
});
} else {
var altFieldNames = this.getFieldNames(true);
altFieldNames.remove(fieldName);
var fieldMap = {};
for (var i = 0; i < altFieldNames.length; i++) {
var nextFieldName = altFieldNames[i];
if (this.showFieldTitles) {
var fieldTitle = this.getField(nextFieldName).title;
fieldTitle = fieldTitle ? fieldTitle : nextFieldName;
fieldMap[nextFieldName] = fieldTitle;
} else {
fieldMap[nextFieldName] = nextFieldName;
}
}
props = isc.addProperties(props, { valueMap: fieldMap });
}
items.add(isc.addProperties(props, this.getValueFieldProperties(field.type, fieldName)));
} else if (valueType == "valueRange") {
// two values of the same type as the field
props = this.combineFieldData(
isc.addProperties({
type: baseFieldType, showTitle: false, width: this.valueItemWidth,
changed : function () {
this.form.creator.valueChanged(this, this.form);
}
}, this.getValueFieldProperties(field.type, fieldName)
), field);
items.addList([
isc.addProperties({}, props, { name: "start" }),
isc.addProperties(
{ type: "staticText", name: "rangeSeparator", showTitle: false,
width: 1, defaultValue: this.rangeSeparator, shouldSaveValue:false,
changed : function () {
this.form.creator.valueChanged(this, this.form);
}
}, this.getValueFieldProperties(field.type, fieldName)
),
isc.addProperties({}, props, { name: "end" })
]);
}
if (this.validateOnChange) {
for (var i = 0; i < items.length; i++) {
isc.addProperties(items[i], {
blur : function(form, item) {
if (!form.creator.itemsInError) form.creator.itemsInError = [];
if (!form.validate(null, null, true)) {
item.focusInItem();
if (!form.creator.itemsInError.contains(item)) {
form.creator.itemsInError.add(item);
}
} else {
if (form.creator.itemsInError.contains(item)) {
form.creator.itemsInError.remove(item);
}
}
}
});
}
}
for (var i=0; i @method filterClause.getValueFieldProperties()
// Override to return properties for the FormItem used for the "value" field, that is, the
// user-entered value that becomes +link{criterion.value}.
//
// @param type (FieldType) type of the DataSource field for this filter row
// @param type (String) name of the DataSource field for this filter row
// @return (FormItem Properties) properties for the value field
// @visibility external
//<
getValueFieldProperties : function (type, fieldName) {
},
//> @method filterClause.remove()
// Remove this clause by destroy()ing it.
//
// @visibility external
//<
remove : function () {
this.markForDestroy();
},
getValues : function () {
var clause = this.clause;
return clause.getValues();
},
getFieldName : function () {
return this.fieldPicker.getValue() || this.fieldName;
},
//> @method filterClause.getCriterion()
// Return the criterion specified by this FilterClause.
//
// @return (Criteria) The single criterion for this FilterClause
// @visibility external
//<
getCriterion : function () {
if (!this.clause) return null;
var clause = this.clause,
fieldName = this.getFieldName(),
operator = this.operatorPicker.getValue(),
valueField = clause.getField("value"),
startField = clause.getField("start"),
endField = clause.getField("end"),
criterion = clause.getValues()
;
if (isc.isA.String(operator)) operator = this.getSearchOperator(operator);
if (operator == null) return;
if (operator.getCriterion && isc.isA.Function(operator.getCriterion)) {
if (valueField) {
criterion = operator.getCriterion(fieldName, valueField);
} else {
var startCriterion = operator.getCriterion(fieldName, startField),
endCriterion = operator.getCriterion(fieldName, endField);
criterion.fieldName = startCriterion.fieldName;
criterion.operator = startCriterion.operator;
delete criterion.value;
criterion.start = startCriterion.value;
criterion.end = endCriterion.value;
}
}
if (this.fieldName) criterion.fieldName = this.fieldName;
// flag dates as logicalDates unless the field type inherits from datetime
var field = this.getDataSource().getField(fieldName);
if (isc.isA.Date(criterion.value) &&
(!field || !isc.SimpleType.inheritsFrom(field.type, "datetime")))
{
criterion.value.logicalDate = true;
}
// Ignore criteria where no value has been set, unless it is an operator (eg, isNull)
// that does not require a value, or requires a start/end rather than a value
if (!operator || (operator.valueType != "none" &&
operator.valueType != "valueRange" &&
(criterion.value == null ||
(isc.isA.String(criterion.value) && criterion.value == ""))))
{
return null;
}
return criterion;
},
setDefaultFocus : function () {
if (!this.clause) return;
if (isc.isA.Function(this.clause.focusInItem)) this.clause.focusInItem("fieldName");
},
//> @method filterClause.validate
// Validate this clause.
// @return (Boolean) true if if the clause is valid, false otherwise
// @visibility external
//<
validate : function () {
return this.clause ? this.clause.validate(null, null, true) : true;
},
itemChanged : function () {
if (this.creator && isc.isA.Function(this.creator.itemChanged)) this.creator.itemChanged();
},
valueChanged : function (valueField, form) {
},
fieldNameChanged : function () {
this.clause.getItem("operator").disabled = false;
this.updateFields();
},
removeValueFields : function () {
if (!this.clause) return;
var form = this.clause;
if (form.getItem("value")) form.removeItem("value");
if (form.getItem("rangeSeparator")) form.removeItem("rangeSeparator");
if (form.getItem("start")) form.removeItem("start");
if (form.getItem("end")) form.removeItem("end");
},
operatorChanged : function () {
if (!this.clause) return;
var form = this.clause,
fieldName = this.fieldName || form.getValue("fieldName")
;
if (fieldName == null) return;
var field = this.getField(fieldName);
var operator = this.getSearchOperator(form.getValue("operator"));
this.removeValueFields();
var items = this.buildValueItemList(field, operator)
form.addItems(items);
var valueItem = form.getItem("value");
if (valueItem &&
(valueItem.getValueMap() && valueItem._valueInValueMap &&
!valueItem._valueInValueMap(valueItem.getValue()) ||
valueItem.optionDataSource ||
!this.retainValuesAcrossFields)
) {
valueItem.clearValue();
}
},
// updateFields() Fired when the user changes the fieldName field of this clause.
// Opportunity to determine the newly selected field type and update the operator valueMap and
// appropriate valueFields.
updateFields : function () {
if (!this.clause) return;
var form = this.clause,
oldFieldName = this._lastFieldName,
fieldName = this.fieldName || form.getValue("fieldName")
;
if (fieldName == null) return;
var field = this.getField(fieldName),
oldField = this.getField(oldFieldName);
if (!field) return;
var operator = form.getValue("operator");
// note this setValueMap() call means if an operator was already chosen, it will be
// preserved unless no longer valid for the new field
form.getItem("operator").setValueMap(
this.getFieldOperatorMap(field, false, "criteria", true)
);
if (operator == null || form.getValue("operator") != operator) {
// if the operator was lost from the valueMap, the value will have been cleared
// Reset to the first option
if (form.getValue("operator") == null) {
form.getItem("operator").setValue(form.getItem("operator").getFirstOptionValue());
}
operator = form.getValue("operator");
}
// Now we've got the operator type we want, normalize it to a config object
operator = this.getSearchOperator(operator);
var typeChanged;
if (form.getItem("value")) {
var currentType = form.getItem("value").type,
newType = field.type || "text";
typeChanged = (currentType != newType);
}
// otherwise rebuild the value fields
this.removeValueFields();
form.addItems(this.buildValueItemList(field, operator));
// Clear out the currently entered value if
// 1) the valueField data type has changed
// 2) the new valueField has a valueMap and the current value doesn't appear in it
// 3) either the old or new field has a valueMap or optionDataSource
// 4) this.retainValuesAcrossFields is false
if (typeChanged) {
form.clearValue("value");
} else {
var valueItem = form.getItem("value"),
shouldClear = (
(field.valueMap || field.optionDataSource) ||
(oldField && (oldField.valueMap || oldField.optionDataSource)) ||
!this.retainValuesAcrossFields
)
;
if (shouldClear) valueItem.clearValue();
}
// For now always clear out range fields
if (form.getItem("start")) form.setValue("start", null);
if (form.getItem("end")) form.setValue("end", null);
this._lastFieldName = field.name;
},
//> @method filterClause.getFieldOperators()
// Get the list of +link{Operators} that are valid on this field. By default, all operators
// returned by +link{dataSource.getFieldOperators()} are used.
//
// Called whenever the fieldName is changed.
//
// @return (Array of Operator) valid operators for this field
// @visibility external
//<
getFieldOperators : function (fieldName) {
var field = this.getField(fieldName)
return this.getPrimaryDS().getFieldOperators(field);
}
});
isc.FilterClause.registerStringMethods({
remove : ""
});
//> @class FilterBuilder
// A form that allows the user to input advanced search criteria, including operators on
// field values such as "less than", and sub-clauses using "AND" and "OR" operators.
//
// A FilterBuilder produces an +link{AdvancedCriteria} object, which the +link{DataSource}
// subsystem can use to filter datasets, including the ability to perform such filtering within
// the browser for datasets that are completely loaded.
// @treeLocation Client Reference/Forms
// @visibility external
//<
isc.defineClass("FilterBuilder", "Layout");
isc.FilterBuilder.addClassProperties({
//> @attr filterBuilder.missingFieldPrompt (String: "[missing field definition]" : IR)
// The message to display next to fieldNames that do not exist in the available dataSource.
// @group i18nMessages
// @visibility external
//<
missingFieldPrompt: "[missing field definition]"
});
isc.FilterBuilder.addClassMethods({
//> @classMethod filterBuilder.getFilterDescription()
// Returns a human-readable string describing the clauses in this filter.
//
// @param type (AdvancedCriteria or Criterion) Criteria to convert to a readable string
// @return (String) Human-readable string describing the clauses in the passed criteria
// @visibility external
//<
getFilterDescription : function (criteria, dataSource) {
if (!isc.isA.DataSource(dataSource)) dataSource = isc.DS.getDataSource(dataSource);
if (!dataSource) return "No dataSource";
var result = "";
if (criteria.criteria && isc.isAn.Array(criteria.criteria)) {
// complex criteria, call this method again with that criteria
var operator = criteria.operator,
subCriteria = criteria.criteria;
for (var i = 0; i 0) result += " " + operator + " ";
if (subItem.criteria && isc.isAn.Array(subItem.criteria)) {
result += "("
result += isc.FilterBuilder.getFilterDescription(subItem, dataSource);
result += ")"
} else {
result += isc.FilterBuilder.getCriterionDescription(subItem, dataSource);
}
}
} else {
// simple criterion
result += isc.FilterBuilder.getCriterionDescription(criteria, dataSource);
}
return result;
},
// helper method to return the description of a single criterion
getCriterionDescription : function (criterion, dataSource) {
if (!isc.isA.DataSource(dataSource)) dataSource = isc.DS.getDataSource(dataSource);
if (!dataSource) return "No DataSource";
var fieldName = criterion.fieldName,
operatorName = criterion.operator,
value = criterion.value,
start = criterion.start,
end = criterion.end,
field = dataSource.getField(fieldName),
operator = dataSource.getSearchOperator(operatorName),
operatorMap = dataSource.getFieldOperatorMap(field, true, operator.valueType, false),
result=""
;
if (!field) {
if (criterion.criteria && isc.isAn.Array(criterion.criteria)) {
// we've been passed an AdvancedCriteria as a simple criterion - log a warning and
// return the result of getFilterDescription(), rather than just bailing
isc.logWarn("FilterBuilder.getCriterionDescription: Passed an AdvancedCriteria - "+
"returning through getFilterDescription.");
return isc.FilterBuilder.getFilterDescription(criterion, dataSource);
}
// just an unknown field - log a warning and bail
result = fieldName + " " + isc.FilterBuilder.missingFieldPrompt + " ";
isc.logWarn("FilterBuilder.getCriterionDescription: No such field '"+fieldName+"' "+
"in DataSource '"+dataSource.ID+"'.");
} else {
result = (field.title ? field.title : fieldName) + " ";
}
switch (operatorName)
{
case "notEqual":
case "lessThan":
case "greaterThan":
case "lessOrEqual":
case "greaterOrEqual":
case "between":
case "notNull" :
result += "is " + operatorMap[operatorName] || operatorName;
break;
case "equals":
result += "is equal to";
break;
case "notEqual":
result += "is not equal to";
break;
default: result += operatorMap[operatorName] || operatorName;
}
if (operator.valueType == "valueRange") result += " " + start + " and " + end;
else if (operatorName != "notNull") result += " " + value;
return result;
}
});
isc.FilterBuilder.addProperties({
// Layout: be a minimum height stack by default
// ---------------------------------------------------------------------------------------
vertical:false,
vPolicy:"none",
height:1,
defaultWidth:400,
//> @attr filterBuilder.fieldDataSource (DataSource : null : IR)
// If specified, the FilterBuilder will dynamically fetch DataSourceField definitions from
// this DataSource rather than using +link{filterBuilder.dataSource}. The +link{fieldPicker}
// will default to being a +link{ComboBoxItem} rather than a +link{SelectItem} so that the user
// will have type-ahead auto-completion.
//
// The records returned from the fieldDataSource
must have properties
// corresponding to a +link{DataSourceField} definition, at a minimum,
// +link{DataSourceField.name,"name"} and +link{DataSourceField.type,"type"}. Any property
// legal on a DataSourceField is legal on the returned records, including
// +link{DataSourceField.valueMap,valueMap}.
//
// Even when a fieldDataSource
is specified, +link{filterBuilder.dataSource} may
// still be specified in order to control the list of
// +link{DataSource.setTypeOperators,valid operators} for each field.
//
// @visibility external
//<
//> @attr filterBuilder.fieldPicker (AutoChild : null : IR)
// AutoChild for the +link{FormItem} that allows a user to pick a DataSource field when
// creating filter clauses.
//
// This will be a +link{SelectItem} by default, or a +link{ComboBoxItem} if
// +link{filterBuilder.fieldDataSource} has been specified.
//
// @visibility external
//<
fieldPickerDefaults: {
type: "SelectItem",
name: "fieldName",
textMatchStyle: "startsWith",
showTitle: false,
changed : function () { this.form.creator.fieldNameChanged(this.form); }
},
//> @attr filterBuilder.fieldPickerProperties (FormItem Properties : null : IR)
// Properties to combine with the +link{fieldPicker} autoChild FormItem.
//
// @visibility external
//<
// Schema and operators
// ---------------------------------------------------------------------------------------
//> @attr filterBuilder.dataSource (DataSource or ID : null : IRW)
// DataSource this filter should use for field definitions and available +link{Operator}s.
// @visibility external
//<
setDataSource : function(ds) {
if (isc.DataSource.get(this.dataSource).ID != isc.DataSource.get(ds).ID) {
this.dataSource = ds;
this.clearCriteria();
}
},
//> @attr filterBuilder.criteria (AdvancedCriteria : null : IRW)
// Initial criteria.
//
// When initialized with criteria, appropriate clauses for editing the provided criteria will
// be automatically generated.
//
// Note that empty or partial criteria are allowed, for example, criteria that specify
// +link{criterion.fieldName} only will generate an expression with the operator not chosen
// yet, and a +link{criterion} with a logical operator ("and" or "or") but not
// +link{criterion.criteria,subcriteria} defined will generate an empty subclause.
// @visibility external
//<
//> @attr filterBuilder.saveOnEnter (boolean : null : IR)
// If true, when the user hits the Enter key while focused in a text-item in this
// FilterBuilder, we automatically invoke the user-supplied +link{filterBuilder.search()} method.
// @visibility external
//<
//> @attr filterBuilder.showFieldTitles (boolean : true : IR)
// If true (the default), show field titles in the drop-down box used to select a field for querying.
// If false, show actual field names instead.
// @visibility external
//<
showFieldTitles: true,
//> @attr filterBuilder.validateOnChange (boolean : true : IR)
// If true (the default), validates each entered value when it changes, to make sure it is a
// a valid value of its type (valid string, number, and so on). No other validation is
// carried out. If you switch this property off, it is still possible to validate the
// FilterBuilder
by calling +link{filterBuilder.validate()} from your own code.
// @visibility external
//<
validateOnChange: true,
// Add/remove buttons
// ---------------------------------------------------------------------------------------
//> @attr filterBuilder.showRemoveButton (boolean : true : IR)
// If set, a button will be shown for each clause allowing it to be removed.
// @visibility external
//<
showRemoveButton:true,
//> @attr filterBuilder.removeButtonPrompt (string : "Remove" : IR)
// The hover prompt text for the remove button.
//
// @group i18nMessages
// @visibility external
//<
removeButtonPrompt: "Remove",
//> @attr filterBuilder.removeButton (AutoChild : null : IR)
// The removal ImgButton that appears before each clause if
// +link{showRemoveButton} is set.
// @visibility external
//<
removeButtonDefaults : {
_constructor:isc.ImgButton,
width:18, height:18, layoutAlign:"center",
src:"[SKIN]/actions/remove.png",
showRollOver:false, showDown:false,
showDisabled:false, // XXX
//prompt:"Remove",
click: function () { this.creator.removeButtonClick(this.clause); }
},
//> @attr filterBuilder.showAddButton (boolean : true : IR)
// If set, a button will be shown underneath all current clauses allowing a new clause to be
// added.
// @visibility external
//<
showAddButton:true,
//> @attr filterBuilder.addButtonPrompt (string : "Add" : IR)
// The hover prompt text for the add button.
//
// @group i18nMessages
// @visibility external
//<
addButtonPrompt: "Add",
//> @attr filterBuilder.addButton (AutoChild : null : IR)
// An ImgButton that allows new clauses to be added if +link{showAddButton}
// is set.
// @visibility external
//<
addButtonDefaults : {
_constructor:isc.ImgButton,
autoParent:"buttonBar",
width:18, height:18,
src:"[SKIN]/actions/add.png",
showRollOver:false, showDown:false,
//prompt:"Add",
click: function () { this.creator.addButtonClick(this.clause); }
},
buttonBarDefaults : {
_constructor:isc.HStack,
autoParent:"clauseStack",
membersMargin:4,
defaultLayoutAlign:"center",
height:1
},
addButtonClick : function () {
this.addNewClause();
},
removeButtonClick : function (clause) {
if (!clause) return;
this.removeClause(clause);
},
//> @method filterBuilder.removeClause()
// Remove a clause this FilterBuilder is currently showing.
// @param clause (Clause) clause as retrieved from filterBuilder.clauses
// @visibility external
//<
removeClause : function (clause) {
// remove the clause from the clauses array and destroy it
this.clauses.remove(clause);
if (this.clauseStack) this.clauseStack.hideMember(clause, function () { clause.destroy(); });
// update the first removeButton
this.updateFirstRemoveButton();
},
//> @attr filterBuilder.allowEmpty (boolean : false : IR)
// If set to false, the last clause cannot be removed.
// @visibility external
//<
updateFirstRemoveButton : function () {
var firstClause = this.clauses[0];
if (!firstClause || !firstClause.removeButton) return;
if (this.clauses.length == 1 && !this.allowEmpty) {
firstClause.removeButton.disable();
firstClause.removeButton.setOpacity(50); // XXX need media with disabled state
} else if (this.clauses.length > 1) {
firstClause.removeButton.enable();
firstClause.removeButton.setOpacity(100); // XXX need media with disabled state
}
},
// Top-level Operator
// ---------------------------------------------------------------------------------------
//> @type LogicalOperator
// Operators that can evaluate a set of criteria and produce a combined result.
//
// @value "and" true if all criteria are true
// @value "or" true if any criteria are true
// @value "not" true if all criteria are false
// @visibility external
//<
//> @attr filterBuilder.retainValuesAcrossFields (boolean : true : IRW)
// Dictates whether values entered by a user should be retained in the value fields when a
// different field is selected. Default value is true.
//
// Note that, when switching between fields that have an optionDataSource or valueMap, this
// property is ignored and the values are never retained.
// @visibility external
//<
retainValuesAcrossFields: true,
//> @attr filterBuilder.topOperator (LogicalOperator : "and" : IRW)
// Default logical operator for all top-level clauses in the FilterBuilder.
//
// May be able to be changed by the user via the UI, according to +link{topOperatorAppearance}.
// @visibility external
//<
topOperator:"and",
//> @attr filterBuilder.radioOptions (Array of OperatorId : ["and", "or", "not"] : IR)
// Logical operators to allow if we have a +link{topOperatorAppearance} of "radio".
//
// @visibility external
//<
radioOptions: ["and", "or", "not"],
//> @method filterBuilder.setTopOperator()
// Programmatically change the +link{topOperator} for this FilterBuilder.
// @param operator (OperatiorId) new top-level operator
// @visibility external
//<
setTopOperator : function (newOp) {
this.topOperator = newOp;
var appearance = this.topOperatorAppearance;
if (appearance == "bracket") {
this.topOperatorForm.setValue("operator", newOp);
} else if (appearance == "radio") {
this.radioOperatorForm.setValue("operator", newOp);
}
},
// called when the user changes the topOperator via a form
topOperatorChanged : function (newOp) {
this.topOperator = newOp;
},
//> @type TopOperatorAppearance
// Interface to use for showing and editing the +link{filterBuilder.topOperator,top-level operator}
// of a FilterBuilder.
//
// @value "radio" radio buttons appear at the top of the form
//
// @value "bracket" a SelectItem appears with a "bracket" spanning all top-level clauses,
// exactly the same appearance used for showing
// +link{filterBuilder.showSubClauseButton,subClauses}, if enabled.
//
// @value "none" no interface is shown. The top-level operator is expected to be shown to
// the user outside the FilterBuilder, and, if editable, +link{filterBuilder.setTopOperator()}
// should be called to update it
// @visibility external
//<
//> @attr filterBuilder.topOperatorAppearance (TopOperatorAppearance : "bracket" : IR)
// How to display and edit the +link{topOperator,top-level operator} for this FilterBuilder.
//
// See +link{type:TopOperatorAppearance} for a list of options.
// @visibility external
//<
topOperatorAppearance:"bracket",
//> @attr filterBuilder.radioOperatorForm (AutoChild : null : IR)
// With +link{topOperatorAppearance}:"radio", form that appears above the stack of clauses
// and allows picking the +link{LogicalOperator} for the overall FilterBuilder.
//
// By default, consists of a simple RadioGroupItem.
// @visibility external
//<
radioOperatorFormDefaults : {
_constructor:isc.DynamicForm,
autoParent:"clauseStack",
height:1,
items : [
{ name:"operator", type:"radioGroup", showTitle:false, vertical:false,
width: 250,
changed : function (form, item, value) {
form.creator.topOperatorChanged(value);
}
}
]
},
//> @attr filterBuilder.topOperatorForm (AutoChild : null : IR)
// With +link{topOperatorAppearance}:"bracket", form that appears to the left of the stack of
// clauses and allows picking the +link{LogicalOperator} for the overall FilterBuilder.
//
// By default, consists of a simple SelectItem.
// @visibility external
//<
topOperatorFormDefaults : {
height:1,
width:80, numCols:1, colWidths:["*"],
layoutAlign:"center",
_constructor:isc.DynamicForm,
items : [{
name:"operator",
type: "select",
showTitle:false,
width:"*",
changed : function (form, item, value) {
form.creator.topOperatorChanged(value);
}
}]
},
//> @attr filterBuilder.defaultSubClauseOperator (LogicalOperator : "or" : IR)
// Default operator for subclauses added via the +link{subClauseButton}.
// @visibility external
//<
defaultSubClauseOperator:"or",
//> @attr FilterBuilder.matchAllTitle (String : "Match All" : IR)
// Title for the "Match All" (and) operator
// @group i18nMessages
// @visibility external
//<
matchAllTitle: "Match All",
//> @attr FilterBuilder.matchNoneTitle (String : "Match None" : IR)
// Title for the "Match None" (not) operator
// @group i18nMessages
// @visibility external
//<
matchNoneTitle: "Match None",
//> @attr FilterBuilder.matchAnyTitle (String : "Match Any" : IR)
// Title for the "Match Any" (or) operator
// @group i18nMessages
// @visibility external
//<
matchAnyTitle: "Match Any",
// Init
// ---------------------------------------------------------------------------------------
getPrimaryDS : function () {
if (this.dataSource) return this.getDataSource();
else if (this.fieldDataSource) return this.fieldDataSource;
},
initWidget : function () {
this.Super("initWidget", arguments);
if (this.fieldDataSource && this.criteria) this._initializingClauses = true;
// set strings for button defaults
this.addButtonDefaults.prompt = this.addButtonPrompt;
this.removeButtonDefaults.prompt = this.removeButtonPrompt;
this.subClauseButtonDefaults.prompt = this.subClauseButtonPrompt;
this.subClauseButtonDefaults.title = this.subClauseButtonTitle;
var undef;
if (this.showSubClauseButton == undef) {
this.showSubClauseButton = (this.topOperatorAppearance != "radio");
}
this.clauses = [];
var topOp = this.topOperatorAppearance;
if (isc.isA.String(this.fieldDataSource))
this.fieldDataSource = isc.DS.get(this.fieldDataSource);
if (isc.isA.String(this.dataSource))
this.dataSource = isc.DS.get(this.dataSource);
var ds = this.getPrimaryDS(),
tempMap = ds.getTypeOperatorMap("text", true, "criteria"),
tempArr = [];
var radioItemMap = {
"and": this.matchAllTitle,
"or": this.matchAnyTitle,
"not": this.matchNoneTitle
};
// We haven't got a lot of room, so we really want to be saying "and" in this
// select box, rather than "All subcriteria are true"
for (var prop in tempMap) {
tempArr.add(prop);
}
if (topOp == "bracket") {
if (this.showTopRemoveButton) {
// When the FilterBuilder is being used as a subclause it needs a remove button.
// Our parent FilterBuilder could tack one on, but only by introducing an extra
// layer of nesting, so we manage it here.
var removeButton = this.removeButton = this.createAutoChild("removeButton", {
click : function () {
this.creator.parentClause.removeButtonClick(this.creator);
}
});
this.addMember(removeButton);
}
this.addAutoChild("topOperatorForm");
this.topOperatorForm.items[0].valueMap = tempMap;
this.topOperatorForm.items[0].defaultValue = this.topOperator;
this.addAutoChild("bracket");
}
this.addAutoChild("clauseStack");
this.clauseStack.hide();
if (topOp == "radio") {
this.addAutoChild("radioOperatorForm");
var radioMap = {};
for (var i = 0; i < this.radioOptions.length; i++) {
radioMap[this.radioOptions[i]] = radioItemMap[this.radioOptions[i]];
}
this.radioOperatorForm.items[0].valueMap = radioMap;
this.radioOperatorForm.items[0].defaultValue = this.topOperator;
}
this.addAutoChildren(["buttonBar", "addButton", "subClauseButton"]);
// support criteria being passed with null elements
this.stripNullCriteria(this.criteria);
this.setCriteria(this.criteria);
},
//> @attr filterBuilder.clauseStack (AutoChild : null : IR)
// VStack of all clauses that are part of this FilterBuilder
// @visibility external
//<
clauseStackDefaults : {
_constructor:isc.VStack,
height:1,
membersMargin:1, // otherwise brackets on subclauses are flush
animateMembers: true,
animateMemberTime: 150
},
// Clause creation
// ---------------------------------------------------------------------------------------
clauseConstructor: "FilterClause",
addNewClause : function (criterion, field) {
// create a new isc.FilterClause
var filterClause = this.createAutoChild("clause", {
visibility: "hidden",
flattenItems: true,
criterion: criterion,
dataSource: this.dataSource,
validateOnChange: this.validateOnChange,
showFieldTitles: this.showFieldTitles,
showRemoveButton: this.showRemoveButton,
removeButtonPrompt: this.removeButtonPrompt,
retainValuesAcrossFields: this.retainValuesAcrossFields,
fieldDataSource: this.fieldDataSource,
field: field,
fieldData: this.fieldData,
fieldPickerDefaults: this.fieldPickerDefaults,
fieldPickerProperties: this.fieldPickerProperties,
remove : function () {
this.creator.removeClause(this);
},
fieldNameChanged : function () {
this.Super("fieldNameChanged", arguments);
this.creator.fieldNameChanged(this);
}
});
return this._addClause(filterClause);
},
//> @method filterBuilder.addClause()
// Add a new +link{FilterClause} to this FilterBuilder.
//
// @param filterClause (FilterClause) A +link{FilterClause} instance
// @visibility external
//<
addClause : function (filterClause) {
// add the passed filterClause
if (!filterClause) return filterClause;
var _this = this;
filterClause.fieldDataSource = this.fieldDataSource;
filterClause.remove = function () {
_this.removeClause(this);
};
filterClause.fieldNameChanged = function () {
this.Super("fieldNameChanged", arguments);
_this.fieldNameChanged(this);
};
return this._addClause(filterClause);
},
_addClause : function (filterClause) {
this.clauses.add(filterClause);
var clauseStack = this.clauseStack;
var position = Math.max(0, clauseStack.getMemberNumber(this.buttonBar));
clauseStack.addMember(filterClause, position);
clauseStack.showMember(filterClause, function () { filterClause.setDefaultFocus(); });
this.updateFirstRemoveButton();
return filterClause;
},
//> @method filterBuilder.getChildFilters()
// Returns an array of child +link{class:FilterBuilder}s, representing the list of complex
// clauses, or an empty array if there aren't any.
//
// @return (Array of FilterBuilder) The list of complex clauses for this filterBuilder
// @visibility external
//<
getChildFilters : function () {
var childFilters = [];
for (var i = 0; i @method filterBuilder.getFilterDescription()
// Returns a human-readable string describing the clauses in this filterBuilder.
//
// @param type (AdvancedCriteria or Criterion) Criteria to convert to a readable string
// @return (String) Human-readable string describing the clauses in the passed criteria
// @visibility external
//<
getFilterDescription : function () {
return isc.FilterBuilder.getFilterDescription(this.getCriteria(), this.dataSource);
},
//> @attr filterBuilder.rangeSeparator (String : "and" : IR)
// For operators that check that a value is within a range, text to show between the start and
// end input fields for specifying the limits of the range.
// @visibility external
// @group i18nMessages
//<
rangeSeparator: "and",
//> @method filterBuilder.validate
// Validate the clauses of this FilterBuilder.
// @return (Boolean) true if all clauses are valid, false otherwise
// @visibility external
//<
validate : function () {
var valid = true;
for (var i = 0; i < this.clauses.length; i++) {
if (!this.clauses[i].validate(null, null, true)) valid = false;
}
return valid;
},
//> @method filterBuilder.getFieldOperators()
// Get the list of +link{Operators} that are valid on this field. By default, all operators
// returned by +link{dataSource.getFieldOperators()} are used.
//
// Called whenever the fieldName is changed.
//
// @return (Array of Operator) valid operators for this field
// @visibility external
//<
getFieldOperators : function (fieldName) {
var field = this.getPrimaryDS().getField(fieldName);
return this.getPrimaryDS().getFieldOperators(field);
},
//> @method filterBuilder.getValueFieldProperties()
// Override to return properties for the FormItem used for the "value" field, that is, the
// user-entered value that becomes +link{criterion.value}.
//
// @param type (FieldType) type of the DataSource field for this filter row
// @param fieldName (String) name of the DataSource field for this filter row
// @visibility external
//<
getValueFieldProperties : function (type, fieldName) {
},
// Subclauses
// ---------------------------------------------------------------------------------------
//> @attr filterBuilder.showSubClauseButton (boolean : See Description : IR)
// Whether to show a button that allows the user to add subclauses. Defaults to false if
// the +link{topOperatorAppearance} is "radio", true in all other cases.
// @visibility external
//<
//> @attr filterBuilder.subClauseButtonTitle (string : "+()" : IR)
// The hover title text of the subClauseButton
//
// @group i18nMessages
// @visibility external
//<
subClauseButtonTitle: "+()",
//> @attr filterBuilder.subClauseButtonPrompt (string : "Add Subclause" : IR)
// The hover prompt text for the subClauseButton.
//
// @group i18nMessages
// @visibility external
//<
subClauseButtonPrompt: "Add Subclause",
//> @attr filterBuilder.subClauseButton (AutoChild : null : IR)
// Button allowing the user to add subclauses grouped by a +link{type:LogicalOperator}.
// @visibility external
//<
subClauseButtonDefaults : {
_constructor:"IButton",
autoParent:"buttonBar",
//title:"+()", // need an icon for this
autoFit:true,
//prompt:"Add Subclause",
click : function () { this.creator.addSubClause(this.clause); }
},
//> @attr filterBuilder.bracket (AutoChild : null : IR)
// Widget used as a "bracket" to hint to the user that a subclause groups several
// field-by-field filter criteria under one logical operator.
//
// By default, a simple CSS-style Canvas with borders on three sides. A vertical StretchImg
// could provide a more elaborate appearance.
// @visibility external
//<
bracketDefaults : {
styleName:"bracketBorders",
width:10
},
childResized : function () {
this.Super("childResized", arguments);
if (this.clauseStack && this.bracket) this.bracket.setHeight(this.clauseStack.getVisibleHeight());
},
draw : function () {
this.Super("draw", arguments);
if (this.clauseStack && this.bracket) this.bracket.setHeight(this.clauseStack.getVisibleHeight());
},
resized : function () {
if (this.clauseStack && this.bracket) this.bracket.setHeight(this.clauseStack.getVisibleHeight());
},
addSubClause : function (criterion) {
var operator;
if (criterion) {
operator = criterion.operator;
}
var clause = this.createAutoChild("subClause", {
dataSource:this.dataSource,
parentClause:this, showTopRemoveButton:true,
topOperatorAppearance:"bracket",
topOperator: operator || this.defaultSubClauseOperator,
clauseConstructor: this.clauseConstructor,
fieldPickerDefaults: this.fieldPickerDefaults,
fieldPickerProperties: this.fieldPickerProperties,
fieldDataSource: this.fieldDataSource,
fieldData: this.fieldData,
visibility:"hidden",
saveOnEnter: this.saveOnEnter,
validateOnChange: this.validateOnChange,
// We don't need (or want) to create empty children of new subclauses if we're
// building up the UI from a passed-in AdvancedCriteria
dontCreateEmptyChild: criterion != null
}, this.Class);
this.clauses.add(clause);
this.clauseStack.addMember(clause, this.clauses.length-1);
this.clauseStack.showMember(clause, function () {
clause.topOperatorForm.focusInItem("operator");
clause.bracket.setHeight(clause.getVisibleHeight());
});
// update the firstRemoveButton on the containing clause
this.updateFirstRemoveButton();
return clause;
},
// Deriving AdvancedCriteria
// ---------------------------------------------------------------------------------------
//> @method filterBuilder.getCriteria()
// Get the criteria entered by the user.
//
// @return (AdvancedCriteria)
// @visibility external
//<
getCriteria : function () {
if (this._initializingClauses) {
// if we were initialized with criteria and the clauses are still being created, just
// return the criteria we were initialized with
return this.criteria;
}
var criteria = {
_constructor:"AdvancedCriteria",
operator:this.topOperator,
criteria:[]
};
for (var i = 0; i < this.clauses.length; i++) {
var clause = this.clauses[i],
criterion,
skipCriterion = false;
if (isc.isA.FilterBuilder(clause)) {
criterion = clause.getCriteria();
} else {
criterion = clause.getCriterion();
skipCriterion = (criterion == null);
}
if (!skipCriterion) {
criteria.criteria.add(criterion);
}
}
// Return a copy - the original contains pointers to the live screen objects
return isc.clone(criteria);
},
// fired when this builder is ready for interactive use
filterReady : function () { },
//> @method filterBuilder.setCriteria()
// Set new criteria for editing.
//
// An interface for editing the provided criteria will be generated identically to what happens
// when initialized with +link{criteria}.
//
// Any existing criteria entered by the user will be discarded.
//
// @param criteria (AdvancedCriteria) new criteria. Pass null or {} to effectively reset the
// filterBuilder to it's initial state when no criteria are
// specified
// @visibility external
//<
setCriteria : function (criteria) {
this.clearCriteria(true);
this.stripNullCriteria(criteria);
if (!this._loadingFieldData && this.fieldDataSource && criteria) {
// fetch the necessary field-entries so they can be passed into the filterClauses
if (isc.isA.String(this.fieldDataSource) )
this.fieldDataSource = isc.DS.getDataSource(this.fieldDataSource);
var _this = this,
fieldsInUse = this.fieldDataSource.getCriteriaFields(criteria),
fieldCriteria = {}
;
if (fieldsInUse && fieldsInUse.length > 0) {
// construct an advanvcedCriteria to use when requesting used fields from the
// fields DS
fieldCriteria = { _constructor: "AdvancedCriteria", operator: "or", criteria: [] };
for (i=0; i0) {
for (var i = criteria.criteria.length-1; i>=0; i--) {
if (criteria.criteria[i] == null) {
criteria.criteria.removeAt(i);
} else {
if (criteria.criteria[i].criteria) this.stripNullCriteria(criteria.criteria[i]);
}
}
}
},
fetchFieldsReply : function (data, criteria) {
if (this.fieldData) {
var newFields = isc.getValues(this.fieldData);
newFields.addList(data.data);
this.fieldData = newFields.makeIndex("name");
} else this.fieldData = data.data.makeIndex("name");
this.setCriteria(criteria);
},
//> @method filterBuilder.clearCriteria()
// Clear all current criteria.
// @visibility external
//<
clearCriteria : function (dontCheckEmpty) {
var animation = this.clauseStack ? this.clauseStack.animateMembers : null;
if (this.clauseStack) this.clauseStack.animateMembers = false;
while (this.clauses.length > 0) {
this.removeClause(this.clauses[0]);
}
if (!dontCheckEmpty && !this.allowEmpty) this.addNewClause();
if (this.clauseStack) this.clauseStack.animateMembers = animation;
},
//> @method filterBuilder.addCriterion()
// Add a new criterion, including recursively adding sub-criteria for a criterion that
// contains other criteria.
//
// @param criterion (Criterion) new criterion to be added
// @visibility external
//<
addCriterion : function (criterion, field) {
if (criterion.criteria) {
var clause = this.addSubClause(criterion);
for (var idx = 0; idx < criterion.criteria.length; idx++) {
field = this.fieldData ? this.fieldData[criterion.criteria[idx].fieldName] : null;
clause.addCriterion(criterion.criteria[idx], field);
}
} else {
this.addNewClause(criterion, field);
}
},
_$Enter:"Enter",
handleKeyPress: function (event, eventInfo){
// Special case for Enter keypress: If this.saveOnEnter is true, and the enter keypress
// occurred in a text item, and this is a top-level FilterBuilder with a search() method
// defined, call the search() method and stop bubbling
if (event.keyName == this._$Enter) {
if (this.saveOnEnter) {
if (eventInfo.firedOnTextItem) {
if (!this.creator && this.search) {
this.search(this.getCriteria());
return isc.EH.STOP_BUBBLING;
}
}
}
}
},
itemChanged : function () {
if (this.creator && isc.isA.Function(this.creator.itemChanged)) {
this.creator.itemChanged();
} else {
if (!this.creator && isc.isA.Function(this.filterChanged)) {
this.filterChanged();
}
}
},
fieldNameChanged : function (filterClause) {
},
//> @method FilterBuilder.getEditorType()
// Returns the type of editor to use for the field.
//
// Default behavior is to use the +link{operator.editorType} for a custom operator, otherwise,
// use +link{RelativeDateItem} for before/after/between operators on date fields, otherwise,
// use the same editor as would be chosen by a +link{SearchForm}.
//
// @param fieldName (DataSourceField) DataSourceField definition
// @param operatorId (OperatorId) +link{OperatorId} for the chosen operator
// @return (SCClassName) SmartClient class to use (must be subclass of FormItem)
// @visibility external
//<
getEditorType : function (fieldName, operatorId) {
var ds = this.getPrimaryDS(),
className,
field;
var operator = ds.getSearchOperator(operatorId);
// return the operator's editorType, if it has one
if (operator.editorType) return operator.editorType;
if (operator.getEditorType && isc.isA.Function(operator.getEditorType))
return operator.getEditorType();
// return RelativeDateItem if the field is a Date
if (ds) field = ds.getField(fieldName);
if (field &&
(operatorId == "equals" || operatorId == "notEqual" ||
operatorId == "lessThan" || operatorId == "greaterThan" ||
operatorId == "between" || operatorId == "betweenInclusive" ||
operatorId == "greaterOrEqual" || operatorId == "lessOrEqual"))
{
if (field && isc.SimpleType.inheritsFrom(field.type, "date")) return "RelativeDateItem";
}
if (field)
return isc.FormItemFactory.getItemClassName({}, field.type, null);
else
return isc.FormItemFactory.getItemClassName({}, "text", null);
}
});
isc.FilterBuilder.registerStringMethods({
//> @method filterBuilder.search()
// A StringMethod that is automatically invoked if +link{filterBuilder.saveOnEnter} is set
// and the user presses Enter whilst in a text-item in any clause or subclause.
//
// @param criteria (AdvancedCriteria) The criteria represented by the filterBuilder
// @visibility external
//<
search : "criteria",
//> @method filterBuilder.filterChanged()
// Handler fired when there is a change() event fired on any FormItem within the
// filterBuilder.
//
// @visibility external
//<
filterChanged : ""
});
} // End of if (isc.DynamicForm)