Adapters

The Waypoints lib directory contains three different builds of the library for different DOM manipulation frameworks: jQuery, Zepto, and no framework. This is accomplished by factoring out all of the code that touches the DOM into an Adapter interface. There is a common shared core of Waypoints code, and an Adapter class for each of the different frameworks. The job of the adapter is to handle all of the DOM interfacing and normalization that would typically be handled by a library like jQuery.

This means if you wanted to extend Waypoints to support a new library, you don't need to fork Waypoints. You need to write an Adapter class that implements the correct interface and uses the new library internally. This guide will go over every method that an Adapter needs to implement. In general, these methods will mimic their jQuery equivalents, but we'll talk about the differences as they come up. To help drive the concept home, each part of the no-framework adapter will accompany each section of this guide.

If you do write a new adapter, please open an issue and we can work on including the adapter and build in the Waypoints repository.

Constructor

Parameters

element: The HTMLElement the adapter will act upon.

function NoFrameworkAdapter(element) {
  this.element = element
  this.handlers = {}
}

There are no requirements for any instance properties on this class, but in cases like the jQuery adapter, instances of the adapter have a $element property, which is a cached jQuery object containing the passed in element. This prevents needing to query the DOM or jQuery wrap every time an adapter method is called.

adapter.innerHeight()

Parameters

None.

Returns

Number. The height of the element in pixels, including padding.

NoFrameworkAdapter.prototype.innerHeight = function() {
  var isWin = isWindow(this.element)
  return isWin ? this.element.innerHeight : this.element.clientHeight
}

This should behave like jQuery's innerHeight method, but only the getter is necessary.

adapter.innerWidth()

Parameters

None.

Returns

Number. The width of the element in pixels, including padding.

NoFrameworkAdapter.prototype.innerWidth = function() {
  var isWin = isWindow(this.element)
  return isWin ? this.element.innerWidth : this.element.clientWidth
}

This should behave like jQuery's innerWidth method, but only the getter is necessary.

adapter.off(event)

Parameters

event: A string name of an event. This string may also contain a namespace separated by a . such as 'click.namespace'.

Returns

undefined.

NoFrameworkAdapter.prototype.off = function(event, handler) {
  function removeListeners(element, listeners, handler) {
    for (var i = 0, end = listeners.length - 1; i < end; i++) {
      var listener = listeners[i]
      if (!handler || handler === listener) {
        element.removeEventListener(listener)
      }
    }
  }

  var eventParts = event.split('.')
  var eventType = eventParts[0]
  var namespace = eventParts[1]
  var element = this.element

  if (namespace && this.handlers[namespace] && eventType) {
    removeListeners(element, this.handlers[namespace][eventType], handler)
    this.handlers[namespace][eventType] = []
  }
  else if (eventType) {
    for (var ns in this.handlers) {
      removeListeners(element, this.handlers[ns][eventType] || [], handler)
      this.handlers[ns][eventType] = []
    }
  }
  else if (namespace && this.handlers[namespace]) {
    for (var type in this.handlers[namespace]) {
      removeListeners(element, this.handlers[namespace][type], handler)
    }
    this.handlers[namespace] = {}
  }
}

This unbinds an event listener from the element. Only a subset of jQuery's off needs to be implemented. Multiple space separated events and event delegation are unnecessary. The adapter must support unbinding all events in a namespace using .off('.namespace').

adapter.offset()

Parameters

None.

Returns

An object with two properties, top and left, whose values are the number of pixels from the top and left of the document respectively.

NoFrameworkAdapter.prototype.offset = function() {
  if (!this.element.ownerDocument) {
    return null
  }

  var documentElement = this.element.ownerDocument.documentElement
  var win = getWindow(this.element.ownerDocument)
  var rect = {
    top: 0,
    left: 0
  }

  if (this.element.getBoundingClientRect) {
    rect = this.element.getBoundingClientRect()
  }

  return {
    top: rect.top + win.pageYOffset - documentElement.clientTop,
    left: rect.left + win.pageXOffset - documentElement.clientLeft
  }
}

The adapter only needs to implement the getter version of jQuery's offset. It also needs to gracefully handle window as the element. It should return undefined or null in that case.

adapter.on(event, handler)

Parameters

event: A string event name to bind to. May also include a namespace separated by a . such as 'click.namespace'.

handler: The event listener handler to bind.

Returns

undefined.

