1 /*
  2  * JBoss, Home of Professional Open Source
  3  * Copyright 2013, Red Hat, Inc. and individual contributors
  4  * by the @authors tag. See the copyright.txt in the distribution for a
  5  * full listing of individual contributors.
  6  *
  7  * This is free software; you can redistribute it and/or modify it
  8  * under the terms of the GNU Lesser General Public License as
  9  * published by the Free Software Foundation; either version 2.1 of
 10  * the License, or (at your option) any later version.
 11  *
 12  * This software 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 GNU
 15  * Lesser General Public License for more details.
 16  *
 17  * You should have received a copy of the GNU Lesser General Public
 18  * License along with this software; if not, write to the Free
 19  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 20  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 21  */
 22 
 23 window.RichFaces = window.RichFaces || {};
 24 RichFaces.jQuery = RichFaces.jQuery || window.jQuery;
 25 
 26 (function($, rf) {
 27 
 28     rf.RICH_CONTAINER = "rf";
 29 
 30     /**
 31      * All input elements which can hold value, which are enabled and visible.
 32      */
 33     rf.EDITABLE_INPUT_SELECTOR = ":not(:submit):not(:button):not(:image):input:visible:enabled";
 34 
 35     //keys codes
 36     rf.KEYS = {
 37         BACKSPACE: 8,
 38         TAB: 9,
 39         RETURN: 13,
 40         ESC: 27,
 41         PAGEUP: 33,
 42         PAGEDOWN: 34,
 43         END: 35,
 44         HOME: 36,
 45         LEFT: 37,
 46         UP: 38,
 47         RIGHT: 39,
 48         DOWN: 40,
 49         DEL: 46
 50     };
 51 
 52     if (window.jsf) {
 53         var jsfAjaxRequest = jsf.ajax.request;
 54         var jsfAjaxResponse = jsf.ajax.response;
 55     }
 56 
 57     // get DOM element by id or DOM element or jQuery object
 58     rf.getDomElement = function (source) {
 59         var type = typeof source;
 60         var element;
 61         if (source == null) {
 62             element = null;
 63         } else if (type == "string") {
 64             // id
 65             element = document.getElementById(source);
 66         } else if (type == "object") {
 67             if (source.nodeType) {
 68                 // DOM element
 69                 element = source;
 70             } else
 71             if (source instanceof $) {
 72                 // jQuery object
 73                 element = source.get(0);
 74             }
 75         }
 76         return element;
 77     };
 78 
 79     // get RichFaces component object by component id or DOM element or jQuery object
 80     rf.component = function (source) {
 81         var element = rf.getDomElement(source);
 82 
 83         if (element) {
 84             return $(element).data("rf.widget") || (element[rf.RICH_CONTAINER] || {})["component"];
 85         }
 86     };
 87 
 88     /**
 89      * jQuery selector ":editable" which selects only input elements which can be edited, are visible and enabled
 90      */
 91     $.extend($.expr[':'], {
 92         editable : function(element) {
 93             return $(element).is(rf.EDITABLE_INPUT_SELECTOR);
 94         }
 95     });
 96 
 97     rf.$$ = function(componentName, element) {
 98         while (element.parentNode) {
 99             var e = element[rf.RICH_CONTAINER];
100             if (e && e.component && e.component.name == componentName)
101                 return e.component;
102             else
103                 element = element.parentNode;
104         }
105     };
106     rf.findNonVisualComponents = function (source) {
107         var element = rf.getDomElement(source);
108 
109         if (element) {
110             return (element[rf.RICH_CONTAINER] || {})["attachedComponents"];
111         }
112     };
113 
114     // find component and call his method
115     rf.invokeMethod = function(source, method) {
116         var c = rf.component(source);
117         var f;
118         if (c && typeof (f = c[method]) == "function") {
119             return f.apply(c, Array.prototype.slice.call(arguments, 2));
120         }
121     };
122 
123     //dom cleaner
124     rf.cleanComponent = function (source) {
125         var component = rf.component(source);
126         if (component && !$(source).data('rf.bridge')) {
127             //TODO fire destroy event
128             component.destroy();
129             component.detach(source);
130         }
131         var attachedComponents = rf.findNonVisualComponents(source);
132         if (attachedComponents) {
133             for (var i in attachedComponents) {
134                 if (attachedComponents[i]) {
135                     attachedComponents[i].destroy();
136                 }
137             }
138         }
139     };
140 
141     rf.cleanDom = function(source) {
142         var e = (typeof source == "string") ? document.getElementById(source) : $('body').get(0);
143         if (source == "javax.faces.ViewRoot") {
144             e = $('body').get(0);
145         }
146         if (e) {
147             // Fire a DOM cleanup event
148             $(e).trigger("beforeDomClean.RICH");
149             var elements = e.getElementsByTagName("*");
150             if (elements.length) {
151                 $.each(elements, function(index) {
152                     rf.cleanComponent(this);
153                 });
154                 $.cleanData(elements);
155             }
156             // clean RF 4 BaseComponent components
157             rf.cleanComponent(e);
158             // cleans data and jQuery UI Widget Factory components
159             $.cleanData([e]);
160             $(e).trigger("afterDomClean.RICH");
161         }
162     };
163 
164     //form.js
165     rf.submitForm = function(form, parameters, target) {
166         if (typeof form === "string") {
167             form = $(form)
168         }
169         var initialTarget = form.attr("target");
170         var parameterInputs = new Array();
171         try {
172             form.attr("target", target);
173 
174             if (parameters) {
175                 for (var parameterName in parameters) {
176                     var parameterValue = parameters[parameterName];
177 
178                     var input = $("input[name='" + parameterName + "']", form);
179                     if (input.length == 0) {
180                         var newInput = $("<input />").attr({type: 'hidden', name: parameterName, value: parameterValue});
181                         if (parameterName === 'javax.faces.portletbridge.STATE_ID' /* fix for fileUpload in portlets */) {
182                             input = newInput.prependTo(form);
183                         } else {
184                             input = newInput.appendTo(form);
185                         }
186                     } else {
187                         input.val(parameterValue);
188                     }
189 
190                     input.each(function() {
191                         parameterInputs.push(this)
192                     });
193                 }
194             }
195 
196             //TODO: inline onsubmit handler is not triggered - http://dev.jquery.com/ticket/4930
197             form.trigger("submit");
198         } finally {
199             if (initialTarget === undefined) {
200                 form.removeAttr("target");
201             } else {
202                 form.attr("target", initialTarget);
203             }
204             $(parameterInputs).remove();
205         }
206     };
207 
208 
209 
210     //utils.js
211     $.fn.toXML = function () {
212         var out = '';
213 
214         if (this.length > 0) {
215             if (typeof XMLSerializer == 'function' ||
216                 typeof XMLSerializer == 'object') {
217 
218                 var xs = new XMLSerializer();
219                 this.each(function() {
220                     out += xs.serializeToString(this);
221                 });
222             } else if (this[0].xml !== undefined) {
223                 this.each(function() {
224                     out += this.xml;
225                 });
226             } else {
227                 this.each(function() {
228                     out += this;
229                 });
230             }
231         }
232 
233         return out;
234     };
235 
236     //there is the same pattern in server-side code:
237     //org.richfaces.javascript.ScriptUtils.escapeCSSMetachars(String)
238     var CSS_METACHARS_PATTERN = /([#;&,.+*~':"!^$\[\]()=>|\/])/g;
239 
240     /**
241      * Escapes CSS meta-characters in string according to
242      *  <a href="http://api.jquery.com/category/selectors/">jQuery selectors</a> document.
243      *
244      * @param s - string to escape meta-characters in
245      * @return string with meta-characters escaped
246      */
247     rf.escapeCSSMetachars = function(s) {
248         //TODO nick - cache results
249 
250         return s.replace(CSS_METACHARS_PATTERN, "\\$1");
251     };
252 
253     var logImpl;
254 
255     rf.setLog = function(newLogImpl) {
256         logImpl = newLogImpl;
257     };
258 
259     rf.log = {
260         debug: function(text) {
261             if (logImpl) {
262                 logImpl.debug(text);
263             }
264         },
265 
266         info: function(text) {
267             if (logImpl) {
268                 logImpl.info(text);
269             }
270         },
271 
272         warn: function(text) {
273             if (logImpl) {
274                 logImpl.warn(text);
275             }
276         },
277 
278         error: function(text) {
279             if (logImpl) {
280                 logImpl.error(text);
281             }
282         },
283 
284         setLevel: function(level) {
285             if (logImpl) {
286                 logImpl.setLevel(level);
287             }
288         },
289 
290         getLevel: function() {
291             if (logImpl) {
292                 return logImpl.getLevel();
293             }
294             return 'info';
295         },
296 
297         clear: function() {
298             if (logImpl) {
299                 logImpl.clear();
300             }
301         }
302     };
303 
304     /**
305      * Evaluates chained properties for the "base" object.
306      * For example, window.document.location is equivalent to
307      * "propertyNamesString" = "document.location" and "base" = window
308      * Evaluation is safe, so it stops on the first null or undefined object
309      *
310      * @param propertyNamesArray - array of strings that contains names of the properties to evaluate
311      * @param base - base object to evaluate properties on
312      * @return returns result of evaluation or empty string
313      */
314     rf.getValue = function(propertyNamesArray, base) {
315         var result = base;
316         var c = 0;
317         do {
318             result = result[propertyNamesArray[c++]];
319         } while (result && c != propertyNamesArray.length);
320 
321         return result;
322     };
323 
324     var VARIABLE_NAME_PATTERN_STRING = "[_A-Z,a-z]\\w*";
325     var VARIABLES_CHAIN = new RegExp("^\\s*" + VARIABLE_NAME_PATTERN_STRING + "(?:\\s*\\.\\s*" + VARIABLE_NAME_PATTERN_STRING + ")*\\s*$");
326     var DOT_SEPARATOR = /\s*\.\s*/;
327 
328     rf.evalMacro = function(macro, base) {
329         var value = "";
330         // variable evaluation
331         if (VARIABLES_CHAIN.test(macro)) {
332             // object's variable evaluation
333             var propertyNamesArray = $.trim(macro).split(DOT_SEPARATOR);
334             value = rf.getValue(propertyNamesArray, base);
335             if (!value) {
336                 value = rf.getValue(propertyNamesArray, window);
337             }
338         } else {
339             //js string evaluation
340             try {
341                 if (base.eval) {
342                     value = base.eval(macro);
343                 } else with (base) {
344                     value = eval(macro);
345                 }
346             } catch (e) {
347                 rf.log.warn("Exception: " + e.message + "\n[" + macro + "]");
348             }
349         }
350 
351         if (typeof value == 'function') {
352             value = value(base);
353         }
354         //TODO 0 and false are also treated as null values
355         return value || "";
356     };
357 
358     var ALPHA_NUMERIC_MULTI_CHAR_REGEXP = /^\w+$/;
359 
360     rf.interpolate = function (placeholders, context) {
361         var contextVarsArray = new Array();
362         for (var contextVar in context) {
363             if (ALPHA_NUMERIC_MULTI_CHAR_REGEXP.test(contextVar)) {
364                 //guarantees that no escaping for the below RegExp is necessary
365                 contextVarsArray.push(contextVar);
366             }
367         }
368 
369         var regexp = new RegExp("\\{(" + contextVarsArray.join("|") + ")\\}", "g");
370         return placeholders.replace(regexp, function(str, contextVar) {
371             return context[contextVar];
372         });
373     };
374 
375     rf.clonePosition = function(element, baseElement, positioning, offset) {
376 
377     };
378     //
379 
380     var jsfEventsAdapterEventNames = {
381         event: {
382             'begin': ['begin'],
383             'complete': ['beforedomupdate'],
384             'success': ['success', 'complete']
385         },
386         error: ['error', 'complete']
387     };
388 
389     var getExtensionResponseElement = function(responseXML) {
390         return $("partial-response extension#org\\.richfaces\\.extension", responseXML);
391     };
392 
393     var JSON_STRING_START = /^\s*(\[|\{)/;
394 
395     rf.parseJSON = function(dataString) {
396         try {
397             if (dataString) {
398                 if (JSON_STRING_START.test(dataString)) {
399                     return $.parseJSON(dataString);
400                 } else {
401                     var parsedData = $.parseJSON("{\"root\": " + dataString + "}");
402                     return parsedData.root;
403                 }
404             }
405         } catch (e) {
406             rf.log.warn("Error evaluating JSON data from element <" + elementName + ">: " + e.message);
407         }
408 
409         return null;
410     }
411 
412     var getJSONData = function(extensionElement, elementName) {
413         var dataString = $.trim(extensionElement.children(elementName).text());
414         return rf.parseJSON(dataString);
415     };
416 
417     rf.createJSFEventsAdapter = function(handlers) {
418         //hash of handlers
419         //supported are:
420         // - begin
421         // - beforedomupdate
422         // - success
423         // - error
424         // - complete
425         var handlers = handlers || {};
426         var ignoreSuccess;
427 
428         return function(eventData) {
429             var source = eventData.source;
430             //that's request status, not status control data
431             var status = eventData.status;
432             var type = eventData.type;
433 
434             if (type == 'event' && status == 'begin') {
435                 ignoreSuccess = false;
436             } else if (type == 'error') {
437                 ignoreSuccess = true;
438             } else if (ignoreSuccess) {
439                 return;
440             } else if (status == 'complete' && rf.ajaxContainer && rf.ajaxContainer.isIgnoreResponse && rf.ajaxContainer.isIgnoreResponse()) {
441                 return;
442             }
443 
444             var typeHandlers = jsfEventsAdapterEventNames[type];
445             var handlerNames = (typeHandlers || {})[status] || typeHandlers;
446 
447             if (handlerNames) {
448                 for (var i = 0; i < handlerNames.length; i++) {
449                     var eventType = handlerNames[i];
450                     var handler = handlers[eventType];
451                     if (handler) {
452                         var event = {};
453                         $.extend(event, eventData);
454                         event.type = eventType;
455                         if (type != 'error') {
456                             delete event.status;
457 
458                             if (event.responseXML) {
459                                 var xml = getExtensionResponseElement(event.responseXML);
460                                 var data = getJSONData(xml, "data");
461                                 var componentData = getJSONData(xml, "componentData");
462 
463                                 event.data = data;
464                                 event.componentData = componentData || {};
465                             }
466                         }
467                         handler.call(source, event);
468                     }
469                 }
470             }
471         };
472     };
473 
474     rf.setGlobalStatusNameVariable = function(statusName) {
475         //TODO: parallel requests
476         if (statusName) {
477             rf['statusName'] = statusName;
478         } else {
479             delete rf['statusName'];
480         }
481     }
482 
483     rf.setZeroRequestDelay = function(options) {
484         if (typeof options.requestDelay == "undefined") {
485             options.requestDelay = 0;
486         }
487     };
488 
489     var chain = function() {
490         var functions = arguments;
491         if (functions.length == 1) {
492             return functions[0];
493         } else {
494             return function() {
495                 var callResult;
496                 for (var i = 0; i < functions.length; i++) {
497                     var f = functions[i];
498                     if (f) {
499                         callResult = f.apply(this, arguments);
500                     }
501                 }
502 
503                 return callResult;
504             };
505         }
506     };
507 
508     var createEventHandler = function(handlerCode) {
509         if (handlerCode) {
510             // ensure safe execution, errors would cause rf.queue to hang up (RF-12132)
511             var safeHandlerCode = "try {" +
512                     handlerCode + 
513                 "} catch (e) {" +
514                     "window.RichFaces.log.error('Error in method execution: ' + e.message)" +
515                 "}";
516             return new Function("event", safeHandlerCode);
517         }
518 
519         return null;
520     };
521 
522     //TODO take events just from .java code using EL-expression
523     var AJAX_EVENTS = (function() {
524         var serverEventHandler = function(clientHandler, event) {
525             var xml = getExtensionResponseElement(event.responseXML);
526 
527             var serverHandler = createEventHandler(xml.children(event.type).text());
528 
529             if (clientHandler) {
530                 clientHandler.call(this, event);
531             }
532 
533             if (serverHandler) {
534                 serverHandler.call(this, event);
535             }
536         };
537 
538         return {
539             'error': null,
540             'begin': null,
541             'complete': serverEventHandler,
542             'beforedomupdate': serverEventHandler
543         }
544     }());
545 
546     rf.ajax = function(source, event, options) {
547         var options = options || {};
548 
549         var sourceId = getSourceId(source, options);
550         var sourceElement = getSourceElement(source);
551 
552         // event source re-targeting finds a RichFaces component root
553         // to setup javax.faces.source correctly - RF-12616)
554         if (sourceElement) {
555             source = searchForComponentRootOrReturn(sourceElement);
556         }
557 
558         parameters = options.parameters || {}; // TODO: change "parameters" to "richfaces.ajax.params"
559         parameters.execute = "@component";
560         parameters.render = "@component";
561 
562         if (options.clientParameters) {
563             $.extend(parameters, options.clientParameters);
564         }
565 
566         if (!parameters["org.richfaces.ajax.component"]) {
567             parameters["org.richfaces.ajax.component"] = sourceId;
568         }
569 
570         if (options.incId) {
571             parameters[sourceId] = sourceId;
572         }
573 
574         if (rf.queue) {
575             parameters.queueId = options.queueId;
576         }
577 
578         // propagates some options to process it in jsf.ajax.request
579         parameters.rfExt = {};
580         parameters.rfExt.status = options.status;
581         for (var eventName in AJAX_EVENTS) {
582             parameters.rfExt[eventName] = options[eventName];
583         }
584 
585         jsf.ajax.request(source, event, parameters);
586     };
587 
588     if (window.jsf) {
589         jsf.ajax.request = function request(source, event, options) {
590 
591             // build parameters, taking options.rfExt into consideration
592             var parameters = $.extend({}, options);
593             parameters.rfExt = null;
594 
595             var eventHandlers;
596 
597             var sourceElement = getSourceElement(source);
598             var form = getFormElement(sourceElement);
599 
600             for (var eventName in AJAX_EVENTS) {
601                 var handlerCode, handler;
602 
603                 if (options.rfExt) {
604                     handlerCode = options.rfExt[eventName];
605                     handler = typeof handlerCode == "function" ? handlerCode : createEventHandler(handlerCode);
606                 }
607 
608                 var serverHandler = AJAX_EVENTS[eventName];
609                 if (serverHandler) {
610                     handler = $.proxy(function(clientHandler, event) {
611                       return serverHandler.call(this, clientHandler, event);
612                     }, sourceElement, handler);
613                 }
614 
615                 if (handler) {
616                     eventHandlers = eventHandlers || {};
617                     eventHandlers[eventName] = handler;
618                 }
619             }
620 
621             if (options.rfExt && options.rfExt.status) {
622                 var namedStatusEventHandler = function() {
623                     rf.setGlobalStatusNameVariable(options.rfExt.status);
624                 };
625 
626                 //TODO add support for options.submit
627                 eventHandlers = eventHandlers || {};
628                 if (eventHandlers.begin) {
629                     eventHandlers.begin = chain(namedStatusEventHandler, eventHandlers.begin);
630                 } else {
631                     eventHandlers.begin = namedStatusEventHandler;
632                 }
633             }
634 
635             // register handlers for form events: ajaxbegin and ajaxbeforedomupdate
636             if (form) {
637                 eventHandlers.begin = chain(eventHandlers.begin, function() { $(form).trigger('ajaxbegin'); });
638                 eventHandlers.beforedomupdate = chain(eventHandlers.beforedomupdate, function() { $(form).trigger('ajaxbeforedomupdate'); });
639                 eventHandlers.complete = chain(eventHandlers.complete, function() { $(form).trigger('ajaxcomplete'); });
640             }
641 
642             if (eventHandlers) {
643                 var eventsAdapter = rf.createJSFEventsAdapter(eventHandlers);
644                 parameters['onevent'] = chain(options.onevent, eventsAdapter);
645                 parameters['onerror'] = chain(options.onerror, eventsAdapter);
646             }
647 
648             // trigger handler for form event: ajaxsubmit
649             if (form) {
650                 $(form).trigger('ajaxsubmit');
651             }
652 
653             return jsfAjaxRequest(source, event, parameters);
654         }
655 
656         jsf.ajax.response = function(request, context) {
657             // for all RichFaces.ajax requests
658             if (context.render == '@component') {
659                 // get list of IDs updated on the server - replaces @render option which is normally available on client
660                 context.render = $("extension[id='org.richfaces.extension'] render", request.responseXML).text();
661             }
662 
663             return jsfAjaxResponse(request, context);
664         }
665     }
666 
667     /**
668      * Returns RichFaces component root for given element in the list of ancestors of sourceElement.
669      * Otherwise returns sourceElement if RichFaces component root can't be located.
670      */
671     var searchForComponentRootOrReturn = function(sourceElement) {
672         if (sourceElement.id && !isRichFacesComponent(sourceElement)) {
673             var parentElement = false;
674             $(sourceElement).parents().each(function() {
675                 if (this.id && sourceElement.id.indexOf(this.id) == 0) { // otherwise parent element is definitely not JSF component
676                     var suffix = sourceElement.id.substring(this.id.length); // extract suffix
677                     if (suffix.match(/^[a-zA-Z]*$/) && isRichFacesComponent(this)) {
678                         parentElement = this;
679                         return false;
680                     }
681                 }
682             });
683             if (parentElement !== false) {
684                 return parentElement;
685             }
686         }
687         return sourceElement;
688     };
689 
690 
691     /**
692      * Detects whether the element has bound RichFaces component.
693      *
694      * Supports detection of RichFaces 5 (bridge-base.js) and RichFaces 4 (richfaces-base-component.js) components.
695      */
696     var isRichFacesComponent = function(element) {
697       return $(element).data('rf.bridge') || rf.component(element);
698     };
699 
700     var getSourceElement = function(source) {
701         if (typeof source === 'string') {
702             return document.getElementById(source);
703         } else if (typeof source === 'object') {
704             return source;
705         } else {
706             throw new Error("jsf.request: source must be object or string");
707         }
708     };
709 
710     var getFormElement = function(sourceElement) {
711         if ($(sourceElement).is('form')) {
712             return sourceElement;
713         } else {
714             return $('form').has(sourceElement).get(0);
715         }
716     };
717 
718     var getSourceId = function(source, options) {
719         if (options.sourceId) {
720             return options.sourceId;
721         } else {
722             return (typeof source == 'object' && (source.id || source.name)) ? (source.id ? source.id : source.name) : source;
723         }
724     };
725 
726     var ajaxOnComplete = function (data) {
727         var type = data.type;
728         var responseXML = data.responseXML;
729 
730         if (data.type == 'event' && data.status == 'complete' && responseXML) {
731             var partialResponse = $(responseXML).children("partial-response");
732             if (partialResponse && partialResponse.length) {
733                 var elements = partialResponse.children('changes').children('update, delete');
734                 $.each(elements, function () {
735                     rf.cleanDom($(this).attr('id'));
736                 });
737             }
738         }
739     };
740 
741     rf.javascriptServiceComplete = function(event) {
742         $(function() {
743             $(document).trigger("javascriptServiceComplete");
744         });
745     };
746 
747     var attachAjaxDOMCleaner = function() {
748         // move this code to somewhere
749         if (typeof jsf != 'undefined' && jsf.ajax) {
750             jsf.ajax.addOnEvent(ajaxOnComplete);
751 
752             return true;
753         }
754 
755         return false;
756     };
757 
758     if (!attachAjaxDOMCleaner()) {
759         $(document).ready(attachAjaxDOMCleaner);
760     }
761 
762     if (window.addEventListener) {
763         window.addEventListener("unload", rf.cleanDom, false);
764     } else {
765         window.attachEvent("onunload", rf.cleanDom);
766     }
767     
768     // browser detection, taken jQuery Migrate plugin (https://github.com/jquery/jquery-migrate)
769     //     and atmosphere.js
770     rf.browser = {};
771     var ua = navigator.userAgent.toLowerCase(),
772         match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
773             /(webkit)[ \/]([\w.]+)/.exec(ua) ||
774             /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
775             /(msie) ([\w.]+)/.exec(ua) ||
776             /(trident)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
777             ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
778             [];
779 
780     rf.browser[match[1] || ""] = true;
781     rf.browser.version = match[2] || "0";
782     
783     // Chrome is Webkit, but Webkit is also Safari.
784 	if ( rf.browser.chrome ) {
785 		rf.browser.webkit = true;
786 	} else if ( rf.browser.webkit ) {
787 		rf.browser.safari = true;
788 	}
789 
790     // Trident is the layout engine of the Internet Explorer
791     // IE 11 has no "MSIE: 11.0" token
792     if (rf.browser.trident) {
793         rf.browser.msie = true;
794     }
795 }(RichFaces.jQuery, RichFaces));
796