From f584d8748ce314643c01eaa84e47f8ae04ab38de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20K=C3=B6hn?= Date: Thu, 6 Oct 2016 10:44:01 +0200 Subject: [PATCH 1/4] Definitions for shopify buy. * Definitions for shopify buy. --- shopify-buy/shopify-buy-tests.ts | 406 +++++++++++++++++++++++++++++++ shopify-buy/shopify-buy.d.ts | 377 ++++++++++++++++++++++++++++ 2 files changed, 783 insertions(+) create mode 100644 shopify-buy/shopify-buy-tests.ts create mode 100644 shopify-buy/shopify-buy.d.ts diff --git a/shopify-buy/shopify-buy-tests.ts b/shopify-buy/shopify-buy-tests.ts new file mode 100644 index 0000000000..3d8d1f5988 --- /dev/null +++ b/shopify-buy/shopify-buy-tests.ts @@ -0,0 +1,406 @@ +/// +/// + +/* Build new ShopifyBuy client +============================================================ */ +let client = ShopifyBuy.buildClient({ + apiKey: 'bf081e860bc9dc1ce0654fdfbc20892d', + myShopifyDomain: 'embeds', + appId: '6' +}); + +var product : ShopifyBuy.Shopify.ProductModel; +var cart : ShopifyBuy.Shopify.CartModel; +var cartLineItemCount : number; +if(localStorage.getItem('lastCartId')) { + client.fetchCart(localStorage.getItem('lastCartId')).then(function(remoteCart : ShopifyBuy.Shopify.CartModel) { + cart = remoteCart; + cartLineItemCount = cart.lineItems.length; + renderCartItems(); + }); +} else { + client.createCart().then(function (newCart : ShopifyBuy.Shopify.CartModel) { + cart = newCart; + localStorage.setItem('lastCartId', ""+cart.id); + cartLineItemCount = 0; + }); +} + +var previousFocusItem : any; + +/* Fetch product and init +============================================================ */ +client.fetchProduct('3614436099').then(function (fetchedProduct : ShopifyBuy.Shopify.ProductModel) { + product = fetchedProduct; + var selectedVariant : ShopifyBuy.Shopify.ProductVariantModel = product.selectedVariant; + var selectedVariantImage : any = product.selectedVariantImage; + var currentOptions : ShopifyBuy.Shopify.Option[] = product.options; + + var variantSelectors = generateSelectors(product); + $('.variant-selectors').html(""+variantSelectors); + + updateProductTitle(product.title); + updateVariantImage(selectedVariantImage); + updateVariantTitle(selectedVariant); + updateVariantPrice(selectedVariant); + attachOnVariantSelectListeners(product); + updateCartTabButton(); + bindEventListeners(); +}); + +/* Generate DOM elements for variant selectors +============================================================ */ +function generateSelectors(product : ShopifyBuy.Shopify.ProductModel) { + var elements = product.options.map(function(option : ShopifyBuy.Shopify.Option) { + var optionsHtml = option.values.map(function(value : any) { + return ''; + }); + + return '
\ + \ + \ +
' + }); + return elements; +} + +/* Bind Event Listeners +============================================================ */ +function bindEventListeners() { + /* cart close button listener */ + $('.cart .btn--close').on('click', closeCart); + + /* click away listener to close cart */ + $(document).on('click', function(evt) { + if((!$(evt.target).closest('.cart').length) && (!$(evt.target).closest('.js-prevent-cart-listener').length)) { + closeCart(); + } + }); + + /* escape key handler */ + var ESCAPE_KEYCODE = 27; + $(document).on('keydown', function (evt) { + if (evt.which === ESCAPE_KEYCODE) { + if (previousFocusItem) { + $(previousFocusItem).focus(); + previousFocusItem = '' + } + closeCart(); + } + }); + + /* checkout button click listener */ + $('.btn--cart-checkout').on('click', function () { + window.open(cart.checkoutUrl, '_self'); + }); + + /* buy button click listener */ + $('.buy-button').on('click', buyButtonClickHandler); + + /* increment quantity click listener */ + $('.cart').on('click', '.quantity-increment', function () { + var variantId = $(this).data('variant-id'); + incrementQuantity(variantId); + }); + + /* decrement quantity click listener */ + $('.cart').on('click', '.quantity-decrement', function() { + var variantId = $(this).data('variant-id'); + decrementQuantity(variantId); + }); + + /* update quantity field listener */ + $('.cart').on('keyup', '.cart-item__quantity', debounce(fieldQuantityHandler, 250)); + + /* cart tab click listener */ + $('.btn--cart-tab').click(function() { + setPreviousFocusItem(this); + openCart(); + }); +} + + +/* Variant option change handler +============================================================ */ +function attachOnVariantSelectListeners(product : ShopifyBuy.Shopify.ProductModel) { + $('.variant-selectors').on('change', 'select', function(event) { + var $element : any = $(event.target); + var name : string = $element.attr('name'); + var value : string = $element.val(); + product.options.filter(function(option : ShopifyBuy.Shopify.Option) { + return option.name === name; + })[0].selected = value; + + var selectedVariant : ShopifyBuy.Shopify.ProductVariantModel = product.selectedVariant; + var selectedVariantImage : any = product.selectedVariantImage; + updateProductTitle(product.title); + updateVariantImage(selectedVariantImage); + updateVariantTitle(selectedVariant); + updateVariantPrice(selectedVariant); + }); +} + +/* Update product title +============================================================ */ +function updateProductTitle(title : string) { + $('#buy-button-1 .product-title').text(title); +} + +/* Update product image based on selected variant +============================================================ */ +function updateVariantImage(image : any) { + var src = (image) ? image.src : ShopifyBuy.NO_IMAGE_URI; + + $('#buy-button-1 .variant-image').attr('src', src); +} + +/* Update product variant title based on selected variant +============================================================ */ +function updateVariantTitle(variant : ShopifyBuy.Shopify.ProductVariantModel) { + $('#buy-button-1 .variant-title').text(variant.title); +} + +/* Update product variant price based on selected variant +============================================================ */ +function updateVariantPrice(variant : ShopifyBuy.Shopify.ProductVariantModel) { + $('#buy-button-1 .variant-price').text('$' + variant.price); +} + +/* Attach and control listeners onto buy button +============================================================ */ +function buyButtonClickHandler(evt : any) { + evt.preventDefault(); + var id : string | number = product.selectedVariant.id; + var quantity : number; + var cartLineItem : ShopifyBuy.Shopify.CartLineItem = findCartItemByVariantId(id); + + quantity = cartLineItem ? cartLineItem.quantity + 1 : 1; + + addOrUpdateVariant(product.selectedVariant, quantity); + setPreviousFocusItem(evt.target); + $('#checkout').focus(); +} + +/* Update product variant quantity in cart +============================================================ */ +function updateQuantity(fn : Function, variantId : string | number) { + var variant : ShopifyBuy.Shopify.ProductVariantModel = product.variants.filter(function (variant : ShopifyBuy.Shopify.ProductVariantModel) { + return (variant.id === variantId); + })[0]; + var quantity : number; + var cartLineItem : ShopifyBuy.Shopify.CartLineItem = findCartItemByVariantId(variant.id); + if (cartLineItem) { + quantity = fn(cartLineItem.quantity); + updateVariantInCart(cartLineItem, quantity); + } +} + +/* Decrease quantity amount by 1 +============================================================ */ +function decrementQuantity(variantId : string | number) { + updateQuantity(function(quantity : number) { + return quantity - 1; + }, variantId); +} + +/* Increase quantity amount by 1 +============================================================ */ +function incrementQuantity(variantId : string | number) { + updateQuantity(function(quantity : number) { + return quantity + 1; + }, variantId); +} + +/* Update product variant quantity in cart through input field +============================================================ */ +function fieldQuantityHandler(evt : any) { + var variantId : string | number = parseInt($(this).closest('.cart-item').attr('data-variant-id'), 10); + var variant : ShopifyBuy.Shopify.ProductVariantModel = product.variants.filter(function (variant : ShopifyBuy.Shopify.ProductVariantModel) { + return (variant.id === variantId); + })[0]; + var cartLineItem : ShopifyBuy.Shopify.CartLineItem = findCartItemByVariantId(variant.id); + var quantity : number = evt.target.value; + if (cartLineItem) { + updateVariantInCart(cartLineItem, quantity); + } +} + +/* Debounce taken from _.js +============================================================ */ +function debounce(func : any, wait : number, immediate? : number) { + var timeout : number; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (!immediate) func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) func.apply(context, args); + } +} + +/* Open Cart +============================================================ */ +function openCart() { + $('.cart').addClass('js-active'); +} + +/* Close Cart +============================================================ */ +function closeCart() { + $('.cart').removeClass('js-active'); + $('.overlay').removeClass('js-active'); +} + +/* Find Cart Line Item By Variant Id +============================================================ */ +function findCartItemByVariantId(variantId : string | number) { + return cart.lineItems.filter(function (item) { + return (item.variant_id === variantId); + })[0]; +} + +/* Determine action for variant adding/updating/removing +============================================================ */ +function addOrUpdateVariant(variant : ShopifyBuy.Shopify.ProductVariantModel, quantity : number) { + openCart(); + var cartLineItem : ShopifyBuy.Shopify.CartLineItem = findCartItemByVariantId(variant.id); + + if (cartLineItem) { + updateVariantInCart(cartLineItem, quantity); + } else { + addVariantToCart(variant, quantity); + } + + updateCartTabButton(); +} + +/* Update details for item already in cart. Remove if necessary +============================================================ */ +function updateVariantInCart(cartLineItem : ShopifyBuy.Shopify.CartLineItem, quantity : number) { + var variantId : string | number = cartLineItem.variant_id; + var cartLength : number = cart.lineItems.length; + cart.updateLineItem(cartLineItem.id, quantity).then(function(updatedCart : ShopifyBuy.Shopify.CartModel) { + var $cartItem = $('.cart').find('.cart-item[data-variant-id="' + variantId + '"]'); + if (updatedCart.lineItems.length >= cartLength) { + $cartItem.find('.cart-item__quantity').val(cartLineItem.quantity); + $cartItem.find('.cart-item__price').text(formatAsMoney(cartLineItem.line_price)); + } else { + $cartItem.addClass('js-hidden').bind('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', function() { + $cartItem.remove(); + }); + } + + updateCartTabButton(); + updateTotalCartPricing(); + if (updatedCart.lineItems.length < 1) { + closeCart(); + } + }).catch(function (errors : string) { + console.log('Fail'); + console.error(errors); + }); +} + +/* Add 'quantity' amount of product 'variant' to cart +============================================================ */ +function addVariantToCart(variant : ShopifyBuy.Shopify.ProductVariantModel, quantity : number) { + openCart(); + + cart.addVariants({ variant: variant, quantity: quantity }).then(function() { + var cartItem : ShopifyBuy.Shopify.CartLineItem = cart.lineItems.filter(function (item : ShopifyBuy.Shopify.CartLineItem ) { + return (item.variant_id === variant.id); + })[0]; + var $cartItem = renderCartItem(cartItem); + var $cartItemContainer = $('.cart-item-container'); + $cartItemContainer.append($cartItem); + setTimeout(function () { + $cartItemContainer.find('.js-hidden').removeClass('js-hidden'); + }, 0) + + }).catch(function (errors : string) { + console.log('Fail'); + console.error(errors); + }); + + updateTotalCartPricing(); + updateCartTabButton(); +} + +/* Return required markup for single item rendering +============================================================ */ +function renderCartItem(lineItem : ShopifyBuy.Shopify.CartLineItem) { + var lineItemEmptyTemplate = $('#CartItemTemplate').html(); + var $lineItemTemplate = $(lineItemEmptyTemplate); + var itemImage = lineItem.image.src; + $lineItemTemplate.attr('data-variant-id', lineItem.variant_id); + $lineItemTemplate.addClass('js-hidden'); + $lineItemTemplate.find('.cart-item__img').css('background-image', 'url(' + itemImage + ')'); + $lineItemTemplate.find('.cart-item__title').text(lineItem.title); + $lineItemTemplate.find('.cart-item__variant-title').text(lineItem.variant_title); + $lineItemTemplate.find('.cart-item__price').text(formatAsMoney(lineItem.line_price)); + $lineItemTemplate.find('.cart-item__quantity').attr('value', lineItem.quantity); + $lineItemTemplate.find('.quantity-decrement').attr('data-variant-id', lineItem.variant_id); + $lineItemTemplate.find('.quantity-increment').attr('data-variant-id', lineItem.variant_id); + + return $lineItemTemplate; +} + +/* Render the line items currently in the cart +============================================================ */ +function renderCartItems() { + var $cartItemContainer = $('.cart-item-container'); + $cartItemContainer.empty(); + var lineItemEmptyTemplate = $('#CartItemTemplate').html(); + var $cartLineItems = cart.lineItems.map(function (lineItem, index) { + return renderCartItem(lineItem); + }); + $cartItemContainer.append($cartLineItems); + + setTimeout(function () { + $cartItemContainer.find('.js-hidden').removeClass('js-hidden'); + }, 0) + updateTotalCartPricing(); +} + +/* Update Total Cart Pricing +============================================================ */ +function updateTotalCartPricing() { + $('.cart .pricing').text(formatAsMoney(cart.subtotal)); +} + +/* Format amount as currency +============================================================ */ +function formatAsMoney(amount : string, currency? : string, thousandSeparator? : string, decimalSeparator? : string , localeDecimalSeparator? : string) { + currency = currency || '$'; + thousandSeparator = thousandSeparator || ','; + decimalSeparator = decimalSeparator || '.'; + localeDecimalSeparator = localeDecimalSeparator || '.'; + var regex = new RegExp('(\\d)(?=(\\d{3})+\\.)', 'g'); + + return currency + parseFloat(amount).toFixed(2) + .replace(localeDecimalSeparator, decimalSeparator) + .replace(regex, '$1' + thousandSeparator) + .toString(); +} + +/* Update cart tab button +============================================================ */ +function updateCartTabButton() { + if (cart.lineItems.length > 0) { + $('.btn--cart-tab .btn__counter').html(""+cart.lineItemCount); + $('.btn--cart-tab').addClass('js-active'); + } else { + $('.btn--cart-tab').removeClass('js-active'); + $('.cart').removeClass('js-active'); + } +} + +/* Set previously focused item for escape handler +============================================================ */ +function setPreviousFocusItem(item : ShopifyBuy.Shopify.CartLineItem) { + previousFocusItem = item; +} diff --git a/shopify-buy/shopify-buy.d.ts b/shopify-buy/shopify-buy.d.ts new file mode 100644 index 0000000000..afcff03c0a --- /dev/null +++ b/shopify-buy/shopify-buy.d.ts @@ -0,0 +1,377 @@ +// // Type definitions for shopify-buy +// // Project: http://shopify.github.io/js-buy-sdk/api/ +// // Definitions by: Martin Köhn <[https://github.com/openminder]> +// // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +/// + +/** + * The JS Buy SDK is a lightweight library that allows you to build ecommerce into any website. + * It is based on Shopify’s API and provides the ability to retrieve products and collections from your shop, + * add products to a cart, and checkout. + */ +declare namespace ShopifyBuy { + /** + * Create a ShopClient. This is the main entry point to the SDK. + */ + export function buildClient(configAttrs : Shopify.Config) : Shopify.ShopClient + + /** + * Internal Image description + */ + interface Image { + id : string|number + src : string + position : number + product_id : string + variant_ids : Array + created_at : string + updated_at : string + } + + /** + * Internal BaseModel class + */ + class BaseModel { + constructor( attrs: any, metaAttrs: any ) + + attrs : any + serializer: any + adapter: any + shopClient: any + } + + var NO_IMAGE_URI : string; + + /** + * This namespace contains all globally accessible classes + */ + export namespace Shopify { + + /** + * Base Shopify Client config object + */ + interface Config + { + /** + * Your api client's public token. + */ + apiKey: string + + /** The app whose listings the client will be using. + * If you are just modifying a buy button, the buy-button's app id is. + * Otherwise, obtain the app id of the app you're modifying or extending. + */ + appId: string + + /** You shop's myshopify.com domain. */ + myShopifyDomain: string + } + + /** + * Cart item, that should be added to the card + */ + interface CartModelItem { + variant : Shopify.ProductVariantModel + quantity : number + } + + class CartLineItem extends BaseModel { + constructor() + + /** + * Line item unique ID + */ + id : string|number + + /** + * Images associated with product. + */ + image : Image + + /** + * Product title + */ + title: string + + /** + * ID of selected variant + */ + variant_id: string|number + + /** + * Title of selected variant + */ + variant_title: string + + /** + * Number of selected items + */ + quantity : number + + /** + * Variant weight in grams + */ + grams: number + + /** + * ID of selected product + */ + product_id: string|number + + /** + * Price of variant, formatted as currency + */ + price: string + + /** + * Compare at price for variant formatted as currency. + */ + compare_at_price: string + + /** + * Calculated price for the number of variant in card + */ + line_price: string + + } + + /** + * Shopify-Buy-SDK Client to handle products and cart + */ + class ShopClient { + + /** + * Config data to be used throughout all API interaction + */ + constructor( config? : Config ) + /** + * Creates a CartModel instance, optionally including attrs. + */ + createCart(attrs? : any) : Promise + + /** + * Updates an existing CartModel instance and persists it to localStorage. + */ + updateCart(attrs? : any) : Promise + + /** + * Retrieve a previously created cart by its key. + */ + fetchCart(id : string) : Promise + + /** + * Fetch one collection by its ID. + */ + fetchCollection(id : string|number) : Promise //TODO: Find declaration for Collection + + /** + * Fetch one product by its ID. + */ + fetchProduct(id: string|number) : Promise + + /** + * Fetches a list of collections matching a specified query. + */ + fetchQueryCollections(query? : any) : Promise> + + /** + * Fetches a list of products matching a specified query. + */ + fetchQueryProducts(query? : any) : Promise> + + } + + /** + * Class for cart model + */ + class CartModel extends BaseModel { + + constructor() + + /** + * Get checkout URL for current cart + */ + checkoutUrl : string + + /** + * get ID for current cart + */ + id : string|number + + /** + * Get current line items for cart + */ + lineItems : Array + + /** + * Get current subtotal price for all line items + */ + subtotal: string + + /** + * Gets the sum quantity of each line item + */ + lineItemCount : number + + /** + * Add items to cart. Updates cart's lineItems + */ + addVariants(item : CartModelItem, nextItem? : Array) : Promise + + /** + * Remove all line items from cart + */ + clearLineItems() : Promise + + /** + * Remove line item from cart + */ + removeLineItem(id : string|number) : Promise + + /** + * Update line item quantity + */ + updateLineItem(id : string|number, quantitiy : number ) : Promise + + } + + /** + * Class for products returned by fetch('product') + */ + class ProductModel extends BaseModel { + + constructor() + + /** + * Product unique ID + */ + id : string|number + + /** + * All images associated with product. + */ + images : Array + + /** + * Product title + */ + title: string + + /** + * All variants of a product. + */ + variants: Array + + /** + * Get array of options with nested values. Useful for creating UI for selecting options. + */ + options : Array