API Docs for: 1.0.0
Show:

File: src/gallery-exprbuilder/js/ExpressionBuilder.js

"use strict";

/**
 * @module gallery-exprbuilder
 */

/**
 * Widget which helps user to build a query expression.
 * 
 * @main gallery-exprbuilder
 * @class ExpressionBuilder
 * @extends Widget
 * @constructor
 * @param config {Object} Widget configuration
 */
function ExpressionBuilder(config)
{
	ExpressionBuilder.superclass.constructor.call(this, config);
}

ExpressionBuilder.NAME = "exprbuilder";

ExpressionBuilder.ATTRS =
{
	/**
	 * The id of the textarea form field.
	 * 
	 * @attribute fieldId
	 * @type {String}
	 * @default Y.guid()
	 * @writeonce
	 */
	fieldId:
	{
		value:     Y.guid(),
		validator: Y.Lang.isString,
		writeOnce: true
	},

	/**
	 * The name of the textarea form field.
	 * 
	 * @attribute fieldName
	 * @type {String}
	 * @default ""
	 * @writeonce
	 */
	fieldName:
	{
		value:     '',
		validator: Y.Lang.isString,
		writeOnce: true
	},

	/**
	 * The FormManager to use when validating the constructed expression.
	 * 
	 * @attribute formMgr
	 * @type {Y.FormManager}
	 * @default null
	 * @writeonce
	 */
	formMgr:
	{
		validator: function(o) { return (!o || o instanceof Y.FormManager); },
		writeOnce: true
	},

	/**
	 * The QueryBuilder to help the user construct the expression.  The
	 * widget must not be rendered.  For each variable type, the values of
	 * the configured operations must be the pattern to be inserted into
	 * the expression. {value} will be replaced by the value entered by the
	 * user.
	 * 
	 * @attribute queryBuilder
	 * @type {Y.QueryBuilder}
	 * @default null
	 * @required
	 * @writeonce
	 */
	queryBuilder:
	{
		validator: function(o) { return (!o || o instanceof Y.QueryBuilder); },
		writeOnce: true
	},

	/**
	 * A map of QueryBuilder operators to objects defining
	 * {operator,pattern}.  This is needed if a variable type generates
	 * multiple values, and the values must be combined with something
	 * other than AND.
	 * 
	 * @attribute combinatorMap
	 * @type {Object}
	 * @default null
	 */
	combinatorMap:
	{
		validator: Y.Lang.isObject
	},

	/**
	 * The label for the Insert Parentheses button.
	 * 
	 * @attribute parenLabel
	 * @type {String}
	 * @default "()"
	 * @writeonce
	 */
	parenLabel:
	{
		value:     '()',
		validator: Y.Lang.isString,
		writeOnce: true
	},

	/**
	 * The label for the AND button.
	 * 
	 * @attribute andLabel
	 * @type {String}
	 * @default "AND"
	 * @writeonce
	 */
	andLabel:
	{
		value:     'AND',
		validator: Y.Lang.isString,
		writeOnce: true
	},

	/**
	 * The label for the OR button.
	 * 
	 * @attribute orLabel
	 * @type {String}
	 * @default "OR"
	 * @writeonce
	 */
	orLabel:
	{
		value:     'OR',
		validator: Y.Lang.isString,
		writeOnce: true
	},

	/**
	 * The label for the NOT button.
	 * 
	 * @attribute notLabel
	 * @type {String}
	 * @default "NOT"
	 * @writeonce
	 */
	notLabel:
	{
		value:     'NOT',
		validator: Y.Lang.isString,
		writeOnce: true
	},

	/**
	 * The label for the Clear button.
	 * 
	 * @attribute clearLabel
	 * @type {String}
	 * @default "Clear"
	 * @writeonce
	 */
	clearLabel:
	{
		value:     'Clear',
		validator: Y.Lang.isString,
		writeOnce: true
	},

	/**
	 * The label for the Insert button.
	 * 
	 * @attribute insertLabel
	 * @type {String}
	 * @default "Insert"
	 * @writeonce
	 */
	insertLabel:
	{
		value:     'Insert',
		validator: Y.Lang.isString,
		writeOnce: true
	},

	/**
	 * The label for the Reset button.
	 * 
	 * @attribute resetLabel
	 * @type {String}
	 * @default "Cancel"
	 * @writeonce
	 */
	resetLabel:
	{
		value:     'Cancel',
		validator: Y.Lang.isString,
		writeOnce: true
	},

	/**
	 * The error message for an unclosed parenthesis. <q>context</q> is
	 * replaced by the portion of the expression that generated the error.
	 * 
	 * @attribute tooManyParensError
	 * @type {String}
	 * @default 'The expression contains an extra closing parenthesis at "{context}".'
	 */
	tooManyParensError:
	{
		value:     'The expression contains an extra closing parenthesis at "{context}...".',
		validator: Y.Lang.isString
	},

	/**
	 * The error message for an unmatched single quote.
	 * 
	 * @attribute unmatchedSingleQuoteError
	 * @type {String}
	 * @default 'The expression contains an unmatched single quote.'
	 */
	unmatchedSingleQuoteError:
	{
		value:     'The expression contains an unmatched single quote at "{context}...".',
		validator: Y.Lang.isString
	},

	/**
	 * The error message for an unclosed parenthesis.
	 * 
	 * @attribute unclosedParenError
	 * @type {String}
	 * @default 'The expression contains an unclosed parenthesis.'
	 */
	unclosedParenError:
	{
		value:     'The expression contains an unclosed parenthesis at "{context}...".',
		validator: Y.Lang.isString
	},

	/**
	 * The error message when the user forgets to select a variable for
	 * insertion.
	 * 
	 * @attribute noVariableSelectedError
	 * @type {String}
	 * @default 'Please choose a variable.'
	 */
	noVariableSelectedError:
	{
		value:     'Please choose a variable.',
		validator: Y.Lang.isString
	}
};

