5c2bc3f4e62fb6f7a04865f5c38853f9af4a7fdd
[tine20] / tine20 / Tinebase / js / extFixes.js
1 (function(){
2     var ua = navigator.userAgent.toLowerCase(),
3         check = function(r){
4             return r.test(ua);
5         },
6         docMode = document.documentMode,
7         isIE10 = ((check(/msie 10/) && docMode != 7 && docMode != 8  && docMode != 9) || docMode == 10),
8         isIE11 = ((check(/trident\/7\.0/) && docMode != 7 && docMode != 8 && docMode != 9 && docMode != 10) || docMode == 11),
9         isNewIE = (Ext.isIE9 || isIE10 || isIE11);
10
11     Ext.apply(Ext, {
12         isIE10 : isIE10,
13         isIE11 : isIE11,
14         
15         isNewIE : isNewIE
16     })
17 })();
18
19 Ext.override(Ext.data.Store, {
20     /**
21      * String cast the id, as otherwise the entries are not found
22      */
23     indexOfId : function(id){
24         return this.data.indexOfKey(String(id));
25     }
26 });
27
28 /**
29  * for some reasons the original fix insertes two <br>'s on enter for webkit. But this is one to much
30  */
31 Ext.apply(Ext.form.HtmlEditor.prototype, {
32         fixKeys : function(){ // load time branching for fastest keydown performance
33         if(Ext.isIE){
34             return function(e){
35                 var k = e.getKey(),
36                     doc = this.getDoc(),
37                         r;
38                 if(k == e.TAB){
39                     e.stopEvent();
40                     r = doc.selection.createRange();
41                     if(r){
42                         r.collapse(true);
43                         r.pasteHTML('&nbsp;&nbsp;&nbsp;&nbsp;');
44                         this.deferFocus();
45                     }
46                 }else if(k == e.ENTER){
47                     r = doc.selection.createRange();
48                     if(r){
49                         var target = r.parentElement();
50                         if(!target || target.tagName.toLowerCase() != 'li'){
51                             e.stopEvent();
52                             r.pasteHTML('<br />');
53                             r.collapse(false);
54                             r.select();
55                         }
56                     }
57                 }
58             };
59         }else if(Ext.isOpera){
60             return function(e){
61                 var k = e.getKey();
62                 if(k == e.TAB){
63                     e.stopEvent();
64                     this.win.focus();
65                     this.execCmd('InsertHTML','&nbsp;&nbsp;&nbsp;&nbsp;');
66                     this.deferFocus();
67                 }
68             };
69         }else if(Ext.isWebKit){
70             return function(e){
71                 var k = e.getKey();
72                 if(k == e.TAB){
73                     e.stopEvent();
74                     this.execCmd('InsertText','\t');
75                     this.deferFocus();
76                 }
77              };
78         }
79     }()
80 });
81
82 /**
83  * fix broken ext email validator
84  * 
85  * @type RegExp
86  */
87 Ext.apply(Ext.form.VTypes, {
88     // 2011-01-05 replace \w with [^\s,\x00-\x2F,\x3A-\x40,\x5B-\x60,\x7B-\x7F] to allow idn's
89     emailFixed: /^(("[\w-\s]+")|([\w-]+(?:\.[\w-]+)*)|("[\w-\s]+")([\w-]+(?:\.[\w-]+)*))(@((?:([^\s,\x00-\x2F,\x3A-\x40,\x5B-\x60,\x7B-\x7F]|-)+\.)*[^\s,\x00-\x2F,\x3A-\x40,\x5B-\x60,\x7B-\x7F]([^\s,\x00-\x2F,\x3A-\x40,\x5B-\x60,\x7B-\x7F]|-){0,63})\.([^\s,\x00-\x2F,\x3A-\x40,\x5B-\x60,\x7B-\x7F]{2,63}?)$)|(@\[?((25[0-5]\.|2[0-4][0-9]\.|1[0-9]{2}\.|[0-9]{1,63}\.))((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,63})\.){2}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,63})\]?$)/i,
90
91     urlFixed: /(((^https?)|(^ftp)):\/\/(([^\s,\x00-\x2F,\x3A-\x40,\x5B-\x60,\x7B-\x7F]|-)+\.)+[^\s,\x00-\x2F,\x3A-\x40,\x5B-\x60,\x7B-\x7F]{2,63}(\/([^\s,\x00-\x2F,\x3A-\x40,\x5B-\x60,\x7B-\x7F]|-|%)+(\.[^\s,\x00-\x2F,\x3A-\x40,\x5B-\x60,\x7B-\x7F]{2,})?)*((([^\s,\x00-\x2F,\x3A-\x40,\x5B-\x60,\x7B-\x7F]|[\-\.\?\\\/+@&#;`~=%!])*)(\.[^\s,\x00-\x2F,\x3A-\x40,\x5B-\x60,\x7B-\x7F]{2,})?)*\/?)/i,
92     
93     email:  function(v) {
94         return this.emailFixed.test(v);
95     },
96     
97     url: function(v) {
98         return this.urlFixed.test(v);
99     }
100 });
101
102
103 /**
104  * fix textfield allowBlank validation
105  * taken from ext, added trim
106  */
107 Ext.override(Ext.form.TextField, {
108     validateValue : function(value){
109         if(Ext.isFunction(this.validator)){
110             var msg = this.validator(value);
111             if(msg !== true){
112                 this.markInvalid(msg);
113                 return false;
114             }
115         }
116         if(Ext.util.Format.trim(value).length < 1 || value === this.emptyText){ // if it's blank
117              if(this.allowBlank){
118                  this.clearInvalid();
119                  return true;
120              }else{
121                  this.markInvalid(this.blankText);
122                  return false;
123              }
124         }
125         if(Ext.util.Format.trim(value).length < this.minLength){
126             this.markInvalid(String.format(this.minLengthText, this.minLength));
127             return false;
128         }
129         if(Ext.util.Format.trim(value).length > this.maxLength){
130             this.markInvalid(String.format(this.maxLengthText, this.maxLength));
131             return false;
132         }   
133         if(this.vtype){
134             var vt = Ext.form.VTypes;
135             if(!vt[this.vtype](value, this)){
136                 this.markInvalid(this.vtypeText || vt[this.vtype +'Text']);
137                 return false;
138             }
139         }
140         if(this.regex && !this.regex.test(value)){
141             this.markInvalid(this.regexText);
142             return false;
143         }
144         return true;
145     }
146 });
147
148 Ext.applyIf(Ext.tree.MultiSelectionModel.prototype, {
149     /**
150      * implement convinience function as expected from grid selection models
151      * 
152      * @namespace {Ext.tree}
153      * @return {Node}
154      */
155     getSelectedNode: function() {
156         var selection = this.getSelectedNodes();
157         return Ext.isArray(selection) ? selection[0] : null
158     }
159 });
160
161 /**
162  * fix timezone handling for date picker
163  * 
164  * The getValue function always returns 00:00:00 as time. So if a form got filled
165  * with a date like 2008-10-01T21:00:00 the form returns 2008-10-01T00:00:00 although 
166  * the user did not change the fieled.
167  * 
168  * In a multi timezone context this is fatal! When a user in a certain timezone set
169  * a date (just a date and no time information), this means in his timezone the 
170  * time range from 2008-10-01T00:00:00 to 2008-10-01T23:59:59. 
171  * _BUT_ for an other user sitting in a different timezone it means e.g. the 
172  * time range from 2008-10-01T02:00:00 to 2008-10-02T21:59:59.
173  * 
174  * So on the one hand we need to make sure, that the date picker only returns 
175  * changed datetime information when the user did a change. 
176  * 
177  * @todo On the other hand we
178  * need adjust the day +/- one day according to the timeshift. 
179  */
180 /**
181  * @private
182  */
183  Ext.form.DateField.prototype.setValue = function(date){
184     // get value must not return a string representation, so we convert this always here
185     // before memorisation
186     if (Ext.isString(date)) {
187         var v = Date.parseDate(date, Date.patterns.ISO8601Long);
188         if (Ext.isDate(v)) {
189             date = v;
190         } else {
191             date = Ext.form.DateField.prototype.parseDate.call(this, date);
192         }
193     }
194     
195     // preserve original datetime information
196     this.fullDateTime = date;
197     
198     Ext.form.DateField.superclass.setValue.call(this, this.formatDate(this.parseDate(date)));
199 };
200 /**
201  * @private
202  */
203 Ext.form.DateField.prototype.getValue = function(){
204     //var value = this.parseDate(Ext.form.DateField.superclass.getValue.call(this));
205     
206     // return the value that was set (has time information when unchanged in client) 
207     // and not just the date part!
208     var value =  this.fullDateTime;
209     return value || "";
210 };
211
212 /**
213  * We need to overwrite to preserve original time information because 
214  * Ext.form.TimeField does not support seconds
215  * 
216  * @param {} time
217  * @private
218  */
219  Ext.form.TimeField.prototype.setValue = function(time){
220     this.fullDateTime = time;
221     Ext.form.TimeField.superclass.setValue.call(this, this.formatDate(this.parseDate(time)));
222 };
223 /**
224  * @private
225  */
226 Ext.form.TimeField.prototype.getValue = function(){
227     // return the value that was set (has time information when unchanged in client) 
228     // and not just the date part!
229     var value =  this.fullDateTime;
230     
231     return value ? this.parseDate(value).dateFormat('H:i') : "";
232 };
233
234 /**
235  * check if sort field is in column list to avoid server exceptions
236  */
237 Ext.grid.GridPanel.prototype.applyState  = Ext.grid.GridPanel.prototype.applyState.createInterceptor(function(state){
238     var cm = this.colModel,
239         s = state.sort;
240     
241     if (s && cm.getIndexById(s.field) < 0) {
242         delete state.sort;
243     }
244 });
245
246 /**
247  * fix interpretation of ISO-8601  formatcode (Date.patterns.ISO8601Long) 
248  * 
249  * Browsers do not support timezones and also javascripts Date object has no 
250  * support for it.  All Date Objects are in _one_ timezone which may ore may 
251  * not be the operating systems timezone the browser runs on.
252  * 
253  * parsing dates in ISO format having the timeshift appended (Date.patterns.ISO8601Long) lead to 
254  * correctly converted Date Objects in the browsers timezone. This timezone 
255  * conversion changes the the Date Parts and as such, javascipt widget 
256  * representing date time information print values of the browsers timezone 
257  * and _not_ the values send by the server!
258  * 
259  * So in a multi timezone envireonment, datetime information in the browser 
260  * _must not_ be parsed including the offset. Just the values of the server 
261  * side converted datetime information are allowed to be parsed.
262  */
263 Date.parseIso = function(isoString) {
264     return Date.parseDate(isoString.replace(/\+\d{2}\d{2}/, ''), 'Y-m-d\\Th:i:s');
265 };
266
267 /**
268  * rename window
269  */
270 Ext.Window.prototype.rename = function(newId) {
271     // Note PopupWindows are identified by name, whereas Ext.windows
272     // get identified by id this should be solved some time ;-)
273     var manager = this.manager || Ext.WindowMgr;
274     manager.unregister(this);
275     this.id = newId;
276     manager.register(this);
277 };
278
279 /**
280  * utility class used by Button
281  * 
282  * Fix: http://yui-ext.com/forum/showthread.php?p=142049
283  * adds the ButtonToggleMgr.getSelected(toggleGroup, handler, scope) function
284  */
285 Ext.ButtonToggleMgr = function(){
286    var groups = {};
287    
288    function toggleGroup(btn, state){
289        if(state){
290            var g = groups[btn.toggleGroup];
291            for(var i = 0, l = g.length; i < l; i++){
292                if(g[i] != btn){
293                    g[i].toggle(false);
294                }
295            }
296        }
297    }
298    
299    return {
300        register : function(btn){
301            if(!btn.toggleGroup){
302                return;
303            }
304            var g = groups[btn.toggleGroup];
305            if(!g){
306                g = groups[btn.toggleGroup] = [];
307            }
308            g.push(btn);
309            btn.on("toggle", toggleGroup);
310        },
311        
312        unregister : function(btn){
313            if(!btn.toggleGroup){
314                return;
315            }
316            var g = groups[btn.toggleGroup];
317            if(g){
318                g.remove(btn);
319                btn.un("toggle", toggleGroup);
320            }
321        },
322        
323        getSelected : function(toggleGroup, handler, scope){
324            var g = groups[toggleGroup];
325            for(var i = 0, l = g.length; i < l; i++){
326                if(g[i].pressed === true){
327                    if(handler) {
328                         handler.call(scope || g[i], g[i]);
329                    }
330                    return g[i];
331                }
332            }
333            return;
334        }
335    };
336 }();
337
338 /**
339  * add beforeloadrecords event
340  */
341 Ext.data.Store.prototype.loadRecords = Ext.data.Store.prototype.loadRecords.createInterceptor(function(o, options, success) {
342     return this.fireEvent('beforeloadrecords', o, options, success, this);
343 });
344
345 /**
346  * state encoding converts null to empty object
347  * 
348  * -> take encoder/decoder from Ext 4.1 where this is fixed
349  */
350 Ext.override(Ext.state.Provider, {
351     /**
352      * Decodes a string previously encoded with {@link #encodeValue}.
353      * @param {String} value The value to decode
354      * @return {Object} The decoded value
355      */
356     decodeValue : function(value){
357
358         // a -> Array
359         // n -> Number
360         // d -> Date
361         // b -> Boolean
362         // s -> String
363         // o -> Object
364         // -> Empty (null)
365
366         var me = this,
367             re = /^(a|n|d|b|s|o|e)\:(.*)$/,
368             matches = re.exec(unescape(value)),
369             all,
370             type,
371             keyValue,
372             values,
373             vLen,
374             v;
375             
376         if(!matches || !matches[1]){
377             return; // non state
378         }
379         
380         type = matches[1];
381         value = matches[2];
382         switch (type) {
383             case 'e':
384                 return null;
385             case 'n':
386                 return parseFloat(value);
387             case 'd':
388                 return new Date(Date.parse(value));
389             case 'b':
390                 return (value == '1');
391             case 'a':
392                 all = [];
393                 if(value != ''){
394                     values = value.split('^');
395                     vLen   = values.length;
396
397                     for (v = 0; v < vLen; v++) {
398                         value = values[v];
399                         all.push(me.decodeValue(value));
400                     }
401                 }
402                 return all;
403            case 'o':
404                 all = {};
405                 if(value != ''){
406                     values = value.split('^');
407                     vLen   = values.length;
408
409                     for (v = 0; v < vLen; v++) {
410                         value = values[v];
411                         keyValue         = value.split('=');
412                         all[keyValue[0]] = me.decodeValue(keyValue[1]);
413                     }
414                 }
415                 return all;
416            default:
417                 return value;
418         }
419     },
420
421     /**
422      * Encodes a value including type information.  Decode with {@link #decodeValue}.
423      * @param {Object} value The value to encode
424      * @return {String} The encoded value
425      */
426     encodeValue : function(value){
427         var flat = '',
428             i = 0,
429             enc,
430             len,
431             key;
432             
433         if (value == null) {
434             return 'e:1';    
435         } else if(typeof value == 'number') {
436             enc = 'n:' + value;
437         } else if(typeof value == 'boolean') {
438             enc = 'b:' + (value ? '1' : '0');
439         } else if(Ext.isDate(value)) {
440             enc = 'd:' + value.toGMTString();
441         } else if(Ext.isArray(value)) {
442             for (len = value.length; i < len; i++) {
443                 flat += this.encodeValue(value[i]);
444                 if (i != len - 1) {
445                     flat += '^';
446                 }
447             }
448             enc = 'a:' + flat;
449         } else if (typeof value == 'object') {
450             for (key in value) {
451                 if (typeof value[key] != 'function' && value[key] !== undefined) {
452                     flat += key + '=' + this.encodeValue(value[key]) + '^';
453                 }
454             }
455             enc = 'o:' + flat.substring(0, flat.length-1);
456         } else {
457             enc = 's:' + value;
458         }
459         return escape(enc);
460     }
461 });
462
463 /**
464  * add beforeloadrecords event
465  */
466 Ext.data.Store.prototype.loadRecords = Ext.data.Store.prototype.loadRecords.createInterceptor(function(o, options, success) {
467     return this.fireEvent('beforeloadrecords', o, options, success, this);
468 });
469
470 /**
471  * fix focus related emptyText problems
472  * 0008616: emptyText gets inserted into ComboBoxes when the Box gets Hidden while focused 
473  */
474 Ext.form.TriggerField.prototype.cmpRegforFocusFix = [];
475
476 Ext.form.TriggerField.prototype.initComponent = Ext.form.TriggerField.prototype.initComponent.createSequence(function() {
477     if (this.emptyText) {
478         Ext.form.TriggerField.prototype.cmpRegforFocusFix.push(this);
479     }
480 });
481
482 Ext.form.TriggerField.prototype.onDestroy = Ext.form.TriggerField.prototype.onDestroy.createInterceptor(function() {
483     Ext.form.TriggerField.prototype.cmpRegforFocusFix.remove(this);
484 });
485
486 Ext.form.TriggerField.prototype.taskForFocusFix = new Ext.util.DelayedTask(function(){
487     Ext.each(Ext.form.TriggerField.prototype.cmpRegforFocusFix, function(cmp) {
488         if (cmp.rendered && cmp.el.dom == document.activeElement) {
489             if(cmp.el.dom.value == cmp.emptyText){
490                 cmp.preFocus();
491                 cmp.hasFocus = true;
492                 cmp.setRawValue('');
493             }
494         }
495     });
496     Ext.form.TriggerField.prototype.taskForFocusFix.delay(1000);
497 });
498
499 Ext.form.TriggerField.prototype.taskForFocusFix.delay(1000);
500
501 // fixing layers in LayerCombo
502 // TODO maybe expand this to all Ext.Layers:
503 // Ext.Layer.prototype.showAction = Ext.Layer.prototype.showAction.createSequence(function() {
504 // Ext.Layer.prototype.hideAction = Ext.Layer.prototype.hideAction.createSequence(function() {
505 Ext.form.ComboBox.prototype.expand = Ext.form.ComboBox.prototype.expand.createSequence(function() {
506     // fix z-index problem when used in editorGrids
507     // manage z-index by windowMgr
508     this.list.setActive = Ext.emptyFn;
509     this.list.setZIndex = Ext.emptyFn;
510     Ext.WindowMgr.register(this.list);
511     Ext.WindowMgr.bringToFront(this.list);
512 });
513 Ext.form.ComboBox.prototype.collapse = Ext.form.ComboBox.prototype.collapse.createSequence(function() {
514     if (this.list) {
515         Ext.WindowMgr.unregister(this.list);
516     }
517 });
518
519 /*!
520  * Ext JS Library 3.1.1
521  * Copyright(c) 2006-2010 Ext JS, LLC
522  * licensing@extjs.com
523  * http://www.extjs.com/license
524  */
525 /**
526  * fix for nested css
527  * @see https://forge.tine20.org/view.php?id=11884
528  *
529  * only cacheStyleSheet is affected, but we need to overwrite whole class (closure)
530  */
531 Ext.util.CSS = function(){
532     var rules = null;
533     var doc = document;
534
535     var camelRe = /(-[a-z])/gi;
536     var camelFn = function(m, a){ return a.charAt(1).toUpperCase(); };
537
538     return {
539         /**
540          * Creates a stylesheet from a text blob of rules.
541          * These rules will be wrapped in a STYLE tag and appended to the HEAD of the document.
542          * @param {String} cssText The text containing the css rules
543          * @param {String} id An id to add to the stylesheet for later removal
544          * @return {StyleSheet}
545          */
546         createStyleSheet : function(cssText, id){
547             var ss;
548             var head = doc.getElementsByTagName("head")[0];
549             var rules = doc.createElement("style");
550             rules.setAttribute("type", "text/css");
551             if(id){
552                 rules.setAttribute("id", id);
553             }
554             if(Ext.isIE){
555                 head.appendChild(rules);
556                 ss = rules.styleSheet;
557                 ss.cssText = cssText;
558             }else{
559                 try{
560                     rules.appendChild(doc.createTextNode(cssText));
561                 }catch(e){
562                     rules.cssText = cssText;
563                 }
564                 head.appendChild(rules);
565                 ss = rules.styleSheet ? rules.styleSheet : (rules.sheet || doc.styleSheets[doc.styleSheets.length-1]);
566             }
567             this.cacheStyleSheet(ss);
568             return ss;
569         },
570
571         /**
572          * Removes a style or link tag by id
573          * @param {String} id The id of the tag
574          */
575         removeStyleSheet : function(id){
576             var existing = doc.getElementById(id);
577             if(existing){
578                 existing.parentNode.removeChild(existing);
579             }
580         },
581
582         /**
583          * Dynamically swaps an existing stylesheet reference for a new one
584          * @param {String} id The id of an existing link tag to remove
585          * @param {String} url The href of the new stylesheet to include
586          */
587         swapStyleSheet : function(id, url){
588             this.removeStyleSheet(id);
589             var ss = doc.createElement("link");
590             ss.setAttribute("rel", "stylesheet");
591             ss.setAttribute("type", "text/css");
592             ss.setAttribute("id", id);
593             ss.setAttribute("href", url);
594             doc.getElementsByTagName("head")[0].appendChild(ss);
595         },
596
597         /**
598          * Refresh the rule cache if you have dynamically added stylesheets
599          * @return {Object} An object (hash) of rules indexed by selector
600          */
601         refreshCache : function(){
602             return this.getRules(true);
603         },
604
605         // private
606         cacheStyleSheet : function(ss){
607             if(!rules){
608                 rules = {};
609             }
610             try{// try catch for cross domain access issue
611                 var ssRules = ss.cssRules || ss.rules;
612                 for(var j = ssRules.length-1; j >= 0; --j){
613                     // nested rules
614                     if (ssRules[j].styleSheet) {
615                         Ext.util.CSS.cacheStyleSheet(ssRules[j].styleSheet);
616                     } else {
617                         rules[ssRules[j].selectorText.toLowerCase()] = ssRules[j];
618                     }
619                 }
620             }catch(e){}
621         },
622
623         /**
624          * Gets all css rules for the document
625          * @param {Boolean} refreshCache true to refresh the internal cache
626          * @return {Object} An object (hash) of rules indexed by selector
627          */
628         getRules : function(refreshCache){
629             if(rules === null || refreshCache){
630                 rules = {};
631                 var ds = doc.styleSheets;
632                 for(var i =0, len = ds.length; i < len; i++){
633                     try{
634                         this.cacheStyleSheet(ds[i]);
635                     }catch(e){}
636                 }
637             }
638             return rules;
639         },
640
641         /**
642          * Gets an an individual CSS rule by selector(s)
643          * @param {String/Array} selector The CSS selector or an array of selectors to try. The first selector that is found is returned.
644          * @param {Boolean} refreshCache true to refresh the internal cache if you have recently updated any rules or added styles dynamically
645          * @return {CSSRule} The CSS rule or null if one is not found
646          */
647         getRule : function(selector, refreshCache){
648             var rs = this.getRules(refreshCache);
649             if(!Ext.isArray(selector)){
650                 return rs[selector.toLowerCase()];
651             }
652             for(var i = 0; i < selector.length; i++){
653                 if(rs[selector[i]]){
654                     return rs[selector[i].toLowerCase()];
655                 }
656             }
657             return null;
658         },
659
660
661         /**
662          * Updates a rule property
663          * @param {String/Array} selector If it's an array it tries each selector until it finds one. Stops immediately once one is found.
664          * @param {String} property The css property
665          * @param {String} value The new value for the property
666          * @return {Boolean} true If a rule was found and updated
667          */
668         updateRule : function(selector, property, value){
669             if(!Ext.isArray(selector)){
670                 var rule = this.getRule(selector);
671                 if(rule){
672                     rule.style[property.replace(camelRe, camelFn)] = value;
673                     return true;
674                 }
675             }else{
676                 for(var i = 0; i < selector.length; i++){
677                     if(this.updateRule(selector[i], property, value)){
678                         return true;
679                     }
680                 }
681             }
682             return false;
683         }
684     };
685 }();