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 		if ( target ) {
 99 			if ( color = target.attr( 'data-original-background-color' ) ) {
100 				jQuery( target ).css( 'background-color', color );
101 			} else {
102 				jQuery( target ).css( 'background-color', '' );
103 			}
104 			jQuery( target ).removeAttr( 'data-original-background-color' );
105 		}
106 		
107 		// Check whether the attribute was changed since the last focus event. Return early when the attribute was not changed.
108 		if ( lastAttributeValue === target.attr( this.targetAttribute ) ) {
109 			return;
110 		}
111 
112 		// when no resource item was selected, remove any marking of the target object
113 		if ( !this.resourceItem ) {
114 			RepositoryManager.markObject( this.targetObject );
115 		}
116 
117 		if ( this.getValue() === '' ) {
118 			if ( this.wrap ) {
119 				jQuery( this.wrap.dom.children[0] ).css( 'color', '#AAA' );
120 			}
121 			this.setValue( this.placeholder );
122 		}
123 	},
124     listeners: {
125 		// repository object types could have changed
126 		'beforequery': function ( event ) {
127 			if ( this.noQuery ) {
128 				event.cancel = true;
129 				return;
130 			}
131 			if ( this.store !== null && this.store.proxy !== null ) {
132 				this.store.proxy.setParams( {
133 					objectTypeFilter: this.getObjectTypeFilter(),
134 					queryString: event.query
135 				} );
136 			}
137 		},
138 		'afterrender': function ( obj, event ) {
139 			jQuery( this.wrap.dom.children[0] ).css( 'color', '#AAA' );
140 			this.setValue( this.placeholder );
141 		},
142 		'keydown': function ( obj, event ) {
143 			// on ENTER or ESC leave the editing
144 			// just remember here the status and remove cursor on keyup event
145 			// Otherwise cursor moves to content and no more blur event happens!!??
146 			if ( event.keyCode == 13 || event.keyCode == 27 ) {
147 				if ( this.isExpanded() ) {
148 					this.ALOHAwasExpanded = true;
149 				} else {
150 					this.ALOHAwasExpanded = false;
151 				}
152 				event.preventDefault();
153 			}
154 		},
155 		'keyup': function ( obj, event ) {
156 			if ( ( event.keyCode == 13 || event.keyCode == 27 ) &&
157 					!this.ALOHAwasExpanded ) {
158 				// work around stupid behavior when moving focus
159 				window.setTimeout( function () {
160 					// Set focus to link element and select the object
161 					Selection.getRangeObject().select();
162 				}, 0 );
163 			}
164 
165 			// when a resource item was (initially) set, but the current value
166 			// is different from the reference value, we unset the resource item
167 			if ( this.resourceItem &&
168 					this.resourceValue !== this.wrap.dom.children[0].value ) {
169 				this.resourceItem = null;
170 				this.resourceValue = null;
171 			}
172 
173 			// update attribute, but only if no resource item was selected
174 			if ( !this.resourceItem ) {
175 				var v = this.wrap.dom.children[0].value;
176 				this.setAttribute( this.targetAttribute, v );
177 			}
178 		},
179 		'focus': function ( obj, event ) {
180 			// set background color to give visual feedback which link is modified
181 			var	target = jQuery( this.getTargetObject() ),
182 				s = target.css( 'background-color' );
183 			
184 			if ( this.getValue() === this.placeholder ) {
185 				this.setValue( '' );
186 				jQuery( this.wrap.dom.children[0] ).css( 'color', 'black' );
187 			}
188 			if ( target && target.context && target.context.style &&
189 					target.context.style[ 'background-color' ] ) {
190 				target.attr( 'data-original-background-color',
191 					target.context.style[ 'background-color' ] );
192 			}
193 			target.css( 'background-color', '#80B5F2' );
194 		},
195 		'blur': function ( obj, event ) {
196 			this.finishEditing();
197 		},
198 		'hide': function ( obj, event ) {
199 			this.finishEditing();
200 		},
201 		'expand': function ( combo ) {
202 			if ( this.noQuery ) {
203 				this.collapse();
204 			}
205 			if ( !this.clickAttached ) {
206 				var that = this;
207 				// attach the mousedown event to set the event handled,
208 				// so that the editable will not get deactivated
209 				this.mon( this.innerList, 'mousedown', function ( event ) {
210 211 					Aloha.eventHandled = true;
212 				}, this );
213 				// in the mouseup event, the flag will be reset
214 				this.mon( this.innerList, 'mouseup', function ( event ) {
215 					Aloha.eventHandled = false;
216 				}, this );
217 				this.clickAttached = true;
218 			}
219 		}
220 	},
221 	setItem: function ( item, displayField ) {
222 		this.resourceItem = item;
223 		
224 		if ( item ) {
225 			displayField = ( displayField ) ? displayField : this.displayField;
226 			// TODO split display field by '.' and get corresponding attribute, because it could be a properties attribute.
227 			var v = item[ displayField ];
228 			// set the value into the field
229 			this.setValue( v );
230 			// store the value to be the "reference" value for the currently selected resource item
231 			this.resourceValue = v;
232 			// set the attribute to the target object
233 			this.setAttribute( this.targetAttribute, item[ this.valueField ] );
234 			// call the repository marker
235 			RepositoryManager.markObject( this.targetObject, item );
236 		} else {
237 			// unset the reference value, since no resource item is selected
238 			this.resourceValue = null;
239 		}
240 	},
241 	getItem: function () {
242 		return this.resourceItem;
243 	},
244 	// Private hack to allow attribute setting by regex
245 	setAttribute: function ( attr, value, regex, reference ) {
246 		var setAttr = true, regxp;
247 		if ( this.targetObject) {
248 			// check if a reference value is submitted to check against with a regex
249 			if ( typeof reference != 'undefined' ) {
250 				regxp = new RegExp( regex );
251 				if ( !reference.match( regxp ) ) {
252 					setAttr = false;
253 				}
254 			}
255 
256 			// if no regex was successful or no reference value
257 			// was submitted remove the attribute
258 			if ( setAttr ) {
259 				jQuery( this.targetObject ).attr( attr, value );
260 			} else {
261 				jQuery( this.targetObject ).removeAttr( attr );
262 			}
263 		}
264 	},
265 	setTargetObject : function ( obj, attr ) {
266 	    var that = this;
267 		this.targetObject = obj;
268 	    this.targetAttribute = attr;
269 	    this.setItem( null );
270 	    
271 	    if ( obj && attr ) {
272 	    	lastAttributeValue = jQuery( obj ).attr( attr );
273 	    }
274 
275 		if ( this.targetObject && this.targetAttribute ) {
276 			this.setValue( jQuery( this.targetObject ).attr( this.targetAttribute ) );
277 		} else {
278 			this.setValue( '' );
279 		}
280 
281 		// check whether a repository item is linked to the object
282 		var that = this;
283 		RepositoryManager.getObject( obj, function ( items ) {
284 			if ( items && items.length > 0 ) {
285 				that.setItem( items[0] );
286 			}
287 		} );
288 	},
289 	getTargetObject : function () {
290 	    return this.targetObject;
291 	},
292 	setObjectTypeFilter : function ( otFilter ) {
293 		this.objectTypeFilter = otFilter;
294 	},
295 	getObjectTypeFilter : function () {
296 		return this.objectTypeFilter;
297 	},
298 	noQuery: true
299 });
300 
301 /**
302  * Register the Aloha attribute field
303  * @hide
304  */
305 Ext.reg( 'alohaattributefield', Ext.ux.AlohaAttributeField );
306 
307 /**
308  * Aloha Attribute Field Button
309  * @namespace Aloha.ui
310  * @class AttributeField
311  */
312 Ui.AttributeField = Ui.Button.extend( {
313 	_constructor: function ( properties ) {
314 		/**
315 		 * @cfg Function called when an element is selected
316 		 */
317 		this.onSelect = null;
318 		this.listenerQueue = [];
319 		this.objectTypeFilter = null;
320 		this.tpl = null;
321 		this.displayField = null;
322 		this.valueField = null;
323 
324 		this.init( properties );
325 	},
326 
327 	/**
328 	 * Create a extjs alohaattributefield
329 	 * @hide
330 	 */
331 	getExtConfigProperties: function () {
332 		var props = {
333 		    alohaButton : this,
334 		    xtype       : 'alohaattributefield',
335 		    rowspan     : this.rowspan || undefined,
336 		    width       : this.width || undefined,
337 		    placeholder : this.placeholder || undefined,
338 		    id          : this.id,
339 		    cls         : this.cls || undefined
340 		};
341 		if ( this.valueField ) {
342 			props.valueField = this.valueField;
343 		}
344 		if ( this.displayField ) {
345 			props.displayField = this.displayField;
346 		}
347 		if ( this.minChars ) {
348 			props.minChars = this.minChars;
349 		}
350 		return props;
351 	},
352 
353 	/**
354 	 * Sets the target Object of which the Attribute should be modified
355 	 * @param {jQuery} obj the target object
356 	 * @param {String} attr Attribute to be modified ex. "href" of a link
357 	 * @void
358 	 */
359 	setTargetObject: function ( obj, attr ) {
360 		if ( this.extButton ) {
361 			this.extButton.setTargetObject( obj, attr );
362 		}
363 	},
364 
365 	/**
366 	 * @return {jQuery} object Returns the current target Object
367 	 */
368 	getTargetObject: function () {
369 		return this.extButton ? this.extButton.getTargetObject() : null;
370 	},
371 
372 	/**
373 	 * Focus to this field
374 	 * @void
375 	 */
376 	focus: function () {
377 		if ( this.extButton ) {
378 			this.extButton.focus();
379 			if ( this.extButton.getValue().length > 0 ) {
380 				this.extButton.selectText( 0, this.extButton.getValue().length );
381 			}
382 		}
383 	},
384 
385 	/**
386 	 * Adding a listener to the field
387 	 * @param {String} eventname The name of the event. Ex. 'keyup'
388 	 * @param {function} handler The function that should be called when the event happens.
389 	 * @param {Object} scope The scope object which the event should be attached
390 	 */
391 	addListener: function ( eventName, handler, scope ) {
392 		var listener;
393 
394 		if ( this.extButton ) {
395 			this.extButton.addListener( eventName, handler, null );
396 		} else {
397 			// if extButton not yet initialized adding listeners could be a problem
398 			// so all events are collected in a queue and added on initalizing
399 			listener = {
400 				'eventName' : eventName,
401 				'handler'   : handler,
402 				'scope'     : scope,
403 				'options'   : null
404 			};
405 			this.listenerQueue.push( listener );
406 		}
407 	},
408 
409 	/**
410 	 * Sets an attribute optionally based on a regex on reference
411 	 * @param {String} attr The Attribute name which should be set. Ex. "lang"
412 	 * @param {String} value The value to set. Ex. "de-AT"
413 	 * @param {String} regex The regex when the attribute should be set. The regex is applied to the value of refernece.
414 	 * @param {String} reference The value for the regex.
415 	 */
416 	setAttribute: function ( attr, value, regex, reference ) {
417 		if ( this.extButton ) {
418 			this.extButton.setAttribute( attr, value, regex, reference );
419 		}
420 	},
421 
422 	/**
423 	 * When at least on objectType is set the value in the Attribute field does a query to all registered repositories.
424 	 * @param {Array} objectTypeFilter The array of objectTypeFilter to be searched for.
425 	 * @void
426 	 */
427 	setObjectTypeFilter: function ( objectTypeFilter ) {
428 		if ( this.extButton ) {
429 			this.noQuery = false;
430 			this.extButton.setObjectType( objectTypeFilter );
431 		} else {
432 			if ( !objectTypeFilter ) {
433 				objectTypeFilter = 'all';
434 			}
435 			this.objectTypeFilter = objectTypeFilter;
436 		}
437 	},
438 
439 	/**
440 	 * Sets an item to the link tag.
441 	 * @param {resourceItem} item
442 	 */
443 	setItem: function ( item , displayField ) {
444 		if ( this.extButton ) {
445 			this.extButton.setItem( item, displayField );
446 		}
447 	},
448 
449 	/**
450 	 * Gets current item set.
451 	 * @return {resourceItem} item
452 	 */
453 	getItem: function () {
454 		if ( this.extButton ) {
455 			return this.extButton.getItem();
456 		}
457 		return null;
458 	},
459 
460 	/**
461 	 * Returns the current value
462 	 * @return {String} attributeValue
463 	 */
464 	getValue: function () {
465 		if ( this.extButton ) {
466 			return this.extButton.getValue();
467 		}
468 		return null;
469 	},
470 
471 	/**
472 	 * Sets the current value
473 	 * @param {String} v an attributeValue
474 	 */
475 	setValue: function ( v ) {
476 		if ( this.extButton ) {
477 			this.extButton.setValue( v );
478 		}
479 	},
480 
481 	/**
482 	 * Returns the current query value.
483 	 * @return {String} queryValue
484 	 */
485 	getQueryValue: function () {
486 		if ( this.extButton ) {
487 			return this.extButton.getValue();
488 
489 			// Petro:
490 			// It is not clear why the value was being read in this "low-level" way and
491 			// not through `getValue()'. In any case, doing so, occasionally caused
492 			// errors, when this.extButton.wrap is `undefined'. We will therefore read
493 			// the value in the manner we do above.
494 			// return this.extButton.wrap.dom.children[0].value;
495 		}
496 		return null;
497 	},
498 
499 	/**
500 	 * Set the display field, which is displayed in the combobox
501 	 * @param {String} displayField name of the field to be displayed
502 	 * @return display field name on success, null otherwise
503 	 */
504 	setDisplayField: function ( displayField ) {
505 		var result;
506 		if ( this.extButton ) {
507 			result = this.extButton.displayField = displayField;
508 		} else {
509 			result = this.displayField = displayField;
510 		}
511 		return result;
512 	},
513 
514 	/**
515 	 * Set the row template for autocomplete hints. The default template is:
516 	 * <span><b>{name}</b><br />{url}</span>
517 	 * @param {String} tpl template to be rendered for each row
518 	 * @return template on success or null otherwise
519 	 */
520 	setTemplate: function ( tpl ) {
521 		var result;
522 		if ( this.extButton ) {
523 			result = this.extButton.tpl = extTemplate( tpl );
524 		} else {
525 			result = this.tpl = extTemplate( tpl );
526 		}
527 		return result;
528 	}
529 
530 } );
531 
532 } );
533