var czrapp = czrapp || {}; /************************* * JS LOG VARIOUS UTILITIES *************************/ (function($, czrapp) { //Utility : print a js log on frontd czrapp._printLog = function( log ) { var _render = function() { return $.Deferred( function() { var dfd = this; $.when( $('#footer').before( $('
', { id : "bulklog" }) ) ).done( function() { $('#bulklog').css({ position: 'fixed', 'z-index': '99999', 'font-size': '0.8em', color: '#000', padding: '5%', width: '90%', height: '20%', overflow: 'hidden', bottom: '0', left: '0', background: 'yellow' }); dfd.resolve(); }); }).promise(); }, _print = function() { $('#bulklog').prepend('

' + czrapp._prettyfy( { consoleArguments : [ log ], prettyfy : false } ) + '

'); }; if ( 1 != $('#bulk-log').length ) { _render().done( _print ); } else { _print(); } }; czrapp._truncate = function( string , length ){ length = length || 150; if ( ! _.isString( string ) ) return ''; return string.length > length ? string.substr( 0, length - 1 ) : string; }; //@return [] for console method //@bgCol @textCol are hex colors //@arguments : the original console arguments var _prettyPrintLog = function( args ) { var _defaults = { bgCol : '#5ed1f5', textCol : '#000', consoleArguments : [] }; args = _.extend( _defaults, args ); var _toArr = Array.from( args.consoleArguments ), _truncate = function( string ){ if ( ! _.isString( string ) ) return ''; return string.length > 300 ? string.substr( 0, 299 ) + '...' : string; }; //if the array to print is not composed exclusively of strings, then let's stringify it //else join(' ') if ( ! _.isEmpty( _.filter( _toArr, function( it ) { return ! _.isString( it ); } ) ) ) { _toArr = JSON.stringify( _toArr.join(' ') ); } else { _toArr = _toArr.join(' '); } return [ '%c ' + _truncate( _toArr ), [ 'background:' + args.bgCol, 'color:' + args.textCol, 'display: block;' ].join(';') ]; }; var _wrapLogInsideTags = function( title, msg, bgColor ) { //fix for IE, because console is only defined when in F12 debugging mode in IE if ( ( _.isUndefined( console ) && typeof window.console.log != 'function' ) ) return; if ( czrapp.localized.isDevMode ) { if ( _.isUndefined( msg ) ) { console.log.apply( console, _prettyPrintLog( { bgCol : bgColor, textCol : '#000', consoleArguments : [ '<' + title + '>' ] } ) ); } else { console.log.apply( console, _prettyPrintLog( { bgCol : bgColor, textCol : '#000', consoleArguments : [ '<' + title + '>' ] } ) ); console.log( msg ); console.log.apply( console, _prettyPrintLog( { bgCol : bgColor, textCol : '#000', consoleArguments : [ '' ] } ) ); } } else { console.log.apply( console, _prettyPrintLog( { bgCol : bgColor, textCol : '#000', consoleArguments : [ title ] } ) ); } }; //Dev mode aware and IE compatible czrapp.consoleLog() czrapp.consoleLog = function() { if ( ! czrapp.localized.isDevMode ) return; //fix for IE, because console is only defined when in F12 debugging mode in IE if ( ( _.isUndefined( console ) && typeof window.console.log != 'function' ) ) return; console.log.apply( console, _prettyPrintLog( { consoleArguments : arguments } ) ); console.log( 'Unstyled console message : ', arguments ); }; czrapp.errorLog = function() { //fix for IE, because console is only defined when in F12 debugging mode in IE if ( ( _.isUndefined( console ) && typeof window.console.log != 'function' ) ) return; console.log.apply( console, _prettyPrintLog( { bgCol : '#ffd5a0', textCol : '#000', consoleArguments : arguments } ) ); // if ( czrapp.localized.isDevMode ) { // console.log( 'Unstyled error message : ', arguments ); // } }; czrapp.errare = function( title, msg ) { _wrapLogInsideTags( title, msg, '#ffd5a0' ); }; czrapp.infoLog = function( title, msg ) { _wrapLogInsideTags( title, msg, '#5ed1f5' ); }; //encapsulates a WordPress ajax request in a normalize method //@param queryParams = {} czrapp.doAjax = function( queryParams ) { //do we have a queryParams ? queryParams = queryParams || ( _.isObject( queryParams ) ? queryParams : {} ); var ajaxUrl = queryParams.ajaxUrl || czrapp.localized.ajaxUrl,//the ajaxUrl can be specified when invoking doAjax nonce = czrapp.localized.frontNonce,//{ 'id' => 'HuFrontNonce', 'handle' => wp_create_nonce( 'hu-front-nonce' ) }, dfd = $.Deferred(), _query_ = _.extend( { action : '', withNonce : false }, queryParams ); // HTTP ajaxurl when site is HTTPS causes Access-Control-Allow-Origin failure in Desktop and iOS Safari if ( "https:" == document.location.protocol ) { ajaxUrl = ajaxUrl.replace( "http://", "https://" ); } //check if we're good if ( _.isEmpty( _query_.action ) || ! _.isString( _query_.action ) ) { czrapp.errorLog( 'czrapp.doAjax : unproper action provided' ); return dfd.resolve().promise(); } //setup nonce //Note : the nonce might be checked server side ( not in all cases, only when writing in db ) with check_ajax_referer( 'hu-front-nonce', 'HuFrontNonce' ) _query_[ nonce.id ] = nonce.handle; if ( ! _.isObject( nonce ) || _.isUndefined( nonce.id ) || _.isUndefined( nonce.handle ) ) { czrapp.errorLog( 'czrapp.doAjax : unproper nonce' ); return dfd.resolve().promise(); } $.post( ajaxUrl, _query_ ) .done( function( _r ) { // Check if the user is logged out. if ( '0' === _r || '-1' === _r || false === _r.success ) { czrapp.errare( 'czrapp.doAjax : done ajax error for action : ' + _query_.action , _r ); dfd.reject( _r ); } dfd.resolve( _r ); }) .fail( function( _r ) { czrapp.errare( 'czrapp.doAjax : failed ajax error for : ' + _query_.action, _r ); dfd.reject( _r ); }); //.always( function( _r ) { dfd.resolve( _r ); }); return dfd.promise(); }; })(jQuery, czrapp); /************************* * ADD DOM LISTENER UTILITY *************************/ (function($, czrapp) { /** * Return whether the supplied Event object is for a keydown event but not the Enter key. * * @since 4.1.0 * * @param {jQuery.Event} event * @returns {boolean} */ czrapp.isKeydownButNotEnterEvent = function ( event ) { return ( 'keydown' === event.type && 13 !== event.which ); }; //@args = {model : model, dom_el : $_view_el, refreshed : _refreshed } czrapp.setupDOMListeners = function( event_map , args, instance ) { var _defaultArgs = { model : {}, dom_el : {} }; if ( _.isUndefined( instance ) || ! _.isObject( instance ) ) { czrapp.errorLog( 'setupDomListeners : instance should be an object', args ); return; } //event_map : are we good ? if ( ! _.isArray( event_map ) ) { czrapp.errorLog( 'setupDomListeners : event_map should be an array', args ); return; } //args : are we good ? if ( ! _.isObject( args ) ) { czrapp.errorLog( 'setupDomListeners : args should be an object', event_map ); return; } args = _.extend( _defaultArgs, args ); // => we need an existing dom element if ( ! ( args.dom_el instanceof jQuery ) || 1 != args.dom_el.length ) { czrapp.errorLog( 'setupDomListeners : dom element should be an existing dom element', args ); return; } //loop on the event map and map the relevant callbacks by event name // @param _event : //{ // trigger : '', // selector : '', // name : '', // actions : '' // once : false // }, _.map( event_map , function( _event ) { if ( ! _.isString( _event.selector ) || _.isEmpty( _event.selector ) ) { czrapp.errorLog( 'setupDOMListeners : selector must be a string not empty. Aborting setup of action(s) : ' + _event.actions.join(',') ); return; } //Are we good ? if ( ! _.isString( _event.selector ) || _.isEmpty( _event.selector ) ) { czrapp.errorLog( 'setupDOMListeners : selector must be a string not empty. Aborting setup of action(s) : ' + _event.actions.join(',') ); return; } var once = _event.once ? _event.once : false; //LISTEN TO THE DOM => USES EVENT DELEGATION args.dom_el[ once ? 'one' : 'on' ]( _event.trigger , _event.selector, function( e, event_params ) { //stop propagation to ancestors modules, typically a sektion e.stopPropagation(); //particular treatment if ( czrapp.isKeydownButNotEnterEvent( e ) ) { return; } e.preventDefault(); // Keep this AFTER the key filter above //It is important to deconnect the original object from its source //=> because we will extend it when used as params for the action chain execution var actionsParams = $.extend( true, {}, args ); //always get the latest model from the collection if ( _.has( actionsParams, 'model') && _.has( actionsParams.model, 'id') ) { if ( _.has( instance, 'get' ) ) actionsParams.model = instance(); else actionsParams.model = instance.getModel( actionsParams.model.id ); } //always add the event obj to the passed args //+ the dom event $.extend( actionsParams, { event : _event, dom_event : e } ); //add the event param => useful for triggered event $.extend( actionsParams, event_params ); //SETUP THE EMITTERS //inform the container that something has happened //pass the model and the current dom_el //the model is always passed as parameter if ( ! _.has( actionsParams, 'event' ) || ! _.has( actionsParams.event, 'actions' ) ) { czrapp.errorLog( 'executeEventActionChain : missing obj.event or obj.event.actions' ); return; } try { czrapp.executeEventActionChain( actionsParams, instance ); } catch( er ) { czrapp.errorLog( 'In setupDOMListeners : problem when trying to fire actions : ' + actionsParams.event.actions ); czrapp.errorLog( 'Error : ' + er ); } });//.on() });//_.map() };//setupDomListeners //GENERIC METHOD TO SETUP EVENT LISTENER //NOTE : the args.event must alway be defined czrapp.executeEventActionChain = function( args, instance ) { //if the actions param is a anonymous function, fire it and stop there if ( 'function' === typeof( args.event.actions ) ) return args.event.actions.call( instance, args ); //execute the various actions required //first normalizes the provided actions into an array of callback methods //then loop on the array and fire each cb if exists if ( ! _.isArray( args.event.actions ) ) args.event.actions = [ args.event.actions ]; //if one of the callbacks returns false, then we break the loop //=> allows us to stop a chain of callbacks if a condition is not met var _break = false; _.map( args.event.actions, function( _cb ) { if ( _break ) return; if ( 'function' != typeof( instance[ _cb ] ) ) { throw new Error( 'executeEventActionChain : the action : ' + _cb + ' has not been found when firing event : ' + args.event.selector ); } //Allow other actions to be bound before action and after // //=> we don't want the event in the object here => we use the one in the event map if set //=> otherwise will loop infinitely because triggering always the same cb from args.event.actions[_cb] //=> the dom element shall be get from the passed args and fall back to the controler container. var $_dom_el = ( _.has(args, 'dom_el') && -1 != args.dom_el.length ) ? args.dom_el : false; if ( ! $_dom_el ) { czrapp.errorLog( 'missing dom element'); return; } $_dom_el.trigger( 'before_' + _cb, _.omit( args, 'event' ) ); //executes the _cb and stores the result in a local var var _cb_return = instance[ _cb ].call( instance, args ); //shall we stop the action chain here ? if ( false === _cb_return ) _break = true; //allow other actions to be bound after $_dom_el.trigger( 'after_' + _cb, _.omit( args, 'event' ) ); });//_.map }; })(jQuery, czrapp);var czrapp = czrapp || {}; czrapp.methods = {}; (function( $ ){ var ctor, inherits, slice = Array.prototype.slice; // Shared empty constructor function to aid in prototype-chain creation. ctor = function() {}; /** * Helper function to correctly set up the prototype chain, for subclasses. * Similar to `goog.inherits`, but uses a hash of prototype properties and * class properties to be extended. * * @param object parent Parent class constructor to inherit from. * @param object protoProps Properties to apply to the prototype for use as class instance properties. * @param object staticProps Properties to apply directly to the class constructor. * @return child The subclassed constructor. */ inherits = function( parent, protoProps, staticProps ) { var child; // The constructor function for the new subclass is either defined by you // (the "constructor" property in your `extend` definition), or defaulted // by us to simply call `super()`. if ( protoProps && protoProps.hasOwnProperty( 'constructor' ) ) { child = protoProps.constructor; } else { child = function() { // Storing the result `super()` before returning the value // prevents a bug in Opera where, if the constructor returns // a function, Opera will reject the return value in favor of // the original object. This causes all sorts of trouble. var result = parent.apply( this, arguments ); return result; }; } // Inherit class (static) properties from parent. $.extend( child, parent ); // Set the prototype chain to inherit from `parent`, without calling // `parent`'s constructor function. ctor.prototype = parent.prototype; child.prototype = new ctor(); // Add prototype properties (instance properties) to the subclass, // if supplied. if ( protoProps ) $.extend( child.prototype, protoProps ); // Add static properties to the constructor function, if supplied. if ( staticProps ) $.extend( child, staticProps ); // Correctly set child's `prototype.constructor`. child.prototype.constructor = child; // Set a convenience property in case the parent's prototype is needed later. child.__super__ = parent.prototype; return child; }; /** * Base class for object inheritance. */ czrapp.Class = function( applicator, argsArray, options ) { var magic, args = arguments; if ( applicator && argsArray && czrapp.Class.applicator === applicator ) { args = argsArray; $.extend( this, options || {} ); } magic = this; /* * If the class has a method called "instance", * the return value from the class' constructor will be a function that * calls the "instance" method. * * It is also an object that has properties and methods inside it. */ if ( this.instance ) { magic = function() { return magic.instance.apply( magic, arguments ); }; $.extend( magic, this ); } magic.initialize.apply( magic, args ); return magic; }; /** * Creates a subclass of the class. * * @param object protoProps Properties to apply to the prototype. * @param object staticProps Properties to apply directly to the class. * @return child The subclass. */ czrapp.Class.extend = function( protoProps, classProps ) { var child = inherits( this, protoProps, classProps ); child.extend = this.extend; return child; }; czrapp.Class.applicator = {}; /** * Initialize a class instance. * * Override this function in a subclass as needed. */ czrapp.Class.prototype.initialize = function() {}; /* * Checks whether a given instance extended a constructor. * * The magic surrounding the instance parameter causes the instanceof * keyword to return inaccurate results; it defaults to the function's * prototype instead of the constructor chain. Hence this function. */ czrapp.Class.prototype.extended = function( constructor ) { var proto = this; while ( typeof proto.constructor !== 'undefined' ) { if ( proto.constructor === constructor ) return true; if ( typeof proto.constructor.__super__ === 'undefined' ) return false; proto = proto.constructor.__super__; } return false; }; /** * An events manager object, offering the ability to bind to and trigger events. * * Used as a mixin. */ czrapp.Events = { trigger: function( id ) { if ( this.topics && this.topics[ id ] ) this.topics[ id ].fireWith( this, slice.call( arguments, 1 ) ); return this; }, bind: function( id ) { this.topics = this.topics || {}; this.topics[ id ] = this.topics[ id ] || $.Callbacks(); this.topics[ id ].add.apply( this.topics[ id ], slice.call( arguments, 1 ) ); return this; }, unbind: function( id ) { if ( this.topics && this.topics[ id ] ) this.topics[ id ].remove.apply( this.topics[ id ], slice.call( arguments, 1 ) ); return this; } }; /** * Observable values that support two-way binding. * * @constructor */ czrapp.Value = czrapp.Class.extend({ /** * @param {mixed} initial The initial value. * @param {object} options */ initialize: function( initial, options ) { this._value = initial; // @todo: potentially change this to a this.set() call. this.callbacks = $.Callbacks(); this._dirty = false; $.extend( this, options || {} ); this.set = $.proxy( this.set, this ); }, /* * Magic. Returns a function that will become the instance. * Set to null to prevent the instance from extending a function. */ instance: function() { return arguments.length ? this.set.apply( this, arguments ) : this.get(); }, /** * Get the value. * * @return {mixed} */ get: function() { return this._value; }, /** * Set the value and trigger all bound callbacks. * * @param {object} to New value. */ set: function( to, o ) { var from = this._value, dfd = $.Deferred(), self = this, _promises = []; to = this._setter.apply( this, arguments ); to = this.validate( to ); var args = _.extend( { silent : false }, _.isObject( o ) ? o : {} ); // Bail if the sanitized value is null or unchanged. if ( null === to || _.isEqual( from, to ) ) { return dfd.resolveWith( self, [ to, from, o ] ).promise(); } this._value = to; this._dirty = true; if ( true === args.silent ) { return dfd.resolveWith( self, [ to, from, o ] ).promise(); } if ( this._deferreds ) { _.each( self._deferreds, function( _prom ) { _promises.push( _prom.apply( null, [ to, from, o ] ) ); }); $.when.apply( null, _promises ) .fail( function() { czrapp.errorLog( 'A deferred callback failed in api.Value::set()'); }) .then( function() { self.callbacks.fireWith( self, [ to, from, o ] ); dfd.resolveWith( self, [ to, from, o ] ); }); } else { this.callbacks.fireWith( this, [ to, from, o ] ); return dfd.resolveWith( self, [ to, from, o ] ).promise( self ); } return dfd.promise( self ); }, /***************************************************************************** * A SILENT SET METHOD : * => keep the dirtyness param unchanged * => stores the api state before callback calls, and reset it after * => add an object param to the callback to inform that this is a silent process * , this is typically used in the overridden api.Setting.preview method *****************************************************************************/ //@param to : the new value to set //@param dirtyness : the current dirtyness status of this setting in the skope silent_set : function( to, dirtyness ) { var from = this._value; to = this._setter.apply( this, arguments ); to = this.validate( to ); // Bail if the sanitized value is null or unchanged. if ( null === to || _.isEqual( from, to ) ) { return this; } this._value = to; this._dirty = ( _.isUndefined( dirtyness ) || ! _.isBoolean( dirtyness ) ) ? this._dirty : dirtyness; this.callbacks.fireWith( this, [ to, from, { silent : true } ] ); return this; }, _setter: function( to ) { return to; }, setter: function( callback ) { var from = this.get(); this._setter = callback; // Temporarily clear value so setter can decide if it's valid. this._value = null; this.set( from ); return this; }, resetSetter: function() { this._setter = this.constructor.prototype._setter; this.set( this.get() ); return this; }, validate: function( value ) { return value; }, /** * Bind a function to be invoked whenever the value changes. * * @param {...Function} A function, or multiple functions, to add to the callback stack. */ //allows us to specify a list of callbacks + a { deferred : true } param //if deferred is found and true, then the callback(s) are added in a list of deferred //@see how this deferred list is used in api.Value.prototype.set() bind: function() { //find an object in the argument var self = this, _isDeferred = false, _cbs = []; $.each( arguments, function( _key, _arg ) { if ( ! _isDeferred ) _isDeferred = _.isObject( _arg ) && _arg.deferred; if ( _.isFunction( _arg ) ) _cbs.push( _arg ); }); if ( _isDeferred ) { self._deferreds = self._deferreds || []; _.each( _cbs, function( _cb ) { if ( ! _.contains( _cb, self._deferreds ) ) self._deferreds.push( _cb ); }); } else { //original method self.callbacks.add.apply( self.callbacks, arguments ); } return this; }, /** * Unbind a previously bound function. * * @param {...Function} A function, or multiple functions, to remove from the callback stack. */ unbind: function() { this.callbacks.remove.apply( this.callbacks, arguments ); return this; }, // link: function() { // values* // var set = this.set; // $.each( arguments, function() { // this.bind( set ); // }); // return this; // }, // unlink: function() { // values* // var set = this.set; // $.each( arguments, function() { // this.unbind( set ); // }); // return this; // }, // sync: function() { // values* // var that = this; // $.each( arguments, function() { // that.link( this ); // this.link( that ); // }); // return this; // }, // unsync: function() { // values* // var that = this; // $.each( arguments, function() { // that.unlink( this ); // this.unlink( that ); // }); // return this; // } }); /** * A collection of observable values. * * @constructor */ czrapp.Values = czrapp.Class.extend({ /** * The default constructor for items of the collection. * * @type {object} */ defaultConstructor: czrapp.Value, initialize: function( options ) { $.extend( this, options || {} ); this._value = {}; this._deferreds = {}; }, /** * Get the instance of an item from the collection if only ID is specified. * * If more than one argument is supplied, all are expected to be IDs and * the last to be a function callback that will be invoked when the requested * items are available. * * @see {czrapp.Values.when} * * @param {string} id ID of the item. * @param {...} Zero or more IDs of items to wait for and a callback * function to invoke when they're available. Optional. * @return {mixed} The item instance if only one ID was supplied. * A Deferred Promise object if a callback function is supplied. */ instance: function( id ) { if ( arguments.length === 1 ) return this.value( id ); return this.when.apply( this, arguments ); }, /** * Get the instance of an item. * * @param {string} id The ID of the item. * @return {[type]} [description] */ value: function( id ) { return this._value[ id ]; }, /** * Whether the collection has an item with the given ID. * * @param {string} id The ID of the item to look for. * @return {Boolean} */ has: function( id ) { return typeof this._value[ id ] !== 'undefined'; }, /** * Add an item to the collection. * * @param {string} id The ID of the item. * @param {mixed} value The item instance. * @return {mixed} The new item's instance. */ add: function( id, value ) { if ( this.has( id ) ) return this.value( id ); this._value[ id ] = value; value.parent = this; // Propagate a 'change' event on an item up to the collection. if ( value.extended( czrapp.Value ) ) value.bind( this._change ); this.trigger( 'add', value ); // If a deferred object exists for this item, // resolve it. if ( this._deferreds[ id ] ) this._deferreds[ id ].resolve(); return this._value[ id ]; }, /** * Create a new item of the collection using the collection's default constructor * and store it in the collection. * * @param {string} id The ID of the item. * @param {mixed} value Any extra arguments are passed into the item's initialize method. * @return {mixed} The new item's instance. */ create: function( id ) { return this.add( id, new this.defaultConstructor( czrapp.Class.applicator, slice.call( arguments, 1 ) ) ); }, /** * Iterate over all items in the collection invoking the provided callback. * * @param {Function} callback Function to invoke. * @param {object} context Object context to invoke the function with. Optional. */ each: function( callback, context ) { context = typeof context === 'undefined' ? this : context; $.each( this._value, function( key, obj ) { callback.call( context, obj, key ); }); }, /** * Remove an item from the collection. * * @param {string} id The ID of the item to remove. */ remove: function( id ) { var value; if ( this.has( id ) ) { value = this.value( id ); this.trigger( 'remove', value ); if ( value.extended( czrapp.Value ) ) value.unbind( this._change ); delete value.parent; } delete this._value[ id ]; delete this._deferreds[ id ]; }, /** * Runs a callback once all requested values exist. * * when( ids*, [callback] ); * * For example: * when( id1, id2, id3, function( value1, value2, value3 ) {} ); * * @returns $.Deferred.promise(); */ when: function() { var self = this, ids = slice.call( arguments ), dfd = $.Deferred(); // If the last argument is a callback, bind it to .done() if ( $.isFunction( ids[ ids.length - 1 ] ) ) dfd.done( ids.pop() ); /* * Create a stack of deferred objects for each item that is not * yet available, and invoke the supplied callback when they are. */ $.when.apply( $, $.map( ids, function( id ) { if ( self.has( id ) ) return; /* * The requested item is not available yet, create a deferred * object to resolve when it becomes available. */ return self._deferreds[ id ] || $.Deferred(); })).done( function() { var values = $.map( ids, function( id ) { return self( id ); }); // If a value is missing, we've used at least one expired deferred. // Call Values.when again to generate a new deferred. if ( values.length !== ids.length ) { // ids.push( callback ); self.when.apply( self, ids ).done( function() { dfd.resolveWith( self, values ); }); return; } dfd.resolveWith( self, values ); }); return dfd.promise(); }, /** * A helper function to propagate a 'change' event from an item * to the collection itself. */ _change: function() { this.parent.trigger( 'change', this ); } }); // Create a global events bus $.extend( czrapp.Values.prototype, czrapp.Events ); })( jQuery );//@global HUParams var czrapp = czrapp || {}; /************************* * ADD BASE CLASS METHODS *************************/ (function($, czrapp) { //adds the server params to the app now czrapp.localized = HUParams || {}; var _methods = { /** * Cache properties on Dom Ready * @return {[type]} [description] */ cacheProp : function() { var self = this; $.extend( czrapp, { //cache various jQuery el in czrapp obj $_window : $(window), $_html : $('html'), $_body : $('body'), $_header : $('#header'), $_wpadminbar : $('#wpadminbar'), $_mainWrapper : $('.main', '#wrapper'), $_mainContent : $('.main', '#wrapper').find('.content'), //various properties definition is_responsive : self.isResponsive(),//store the initial responsive state of the window current_device : self.getDevice()//store the initial device }); }, //bool isResponsive : function() { return this.matchMedia(979); }, //@return string of current device getDevice : function() { var _devices = { desktop : 979, tablet : 767, smartphone : 480 }, _current_device = 'desktop', that = this; _.map( _devices, function( max_width, _dev ){ if ( that.matchMedia( max_width ) ) _current_device = _dev; } ); return _current_device; }, matchMedia : function( _maxWidth ) { if ( window.matchMedia ) return ( window.matchMedia("(max-width: "+_maxWidth+"px)").matches ); //old browsers compatibility $_window = czrapp.$_window || $(window); return $_window.width() <= ( _maxWidth - 15 ); }, emit : function( cbs, args ) { cbs = _.isArray(cbs) ? cbs : [cbs]; var self = this; _.map( cbs, function(cb) { if ( 'function' == typeof(self[cb]) ) { args = 'undefined' == typeof( args ) ? Array() : args ; self[cb].apply(self, args ); czrapp.trigger( cb, _.object( _.keys(args), args ) ); } });//_.map }, triggerSimpleLoad : function( $_imgs ) { if ( 0 === $_imgs.length ) return; $_imgs.map( function( _ind, _img ) { $(_img).on('load', function () { $(_img).trigger('simple_load'); });//end load if ( $(_img)[0] && $(_img)[0].complete ) $(_img).trigger('load'); } );//end map },//end of fn isUserLogged : function() { return czrapp.$_body.hasClass('logged-in') || 0 !== czrapp.$_wpadminbar.length; }, isSelectorAllowed : function( $_el, skip_selectors, requested_sel_type ) { var sel_type = 'ids' == requested_sel_type ? 'id' : 'class', _selsToSkip = skip_selectors[requested_sel_type]; //check if option is well formed if ( 'object' != typeof(skip_selectors) || ! skip_selectors[requested_sel_type] || ! $.isArray( skip_selectors[requested_sel_type] ) || 0 === skip_selectors[requested_sel_type].length ) return true; //has a forbidden parent? if ( $_el.parents( _selsToSkip.map( function( _sel ){ return 'id' == sel_type ? '#' + _sel : '.' + _sel; } ).join(',') ).length > 0 ) return false; //has requested sel ? if ( ! $_el.attr( sel_type ) ) return true; var _elSels = $_el.attr( sel_type ).split(' '), _filtered = _elSels.filter( function(classe) { return -1 != $.inArray( classe , _selsToSkip ) ;}); //check if the filtered selectors array with the non authorized selectors is empty or not //if empty => all selectors are allowed //if not, at least one is not allowed return 0 === _filtered.length; }, //@return bool _isMobileScreenSize : function() { return ( _.isFunction( window.matchMedia ) && matchMedia( 'only screen and (max-width: 720px)' ).matches ) || ( this._isCustomizing() && 'desktop' != this.previewDevice() ); }, //@return bool _isCustomizing : function() { return czrapp.$_body.hasClass('is-customizing') || ( 'undefined' !== typeof wp && 'undefined' !== typeof wp.customize ); }, //Helpers //Check if the passed element(s) contains an iframe //@return list of containers //@param $_elements = mixed _has_iframe : function ( $_elements ) { var that = this, to_return = []; _.each( $_elements, function( $_el, container ){ if ( $_el.length > 0 && $_el.find('IFRAME').length > 0 ) to_return.push(container); }); return to_return; }, // Observer Mutations of the DOM for a given element selector // <=> of previous $(document).bind( 'DOMNodeInserted', fn ); // implemented to fix https://github.com/presscustomizr/hueman/issues/880 // see https://stackoverflow.com/questions/10415400/jquery-detecting-div-of-certain-class-has-been-added-to-dom#10415599 observeAddedNodesOnDom : function(containerSelector, elementSelector, callback) { var onMutationsObserved = function(mutations) { mutations.forEach(function(mutation) { if (mutation.addedNodes.length) { var elements = $(mutation.addedNodes).find(elementSelector); for (var i = 0, len = elements.length; i < len; i++) { callback(elements[i]); } } }); }, target = $(containerSelector)[0], config = { childList: true, subtree: true }, MutationObserver = window.MutationObserver || window.WebKitMutationObserver, observer = new MutationObserver(onMutationsObserved); observer.observe(target, config); } };//_methods{} czrapp.methods.Base = czrapp.methods.Base || {}; $.extend( czrapp.methods.Base , _methods );//$.extend })(jQuery, czrapp);/*************************** * ADD BROWSER DETECT METHODS ****************************/ (function($, czrapp) { var _methods = { addBrowserClassToBody : function() { // $.browser property was removed in jQuery 1.9 and is available only through the jQuery.migrate plugin if ( !$.browser ) return; // Chrome is Webkit, but Webkit is also Safari. If browser = ie + strips out the .0 suffix if ( $.browser.chrome ) czrapp.$_body.addClass("chrome"); else if ( $.browser.webkit ) czrapp.$_body.addClass("safari"); if ( $.browser.mozilla ) czrapp.$_body.addClass("mozilla"); else if ( $.browser.msie || '8.0' === $.browser.version || '9.0' === $.browser.version || '10.0' === $.browser.version || '11.0' === $.browser.version ) czrapp.$_body.addClass("ie").addClass("ie" + $.browser.version.replace(/[.0]/g, '')); //Adds version if browser = ie if ( czrapp.$_body.hasClass("ie") ) czrapp.$_body.addClass($.browser.version); } };//_methods{} czrapp.methods.BrowserDetect = czrapp.methods.BrowserDetect || {}; $.extend( czrapp.methods.BrowserDetect , _methods ); })(jQuery, czrapp); var czrapp = czrapp || {}; /*************************** * ADD JQUERY PLUGINS METHODS ****************************/ (function( $, czrapp ) { var _methods = { //IMG SMART LOAD //.article-container covers all post / page content : single and list //__before_main_wrapper covers the single post thumbnail case //.widget-front handles the featured pages imgSmartLoad : function() { var smartLoadEnabled = 1 == HUParams.imgSmartLoadEnabled, //Default selectors for where are : $( '.article-container, .__before_main_wrapper, .widget-front' ).find('img'); _where = HUParams.imgSmartLoadOpts.parentSelectors.join(); _params = _.size( HUParams.imgSmartLoadOpts.opts ) > 0 ? HUParams.imgSmartLoadOpts.opts : {}; //Smart-Load images //imgSmartLoad plugin will trigger the smartload event when the img will be loaded //the centerImages plugin will react to this event centering them var _doLazyLoad = function() { if ( !smartLoadEnabled ) return; $(_where).each( function() { // if the element already has an instance of LazyLoad, simply trigger an event if ( !$(this).data('smartLoadDone') ) { $(this).imgSmartLoad(_params); } else { $(this).trigger('trigger-smartload'); } }); //$(_where).imgSmartLoad(_params); }; _doLazyLoad(); // Observer Mutations off the DOM to detect images // <=> of previous $(document).bind( 'DOMNodeInserted', fn ); // implemented to fix https://github.com/presscustomizr/hueman/issues/880 this.observeAddedNodesOnDom('body', 'img', _.debounce( function(element) { _doLazyLoad(); }, 50 )); //If the centerAllImg is on we have to ensure imgs will be centered when simple loaded, //for this purpose we have to trigger the simple-load on: //1) imgs which have been excluded from the smartloading if enabled //2) all the images in the default 'where' if the smartloading isn't enaled //simple-load event on holders needs to be triggered with a certain delay otherwise holders will be misplaced (centering) if ( 1 == HUParams.centerAllImg ) { var self = this, $_to_center = smartLoadEnabled ? $( _.filter( $( _where ).find('img'), function( img ) { return $(img).is(HUParams.imgSmartLoadOpts.opts.excludeImg.join()); }) ): //filter $( _where ).find('img'); $_to_center_with_delay = $( _.filter( $_to_center, function( img ) { return $(img).hasClass('tc-holder-img'); }) ); //imgs to center with delay setTimeout( function(){ self.triggerSimpleLoad( $_to_center_with_delay ); }, 300 ); //all other imgs to center self.triggerSimpleLoad( $_to_center ); } }, //FIRE EXT LINKS PLUGIN //May be add (check if activated by user) external class + target="_blank" to relevant links //images are excluded by default //links inside post/page content extLinks : function() { if ( ! HUParams.extLinksStyle && ! HUParams.extLinksTargetExt ) return; $('a' , '.post-inner .entry p, .post-inner .entry li, .type-page .entry p, .type-page .entry li').extLinks({ addIcon : HUParams.extLinksStyle, iconClassName : 'hu-external', newTab : HUParams.extLinksTargetExt, skipSelectors : _.isObject(HUParams.extLinksSkipSelectors) ? HUParams.extLinksSkipSelectors : {} }); }, parallax : function() { $( '.parallax-item' ).czrParallax(); }, };//_methods{} czrapp.methods.JQPlugins = czrapp.methods.JQPlugins || {}; $.extend( czrapp.methods.JQPlugins = {} , _methods ); })(jQuery, czrapp); var czrapp = czrapp || {}; /************************************************ * USER EXPERIENCE SUB CLASS *************************************************/ (function($, czrapp) { var _methods = { setupUIListeners : function() { var self = this; //declare and store the main user xp properties and obervable values this.windowWidth = new czrapp.Value( czrapp.$_window.width() ); this.isScrolling = new czrapp.Value( false ); this.isResizing = new czrapp.Value( false ); this.scrollPosition = new czrapp.Value( czrapp.$_window.scrollTop() ); this.scrollDirection = new czrapp.Value('down'); self.previewDevice = new czrapp.Value( 'desktop' ); //PREVIEWED DEVICE ? //Listen to the customizer previewed device if ( self._isCustomizing() ) { var _setPreviewedDevice = function() { wp.customize.preview.bind( 'previewed-device', function( device ) { self.previewDevice( device ); }); }; if ( wp.customize.preview ) { _setPreviewedDevice(); } else { wp.customize.bind( 'preview-ready', function() { _setPreviewedDevice(); }); } } //ABSTRACTION LAYER //listen to windowWidth self.windowWidth.bind( function( to, from ) { //Always bail if is not "real" resize. //=> Resize events can be triggered when scrolling on mobile devices, whitout actually resizing the screen self.isResizing( self._isMobileScreenSize ? Math.abs( from - to ) > 2 : Math.abs( from - to ) > 0 ); clearTimeout( $.data( this, 'resizeTimer') ); $.data( this, 'resizeTimer', setTimeout(function() { self.isResizing( false ); }, 50 ) ); }); //"real" horizontal resize reaction : refreshed every 50 ms self.isResizing.bind( function( is_resizing ) { czrapp.$_body.toggleClass( 'is-resizing', is_resizing ); }); //react when scrolling status change //=> auto set it self to false after a while this.isScrolling.bind( function( to, from ) { //self.scrollPosition( czrapp.$_window.scrollTop() ); czrapp.$_body.toggleClass( 'is-scrolling', to ); if ( ! to ) { czrapp.trigger( 'scrolling-finished' ); } }); //scroll position is set when scrolling this.scrollPosition.bind( function( to, from ) { //handle scrolling classes czrapp.$_body.toggleClass( 'is-scrolled', to > 100 ); if ( to <= 50 ) { czrapp.trigger( 'page-scrolled-top', {} ); } self.scrollDirection( to >= from ? 'down' : 'up' ); }); //BROWSER LAYER : RESIZE AND SCROLL //listen to user DOM actions czrapp.$_window.on('resize', _.throttle( function( ev ) { self.windowWidth( czrapp.$_window.width() ); }, 10 ) ); czrapp.$_window.on('scroll', _.throttle( function() { self.isScrolling( true ); //self.previousScrollPosition = self.scrollPosition() || czrapp.$_window.scrollTop(); self.scrollPosition( czrapp.$_window.scrollTop() ); clearTimeout( $.data( this, 'scrollTimer') ); $.data( this, 'scrollTimer', setTimeout(function() { self.isScrolling( false ); }, 100 ) ); }, 10 ) ); }, //useful function which resets relevant inline CSS rules of an element //which has been slided UP/Down (http://api.jquery.com/slidedown/, http://api.jquery.com/slideup/) //This function can be called as complete callback to: //1) reset the display property => e.g. we want to proper handle the visibility of the element, after the animation ends, within our stylesheet //2) reset height/padding/margin => it might happen (due to the stop() function call while animating, e.g. when fast clicking/submenu hovering) that the slided element //retains these rules as inline style. This can have a very bad impact on elements which display differently in different viewports, e.g. the footer menu. // //While we have a different html element for the header menu in mobile devices, we use the same html element for the footer menu. //Following some examples of the issues the use of this code prevents: //- case 1: // a) we shrink the window till reaching the mobile viewport // b) we open the footer mobile menu // c) we close it // d) we re-enlarge the window // => without the 1) the display:none (produced by the slideUp) rule would have been added (step c) ) to the menu making it not visible in step d) // similar thing can happen with the height property (due to the stop() aforementioned), if we don't reset it with 2) //- case 2: // a) in desktop viewport we rapidly hover over different menu items with submenus, this, as said above, can produce some spurious inline style // b) we shrink the window // c) we open the footer mobile menu // => without the 2) because of the retained inline style we can have submenu ul with a fixed height set, so potentially we can have adjacent menu items // overlapping onSlidingCompleteResetCSS : function( $_el ) { $_el = $_el ? $_el : $(this); $_el.css({ 'display' : '', 'paddingTop' : '', 'marginTop' : '', 'paddingBottom' : '', 'marginBottom' : '', 'height' : '' }); }, };//_methods{} czrapp.methods.UserXP = czrapp.methods.UserXP || {}; $.extend( czrapp.methods.UserXP , _methods ); })(jQuery, czrapp);var czrapp = czrapp || {}; /************************************************ * USER EXPERIENCE SUB CLASS *************************************************/ (function($, czrapp) { var _methods = { mobileMenu : function() { var self = this; self.mobileMenu = new czrapp.Values(); //Instantiate the menu candidates //data-menu-id should be unique. Are not synchronized with the actual menu css # id attribute $('.nav-container').each( function( _index ) { if ( ! _.isString( $(this).attr( 'data-menu-id' ) ) ) return; var $container = $(this), is_scrollable = _.isString( $(this).attr( 'data-menu-scrollable' ) ) && "false" == $(this).attr( 'data-menu-scrollable' ) ? false : true, _candidateId = $container.attr( 'data-menu-id' ), ctor; if ( self.mobileMenu.has( _candidateId ) ) return; var $navWrap = $container.find( '.nav-wrap' ); // if ( 1 != $navWrap.length ) { // czrapp.errorLog( 'Mobile menu : missing .nav-wrap for menu-id : ' + _candidateId ); // } var button_selectors = '.nav-toggle, .ham__navbar-toggler, .ham__navbar-toggler-two', $button = $container.find( button_selectors ); // if ( 1 != $button.length ) { // czrapp.errorLog( 'Mobile menu : missing container for menu-id : ' + _candidateId ); // } //A mobile menu candidate should have a wrapper and a button selector if ( 1 == $navWrap.length && 1 == $button.length ) { ctor = czrapp.Value.extend( self.MobileCTOR ); //do instantiate self.mobileMenu.add( _candidateId, new ctor( _candidateId, { container : $container, menu_wrapper : $navWrap, button : $button, button_selectors : button_selectors, is_scrollable : is_scrollable })); } }); }, //CTOR for each mobile menu Value MobileCTOR : { //@param constructor_options : // { // container : $container, // menu_wrapper : $navWrap, // button : $button, // button_selectors : '.nav-toggle, .ham__navbar-toggler, .ham__navbar-toggler-two' // } initialize: function( mobile_id, constructor_options ) { var mobMenu = this; czrapp.Value.prototype.initialize.call( mobMenu, null, constructor_options ); //write the options as properties $.extend( mobMenu, constructor_options || {} ); //set initial state mobMenu( 'collapsed' ).button .toggleClass( 'collapsed', true ) .toggleClass( 'active', false ) .attr('aria-expanded', false ); //react on state change //@return a deferred //=> in a scenario of menu expanded and scroll down, allow us to nicely run the sequence of animation: //1) menu collapse //2) animate up mobMenu.bind( function( state ) { return $.Deferred( function() { var dfd = this; //Always close the search bar before doing anything else czrapp.userXP.headerSearchExpanded( false ).done( function() { mobMenu._toggleMobileMenu() .done( function( state ){ //remove classes that modify the appearance of the button //=> needed for mobile devices because the focus is not automatically removed mobMenu.button.toggleClass( 'hovering', 'expanded' == state ).toggleClass( 'focusing', 'expanded' == state ); dfd.resolve(); }); }); }).promise(); }, { deferred : true } ); //Listen to user actions czrapp.setupDOMListeners( [ { trigger : 'mousedown focusin keydown', selector : mobMenu.button_selectors, actions : function() { var mobMenu = this; mobMenu( 'collapsed' == mobMenu() ? 'expanded' : 'collapsed' ); } }, { trigger : 'mouseenter', selector : mobMenu.button_selectors, actions : function() { this.button.addClass( 'hovering' ); } }, { trigger : 'mouseleave', selector : mobMenu.button_selectors, actions : function() { this.button.removeClass( 'hovering' ); } } ],//actions to execute { dom_el: mobMenu.container },//dom scope mobMenu //instance where to look for the cb methods ); //maybe init mobile submenu expand on click, on mobile button first click if ( czrapp.localized.mobileSubmenuExpandOnClick ) { //add specific class to this mobile menu which tells its submenus have to be expanded on click (purpose: style) mobMenu.menu_wrapper.addClass( 'submenu-click-expand' ); czrapp.setupDOMListeners( [ { trigger : 'mousedown focusin keydown', selector : mobMenu.button_selectors, actions : function() { var mobMenu = this; mobMenu._collapsibleSubmenu(); }, once : true } ],//actions to execute { dom_el: mobMenu.container },//dom scope mobMenu //instance where to look for the cb methods ); } //listen to czrapp events //Collapse on resize czrapp.userXP.isResizing.bind( function( is_resizing ) { if ( ! is_resizing ) return; mobMenu( 'collapsed' ); }); //when clicking on a menu item, always collapse the menu //@fixes https://github.com/presscustomizr/hueman/issues/830 $( mobMenu.container ) .on( 'mouseup', '.menu-item a', function(evt) { if ( ! czrapp.userXP._isMobileScreenSize() ) return; // Hack to fix the issue => [mobile menu] clicking on an anchor link that has child submenu should unfold the submenu // see https://github.com/presscustomizr/hueman/issues/857 if ( '#' === $(this).attr('href') ) return; evt.preventDefault(); evt.stopPropagation(); mobMenu( 'collapsed'); }); }, //@return dfd promise() //react on mobMenu( 'collapsed' or 'expanded' ) _toggleMobileMenu : function() { var mobMenu = this, expand = 'expanded' == mobMenu(), dfd = $.Deferred(); //Set the button dom state mobMenu.button .toggleClass( 'collapsed', ! expand ) .toggleClass( 'active', expand ) .attr('aria-expanded', expand ); $.when( mobMenu.menu_wrapper.toggleClass( 'expanded', expand ) ).done( function() { var $navWrap = $(this); $navWrap.find('.nav').stop()[ ! expand ? 'slideUp' : 'slideDown' ]( { duration : 300, complete : function() { //makes it scrollable ( currently true for all menu but the footer ) //scrollable is set in the DOM with data-menu-scrollable if ( mobMenu.is_scrollable ) { var _winHeight = 'undefined' === typeof window.innerHeight ? window.innerHeight : czrapp.$_window.height(), _visibleHeight = _winHeight - $navWrap.offset().top + czrapp.$_window.scrollTop(); $navWrap.css( { 'max-height' : expand ? _visibleHeight : '', 'overflow' : 'auto' }); } czrapp.userXP.onSlidingCompleteResetCSS($(this).toggleClass( 'expanded', expand )); dfd.resolve( expand ); } } ); }); return dfd.promise(); }, //twentyseventeen inspired _collapsibleSubmenu : function() { var mobMenu = this; var EVENT_KEY = '.hu.submenu', Event = { SHOW : 'show' + EVENT_KEY, HIDE : 'hide' + EVENT_KEY, CLICK : 'mousedown' + EVENT_KEY, FOCUSIN : 'focusin' + EVENT_KEY, FOCUSOUT : 'focusout' + EVENT_KEY }, Classname = { DD_TOGGLE_ON_CLICK : 'submenu-click-expand', SHOWN : 'expanded', DD_TOGGLE : 'hu-dropdown-toggle', DD_TOGGLE_WRAPPER : 'hu-dropdown-toggle-wrapper', SCREEN_READER : 'screen-reader-text', }, Selector = { DD_TOGGLE_PARENT : '.menu-item-has-children, .page_item_has_children', CURRENT_ITEM_ANCESTOR : '.current-menu-ancestor', SUBMENU : '.sub-menu' }, // Add dropdown toggle that displays child menu items. dropdownToggle = $( '