1 /*!
  2 * This file is part of Aloha Editor Project http://aloha-editor.org
  3 * Copyright © 2010-2011 Gentics Software GmbH, aloha@gentics.com
  4 * Contributors http://aloha-editor.org/contribution.php 
  5 * Licensed unter the terms of http://www.aloha-editor.org/license.html
  6 *//*
  7 * Aloha Editor is free software: you can redistribute it and/or modify
  8 * it under the terms of the GNU Affero General Public License as published by
  9 * the Free Software Foundation, either version 3 of the License, or
 10 * (at your option) any later version.*
 11 *
 12 * Aloha Editor is distributed in the hope that it will be useful,
 13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 15 * GNU Affero General Public License for more details.
 16 *
 17 * You should have received a copy of the GNU Affero General Public License
 18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 19 */
 20 
 21 define( [ 'aloha/core', 'aloha/jquery', 'aloha/ext', 'i18n!aloha/nls/i18n', 'aloha/ui',
 22   'aloha/repositorymanager', 'aloha/selection', 'aloha/ext-alohaproxy',
 23   'aloha/ext-alohareader'
 24 ], function ( Aloha, jQuery, Ext, i18n, Ui, RepositoryManager, Selection ) {
 25 // TODO add parameter for UI class after refactoring UI to requirejs
 26 	'use strict';
 27 
 28 var undefined = void 0;
 29 
 30 var extTemplate = function ( tpl ) {
 31 	if ( tpl ) {
 32 		tpl = '<tpl for="."><div class="x-combo-list-item">' +
 33 			'<tpl if="this.hasRepositoryTemplate(values)">{[ this.renderRepositoryTemplate(values) ]}</tpl>' +
 34 			'<tpl if="!this.hasRepositoryTemplate(values)">' + tpl + '</tpl>' +
 35 			'</div></tpl>';
 36 	} else {
 37 		tpl = '<tpl for="."><div class="x-combo-list-item">' +
 38 			'<tpl if="this.hasRepositoryTemplate(values)">{[ this.renderRepositoryTemplate(values) ]}</tpl>' +
 39 			'<tpl if="!this.hasRepositoryTemplate(values)"><span><b>{name}</b></span></tpl>' +
 40 			'</div></tpl>';
 41 	}
 42 	return new Ext.XTemplate(
 43 		tpl,
 44 		{
 45  46 			hasRepositoryTemplate : function ( values ) {
 47 				var rep = RepositoryManager.getRepository( values.repositoryId );
 48 				return rep && rep.hasTemplate();
 49 			},
 50 			renderRepositoryTemplate : function ( values ) {
 51 				var rep = RepositoryManager.getRepository( values.repositoryId );
 52 				if ( rep && rep.hasTemplate() ) {
 53 				// create extTemplate if template changed
 54 				if ( !rep._ExtTPL || rep.template !== rep._ExtTPLcache ) {
 55 					rep._ExtTPL = new Ext.XTemplate( rep.template );
 56 					rep._ExtTPLcache = rep.template;
 57 				}
 58 				return rep._ExtTPL.apply( values );
 59 			}
 60 		}
 61 	} );
 62 };
 63 
 64 
 65 // This will store the last attribute value. We need to keep track of this value
 66 // due to decide whether to update the value on finish editing
 67 var lastAttributeValue;
 68 
 69 Ext.ux.AlohaAttributeField = Ext.extend( Ext.form.ComboBox, {
 70 	typeAhead     : false,
 71 	mode          : 'remote',
 72 	triggerAction : 'all',
 73 	width         : 300,
 74 	hideTrigger   : true,
 75 	minChars      : 3,
 76 	valueField    : 'id',
 77 	displayField  : 'name',
 78 	listEmptyText : i18n.t( 'repository.no_item_found' ),
 79 	loadingText   : i18n.t( 'repository.loading' ) + '...',
 80 	enableKeyEvents : true,
 81 	store: new Ext.data.Store( {
 82 		proxy: new Ext.data.AlohaProxy(),
 83 		reader: new Ext.data.AlohaObjectReader()
 84 	} ),
 85     clickAttached: false, // remember that the click event has been attached to the innerList, as this is not implemented in the combobox
 86     tpl      : extTemplate(),
 87     onSelect : function ( item ) {
 88 		this.setItem( item.data );
 89 		if ( typeof this.alohaButton.onSelect == 'function' ) {
 90 			this.alohaButton.onSelect.call( this.alohaButton, item.data );
 91 		}
 92 		this.collapse();
 93 	},
 94 	finishEditing : function () {
 95 		var target = jQuery( this.getTargetObject() ), color;
 96 		
 97 		// Remove the highlighting and restore original color if was set before
 98 		
 99 		// changed if check from 'target' to this.getTargetObject(), because
100 		// target is a jQuery Object and always true
101 		if ( this.getTargetObject() ) {
102 			if ( color = target.attr( 'data-original-background-color' ) ) {
103 				jQuery( target ).css( 'background-color', color );
104 			} else {
105 				jQuery( target ).css( 'background-color', '' );
106 			}
107 			jQuery( target ).removeAttr( 'data-original-background-color' );
108 		
109 			// Check whether the attribute was changed since the last focus event. Return early when the attribute was not changed.
110 			if ( lastAttributeValue === target.attr( this.targetAttribute ) ) {
111 				return;
112 			}
113 		}
114 		
115 		// when no resource item was selected, remove any marking of the target object
116 		if ( !this.resourceItem ) {
117 			RepositoryManager.markObject( this.targetObject );
118 		}
119 
120 		if ( this.getValue() === '' ) {
121 			if ( this.wrap ) {
122 				jQuery( this.wrap.dom.children[0] ).css( 'color', '#AAA' );
123 			}
124 			this.setValue( this.placeholder );
125 		}
126 	},
127     listeners: {
128 		// repository object types could have changed
129 		'beforequery': function ( event ) {
130 			if ( this.noQuery ) {
131 				event.cancel = true;
132 				return;
133 			}
134 			if ( this.store !== null && this.store.proxy !== null ) {
135 				this.store.proxy.setParams( {
136 					objectTypeFilter: this.getObjectTypeFilter(),
137 					queryString: event.query
138 				} );
139 			}
140 		},
141 		'afterrender': function ( obj, event ) {
142 			jQuery( this.wrap.dom.children[0] ).css( 'color', '#AAA' );
143 			this.setValue( this.placeholder );
144 		},
145 		'keydown': function ( obj, event ) {
146 			// on ENTER or ESC leave the editing
147 			// just remember here the status and remove cursor on keyup event
148 			// Otherwise cursor moves to content and no more blur event happens!!??
149 			if ( event.keyCode == 13 || event.keyCode == 27 ) {
150 				if ( this.isExpanded() ) {
151 					this.ALOHAwasExpanded = true;
152 				} else {
153 					this.ALOHAwasExpanded = false;
154 				}
155 				event.preventDefault();
156 			}
157 		},
158 		'keyup': function ( obj, event ) {
159 			if ( ( event.keyCode == 13 || event.keyCode == 27 ) &&
160 					!this.ALOHAwasExpanded ) {
161 				// work around stupid behavior when moving focus
162 				window.setTimeout( function () {
163 					// Set focus to link element and select the object
164 					Selection.getRangeObject().select();
165 				}, 0 );
166 			}
167 
168 			// when a resource item was (initially) set, but the current value
169 			// is different from the reference value, we unset the resource item
170 			if ( this.resourceItem &&
171 					this.resourceValue !== this.wrap.dom.children[0].value ) {
172 				this.resourceItem = null;
173 				this.resourceValue = null;
174 			}
175 
176 			// update attribute, but only if no resource item was selected
177 			if ( !this.resourceItem ) {
178 				var v = this.wrap.dom.children[0].value;
179 				this.setAttribute( this.targetAttribute, v );
180 			}
181 		},
182 		'focus': function ( obj, event ) {
183 			// set background color to give visual feedback which link is modified
184 			var	target = jQuery( this.getTargetObject() ),
185 				s = target.css( 'background-color' );
186 			
187 			if ( this.getValue() === this.placeholder ) {
188 				this.setValue( '' );
189 				jQuery( this.wrap.dom.children[0] ).css( 'color', 'black' );
190 			}
191 			if ( target && target.context && target.context.style &&
192 					target.context.style[ 'background-color' ] ) {
193 				target.attr( 'data-original-background-color',
194 					target.context.style[ 'background-color' ] );
195 			}
196 			target.css( 'background-color', '#80B5F2' );
197 		},
198 		'blur': function ( obj, event ) {
199 			this.finishEditing();
200 		},
201 		'hide': function ( obj, event ) {
202 			this.finishEditing();
203 		},
204 		'expand': function ( combo ) {
205 			if ( this.noQuery ) {
206 				this.collapse();
207 			}
208 			if ( !this.clickAttached ) {
209 				var that = this;
210 				// attach the mousedown event to set the event handled,
211 				// so that the editable will not get deactivated
212 				this.mon( this.innerList, 'mousedown', function ( event ) {
213 					Aloha.eventHandled = true;
214 				}, this );
215 				// in the mouseup event, the flag will be reset
216 				this.mon( this.innerList, 'mouseup', function ( event ) {
217 					Aloha.eventHandled = false;
218 				}, this );
219 				this.clickAttached = true;
220 			}
221 		}
222 	},
223 	setItem: function ( item, displayField ) {
224 		this.resourceItem = item;
225 		
226 		if ( item ) {
227 			displayField = ( displayField ) ? displayField : this.displayField;
228 			// TODO split display field by '.' and get corresponding attribute, because it could be a properties attribute.
229 			var v = item[ displayField ];
230 			// set the value into the field
231 			this.setValue( v );
232 			// store the value to be the "reference" value for the currently selected resource item
233 			this.resourceValue = v;
234 			// set the attribute to the target object
235 			this.setAttribute( this.targetAttribute, item[ this.valueField ] );
236 			// call the repository marker
237 			RepositoryManager.markObject( this.targetObject, item );
238 		} else {
239 			// unset the reference value, since no resource item is selected
240 			this.resourceValue = null;
241 		}
242 	},
243 	getItem: function () {
244 		return this.resourceItem;
245 	},
246 	// Private hack to allow attribute setting by regex
247 	setAttribute: function ( attr, value, regex, reference ) {
248 		var setAttr = true, regxp;
249 		if ( this.targetObject) {
250 			// check if a reference value is submitted to check against with a regex
251 			if ( typeof reference != 'undefined' ) {
252 				regxp = new RegExp( regex );
253 				if ( !reference.match( regxp ) ) {
254 					setAttr = false;
255 				}
256 			}
257 
258 			// if no regex was successful or no reference value
259 			// was submitted remove the attribute
260 			if ( setAttr ) {
261 				jQuery( this.targetObject ).attr( attr, value );
262 			} else {
263 				jQuery( this.targetObject ).removeAttr( attr );
264 			}
265 		}
266 	},
267 	setTargetObject : function ( obj, attr ) {
268 	    var that = this;
269 		this.targetObject = obj;
270 	    this.targetAttribute = attr;
271 	    this.setItem( null );
272 	    
273 	    if ( obj && attr ) {
274 	    	lastAttributeValue = jQuery( obj ).attr( attr );
275 	    }
276 
277 		if ( this.targetObject && this.targetAttribute ) {
278 			this.setValue( jQuery( this.targetObject ).attr( this.targetAttribute ) );
279 		} else {
280 			this.setValue( '' );
281 		}
282 
283 		// check whether a repository item is linked to the object
284 		var that = this;
285 		RepositoryManager.getObject( obj, function ( items ) {
286 			if ( items && items.length > 0 ) {
287 				that.setItem( items[0] );
288 			}
289 		} );
290 	},
291 	getTargetObject : function () {
292 	    return this.targetObject;
293 	},
294 	setObjectTypeFilter : function ( otFilter ) {
295 		this.objectTypeFilter = otFilter;
296 	},
297 	getObjectTypeFilter : function () {
298 		return this.objectTypeFilter;
299 	},
300 	noQuery: true
301 });
302 
303 /**
304  * Register the Aloha attribute field
305  * @hide
306  */
307 Ext.reg( 'alohaattributefield', Ext.ux.AlohaAttributeField );
308 
309 /**
310  * Aloha Attribute Field Button
311  * @namespace Aloha.ui
312  * @class AttributeField
313  */
314 Ui.AttributeField = Ui.Button.extend( {
315 	_constructor: function ( properties ) {
316 		/**
317 		 * @cfg Function called when an element is selected
318 		 */
319 		this.onSelect = null;
320 		this.listenerQueue = [];
321 		this.objectTypeFilter = null;
322 		this.tpl = null;
323 		this.displayField = null;
324 		this.valueField = null;
325 
326 		this.init( properties );
327 	},
328 
329 	/**
330 	 * Create a extjs alohaattributefield
331 	 * @hide
332 	 */
333 	getExtConfigProperties: function () {
334 		var props = {
335 		    alohaButton : this,
336 		    xtype       : 'alohaattributefield',
337 		    rowspan     : this.rowspan || undefined,
338 		    width       : this.width || undefined,
339 		    placeholder : this.placeholder || undefined,
340 		    id          : this.id,
341 		    cls         : this.cls || undefined
342 		};
343 		if ( this.valueField ) {
344 			props.valueField = this.valueField;
345 		}
346 		if ( this.displayField ) {
347 			props.displayField = this.displayField;
348 		}
349 		if ( this.minChars ) {
350 			props.minChars = this.minChars;
351 		}
352 		return props;
353 	},
354 
355 	/**
356 	 * Sets the target Object of which the Attribute should be modified
357 	 * @param {jQuery} obj the target object
358 	 * @param {String} attr Attribute to be modified ex. "href" of a link
359 	 * @void
360 	 */
361 	setTargetObject: function ( obj, attr ) {
362 		if ( this.extButton ) {
363 			this.extButton.setTargetObject( obj, attr );
364 		}
365 	},
366 
367 	/**
368 	 * @return {jQuery} object Returns the current target Object
369 	 */
370 	getTargetObject: function () {
371 		return this.extButton ? this.extButton.getTargetObject() : null;
372 	},
373 
374 	/**
375 	 * Focus to this field
376 	 * @void
377 	 */
378 	focus: function () {
379 		if ( this.extButton ) {
380 			this.extButton.focus();
381 			if ( this.extButton.getValue().length > 0 ) {
382 				this.extButton.selectText( 0, this.extButton.getValue().length );
383 			}
384 		}
385 	},
386 
387 	/**
388 	 * Adding a listener to the field
389 	 * @param {String} eventname The name of the event. Ex. 'keyup'
390 	 * @param {function} handler The function that should be called when the event happens.
391 	 * @param {Object} scope The scope object which the event should be attached
392 	 */
393 	addListener: function ( eventName, handler, scope ) {
394 		var listener;
395 
396 		if ( this.extButton ) {
397 			this.extButton.addListener( eventName, handler, null );
398 		} else {
399 			// if extButton not yet initialized adding listeners could be a problem
400 			// so all events are collected in a queue and added on initalizing
401 			listener = {
402 				'eventName' : eventName,
403 				'handler'   : handler,
404 				'scope'     : scope,
405 				'options'   : null
406 			};
407 			this.listenerQueue.push( listener );
408 		}
409 	},
410 
411 	/**
412 	 * Sets an attribute optionally based on a regex on reference
413 	 * @param {String} attr The Attribute name which should be set. Ex. "lang"
414 	 * @param {String} value The value to set. Ex. "de-AT"
415 	 * @param {String} regex The regex when the attribute should be set. The regex is applied to the value of refernece.
416 	 * @param {String} reference The value for the regex.
417 	 */
418 	setAttribute: function ( attr, value, regex, reference ) {
419 		if ( this.extButton ) {
420 			this.extButton.setAttribute( attr, value, regex, reference );
421 		}
422 	},
423 
424 	/**
425 	 * When at least on objectType is set the value in the Attribute field does a query to all registered repositories.
426 	 * @param {Array} objectTypeFilter The array of objectTypeFilter to be searched for.
427 	 * @void
428 	 */
429 	setObjectTypeFilter: function ( objectTypeFilter ) {
430 		if ( this.extButton ) {
431 			this.noQuery = false;
432 			this.extButton.setObjectType( objectTypeFilter );
433 		} else {
434 			if ( !objectTypeFilter ) {
435 				objectTypeFilter = 'all';
436 			}
437 			this.objectTypeFilter = objectTypeFilter;
438 		}
439 	},
440 
441 	/**
442 	 * Sets an item to the link tag.
443 	 * @param {resourceItem} item
444 	 */
445 	setItem: function ( item , displayField ) {
446 		if ( this.extButton ) {
447 			this.extButton.setItem( item, displayField );
448 		}
449 	},
450 
451 	/**
452 	 * Gets current item set.
453 	 * @return {resourceItem} item
454 	 */
455 	getItem: function () {
456 		if ( this.extButton ) {
457 			return this.extButton.getItem();
458 		}
459 		return null;
460 	},
461 
462 	/**
463 	 * Returns the current value
464 	 * @return {String} attributeValue
465 	 */
466 	getValue: function () {
467 		if ( this.extButton ) {
468 			return this.extButton.getValue();
469 		}
470 		return null;
471 	},
472 
473 	/**
474 	 * Sets the current value
475 	 * @param {String} v an attributeValue
476 	 */
477 	setValue: function ( v ) {
478 		if ( this.extButton ) {
479 			this.extButton.setValue( v );
480 		}
481 	},
482 
483 	/**
484 	 * Returns the current query value.
485 	 * @return {String} queryValue
486 	 */
487 	getQueryValue: function () {
488 		if ( this.extButton ) {
489 			return this.extButton.getValue();
490 
491 			// Petro:
492 			// It is not clear why the value was being read in this "low-level" way and
493 			// not through `getValue()'. In any case, doing so, occasionally caused
494 			// errors, when this.extButton.wrap is `undefined'. We will therefore read
495 			// the value in the manner we do above.
496 			// return this.extButton.wrap.dom.children[0].value;
497 		}
498 		return null;
499 	},
500 
501 	/**
502 	 * Set the display field, which is displayed in the combobox
503 	 * @param {String} displayField name of the field to be displayed
504 	 * @return display field name on success, null otherwise
505 	 */
506 	setDisplayField: function ( displayField ) {
507 		var result;
508 		if ( this.extButton ) {
509 510 			result = this.extButton.displayField = displayField;
511 		} else {
512 			result = this.displayField = displayField;
513 		}
514 		return result;
515 	},
516 
517 	/**
518 	 * Set the row template for autocomplete hints. The default template is:
519 	 * <span><b>{name}</b><br />{url}</span>
520 	 * @param {String} tpl template to be rendered for each row
521 	 * @return template on success or null otherwise
522 	 */
523 	setTemplate: function ( tpl ) {
524 		var result;
525 		if ( this.extButton ) {
526 			result = this.extButton.tpl = extTemplate( tpl );
527 		} else {
528 			result = this.tpl = extTemplate( tpl );
529 		}
530 		return result;
531 	}
532 
533 } );
534 
535 } );
536