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