function updateIERange()
{
	this.ie_range = document.selection.createRange();
}

function insertText(text, offset)
{
	offset = offset || text.length;

	this.field.focus();
	var el = this.field.getDOMNode();

	if (el.setSelectionRange)		// For Mozilla/WebKit
	{
		var start = el.selectionStart;
		el.value =
			el.value.substring(0, start) +
			text +
			el.value.substring(el.selectionEnd, el.value.length);

		var index = start + offset;
		el.setSelectionRange(index, index);
	}
	else if (document.selection)	// For IE
	{
		if (!this.ie_range)
		{
			this.ie_range = document.selection.createRange();
		}

		var r  = this.ie_range.duplicate();
		r.text = text;

		this.ie_range.move('character', offset);
		this.ie_range.select();
	}
}

function paren(e)
{
	insertText.call(this, '()', 1);
	e.halt();
}

function handler(key)
{
	return function(e)
	{
		insertText.call(this, ' ' + this.get(key+'Label') + ' ');
		e.halt();
	};
}

function clear(e)
{
	this.clear();
	e.halt();
}

function insertQB(e)
{
	var qb = this.get('queryBuilder');
	if (!qb.validateFields())
	{
		e.halt();
		return;
	}

	var query = qb.toDatabaseQuery();
	if (query.length === 0)
	{
		var el = qb.get('contentBox').one('select');
		qb.displayFieldMessage(el, this.get('noVariableSelectedError'), 'error');
		e.halt();
		return;
	}

	var map = this.get('combinatorMap');

	var s     = '';
	var op    = ' ' + this.get('andLabel') + ' ';
	for (var i=0; i<query.length; i++)
	{
		var q = query[i];

		if (i > 0)
		{
			s += op;
		}
		s += q[0];

		var pattern = q[1];
		if (pattern.indexOf('{') == -1)
		{
			pattern += '{value}';
		}

		var combinator = map && map[ q[1] ];
		if (combinator)
		{
			op      = combinator.operator;
			pattern = combinator.pattern;
		}

		s += Y.Lang.substitute(pattern,
		{
			value: q[2].replace(/'/g, '\\\'')
		});
	}

	insertText.call(this, s);
	qb.reset();
	e.halt();
}

function resetQB(e)
{
	this.get('queryBuilder').reset();

	if (e)
	{
		e.halt();
	}
}

function setValidation(f)
{
	if (!f)
	{
		return;
	}

	var self = this;

	var orig_validateForm = f.validateForm;
	f.validateForm = function()
	{
		resetQB.call(self);
		return orig_validateForm.apply(this, arguments);
	};

	f.setFunction(this.get('fieldId'), function(form, e)
	{
		return self._validateExpression(form, e, this);
	});
}

