class Instafeed constructor: (params, context) -> # default options @options = target: 'instafeed' get: 'popular' resolution: 'thumbnail' sortBy: 'none' links: true mock: false useHttp: false # if an object is passed in, override the default options if typeof params is 'object' @options[option] = value for option, value of params # save a reference to context, which defaults to curr scope # this will be used to cache data from parsing to the real # instance the user interacts with (for pagination) @context = if context? then context else this # generate a unique key for the instance @unique = @_genKey() # method to check if there are more results to load hasNext: -> return typeof @context.nextUrl is 'string' and @context.nextUrl.length > 0 # method to display next results using the pagination # data from API. Manually passing a url to .run() will # bypass the URL creation from options. next: -> # check for a valid next url first return false if not @hasNext() # call run with the next results return @run(@context.nextUrl) # MAKE IT GO! run: (url) -> # make sure either a client id or access token is set if typeof @options.clientId isnt 'string' unless typeof @options.accessToken is 'string' throw new Error "Missing clientId or accessToken." if typeof @options.accessToken isnt 'string' unless typeof @options.clientId is 'string' throw new Error "Missing clientId or accessToken." # run the before() callback, if one is set if @options.before? and typeof @options.before is 'function' @options.before.call(this) # to make it easier to test various parts of the class, # any DOM manipulation first checks for the DOM to exist if document? # make a new script element script = document.createElement 'script' # give the script an id so it can removed later script.id = 'instafeed-fetcher' # assign the script src using _buildUrl(), or by # using the argument passed to the function script.src = url || @_buildUrl() # add the new script object to the header header = document.getElementsByTagName 'head' header[0].appendChild script # create a global object to cache the options instanceName = "instafeedCache#{@unique}" window[instanceName] = new Instafeed @options, this window[instanceName].unique = @unique # return true if everything ran true # Data parser (must be a json object) parse: (response) -> # throw an error if not an object if typeof response isnt 'object' # either throw an error or call the error callback if @options.error? and typeof @options.error is 'function' @options.error.call(this, 'Invalid JSON data') return false else throw new Error 'Invalid JSON response' # check if the api returned an error code if response.meta.code isnt 200 # either throw an error or call the error callback if @options.error? and typeof @options.error is 'function' @options.error.call(this, response.meta.error_message) return false else throw new Error "Error from Instagram: #{response.meta.error_message}" # check if the returned data is empty if response.data.length is 0 # either throw an error or call the error callback if @options.error? and typeof @options.error is 'function' @options.error.call(this, 'No images were returned from Instagram') return false else throw new Error 'No images were returned from Instagram' # call the success callback if no errors in response if @options.success? and typeof @options.success is 'function' @options.success.call(this, response) # cache the pagination data, if it exists. Apply the value # to the "context" object, which will be a true reference # if this instance was created just for parsing @context.nextUrl = '' if response.pagination? @context.nextUrl = response.pagination.next_url # before images are inserted into the DOM, check for sorting if @options.sortBy isnt 'none' # if sort is set to random, don't check for polarity if @options.sortBy is 'random' sortSettings = ['', 'random'] else # get the sort settings from @options sortSettings = @options.sortBy.split('-') # determine if the order should be inverse reverse = if sortSettings[0] is 'least' then true else false # handle the case for sorting switch sortSettings[1] when 'random' response.data.sort () -> return 0.5 - Math.random() when 'recent' response.data = @_sortBy(response.data, 'created_time', reverse) when 'liked' response.data = @_sortBy(response.data, 'likes.count', reverse) when 'commented' response.data = @_sortBy(response.data, 'comments.count', reverse) else throw new Error "Invalid option for sortBy: '#{@options.sortBy}'." # to make it easier to test various parts of the class, # any DOM manipulation first checks for the DOM to exist if document? and @options.mock is false # limit the number of images if needed images = response.data parsedLimit = parseInt(@options.limit, 10) if @options.limit? and images.length > parsedLimit images = images.slice(0, parsedLimit) # create the document fragment fragment = document.createDocumentFragment() # filter the results if @options.filter? and typeof @options.filter is 'function' images = @_filter(images, @options.filter) # determine whether to parse a template, or use html fragments if @options.template? and typeof @options.template is 'string' # create an html string htmlString = '' imageString = '' imgUrl = '' # create a temp dom node that will hold the html tmpEl = document.createElement('div') # loop through the images for image in images imageObj = image.images[@options.resolution] if typeof imageObj isnt 'object' eMsg = "No image found for resolution: #{@options.resolution}." throw new Error eMsg imgWidth = imageObj.width imgHeight = imageObj.height imgOrient = "square" if imgWidth > imgHeight imgOrient = "landscape" if imgWidth < imgHeight imgOrient = "portrait" # use protocol relative image url imageUrl = imageObj.url httpProtocol = window.location.protocol.indexOf("http") >= 0 if httpProtocol and !@options.useHttp imageUrl = imageUrl.replace(/https?:\/\//, '//') # parse the template imageString = @_makeTemplate @options.template, model: image id: image.id link: image.link type: image.type image: imageUrl width: imgWidth height: imgHeight orientation: imgOrient caption: @_getObjectProperty(image, 'caption.text') likes: image.likes.count comments: image.comments.count location: @_getObjectProperty(image, 'location.name') # add the image partial to the html string htmlString += imageString # add the final html string to the temp node tmpEl.innerHTML = htmlString # loop through the contents of the temp node # and append them to the fragment childNodesArr = [] childNodeIndex = 0 childNodeCount = tmpEl.childNodes.length while childNodeIndex < childNodeCount childNodesArr.push(tmpEl.childNodes[childNodeIndex]) childNodeIndex += 1 for node in childNodesArr fragment.appendChild(node) else # loop through the images for image in images # create the image using the @options's resolution img = document.createElement 'img' # use protocol relative image url imageObj = image.images[@options.resolution] if typeof imageObj isnt 'object' eMsg = "No image found for resolution: #{@options.resolution}." throw new Error eMsg # use protocol relative image url imageUrl = imageObj.url httpProtocol = window.location.protocol.indexOf("http") >= 0 if httpProtocol and !@options.useHttp imageUrl = imageUrl.replace(/https?:\/\//, '//') img.src = imageUrl # wrap the image in an anchor tag, unless turned off if @options.links is true # create an anchor link anchor = document.createElement 'a' anchor.href = image.link # add the image to it anchor.appendChild img # add the anchor to the fragment fragment.appendChild anchor else # add the image (without link) to the fragment fragment.appendChild img # add the fragment to the dom: # - if target is string, consider it as element id # - otherwise consider it as element targetEl = @options.target if typeof targetEl == 'string' targetEl = document.getElementById(targetEl) unless targetEl? eMsg = "No element with id=\"#{@options.target}\" on page." throw new Error eMsg targetEl.appendChild fragment # remove the injected script tag header = document.getElementsByTagName('head')[0] header.removeChild document.getElementById 'instafeed-fetcher' # delete the cached instance of the class instanceName = "instafeedCache#{@unique}" window[instanceName] = undefined try delete window[instanceName] catch e # END if document? # run after callback function, if one is set if @options.after? and typeof @options.after is 'function' @options.after.call(this) # return true if everything ran true # helper function that structures a url for the run() # function to inject into the document hearder _buildUrl: -> # set the base API URL base = "https://api.instagram.com/v1" # get the endpoint based on @options.get switch @options.get when "popular" then endpoint = "media/popular" when "tagged" # make sure a tag is defined unless @options.tagName throw new Error "No tag name specified. Use the 'tagName' option." # set the endpoint endpoint = "tags/#{@options.tagName}/media/recent" when "location" # make sure a location id is defined unless @options.locationId throw new Error "No location specified. Use the 'locationId' option." # set the endpoint endpoint = "locations/#{@options.locationId}/media/recent" when "user" # make sure there is a user id set unless @options.userId throw new Error "No user specified. Use the 'userId' option." endpoint = "users/#{@options.userId}/media/recent" # throw an error if any other option is given else throw new Error "Invalid option for get: '#{@options.get}'." # build the final url (uses the instance name) final = "#{base}/#{endpoint}" # use the access token for auth when it's available # otherwise fall back to the client id if @options.accessToken? final += "?access_token=#{@options.accessToken}" else final += "?client_id=#{@options.clientId}" # add the count limit if @options.limit? final += "&count=#{@options.limit}" # add the jsonp callback final += "&callback=instafeedCache#{@unique}.parse" # return the final url final # helper function to generate a unique key _genKey: -> S4 = -> (((1+Math.random())*0x10000)|0).toString(16).substring(1) "#{S4()}#{S4()}#{S4()}#{S4()}" # helper function to parse a template _makeTemplate: (template, data) -> # regex pattern pattern = /// (?:\{{2}) # opening braces ([\w\[\]\.]+) # variable name (?:\}{2}) # closing braces /// # copy the template output = template # process the template (null defaults to empty strings) while (pattern.test(output)) varName = output.match(pattern)[1] varValue = @_getObjectProperty(data, varName) ? '' output = output.replace(pattern, () -> return "#{varValue}") # send back the new string return output # helper function to access an object property by string _getObjectProperty: (object, property) -> # convert [] to dot-syntax property = property.replace /\[(\w+)\]/g, '.$1' # split the object into arrays pieces = property.split '.' # run through the array to find the # nested property while pieces.length # move down the property chain piece = pieces.shift() # if they key exists, copy the value # into 'object', otherwise return null if object? and piece of object object = object[piece] else return null # send back the final object return object # helper function to sort an array objects by an # object property (sorts highest to lowest) _sortBy: (data, property, reverse) -> # comparator function sorter = (a, b) -> valueA = @_getObjectProperty a, property valueB = @_getObjectProperty b, property # sort lowest-to-highest if reverse is true if reverse if valueA > valueB then return 1 else return -1 # otherwise sort highest to lowest if valueA < valueB then return 1 else return -1 # sort the data data.sort(sorter.bind(this)) return data # helper method to filter out images _filter: (images, filter) -> filteredImages = [] for image in images do (image) -> filteredImages.push(image) if filter(image) return filteredImages ((root, factory) -> # set up exports if typeof define == 'function' and define.amd define [], factory else if typeof module == 'object' and module.exports module.exports = factory() else root.Instafeed = factory() )(this, () -> return Instafeed )