/** * @typedef YqEcommerceResultsList * @type {object} */ class YouneeqEcommerceHandler { public box: HTMLElement; public ecom: object; public is_loading: boolean; public currency: string; public currency_format: string; public static instances: YouneeqEcommerceHandler[] = []; public static user_id: string = ''; public static is_waiting_for_id: boolean = false; private static readonly ECOM_PATH = `https://apitest.youneeq.ca/api/ecom`; private static readonly SESSION_ID_FILE = `https://api.youneeq.ca/app/sessionid`; private static readonly ADD_TO_CART_PATH = `https://apitest.youneeq.ca/api/addtocart/get`; public constructor( box: HTMLElement, auto: boolean = true ) { if ( !this || !this.init_all_data ) { throw `YouneeqEcommerceHandler() must be called with "new"`; } this.box = box; this.ecom = {}; this.is_loading = false; this.currency = ''; this.currency_format = ''; YouneeqEcommerceHandler.instances.push( auto ? this.init_all_data().request( [ `first`, `observe` ] ) : this.init_all_data() ); } /** * Generates YouneeqEcommerceHandler instances for each valid HTML element on the page. * * @since 3.1.0 */ public static generate(): void { let ecom_boxes = jQuery( `#youneeq-ecommerce, .youneeq-ecommerce, youneeq-ecommerce` ) .get().sort( ( a, b ) => { let prio_a = jQuery( a ).attr( `data-yq-priority` ), prio_b = jQuery( b ).attr( `data-yq-priority` ); prio_a = prio_a ? parseInt( prio_a ) : 0; prio_b = prio_b ? parseInt( prio_b ) : 0; if ( prio_a > prio_b ) { return 1; } else if ( prio_a < prio_b ) { return -1; } return 0; }); ecom_boxes.forEach( e => { let box = jQuery( e ), handler = new YouneeqEcommerceHandler( e, !box.hasClass( `yq-no-auto` ) ); box.data( `youneeqEcommerceHandler`, handler ); }); } public static init_user_id( user_id: string|null = null ): void { if ( user_id !== null && user_id.length >= 8 ) { YouneeqEcommerceHandler.user_id = user_id; } else { let has_storage = true; try { user_id = localStorage.getItem( `yq_session` ); } catch ( e ) { has_storage = false; } if ( user_id && user_id.length >= 8 ) { YouneeqEcommerceHandler.user_id = user_id; } else { YouneeqEcommerceHandler.is_waiting_for_id = true; jQuery( `` ).load( YouneeqEcommerceHandler.SESSION_ID_FILE, id => { if ( id.length >= 8 ) { YouneeqEcommerceHandler.user_id = id; if ( has_storage ) { localStorage.setItem( `yq_session`, id ); } } YouneeqEcommerceHandler.is_waiting_for_id = false; }); } } } public static add_to_cart( product_data: object ): void { if ( product_data.product_id ) { product_data.domain = product_data.domain ? product_data.domain : window.location.hostname; product_data.variant_id = product_data.variant_id ? product_data.variant_id : 0; product_data.tags = product_data.tags ? product_data.tags : []; product_data.user_id = YouneeqEcommerceHandler.user_id; product_data.url = product_data.url ? product_data.url : window.location.toString(); try { product_data.published_at = new Date( product_data.published_at ).toISOString(); } catch ( e ) { product_data.published_at = new Date().toISOString(); } jQuery.ajax({ url: YouneeqEcommerceHandler.ADD_TO_CART_PATH, crossDomain: true, dataType: `jsonp`, data: { json: JSON.stringify( product_data ) } }); } } public static place_order( order_data: object ): void { order_data.user_id = YouneeqEcommerceHandler.user_id; try { order_data.created_at = new Date( order_data.created_at ).toISOString(); } catch ( e ) { order_data.created_at = new Date().toISOString(); } console.info( order_data ); } /** * Send a Youneeq request. * * @since 3.1.0 * * @return {YouneeqEcommerceHandler} * @param {string[]} tags List of tags specifying request context. */ public request( tags: string[] = [] ): this { if ( !this.is_loading ) { this.is_loading = true; let data_get = this.get_request_data( false, tags ), data_post = this.get_request_data( true, tags ); jQuery.ajax({ url: YouneeqEcommerceHandler.ECOM_PATH, crossDomain: true, dataType: `jsonp`, method: `GET`, data: { json: JSON.stringify( data_get ) } }) .done( this._populate( tags, `ajax_display` in this ) ); jQuery.ajax({ url: YouneeqEcommerceHandler.ECOM_PATH, crossDomain: true, dataType: `jsonp`, method: `POST`, data: { json: JSON.stringify( data_post ) }, beforeSend: console.info }); } return this; } private get_request_data( is_post: boolean, tags: string[] ): object { let data = {}; data.currency = this.ecom.currency.name; data.domain = this.ecom.domain; data.product_id = this.ecom.product && this.ecom.product.id ? this.ecom.product.id : null; data.url = this.ecom.url; data.user_id = YouneeqEcommerceHandler.user_id; if ( is_post ) { data.product = this.ecom.product; } else { // Send page hit data with first request if ( tags.indexOf( `first` ) > -1 ) { data.page_hit = YouneeqEcommerceHandler.get_page_hit_data(); } if ( 'suggest' in this.ecom ) { data.suggest = [ this.ecom.suggest ]; } data.track_user = true; data.index_product = false; } return data; } private static get_page_hit_data(): object { let data = { href: document.location.href, referrer: document.referrer }; try { let tz_info = jzTimezoneDetector.determine_timezone(); data.tzoff = tz_info.timezone.utc_offset; data.tzname = tz_info.timezone.olson_tz; } catch ( e ) {} return data; } /** * Formats a number into a price according to the formatting rules supplied to the ecommerce handler. * * Prices should be positive integers with no decimal places. * * @since 3.1.0 * * @return {string} * @param {number} price A positive integer representing the price. */ public format_price( price: number ): string { let out_num = '', num_format = null, over_1000 = false; num_format = this.ecom.currency.format.match( /#{(\d+)(\D)(\D)}/ ); if ( !num_format || num_format.length != 4 ) { throw new Error( 'YouneeqEcommerceHandler: Invalid currency format' ); } price = Math.floor( Math.abs( price ) ); if ( num_format[1] > 0 ) { out_num = num_format[3] + ( price % Math.pow( 10, num_format[1] ) ) .toString().padStart( num_format[1], '0'); price = Math.floor( price / Math.pow( 10, num_format[1] ) ); } if ( !price ) { out_num = '0' + out_num; } else { while ( price >= 1000 ) { out_num = ( price % 1000 ).toString() .padStart( 3, '0' ) + ( over_1000 ? num_format[2] : '' ) + out_num; price = Math.floor( price / 1000 ); over_1000 = true; } out_num = price + ( over_1000 ? num_format[2] : '' ) + out_num; } return this.ecom.currency.format.replace( /#{\d+\D\D}/, out_num ); } /** * Generate recommended product HTML and display it on the page. * * @since 3.1.0 * * @param {YqEcommerceResultsList} response Returned data from Youneeq request. * @param {string[]} tags List of tags specifying request context. */ private display( response: YqEcommerceResultsList, tags: string[] ): void { if ( response && `results` in response && response.results.length ) { let products = response.results, $box = jQuery( this.box ); for ( let i = 0, max = products.length; i < max; i++ ) { let id = products[ i ].id ? products[ i ].id : ``, title = products[ i ].title ? products [ i ].title : ``, url = products[ i ].url ? products[ i ].url : ``, img = products[ i ].featured_image ? products[ i ].featured_image : ``, price = products[ i ].price ? products[ i ].price : ``; $box.append( `
${ img ? `${ title }` : `` }

${ title }

${ this.format_price( price ) }

` ); } } } /** * Call object initialization methods. * * @since 3.1.0 * * @return {YouneeqEcommerceHandler} */ private init_all_data(): this { let args = this.get_args(); return this.init_request_data( args ) .init_suggest_data( args ) .init_observe_data( args ) .init_method_overrides( args ); } /** * Get element arguments from data attributes, function, and JSON element. * * @since 3.1.0 * * @return {object} Arguments object. */ private get_args(): object { let args = {}, data = {}, json = {}, json_element = null; // Get args from data attributes. for ( let i = 0, max = this.box.attributes.length; i < max; i++ ) { let arg_name = this.box.attributes[ i ].name; if ( arg_name.substr( 0, 8 ) == `data-yq-` ) { args[ arg_name.substr( 8 ).replace( /-/g, `_` ) ] = this.box.attributes[ i ].value; } }; // Get args from ecommerce data function. if ( `ecommerce_function` in args && args.ecommerce_function in window ) { if ( typeof args.ecommerce_function == `function` ) { data = window[ args.ecommerce_function ]( this ); } else if ( typeof args.ecommerce_function == `object` ) { data = window[ args.ecommerce_function ]; } } if ( `ecommerce_json` in data && data.ecommerce_json ) { json_element = data.ecommerce_json; } else if ( `ecommerce_json_id` in args && args.ecommerce_json_id ) { json_element = `#${ args.ecommerce_json_id }`; } if ( json_element ) { try { json = JSON.parse( jQuery( json_element ).text() ); } catch ( e ) { window.console.error( 'YouneeqEcommerceHandler: Could not read JSON data. ' + e.toString() ); } } return { ...args, ...json, ...data }; } /** * Collect basic request data. * * @since 3.1.0 * * @return {YouneeqEcommerceHandler} * @param {object} args Arguments object. */ private init_request_data( args: object ): this { this.ecom.domain = `domain` in args ? args.domain : window.location.hostname; this.ecom.url = `url` in args ? args.url : window.location.href; this.ecom.currency = { name: `currency` in args ? args.currency : `USD`, format: `currency_format` in args ? args.currency_format : `$#{2,.}` } this.init_user_id( args ); return this; } private init_suggest_data( args: object ): this { this.ecom.suggest = `suggest` in args ? args.suggest : {}; if ( `count` in args && !( `count` in this.ecom.suggest ) ) { this.ecom.suggest.count = args.count; } if ( `count` in this.ecom.suggest && !( `variant` in this.ecom.suggest ) ) { this.ecom.suggest.variant = `variant` in args ? args.variant : `standard`; } return this; } /** * Collect observe request data. * * @since 3.1.0 * * @return {YouneeqEcommerceHandler} * @param {object} args Arguments object. */ private init_observe_data( args: object ): this { if ( `product` in args ) { this.ecom.product = args.product; if ( `id` in this.ecom.product && !( `product_id` in this.ecom ) ) { this.ecom.product_id = this.ecom.product.id; } } return this; } /** * Collects the Youneeq UID string. * * @since 3.1.0 * * @param {object} args Arguments object. */ private init_user_id( args: object ): void { if ( `user_id` in args && args.user_id.length >= 8 ) { this.ecom.user_id = args.user_id; } else if ( YouneeqEcommerceHandler.user_id ) { this.ecom.user_id = YouneeqEcommerceHandler.user_id; } else if ( YouneeqEcommerceHandler.is_waiting_for_id ) { let self = this, check_user_id = retries => { if ( YouneeqEcommerceHandler.is_waiting_for_id && retries > 0 ) { window.setTimeout( check_user_id, 100, retries - 1 ); } else if ( !YouneeqEcommerceHandler.is_waiting_for_id && YouneeqEcommerceHandler.user_id ) { self.ecom.user_id = YouneeqEcommerceHandler.user_id; } } window.setTimeout( check_user_id, 100, 20 ); } } /** * Set up method overrides. * * @since 3.1.0 * * @return {YouneeqEcommerceHandler} * @param {object} args Arguments object. */ private init_method_overrides( args: object ): this { if ( args.ajax_display_function ) { this.ajax_display = window[ args.ajax_display_function ]; } else if ( args.display_function ) { this.display = window[ args.display_function ]; } return this; } /** * Return a function that triggers populate events and displays recommendations. * * @since 3.1.0 * * @return {Function} * @param {string[]} tags List of tags specifying request context. * @param {boolean} ajax True if ajax display method should be used */ private _populate( tags: string[], ajax: boolean = false ): Function { if ( ajax ) { return response => { this.is_loading = false; let $box = jQuery( this.box ); $box.trigger( `yq:ecommercePopulatePrepare`, [ response, tags ] ); this.ajax_display( response, tags, r => { $box.trigger( `yq:ecommercePopulateAttach`, [ r, tags ] ); }); } } else { return response => { this.is_loading = false; let $box = jQuery( this.box ); $box.trigger( `yq:ecommercePopulatePrepare`, [ response, tags ] ); this.display( response, tags ); $box.trigger( `yq:ecommercePopulateAttach`, [ response, tags ] ); } } } } // String.padStart polyfill if ( !( 'padStart' in String.prototype ) ) { String.prototype.padStart = function( targetLength: number, padString: string = '' ) { let str_length = this.length, buffer_length = targetLength - str_length, pad_buffer = ''; if ( buffer_length <= 0 ) { return this; } else if ( !padString.length ) { padString = ' '; } while ( pad_buffer.length < buffer_length ) { pad_buffer += padString; } pad_buffer = pad_buffer.substr( 0, buffer_length ); return pad_buffer + this; } } // Automatically detect and initialize YouneeqEcommerceHandler instances. jQuery( function() { if ( ! jQuery( `html.yq-no-auto` ).length ) { YouneeqEcommerceHandler.init_user_id(); YouneeqEcommerceHandler.generate(); } });