Y.extend(ExpressionBuilder, Y.Widget,
{
	initializer: function(config)
	{
		// FormManager

		setValidation.call(this, config.formMgr);
		this.after('formMgrChange', function(e)
		{
			if (e.prevVal)
			{
				e.prevVal.setFunction(this.get('fieldId'), null);
			}

			setValidation.call(this, e.newVal);
		});
	},

	renderUI: function()
	{
		var container = this.get('contentBox');
		container.set('innerHTML', this._field());

		// textarea

		this.field = container.one('#'+this.get('fieldId'));

		if (document.selection)
		{
			this.field.on('change', updateIERange, this);
		}

		// basic controls

		container.one('.'+this.getClassName('paren')).on('click', paren, this);

		var op = [ 'and', 'or', 'not' ];
		for (var i=0; i<op.length; i++)
		{
			container.one('.'+this.getClassName(op[i])).on('click', handler(op[i]), this);
		}

		container.one('.'+this.getClassName('clear')).on('click', clear, this);

		// QueryBuilder

		var qb = this.get('queryBuilder');
		if (qb)
		{
			container.appendChild(Y.Node.create(this._query()));

			qb.render(container.one('.'+this.getClassName('querybuilder')));

			container.one('.'+this.getClassName('insert')).on('click', insertQB, this);
			container.one('.'+this.getClassName('reset')).on('click', resetQB, this);
		}
	},

	destructor: function()
	{
		var qb = this.get('queryBuilder');
		if (qb)
		{
			qb.destroy();
		}

		this.ie_range = null;
	},

	/**
	 * Clears the expression.
	 * 
	 * @method clear
	 */
	clear: function()
	{
		this.field.set('value', '');
		this.field.focus();
	},

	/**
	 * Validate the expression.
	 * 
	 * @method _validateExpression
	 * @protected
	 * @return {Boolean} <code>true</code> if the expression has balanced parens and single quotes
	 */
	_validateExpression: function(form, e, form_mgr)
	{
		var s     = e.get('value');
		var paren = 0;
		var pi    = -1;
		var quote = false;
		var qi    = -1;
		for (var i=0; i<s.length; i++)
		{
			var c = s.charAt(i);
			if (!quote && c == '(')
			{
				if (paren === 0)
				{
					pi = i;
				}
				paren++;
			}
			else if (!quote && c == ')')
			{
				paren--;
				if (paren < 0)
				{
					var msg = Y.Lang.substitute(this.get('tooManyParensError'),
					{
						context: s.substr(0,i+1)
					});
					form_mgr.displayMessage(e, msg, 'error');
					return false;
				}
			}
			else if (c == '\'' && (i === 0 || s.charAt(i-1) != '\\'))
			{
				if (!quote)
				{
					qi = i;
				}
				quote = ! quote;
			}
		}

		if (quote && (paren === 0 || qi < pi))
		{
			var msg = Y.Lang.substitute(this.get('unmatchedSingleQuoteError'),
			{
				context: s.substr(0,qi+1)
			});
			form_mgr.displayMessage(e, msg, 'error');
			return false;
		}
		else if (paren > 0)
		{
			var msg = Y.Lang.substitute(this.get('unclosedParenError'),
			{
				context: s.substr(0,pi+1)
			});
			form_mgr.displayMessage(e, msg, 'error');
			return false;
		}

		return true;
	},

	//
	// Markup
	//

	/**
	 * @method _field
	 * @protected
	 * @return {String} markup for the textarea and basic buttons
	 */
	_field: function()
	{
		var markup =
			'<div class="{td}">' +
				'<textarea id="{tid}" name="{tn}" class="{ff} {ta}"></textarea>' +
			'</div>' +
			'<div class="{fctl}">' +
				'<button type="button" class="yui3-button {pc}">{paren}</button>' +
				'<button type="button" class="yui3-button {ac}">{and}</button>' +
				'<button type="button" class="yui3-button {oc}">{or}</button>' +
				'<button type="button" class="yui3-button {nc}">{not}</button>' +
				'<button type="button" class="yui3-button {cc}">{clear}</button>' +
			'</div>';

		return Y.Lang.substitute(markup,
		{
			td:     this.getClassName('field-container'),
			ff:     Y.FormManager.field_marker_class,
			ta:     this.getClassName('field'),
			tid:    this.get('fieldId'),
			tn:     this.get('fieldName'),
			fctl:   this.getClassName('controls'),
			pc:     this.getClassName('paren'),
			ac:     this.getClassName('and'),
			oc:     this.getClassName('or'),
			nc:     this.getClassName('not'),
			cc:     this.getClassName('clear'),
			paren:  this.get('parenLabel'),
			and:    this.get('andLabel'),
			or:     this.get('orLabel'),
			not:    this.get('notLabel'),
			clear:  this.get('clearLabel')
		});
	},

	/**
	 * @method _query
	 * @protected
	 * @return {String} markup for the QueryBuilder
	 */
	_query: function()
	{
		var markup =
			'<div class="{qb}"></div>' +
			'<div class="{qbctl} {fr}">' +
				'<button type="button" class="yui3-button {ic}">{insert}</button>' +
				'<button type="button" class="yui3-button {rc}">{reset}</button>' +
			'</div>';

		return Y.Lang.substitute(markup,
		{
			qb:     this.getClassName('querybuilder'),
			qbctl:  this.getClassName('querybuilder-controls'),
			fr:     Y.FormManager.row_marker_class,
			ic:     this.getClassName('insert'),
			rc:     this.getClassName('reset'),
			insert: this.get('insertLabel'),
			reset:  this.get('resetLabel')
		});
	}
});

Y.ExpressionBuilder = ExpressionBuilder;