NoFrameworkAdapter.prototype.on = function(event, handler) {
  var eventParts = event.split('.')
  var eventType = eventParts[0]
  var namespace = eventParts[1] || '__default'
  var nsHandlers = this.handlers[namespace] = this.handlers[namespace] || {}
  var nsTypeList = nsHandlers[eventType] = nsHandlers[eventType] || []

  nsTypeList.push(handler)
  this.element.addEventListener(eventType, handler)
}

This binds an event listener to the element. Only a subset of jQuery's on needs to be implemented. Multiple space separated events and event delegation are unnecessary. The adapter must support binding events with a namespace.

adapter.outerHeight(includeMargin)

Parameters

includeMargin: An optional boolean that, if true, will include margins in the element height calculation. Defaults to false.

Returns

The height of the element in pixels, including padding and borders. If includeMargin is true, margins are also included.

NoFrameworkAdapter.prototype.outerHeight = function(includeMargin) {
  var height = this.innerHeight()
  var computedStyle

  if (includeMargin && !isWindow(this.element)) {
    computedStyle = window.getComputedStyle(this.element)
    height += parseInt(computedStyle.marginTop, 10)
    height += parseInt(computedStyle.marginBottom, 10)
  }

  return height
}

This is a direct analog of jQuery's outerHeight.

adapter.outerWidth(includeMargin)

Parameters

includeMargin: An optional boolean that, if true, will include margins in the element width calculation. Defaults to false.

Returns

The width of the element in pixels, including padding and borders. If includeMargin is true, margins are also included.

NoFrameworkAdapter.prototype.outerWidth = function(includeMargin) {
  var width = this.innerWidth()
  var computedStyle

  if (includeMargin && !isWindow(this.element)) {
    computedStyle = window.getComputedStyle(this.element)
    width += parseInt(computedStyle.marginLeft, 10)
    width += parseInt(computedStyle.marginRight, 10)
  }

  return width
}

This is a direct analog of jQuery's outerWidth.

adapter.scrollLeft()

Parameters

None.

Returns

The number of pixels this element has scrolled horizontally.

NoFrameworkAdapter.prototype.scrollLeft = function() {
  var win = getWindow(this.element)
  return win ? win.pageXOffset : this.element.scrollLeft
}

This should behave like jQuery's scrollLeft method, but only the getter is necessary.

adapter.scrollTop()

Parameters

None.

Returns

The number of pixels this element has scrolled vertically.

NoFrameworkAdapter.prototype.scrollTop = function() {
  var win = getWindow(this.element)
  return win ? win.pageYOffset : this.element.scrollTop
}

This should behave like jQuery's scrollTop method, but only the getter is necessary.

Adapter.extend(objects...)

Parameters

One or more objects.

Returns

The first object argument, after all objects have been merged.

NoFrameworkAdapter.extend = function() {
  var args = Array.prototype.slice.call(arguments)

  function merge(target, obj) {
    if (typeof target === 'object' && typeof obj === 'object') {
      for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
          target[key] = obj[key]
        }
      }
    }

    return target
  }

  for (var i = 1, end = args.length; i < end; i++) {
    merge(args[0], args[i])
  }
  return args[0]
}

This method alters the first object argument by taking all proceeding objects and merging their keys and values down into the first object. This is the same behavior as jQuery's extend.

Adapter.inArray(value, array, startIndex)

Parameters

value: The value you are searching for in the array.

array: The array in which to look for the value.

startIndex: A numerical index in the array where the search for the value should begin. Defaults to 0, the beginning of the array.

Returns

The index in array where value is first encountered. If value is not found, -1 is returned.

NoFrameworkAdapter.inArray = function(element, array, i) {
  return array == null ? -1 : array.indexOf(element, i)
}

This is the same behavior as jQuery's inArray.

Adapter.isEmptyObject(object)

Parameters

object: Any JavaScript object.

Returns

True if the object is empty, false if it has any keys.

NoFrameworkAdapter.isEmptyObject = function(obj) {
  for (var name in obj) {
    return false
  }
  return true
}

This is the same behavior as jQuery's isEmptyObject.

Registration

Once you have your custom Adapter class, you need to do two things. First, add it to the Waypoint.adapters array. This array contains objects with two keys, name with a unique name of the adapter, and Adapter with your Adapter class.

Waypoint.adapters.push({
  name: 'noframework',
  Adapter: NoFrameworkAdapter
})

Second, tell Waypoints to use this adapter by assigning it to Waypoint.Adapter.

Waypoint.Adapter = NoFrameworkAdapter