+// ```
+@mixin tip-base {
display: none;
position: absolute;
- z-index: 9999;
> .js-tip-inner {
position: relative;
}
-}
-.js-tip-inner {
- > .js-tip-stem {
- border: {
- width: 0;
- style: solid;
- color: transparent;
- }
+ .js-tip-stem {
+ border: 0 solid transparent; width: 0; height: 0; // Initial values.
background: none;
position: absolute;
- width: 0;
- height: 0;
}
- > .js-tip-content {
+ .js-tip-content {
overflow: hidden;
position: relative;
- // Position adjusted by to stem style mixin.
- }
+ }
}
-.js-tip-stem {
- .js-tip-right.js-snap-tip-y-side &, // Reapply.
- .js-tip-right & {
- left: 0;
+
+// 𝒇 `tip-layout` should be included unless you want to implement your own tip
+// layout, specifically around tip stem position. The sizing logic structures
+// around `em-size`. Most of this mixin is handling the variations of classes on
+// the tip element. For example: `js-tip js-snap-tip js-tip-follow js-tip-bottom
+// js-tip-right js-snap-tip-trigger js-snap-tip-x-side`.
+@mixin tip-layout($em-size: 12px,
+ $content-padding-em: 2/3 1 5/6 1,
+ $max-width-em: 18,
+ $stem-size-em: 1,
+ $stem-wide-base-em: 1.5,
+ $z-index: 9999)
+{
+ // - First calculate `*-em` parameters into pixels.
+ $max-width: $max-width-em * $em-size;
+ $stem-size: round($stem-size-em * $em-size);
+ $stem-wide-base: round($stem-wide-base-em * $stem-size-em * $em-size);
+ $content-padding: ();
+ @each $em in $content-padding-em {
+ $content-padding: $content-padding ($em * $em-size);
}
- .js-tip-left.js-snap-tip-y-side &, // Reapply.
- .js-tip-left & {
- right: 0;
+ // - Then for some basic layout styles.
+ z-index: $z-index;
+ .js-tip-inner {
+ max-width: $max-width;
}
- // By default top and bottom have precedence in growing the stem, so they
- // come last.
- .js-tip-bottom.js-snap-tip-x-side &, // Reapply.
- .js-tip-bottom & {
- top: 0;
+ .js-tip-content {
+ padding: #{$content-padding};
}
- .js-tip-top.js-snap-tip-x-side &, // Reapply.
- .js-tip-top & {
- bottom: 0;
+ // - Finally start layout for the stem. Finish drawing and positioning the
+ // stem, and offset the content to match the stem size.
+ @each $side, $snap-side in $_side-snap-side-map {
+ &.js-tip-#{$side} { &.js-snap-tip-#{$snap-side}-side, & { .js-tip-stem {
+ border-#{$side}-width: $stem-size;
+ & + .js-tip-content {
+ @if $snap-side == x {
+ left: auto; right: auto; // Reset.
+ @if $side == top { bottom: $stem-size; }
+ @if $side == bottom { top: $stem-size; }
+ }
+ @if $snap-side == y {
+ top: auto; bottom: auto; // Reset.
+ @if $side == left { right: $stem-size; }
+ @if $side == right { left: $stem-size; }
+ }
+ }
+ #{map-get($_side-inverse-map, $side)}: 0;
+ }}}
}
- .js-snap-tip-x-side & {
+ // - Then make any stem layout adjustments for when the tip is snapping to an
+ // axis. The stem gets centered in this implementation.
+ &.js-snap-tip-x-side .js-tip-stem {
+ border: {
+ color: transparent; // Partial reset.
+ width: 0 ($stem-wide-base / 2);
+ }
left: 50%;
- border-color: transparent; // Partial reset.
+ margin-left: -$stem-wide-base / 2;
}
- .js-snap-tip-y-side & {
+ &.js-snap-tip-y-side .js-tip-stem {
+ border: {
+ color: transparent; // Partial reset.
+ width: ($stem-wide-base / 2) 0;
+ }
+ margin-top: -$stem-wide-base / 2;
top: 50%;
- border-color: transparent; // Partial reset.
+ }
+}
+
+// 𝒇 `tip-skin` is entirely optional and easy to re-implement. You should use it
+// if you just need to configure colors.
+@mixin tip-skin($fill-color, $text-color, $stem-color: null)
+{
+ $stem-color: $fill-color !default;
+ .js-tip-content {
+ background-color: $fill-color;
+ color: $text-color;
+ }
+ @each $side, $snap-side in $_side-snap-side-map {
+ &.js-tip-#{$side} { &.js-snap-tip-#{$snap-side}-side, & { .js-tip-stem {
+ border: {
+ color: transparent; // Reset.
+ #{$side}-color: $stem-color;
+ }
+ }}}
}
}
diff --git a/src/css/jquery.hlf.editable.scss b/src/css/jquery.hlf.editable.scss
index 779444e..3b1de32 100644
--- a/src/css/jquery.hlf.editable.scss
+++ b/src/css/jquery.hlf.editable.scss
@@ -1,3 +1,8 @@
+/*
+ HLF Editable jQuery Plugin
+ ==========================
+ There should be no css output from this file.
+*/
%skin-clear {
border-color: transparent;
@@ -6,6 +11,7 @@
outline-style: none;
}
}
+
%spacing-none {
margin: 0;
padding: 0;
@@ -16,10 +22,11 @@ $editable-inline-selectors: (
text:'.js-text',
input:'.js-input input'
);
-@mixin editable-inline-skin ($style:clear,
- $rule-color:black,
- $hover-background-color:rgba(black, 0.1),
- $selectors:$editable-inline-selectors)
+
+@mixin editable-inline-skin ($style: clear,
+ $rule-color: black,
+ $hover-background-color: rgba(black, 0.1),
+ $selectors: $editable-inline-selectors)
{
@if $style == clear {
#{map-get($selectors, text)},
@@ -38,8 +45,8 @@ $editable-inline-selectors: (
}
}
-@mixin editable-inline-layout ($width:160px,
- $selectors:$editable-inline-selectors)
+@mixin editable-inline-layout ($width: 160px,
+ $selectors: $editable-inline-selectors)
{
#{map-get($selectors, text)},
#{map-get($selectors, input)} {
diff --git a/src/css/jquery.hlf.tip.scss b/src/css/jquery.hlf.tip.scss
index 985e9e6..1e12579 100644
--- a/src/css/jquery.hlf.tip.scss
+++ b/src/css/jquery.hlf.tip.scss
@@ -1,134 +1,139 @@
-@mixin tip-skin ($fill-color, $text-color, $stem-color:null)
-{
- $stem-color: $fill-color !default;
- .js-tip-content {
- background-color: $fill-color;
- color: $text-color;
- }
- // Declaration structure from base styles.
- &.js-tip-right.js-snap-tip-y-side .js-tip-stem, &.js-tip-right .js-tip-stem { @include _stem-skin(right, $stem-color); }
- &.js-tip-left.js-snap-tip-y-side .js-tip-stem, &.js-tip-left .js-tip-stem { @include _stem-skin(left, $stem-color); }
- &.js-tip-bottom.js-snap-tip-x-side .js-tip-stem, &.js-tip-bottom .js-tip-stem { @include _stem-skin(bottom, $stem-color); }
- &.js-tip-top.js-snap-tip-x-side .js-tip-stem, &.js-tip-top .js-tip-stem { @include _stem-skin(top, $stem-color); }
-}
+/*
+ HLF Tip jQuery Plugin
+ =====================
+ There should be no css output from this file.
+*/
-@mixin _stem-skin ($side, $stem-color) {
- border-color: transparent; // Reset.
- border-#{$side}-color: $stem-color;
-}
+// The main responsibility for the required plugin styling is to provide
+// required layout styles but allow easy customization of skin styles, ie.
+// colors, fonts, borders, shadows.
-// Include in root.
-@mixin tip-layout ($baseline:12px,
- $max-width-em:18,
- $stem-size-em:1,
- $stem-wide-base-em:1.5,
- $content-padding-em:2/3 1 5/6 1)
-{
- $max-width: $max-width-em * $baseline;
- $stem-size: round($stem-size-em * $baseline);
- $stem-wide-base: round($stem-wide-base-em * $stem-size-em * $baseline);
- // TODO: Simplify.
- $content-padding: '';
- $i: 0;
- @each $em in $content-padding-em {
- $content-padding: $content-padding+($em * $baseline);
- $i: $i+1;
- @if $i < length($content-padding-em) {
- $content-padding: $content-padding+' ';
- }
- }
- .js-tip-inner {
- max-width: $max-width;
- }
- .js-tip-content {
- padding: #{$content-padding};
- }
- // Declaration structure from base styles.
- &.js-tip-right.js-snap-tip-y-side .js-tip-stem, &.js-tip-right .js-tip-stem { @include _stem-layout(right, $stem-size); }
- &.js-tip-left.js-snap-tip-y-side .js-tip-stem, &.js-tip-left .js-tip-stem { @include _stem-layout(left, $stem-size); }
- &.js-tip-bottom.js-snap-tip-x-side .js-tip-stem, &.js-tip-bottom .js-tip-stem { @include _stem-layout(bottom, $stem-size); }
- &.js-tip-top.js-snap-tip-x-side .js-tip-stem, &.js-tip-top .js-tip-stem { @include _stem-layout(top, $stem-size); }
- &.js-snap-tip-x-side .js-tip-stem {
- margin-left: -$stem-wide-base/2; // Center.
- border-width: 0 ($stem-wide-base/2);
- }
- &.js-snap-tip-y-side .js-tip-stem {
- margin-top: -$stem-wide-base/2; // Center.
- border-width: ($stem-wide-base/2) 0;
- }
-}
+// Internal
+// --------
-@mixin _stem-layout ($side, $stem-size) {
- border-#{$side}-width: $stem-size;
- @if $side == top or $side == bottom {
- &+.js-tip-content {
- left: auto; right: auto; // Reset.
- @if $side == top { bottom: $stem-size; }
- @if $side == bottom { top: $stem-size; }
- }
- } @else if $side == left or $side == right {
- &+.js-tip-content {
- top: auto; bottom: auto; // Reset.
- @if $side == left { right: $stem-size; }
- @if $side == right { left: $stem-size; }
- }
- }
-}
+// Note the order in this map used to styling the tip based on its side classes.
+// Because it gets more than one side class, the style for the side class that's
+// later wins. So we want the vertical side class styling to override any
+// horizontal side ones.
+$_side-snap-side-map: (right: y, left: y, bottom: x, top: x);
+$_side-inverse-map: (right: left, left: right, bottom: top, top: bottom);
+
+// ❧
-// Base
-// ----
+// Mixins
+// ------
-.js-tip {
+// 𝒇 `tip-base` should always be included. It contains the basic styles for tips
+// to display and position correctly that rarely need to re-implementation. The
+// resulting DOM structure looks like:
+// ```html
+//
+//
+//
+//
...
+//
+//
+// ```
+@mixin tip-base {
display: none;
position: absolute;
- z-index: 9999;
> .js-tip-inner {
position: relative;
}
-}
-.js-tip-inner {
- > .js-tip-stem {
- border: {
- width: 0;
- style: solid;
- color: transparent;
- }
+ .js-tip-stem {
+ border: 0 solid transparent; width: 0; height: 0; // Initial values.
background: none;
position: absolute;
- width: 0;
- height: 0;
}
- > .js-tip-content {
+ .js-tip-content {
overflow: hidden;
position: relative;
- // Position adjusted by to stem style mixin.
- }
+ }
}
-.js-tip-stem {
- .js-tip-right.js-snap-tip-y-side &, // Reapply.
- .js-tip-right & {
- left: 0;
+
+// 𝒇 `tip-layout` should be included unless you want to implement your own tip
+// layout, specifically around tip stem position. The sizing logic structures
+// around `em-size`. Most of this mixin is handling the variations of classes on
+// the tip element. For example: `js-tip js-snap-tip js-tip-follow js-tip-bottom
+// js-tip-right js-snap-tip-trigger js-snap-tip-x-side`.
+@mixin tip-layout($em-size: 12px,
+ $content-padding-em: 2/3 1 5/6 1,
+ $max-width-em: 18,
+ $stem-size-em: 1,
+ $stem-wide-base-em: 1.5,
+ $z-index: 9999)
+{
+ // - First calculate `*-em` parameters into pixels.
+ $max-width: $max-width-em * $em-size;
+ $stem-size: round($stem-size-em * $em-size);
+ $stem-wide-base: round($stem-wide-base-em * $stem-size-em * $em-size);
+ $content-padding: ();
+ @each $em in $content-padding-em {
+ $content-padding: $content-padding ($em * $em-size);
}
- .js-tip-left.js-snap-tip-y-side &, // Reapply.
- .js-tip-left & {
- right: 0;
+ // - Then for some basic layout styles.
+ z-index: $z-index;
+ .js-tip-inner {
+ max-width: $max-width;
}
- // By default top and bottom have precedence in growing the stem, so they
- // come last.
- .js-tip-bottom.js-snap-tip-x-side &, // Reapply.
- .js-tip-bottom & {
- top: 0;
+ .js-tip-content {
+ padding: #{$content-padding};
}
- .js-tip-top.js-snap-tip-x-side &, // Reapply.
- .js-tip-top & {
- bottom: 0;
+ // - Finally start layout for the stem. Finish drawing and positioning the
+ // stem, and offset the content to match the stem size.
+ @each $side, $snap-side in $_side-snap-side-map {
+ &.js-tip-#{$side} { &.js-snap-tip-#{$snap-side}-side, & { .js-tip-stem {
+ border-#{$side}-width: $stem-size;
+ & + .js-tip-content {
+ @if $snap-side == x {
+ left: auto; right: auto; // Reset.
+ @if $side == top { bottom: $stem-size; }
+ @if $side == bottom { top: $stem-size; }
+ }
+ @if $snap-side == y {
+ top: auto; bottom: auto; // Reset.
+ @if $side == left { right: $stem-size; }
+ @if $side == right { left: $stem-size; }
+ }
+ }
+ #{map-get($_side-inverse-map, $side)}: 0;
+ }}}
}
- .js-snap-tip-x-side & {
+ // - Then make any stem layout adjustments for when the tip is snapping to an
+ // axis. The stem gets centered in this implementation.
+ &.js-snap-tip-x-side .js-tip-stem {
+ border: {
+ color: transparent; // Partial reset.
+ width: 0 ($stem-wide-base / 2);
+ }
left: 50%;
- border-color: transparent; // Partial reset.
+ margin-left: -$stem-wide-base / 2;
}
- .js-snap-tip-y-side & {
+ &.js-snap-tip-y-side .js-tip-stem {
+ border: {
+ color: transparent; // Partial reset.
+ width: ($stem-wide-base / 2) 0;
+ }
+ margin-top: -$stem-wide-base / 2;
top: 50%;
- border-color: transparent; // Partial reset.
+ }
+}
+
+// 𝒇 `tip-skin` is entirely optional and easy to re-implement. You should use it
+// if you just need to configure colors.
+@mixin tip-skin($fill-color, $text-color, $stem-color: null)
+{
+ $stem-color: $fill-color !default;
+ .js-tip-content {
+ background-color: $fill-color;
+ color: $text-color;
+ }
+ @each $side, $snap-side in $_side-snap-side-map {
+ &.js-tip-#{$side} { &.js-snap-tip-#{$snap-side}-side, & { .js-tip-stem {
+ border: {
+ color: transparent; // Reset.
+ #{$side}-color: $stem-color;
+ }
+ }}}
}
}
diff --git a/src/js/jquery.extension.hlf.core.coffee b/src/js/jquery.extension.hlf.core.coffee
index 1e564bf..58f0307 100644
--- a/src/js/jquery.extension.hlf.core.coffee
+++ b/src/js/jquery.extension.hlf.core.coffee
@@ -1,8 +1,6 @@
###
HLF Core jQuery Extension
=========================
-Released under the MIT License
-Written with jQuery 1.7.2
###
# The core extension is comprised of several aspects.
@@ -12,40 +10,66 @@ Written with jQuery 1.7.2
# - Plugin creation with support for both classes and mixins, via:
# `$.createPlugin`.
# - Integrated no-conflict handling and debug-logging, via: `$.hlf.noConflict`,
-# `$.hlf.debugLog`.
+# `$.hlf.debugLog`. Child namespaces (for plugins, etc.) automatically inherit
+# these methods unless they provide their own.
# The extension also creates and provides the `hlf` jQuery namespace. Namespaces
# for other extensions and plugins are attached to this main namespace.
-# Export. Prefer AMD.
-((extension) ->
- if define? and define.amd?
+# Export. Support AMD, CommonJS (Browserify), and browser globals.
+((root, factory) ->
+ if typeof define is 'function' and define.amd?
+ # - AMD. Register as an anonymous module.
define [
'jquery'
'underscore'
- ], extension
- else extension jQuery, _
-)(($, _) ->
+ ], factory
+ else if typeof exports is 'object'
+ # - Node. Does not work with strict CommonJS, but only CommonJS-like
+ # environments that support module.exports, like Node.
+ module.exports = factory(
+ require 'jquery',
+ require 'underscore'
+ )
+ else
+ # - Browser globals (root is window). No globals needed.
+ factory jQuery, _, jQuery.hlf
+)(@, ($, _) ->
+ # ❧
+
+ # Namespace
+ # ---------
+
+ # It takes some more boilerplate and helpers to write jQuery modules. That
+ # code and set of conventions is here in the root namespace. Child namespaces
+ # follow suit convention.
hlf =
+ # The `debug` flag here toggles debug logging for everything in the library
+ # that doesn't have a custom debug flag in its namespace.
debug: on # Turn this off when going to production.
+ # 𝒇 `toString` is mainly for namespacing when extending any jQuery API. For
+ # now, its base form is very simple.
toString: _.memoize (context) -> 'hlf'
+ # 𝒇 `noConflict` in its base form will remove assignments to the global
+ # jQuery namespace. Properties will have to be accessed through the `$.hlf`
+ # namespace. See `_safeSet` below. Also see `createPlugin` for its no-
+ # conflict integration.
+ noConflict: -> (fn() for fn in _noConflicts).length
+
+ # 𝒇 `debugLog` in its base form just wraps around `console.log` and links to
+ # the `debug` flag. However, `debugLog` conventionally becomes a no-op if
+ # the `debug` flag is off.
+ hlf.debugLog = if hlf.debug is off then $.noop else
+ (if console.log.bind then console.log.bind(console) else console.log)
- # We keep track of no-conflict procedures with `_noConflicts`. This is
+ # Using `_noConflicts`, we keep track of no-conflict procedures. This is
# essentially working with a callback queue. Calling `$.hlf.noConflict` simply
- # runs these procedures. Procedures should be simple and idempotent, i.e.
+ # runs these procedures. Procedures should be simple and idempotent, ie.
# restoring the property to a saved previous value.
_noConflicts = []
- _.extend hlf,
- # The base `noConflict` behavior will remove assignments to the global
- # jQuery namespace. Properties will have to be accessed through the `$.hlf`
- # namespace. See `safeSet` below. Also see `createPlugin` for its no-
- # conflict integration.
- noConflict: -> (fn() for fn in _noConflicts).length
- # The base debug logging implementation just wraps around `console.log`.
- debugLog: if hlf.debug is off then $.noop else
- (if console.log.bind then console.log.bind(console) else console.log)
+ # ❧
# Plugin Support
# --------------
@@ -58,36 +82,128 @@ Written with jQuery 1.7.2
# plugins inheriting from a base layer, that base layer is integrated on
# instantiation.
- # `_createPluginInstance` is a private subroutine that's part of
+ _.extend hlf,
+ # 𝒇 `createPlugin`, will return an appropriate jQuery plugin method for the
+ # given `createOptions`, comprised of:
+ createPlugin: (createOptions) ->
+ # - `name`, which is required and is the name of the method. The `safeName`
+ # for the method, which needs to be on the jQuery prototype, is prefixed
+ # by `hlf` and should be used after `noConflict` is called.
+ name = createOptions.name
+ safeName = "#{@toString()}#{name[0].toUpperCase()}#{name[1..]}"
+ # - `namespace`, which is required and must correctly implement `debug`,
+ # `toString`, and `defaults`. It can optionally have a `noConflict`
+ # procedure.
+ namespace = createOptions.namespace
+ # - An `apiClass` definition and/or an `apiMixins` collection. It will get
+ # modified with base API additions. A `mixinFilter` can be provided to
+ # limit the mixins in the collection that get applied during
+ # instantiation. If provided, the `apiMixins` collection must have a
+ # `base` mixin, which will get the API additions. Also note that
+ # `apiClass` and `apiMixins` will get published into the namespace, so
+ # additional flexibility is possible, especially with non-specific
+ # mixins.
+ apiAdditions = _createPluginAPIAdditions name, namespace
+ if createOptions.apiClass?
+ apiClass = namespace.apiClass = createOptions.apiClass
+ _.extend apiClass::, apiAdditions
+ if createOptions.apiMixins?
+ mixinFilter = createOptions.mixinFilter
+ mixinFilter ?= (mixin) -> mixin
+ apiMixins = namespace.apiMixins = createOptions.apiMixins
+ $.extend (deep = on), apiMixins, { base: apiAdditions }
+ # - The plugin's `noConflict` procedure, which gets published onto its
+ # namespace, but default just restores to the previous method. If a
+ # `noConflict` procedure is provided by the namespace, it gets run
+ # beforehand as well.
+ _noConflict = namespace.noConflict
+ _plugin = $.fn[name]
+ _noConflicts.push (namespace.noConflict = ->
+ if _.isFunction(_noConflict) then _noConflict()
+ $.fn[name] = _plugin
+ )
+ # 𝒇 Generate and publish the plugin method.
+ #
+ # The method handles two variations of input. A command `type` (name)
+ # and `userInfo` can be passed in to trigger the command route. The
+ # latter is typically additional, command-specific parameters.
+ # Otherwise, if the first argument is an options collection, the normal
+ # route is triggered.
+ plugin = $.fn[name] = $.fn[safeName] = ->
+ if _.isString(arguments[0])
+ command =
+ type: arguments[0]
+ userInfo: arguments[1]
+ else
+ options = arguments[0]
+ $context = arguments[1] if arguments[1]?
+ #- The element's `$context` will default to document body.
+ $context ?= $ 'body'
+ # - With the command route, if there is a plugin instance and it can
+ # `handleCommand`, call the method, but invoke `userInfo` if needed
+ # beforehand. With the normal route, if there is a plugin instance and
+ # no arguments are provided we assume the call is to access the
+ # instance, not reset it.
+ if command?
+ @each ->
+ $el = $(@)
+ instance = $el.data namespace.toString('data')
+ if _.isFunction(instance.handleCommand)
+ if _.isFunction(command.userInfo) then command.userInfo $el
+ sender = null
+ instance.handleCommand command, sender
+ return @ # Follow plugin return conventions.
+ else
+ # - `asSharedInstance` will decide what the plugin instance's main
+ # element will be. The notion is that several elements all share the
+ # same plugin instance.
+ $el = if createOptions.asSharedInstance is yes then $context else @first()
+ instance = $el.data namespace.toString('data')
+ return instance if instance? and instance.$el? and not arguments.length
+ # - Otherwise, continue creating the instance by preparing the options
+ # and deciding the main element before passing things onto
+ # `_createPluginInstance`.
+ options = $.extend (deep = on), {}, namespace.defaults, options
+ $el = @
+ ( ->
+ args = arguments
+ if createOptions.asSharedInstance is yes then _createPluginInstance $el, args...
+ else $el.each -> _createPluginInstance $(@), args...
+ )(options, $context, namespace, apiClass, apiMixins, mixinFilter, createOptions)
+ return @ # Follow plugin return conventions.
+
+ _.bindAll hlf, 'createPlugin'
+
+ # 𝒇 `_createPluginInstance` is a private subroutine that's part of
# `createPlugin`, which has more details on its required input.
_createPluginInstance = ($el, options, $context, namespace, apiClass, apiMixins, mixinFilter, createOptions) ->
- # Check if plugin element has options set in its plugin data attribute. If
- # so, merge those options into our own `finalOptions`.
+ # - Check if plugin element has options set in its plugin data attribute. If
+ # so, merge those options into our own `finalOptions`.
data = $el.data namespace.toString('data')
finalOptions = options
if $.isPlainObject(data)
finalOptions = $.extend (deep = on), {}, options, data
- # Also decide the `$root` element based on the situation. It's where the
- # plugin instance gets stored and the root plugin class gets added.
- # A shared instance, for example, get stored on the `$context`.
+ # - Also decide the `$root` element based on the situation. It's where the
+ # plugin instance gets stored and the root plugin class gets added.
+ # A shared instance, for example, get stored on the `$context`.
$root = $el
else if createOptions.asSharedInstance
$root = $context
else
$root = $el
- # If we're provided with a class for the API, instantiate it. Decorate the
- # instance with additional mixins if applicable.
+ # - If we're provided with a class for the API, instantiate it. Decorate the
+ # instance with additional mixins if applicable.
if apiClass?
instance = new apiClass $el, finalOptions, $context
if createOptions.baseMixins?
hlf.applyMixins instance, namespace, createOptions.baseMixins...
if createOptions.apiMixins?
hlf.applyMixins instance, namespace, createOptions.apiMixins...
- # If instead we're provided with just mixins for the API, create a plain
- # object with the base properties for the instance. Then apply the provided
- # mixins in order: the names of the base mixins, the `base` mixin from the
- # provided mixins collection, and the `otherMixins`. The others are just
- # mixins allowed by the provided filter (if any) that also aren't `base`.
+ # - If instead we're provided with just mixins for the API, create a plain
+ # object with the base properties for the instance. Then apply the provided
+ # mixins in order: the names of the base mixins, the `base` mixin from the
+ # provided mixins collection, and the `otherMixins`. The others are just
+ # mixins allowed by the provided filter (if any) that also aren't `base`.
else if apiMixins?
instance = { $el, options: finalOptions }
if createOptions.baseMixins?
@@ -99,33 +215,33 @@ Written with jQuery 1.7.2
.without apiMixins.base
.value()
hlf.applyMixins instance, namespace, otherMixins...
- # If the `compactOptions` flag is toggled, `finalOptions` will be merged
- # into the instance. This makes accessing options more convenient, but can
- # cause conflicts with larger existing APIs that don't account for such
- # naming conflicts, since _we don't handle conflicts here_. Otherwise, just
- # alias the conventional `selectors` and `classNames` option groups.
+ # - If the `compactOptions` flag is toggled, `finalOptions` will be merged
+ # into the instance. This makes accessing options more convenient, but can
+ # cause conflicts with larger existing APIs that don't account for such
+ # naming conflicts, since _we don't handle conflicts here_. Otherwise, just
+ # alias the conventional `selectors` and `classNames` option groups.
if createOptions.compactOptions is yes
$.extend (deep = yes), instance, finalOptions
delete instance.options
else
if finalOptions.selectors? then instance.selectors = finalOptions.selectors
if finalOptions.classNames? then instance.classNames = finalOptions.classNames
- # If the `autoSelect` flag is toggled and a `select` method is provided
- # (i.e. via `selection` mixin), call it and automatically setup element
- # references prior to initialization.
+ # - If the `autoSelect` flag is toggled and a `select` method is provided
+ # (ie. via `selection` mixin), call it and automatically setup element
+ # references prior to initialization.
if createOptions.autoSelect is yes and _.isFunction(instance.select)
instance.select()
- # If the `cls` API addition exists and provides the root class, add the root
- # class to the decided `$root` prior to initialization.
+ # - If the `cls` API addition exists and provides the root class, add the root
+ # class to the decided `$root` prior to initialization.
if instance.cls isnt $.noop then $root.addClass instance.cls()
- # If an `init` method is provided, and one must be if it's just mixins for
- # the API, call it. Convention is to always provide it.
+ # - If an `init` method is provided, and one must be if it's just mixins for
+ # the API, call it. Convention is to always provide it.
if _.isFunction(instance.init) then instance.init()
else if not apiClass? then hlf.debugLog 'ERROR: No `init` method on instance.', instance
- # Lastly, store the instance on `$root`.
+ # - Lastly, store the instance on `$root`.
$root.data instance.attr(), instance
- # `_createPluginAPIAdditions` is a private subroutine that's part of
+ # 𝒇 `_createPluginAPIAdditions` is a private subroutine that's part of
# `createPlugin`, which has more details on its required input.
_createPluginAPIAdditions = (name, namespace) ->
# - Add the `evt` method to namespace an event name.
@@ -145,97 +261,7 @@ Written with jQuery 1.7.2
debugLog: if namespace.debug is off then $.noop else
-> hlf.debugLog namespace.toString('log'), arguments...
- _.extend hlf,
- # `createPlugin`, will return an appropriate jQuery plugin method for the
- # `given `createOptions`, comprised of:
- createPlugin: (createOptions) ->
- # `name`, which is required and is the name of the method. The `safeName`
- # for the method, which needs to be on the jQuery prototype, is prefixed
- # by `hlf` and should be used after `noConflict` is called.
- name = createOptions.name
- safeName = "#{@toString()}#{name[0].toUpperCase()}#{name[1..]}"
- # `namespace`, which is required and must correctly implement `debug`,
- # `toString`, and `defaults`. It can optionally have a `noConflict`
- # procedure.
- namespace = createOptions.namespace
- # An `apiClass` definition and/or an `apiMixins` collection. It will get
- # modified with base API additions. A `mixinFilter` can be provided to
- # limit the mixins in the collection that get applied during
- # instantiation. If provided, the `apiMixins` collection must have a
- # `base` mixin, which will get the API additions. Also note that
- # `apiClass` and `apiMixins` will get published into the namespace, so
- # additional flexibility is possible, especially with non-specific mixins.
- apiAdditions = _createPluginAPIAdditions name, namespace
- if createOptions.apiClass?
- apiClass = namespace.apiClass = createOptions.apiClass
- _.extend apiClass::, apiAdditions
- if createOptions.apiMixins?
- mixinFilter = createOptions.mixinFilter
- mixinFilter ?= (mixin) -> mixin
- apiMixins = namespace.apiMixins = createOptions.apiMixins
- $.extend (deep = on), apiMixins, { base: apiAdditions }
- # The plugin's `noConflict` procedure, which gets published onto its
- # namespace, but default just restores to the previous method. If a
- # `noConflict` procedure is provided by the namespace, it gets run
- # beforehand as well.
- _noConflict = namespace.noConflict
- _plugin = $.fn[name]
- _noConflicts.push (namespace.noConflict = ->
- if _.isFunction(_noConflict) then _noConflict()
- $.fn[name] = _plugin
- )
- # Generate and publish the plugin method.
- plugin = $.fn[name] = $.fn[safeName] = ->
- # The method handles two variations of input. A command `type` (name)
- # and `userInfo` can be passed in to trigger the command route. The
- # latter is typically additional, command-specific parameters.
- # Otherwise, if the first argument is an options collection, the normal
- # route is triggered.
- if _.isString(arguments[0])
- command =
- type: arguments[0]
- userInfo: arguments[1]
- else
- options = arguments[0]
- $context = arguments[1] if arguments[1]?
- # The element's `$context` will default to document body.
- $context ?= $ 'body'
- # With the command route, if there is a plugin instance and it can
- # `handleCommand`, call the method, but invoke `userInfo` if needed
- # beforehand. With the normal route, if there is a plugin instance and
- # no arguments are provided we assume the call is to access the
- # instance, not reset it.
- if command?
- @each ->
- $el = $(@)
- instance = $el.data namespace.toString('data')
- if _.isFunction(instance.handleCommand)
- if _.isFunction(command.userInfo) then command.userInfo $el
- sender = null
- instance.handleCommand command, sender
- # Follow plugin return conventions.
- return @
- else
- # `asSharedInstance` will decide what the plugin instance's main element
- # will be. The notion is that several elements all share the same
- # plugin instance.
- $el = if createOptions.asSharedInstance is yes then $context else @first()
- instance = $el.data namespace.toString('data')
- return instance if instance? and instance.$el? and not arguments.length
- # Otherwise, continue creating the instance by preparing the options and
- # deciding the main element before passing things onto
- # `_createPluginInstance`.
- options = $.extend (deep = on), {}, namespace.defaults, options
- $el = @
- ( ->
- args = arguments
- if createOptions.asSharedInstance is yes then _createPluginInstance $el, args...
- else $el.each -> _createPluginInstance $(@), args...
- )(options, $context, namespace, apiClass, apiMixins, mixinFilter, createOptions)
- # Follow plugin return conventions.
- return @
-
- _.bindAll hlf, 'createPlugin'
+ # ❧
# Mixin Support
# -------------
@@ -246,38 +272,38 @@ Written with jQuery 1.7.2
# to add helper methods for even more flexible extensions between mixins.
_.extend hlf,
- # `applyMixin`, when given a `context` to decorate with a valid `mixin`, runs
+ # 𝒇 `applyMixin`, when given a `context` to decorate with a valid `mixin`, runs
# any run-once hooks after applying a mixin copy without the hooks.
# `context` is conventionally a class instance.
applyMixin: (context, dependencies, mixin) ->
- # If `mixin` is a string, check the general `$.mixins` for the mixin.
+ # - If `mixin` is a string, check the general `$.mixins` for the mixin.
if _.isString(mixin) then mixin = @mixins[mixin]
return unless mixin?
if _.isFunction(mixin) then mixin = mixin dependencies
onceMethods = []
handlerNames = []
- # Get run-once methods and filter a clean mixin copy. Run-once methods are
- # what's specified in `$.mixinOnceNames` and implemented by the mixin.
- # Also get methods that are conventionally named like event handlers.
+ # - Get run-once methods and filter a clean mixin copy. Run-once methods are
+ # what's specified in `$.mixinOnceNames` and implemented by the mixin.
+ # Also get methods that are conventionally named like event handlers.
for own name, prop of mixin when _.isFunction(prop)
if name in @mixinOnceNames then onceMethods.push prop
if name.indexOf('handle') is 0 and name isnt 'handleCommand'
handlerNames.push name
mixinToApply = _.omit mixin, @mixinOnceNames
- # Apply mixin and call onces with explicit context.
+ # - Apply mixin and call onces with explicit context.
_.extend context, mixinToApply
method.call(context) for method in onceMethods
- # Auto-bind conventionally-named event handlers.
+ # - Auto-bind conventionally-named event handlers.
if handlerNames.length then _.bindAll context, handlerNames...
- # `applyMixins`, when given a `context` (class) to decorate with `mixins`,
+ # 𝒇 `applyMixins`, when given a `context` (class) to decorate with `mixins`,
# which should be passed in order of application, calls `$.applyMixin` for
# each mixin. Conventionally, this should be used instead of
# `$.applyMixin`.
applyMixins: (context, dependencies, mixins...) ->
@applyMixin context, dependencies, mixin for mixin in mixins
- # `createMixin`, when given a collection of `mixins`, adds a new mixin with
+ # 𝒇 `createMixin`, when given a collection of `mixins`, adds a new mixin with
# given `name` and `mixin` method collection. Conventionally, each logical
# package of software should be written as a collection of mixins, with one
# named 'base'.
@@ -291,23 +317,23 @@ Written with jQuery 1.7.2
# Supported decorators:
mixinOnceNames: [
- # `decorate` allows more complex extending of the instance. For example,
- # methods and properties can be removed, handlers can be added to
- # triggered events for more complex extending of existing methods.
+ # - 𝒇 `decorate` allows more complex extending of the instance. For example,
+ # methods and properties can be removed, handlers can be added to
+ # triggered events for more complex extending of existing methods.
'decorate'
- # `decorateOptions` allows extending the context's options, which are
- # conventionally a property named `options`.
+ # - 𝒇 `decorateOptions` allows extending the context's options, which are
+ # conventionally a property named `options`.
'decorateOptions'
]
- # `$.mixins` is the general mixin collection that's provided for writing
+ # 𝒇 `$.mixins` is the general mixin collection that's provided for writing
# foundation-level jQuery mixins. Conventionally, other mixins not shared
# between different logical packages do not belong here.
mixins:
- # `data`, when given a context with a data-attribute-name translator that
- # makes a property-name follow jQuery conventions, as well as with a
- # property `$el`, generate a mixin that applies convenience wrappers
- # around the jQuery data API to simplify data API calls as much as
- # possible.
+ # - 𝒇 `data`, when given a context with a data-attribute-name translator
+ # that makes a property-name follow jQuery conventions, as well as with
+ # a property `$el`, generate a mixin that applies convenience wrappers
+ # around the jQuery data API to simplify data API calls as much as
+ # possible.
data: ->
data: ->
if arguments.length
@@ -319,10 +345,10 @@ Written with jQuery 1.7.2
(pairs[attr(k)] = v) for own k, v of first
arguments[0] = pairs
@$el.data.apply @$el, arguments
- # `event`, when given a context with an event-name translator that makes an
- # event-name follow jQuery conventions, as well as with a property `$el`,
- # generates a mixin that applies convenience wrappers around the jQuery
- # custom event API to simplify event API calls as much as possible.
+ # - 𝒇 `event`, when given a context with an event-name translator that makes an
+ # event-name follow jQuery conventions, as well as with a property `$el`,
+ # generates a mixin that applies convenience wrappers around the jQuery
+ # custom event API to simplify event API calls as much as possible.
event: ->
on: (name) ->
name = @evt name if name?
@@ -333,9 +359,9 @@ Written with jQuery 1.7.2
trigger: (name, userInfo) ->
type = @evt name
@$el.trigger { type, userInfo }
- # `selection`, when given the context has a property `$el` and a property
- # `selectors`, define cached selector results for each name-selector pair.
- # Also provide selection helpers for common tasks.
+ # - 𝒇 `selection`, when given the context has a property `$el` and a property
+ # `selectors`, define cached selector results for each name-selector pair.
+ # Also provide selection helpers for common tasks.
selection: ->
select: ->
for own name, selector of @selectors
@@ -346,20 +372,23 @@ Written with jQuery 1.7.2
classNames ?= @classNames
@$el.find ".#{@classNames[className]}"
+ # ❧
+
# Export
# ------
- safeSet = (key, toContext=$, fromContext=hlf) ->
+ # 𝒇 `_safeSet` is an internal wrapper around `_noConflict`.
+ _safeSet = (key, toContext=$, fromContext=hlf) ->
_oldValue = toContext[key]
toContext[key] = fromContext[key]
_noConflicts.push -> toContext[key] = _oldValue
- safeSet 'applyMixin'
- safeSet 'applyMixins'
- safeSet 'createMixin'
- safeSet 'createPlugin'
- safeSet 'mixinOnceNames'
- safeSet 'mixins'
+ _safeSet 'applyMixin'
+ _safeSet 'applyMixins'
+ _safeSet 'createMixin'
+ _safeSet 'createPlugin'
+ _safeSet 'mixinOnceNames'
+ _safeSet 'mixins'
$.hlf = hlf
diff --git a/src/js/jquery.extension.hlf.event.coffee b/src/js/jquery.extension.hlf.event.coffee
index ce4ec96..0742cb0 100644
--- a/src/js/jquery.extension.hlf.event.coffee
+++ b/src/js/jquery.extension.hlf.event.coffee
@@ -1,12 +1,8 @@
###
HLF Event jQuery Extension
==========================
-Released under the MIT License
-Written with jQuery 1.7.2
###
-#!For working docs generation, disable automatic trailing whitespace trimming.
-
# This extension adds custom events to jQuery. It general, the process is
# composed of three parts:
@@ -14,20 +10,34 @@ Written with jQuery 1.7.2
# 2. Private functions to implement certain behaviors.
# 3. Adapting the behaviors to custom events.
-# Export. Prefer AMD. Note that we don't actually provide any exports because
-# ours are attached to jQuery.
-((extension) ->
- if define? and define.amd?
+# ❧
+
+# Export. Support AMD, CommonJS (Browserify), and browser globals.
+((root, factory) ->
+ if typeof define is 'function' and define.amd?
+ # - AMD. Register as an anonymous module.
define [
'jquery'
'underscore'
'hlf/jquery.extension.hlf.core'
- ], extension
- else extension jQuery, _, jQuery.hlf
-)(($, _, hlf) ->
-
- # I. Hover-Intent
- # ---------------
+ ], factory
+ else if typeof exports is 'object'
+ # - Node. Does not work with strict CommonJS, but only CommonJS-like
+ # environments that support module.exports, like Node.
+ module.exports = factory(
+ require 'jquery',
+ require 'underscore',
+ require 'hlf/jquery.extension.hlf.core'
+ )
+ else
+ # - Browser globals (root is window). No globals needed.
+ factory jQuery, _, jQuery.hlf
+)(@, ($, _, hlf) ->
+
+ # ❧
+
+ # Hover-Intent
+ # ------------
# A set of custom events based on a distance check with a customizable
# `interval` of delay to limit 'un-intentional' mouse-enter's and mouse-
@@ -37,9 +47,9 @@ Written with jQuery 1.7.2
# `truemouseenter` and `truemouseleave` provide `pageX` and `pageY` values.
$.extend true, hlf,
hoverIntent:
- # Switch for debugging just hover intent.
+ # - Switch for debugging just hover intent.
debug: off
- # Stores the global state of the mouse. This is for public use.
+ # - Stores the global state of the mouse. This is for public use.
mouse:
x:
current: 0
@@ -47,11 +57,11 @@ Written with jQuery 1.7.2
y:
current: 0
previous: 0
- # Default options.
+ # - Default options.
sensitivity: 8
interval: 300
- # To get the name of this set of custom events, just use the `toString`
- # function and pass the appropriate context. Note we're memoizing it.
+ # - To get the name of this set of custom events, just use the `toString`
+ # function and pass the appropriate context. Note we're memoizing it.
toString: _.memoize (context) ->
switch context
when 'attr' then 'hlf-hover-intent'
@@ -60,11 +70,11 @@ Written with jQuery 1.7.2
# Alias and don't pollute the extension scope.
do (hoverIntent = hlf.hoverIntent, mouse = hlf.hoverIntent.mouse) ->
- # `attr` is an internal formatter for attribute names,
+ # 𝒇 `attr` is an internal formatter for attribute names,
# mainly those of jQuery data keys.
attr = (name='') -> "#{hoverIntent.toString 'attr'}-#{name}"
- # `debugLog` is our internal logger. It's optimized to be a noop if
+ # 𝒇 `debugLog` is our internal logger. It's optimized to be a noop if
# hover intent debugging is off.
debugLog = if hoverIntent.debug is off then $.noop else
-> hlf.debugLog hoverIntent.toString('log'), arguments...
@@ -83,7 +93,7 @@ Written with jQuery 1.7.2
sensitivity: hoverIntent.sensitivity
interval: hoverIntent.interval
- # `getComputedState` simplifies getting the trigger element's hover intent
+ # 𝒇 `getComputedState` simplifies getting the trigger element's hover intent
# state and using any `defaultState` as fallback. Note that we clone the
# value if it looks like it will be assigned by reference.
getComputedState = ($trigger) ->
@@ -93,7 +103,7 @@ Written with jQuery 1.7.2
state[key] = $trigger.data(attr(key)) or value
state
- # `check` is the main routine that uses the state, setup, and teardown
+ # 𝒇 `check` is the main routine that uses the state, setup, and teardown
# subroutines. It is an event handler (see below).
check = (event) ->
$trigger = $ @
@@ -102,7 +112,7 @@ Written with jQuery 1.7.2
didTeardown = teardownCheckIfNeeded event, $trigger, state
if didTeardown is no then setupCheckIfNeeded event, $trigger, state
- # `setupCheckIfNeeded` will setup to `performCheck` after setting up (again)
+ # 𝒇 `setupCheckIfNeeded` will setup to `performCheck` after setting up (again)
# the timer state, but only if the timer state is properly reset.
setupCheckIfNeeded = (event, $trigger, state) ->
return no if state.timer.cleared is no and state.timer.timeout?
@@ -115,7 +125,7 @@ Written with jQuery 1.7.2
$trigger.data attr('timer'), state.timer
return yes
- # `teardownCheckIfNeeded` will teardown by removing state from trigger data,
+ # 𝒇 `teardownCheckIfNeeded` will teardown by removing state from trigger data,
# thereby defaulting them (see `getComputedState`). It will only work on
# mouse-leave and will always trigger `truemouseleave`.
teardownCheckIfNeeded = (event, $trigger, state) ->
@@ -129,7 +139,7 @@ Written with jQuery 1.7.2
triggerEvent 'truemouseleave', $trigger
return yes
- # `performCheck` is the main hover intent checking subroutine. The state's
+ # 𝒇 `performCheck` is the main hover intent checking subroutine. The state's
# `intentional` flag is updated as a crude change-in-distance comparison. If
# there is intent, then `truemouseenter` is triggered. The timer is reset,
# since this completes the checking cycle. State is also always saved to the
@@ -147,13 +157,13 @@ Written with jQuery 1.7.2
$trigger.data attr('intentional'), state.intentional
$trigger.data attr('timer'), state.timer
- # `trackMouse` tracks mouse position specifically for checking hover intent.
+ # 𝒇 `trackMouse` tracks mouse position specifically for checking hover intent.
trackMouse = _.throttle (event) ->
mouse.x.current = event.pageX
mouse.y.current = event.pageY
, 16
- # `triggerEvent` abstracts away the generation of and support for custom
+ # 𝒇 `triggerEvent` abstracts away the generation of and support for custom
# hover intent events.
triggerEvent = (name, $trigger) ->
switch name
diff --git a/src/js/jquery.hlf.editable.coffee b/src/js/jquery.hlf.editable.coffee
index dc65b73..a968a42 100644
--- a/src/js/jquery.hlf.editable.coffee
+++ b/src/js/jquery.hlf.editable.coffee
@@ -1,21 +1,29 @@
###
HLF Editable jQuery Plugin
==========================
-Released under the MIT License
-Written with jQuery 1.7.2
###
-# Export. Prefer AMD.
-((plugin) ->
- if define? and define.amd?
+# Export. Support AMD, CommonJS (Browserify), and browser globals.
+((root, factory) ->
+ if typeof define is 'function' and define.amd?
+ # - AMD. Register as an anonymous module.
define [
'jquery'
'underscore'
'hlf/jquery.extension.hlf.core'
- ], plugin
+ ], factory
+ else if typeof exports is 'object'
+ # - Node. Does not work with strict CommonJS, but only CommonJS-like
+ # environments that support module.exports, like Node.
+ module.exports = factory(
+ require 'jquery',
+ require 'underscore',
+ require 'hlf/jquery.extension.hlf.core'
+ )
else
- plugin jQuery, _, jQuery.hlf
-)(($, _, hlf) ->
+ # - Browser globals (root is window). No globals needed.
+ factory jQuery, _, jQuery.hlf
+)(@, ($, _, hlf) ->
hlf.editable =
debug: on
@@ -249,6 +257,8 @@ Written with jQuery 1.7.2
bindFileUploader: ->
+ # ❧
+
# Export
# ------
diff --git a/src/js/jquery.hlf.tip.coffee b/src/js/jquery.hlf.tip.coffee
index dbb173b..abc34ea 100644
--- a/src/js/jquery.hlf.tip.coffee
+++ b/src/js/jquery.hlf.tip.coffee
@@ -1,97 +1,86 @@
###
HLF Tip jQuery Plugin
=====================
-Released under the MIT License
-Written with jQuery 1.7.2
###
-# The base `tip` plugin features basic trigger element parsing, direction-based
-# attachment, cursor following, appearance state management and presentation by
-# fading, custom tip content, and use of the `hlf.hoverIntent` event extension.
-# The tip object is shared amongst the provided triggers.
-
-# The extended `snapTip` plugin extends the base tip and adds snapping-to-
-# trigger-element behavior. By default locks into place. If one of the snap-to-
-# axis options is turned off, the tip will slide along the remaining locked
-# axis.
-
-# Note the majority of presentation state logic is in the plugin stylesheet. We
-# update the presentation state by using `classNames`.
-
-# Lastly, like any other module in this library, we're using proper namespacing
-# whenever there is an added endpoint to the jQuery interface. This is done with
-# the custom `toString` methods. Also, plugin namespaces (under the root
-# `$.hlf`) each have a `debug` flag allowing more granular logging. Each
-# plugin's API is also entirely public, although some methods are intended as
-# protected given their name. Access the plugin singleton is as simple as via
-# `$('body').tip()` or `$('body').snapTip()`, although using the `toString` and
-# jQuery data methods is the same.
-
-# Export. Prefer AMD.
-((plugin) ->
- if define? and define.amd?
+# The base `tip` plugin does several things. It does basic parsing of trigger
+# element attributes for the tip content. It can anchor itself to a trigger by
+# selecting the best direction. It can follow the cursor. It toggles its
+# appearance by fading in and out and resizing, all via configurable animations.
+# It can display custom tip content. It uses of the `hlf.hoverIntent` event
+# extension to prevent appearance 'thrashing.' Last, the tip object attaches to
+# the context element. It acts as tip for the the current jQuery selection via
+# event delegation.
+
+# The extended `snapTip` plugin extends the base tip. It allows the tip to snap
+# to the trigger element. And by default the tip locks into place. But turn on
+# only one axis of snapping, and the tip will track the mouse only on the other
+# axis. For example, snapping to the x-axis will only allow the tip to shift
+# along the y-axis. The x will remain constant.
+
+# ❧
+
+# Export. Support AMD, CommonJS (Browserify), and browser globals.
+((root, factory) ->
+ if typeof define is 'function' and define.amd?
+ # - AMD. Register as an anonymous module.
define [
'jquery'
'underscore'
'hlf/jquery.extension.hlf.core'
'hlf/jquery.extension.hlf.event'
- ], plugin
- else plugin jQuery, _, jQuery.hlf
-)(($, _, hlf) ->
+ ], factory
+ else if typeof exports is 'object'
+ # - Node. Does not work with strict CommonJS, but only CommonJS-like
+ # environments that support module.exports, like Node.
+ module.exports = factory(
+ require 'jquery',
+ require 'underscore',
+ require 'hlf/jquery.extension.hlf.core',
+ require 'hlf/jquery.extension.hlf.event'
+ )
+ else
+ # - Browser globals (root is window). No globals needed.
+ factory jQuery, _, jQuery.hlf
+)(@, ($, _, hlf) ->
+ # It takes some more boilerplate to write the plugins. Any of this additional
+ # support API is put into a plugin specific namespace under `$.hlf`.
hlf.tip =
+ # To toggle debug logging for all instances of a plugin, use the `debug` flag.
debug: off
+ # To namespace when extending any jQuery API, we use custom `toString` helpers.
toString: _.memoize (context) ->
switch context
when 'event' then '.hlf.tip'
when 'data' then 'hlf-tip'
when 'class' then 'js-tips'
else 'hlf.tip'
+ # The plugin's default options is available as reference for values like sizes.
+
+ # ❧
# Tip Options
# -----------
- # Note the plugin instance gets extended with the options.
+ #- NOTE: The plugin instance gets extended with the options.
defaults: do (pre = 'js-tip-') ->
- # - `$viewport` is the element in which the tip must fit into. It is not the
- # context, which stores the tip instance and by convention contains the
- # triggers.
+
+ # - `$viewport` is the element in which the tip must fit into. It is _not_
+ # the context, which stores the tip instance and by convention contains
+ # the triggers.
$viewport: $ 'body'
- # - `triggerContent` can be the name of the trigger element's attribute or a
- # function that provides custom content when given the trigger element.
- triggerContent: null
- # - `shouldDelegate` is by default and encouraged to be on for improving
- # event handling performance.
- shouldDelegate: on
- # - `ms.duration` are the durations of sleep and wake animations.
- # - `ms.delay` are the delays before sleeping and waking.
- ms:
- duration:
- in: 200
- out: 200
- resize: 300
- delay:
- in: 300
- out: 300
- # - `easing` stores the custom easing for baked-in animation support. The
- # keys are the same as those of `shouldAnimate` and work if they are
- # specified, with `base` being the default easing
- easing:
- base: 'ease-in-out'
- # - `shouldAnimate` provides baked-in animation support. The `resize`
- # animation is like `$.fn.show` but with CSS transitions.
- shouldAnimate:
- resize: on
+ # - `autoDirection` automatically changes the direction so the tip can
+ # better fit inside the viewport.
+ autoDirection: on
# - `cursorHeight` is the browser's cursor height. We need to know this to
# properly offset the tip to avoid cases of cursor-tip-stem overlap.
cursorHeight: 12
- # - Note that the direction data structure must be an array of
+ # - `defaultDirection` is used as a tie-breaker when selecting the best
+ # direction. Note that the direction data structure must be an array of
# `components`, and conventionally with top/bottom first.
defaultDirection: ['bottom', 'right']
# - `safeToggle` prevents orphan tips, since timers are sometimes unreliable.
safeToggle: on
- # - `autoDirection` automatically changes the direction so the tip can
- # better fit inside the viewport.
- autoDirection: on
# - `tipTemplate` should return interpolated html when given the
# additional container class list. Its context is the plugin instance.
tipTemplate: (containerClass) ->
@@ -104,6 +93,13 @@ Written with jQuery 1.7.2
"""
+ # - `triggerContent` can be the name of the trigger element's attribute or a
+ # function that provides custom content when given the trigger element.
+ triggerContent: null
+ # NOTE: For these tip plugins, the majority of presentation state logic is
+ # in the plugin stylesheet. We update the presentation state by using
+ # namespaced `classNames` generated in a closure.
+ #
# - `classNames.stem` - Empty string to remove the stem.
# - `classNames.follow` - Empty string to disable cursor following.
classNames: do ->
@@ -112,6 +108,20 @@ Written with jQuery 1.7.2
(classNames[key] = "#{pre}#{key}") for key in keys
classNames.tip = 'js-tip'
classNames
+ # - `animations` are very configurable. Individual animations can be
+ # customized and will default to the base animation settings as needed.
+ animations:
+ base:
+ delay: 0
+ duration: 200
+ easing: 'ease-in-out'
+ enabled: yes
+ show:
+ delay: 200
+ hide:
+ delay: 200
+ resize:
+ delay: 300
hlf.tip.snap =
debug: off
@@ -124,12 +134,18 @@ Written with jQuery 1.7.2
# SnapTip Options
# ---------------
- # These options extend the tip options.
+ #- NOTE: The plugin instance gets extended with the options.
defaults: do (pre = 'js-snap-tip-') ->
+ # These options extend the tip options.
$.extend (deep = yes), {}, hlf.tip.defaults,
- # - `snap.toXAxis` is the switch for snapping along x-axis. Off by default.
- # - `snap.toYAxis` is the switch for snapping along y-axis. Off by default.
- # - `snap.toTrigger` is the switch snapping to trigger that builds on top of
+ # NOTE: For each snapping option, the plugin adds its class to the tip
+ # if the option is on.
+ #
+ # - `snap.toXAxis` is the switch for snapping along x-axis and only
+ # tracking along y-axis. Off by default.
+ # - `snap.toYAxis` is the switch for snapping along y-axis and only
+ # tracking along x-axis. Off by default.
+ # - `snap.toTrigger` is the switch for snapping to trigger built on
# axis-snapping. On by default.
snap:
toTrigger: on
@@ -143,74 +159,53 @@ Written with jQuery 1.7.2
toYAxis: 'y-side'
toTrigger: 'trigger'
(classNames.snap[key] = "#{pre}#{value}") for own key, value of dictionary
- # Update our tip class.
classNames.tip = 'js-tip js-snap-tip'
classNames
- # Tip API
- # -------
- # Note that most of the interface is intended as protected.
+ # ❧
+
+ # Tip Implementation
+ # ------------------
+ # Read on to learn about implementation details.
class Tip
- # The base constructor and `init` mostly do setup work that uses other
- # subroutines when needed. Note that we're also keeping `$triggers` and
- # `$context` as properties. `$context` is partly used to avoid directly
- # binding event listeners to triggers, which can improve performance and
- # allow dynamic binding.
+ # 𝒇 `constructor` keeps `$triggers` and `$context` as properties. `options`
+ # is further normalized.
constructor: (@$triggers, options, @$context) ->
+ for own name, animation of options.animations when name isnt 'base'
+ _.defaults animation, options.animations.base
+ # 𝒇 `init` offloads non-trivial setup to other subroutines.
init: ->
- # Bind handler methods here after class setup completes.
_.bindAll @, '_onTriggerMouseMove', '_setBounds'
- # The element represented by this API is `$tip`. We build it. Alias it to
- # `$el` for any mixins to consume.
- @_setTip = ($tip) => @$tip = @$el = $tip
+ # - Initialize tip element.
@_setTip $ '
'
- # Infer `doStem` and `doFollow` flags from respective `classNames` entries.
+ # - Infer `doStem` and `doFollow` flags from respective `classNames` entries.
@doStem = @classNames.stem isnt ''
@doFollow = @classNames.follow isnt ''
- # Updated with `_setState`, `_state` toggles between: `awake`, `asleep`,
- # `waking`, `sleeping`. The use case behind these states is the tip will
- # remain visible and `awake` as long as there is a high enough frequency
- # of relevant mouse activity. This is achieved with a simple base
- # implementation around timers `_sleepCountdown` and `_wakeCountdown`.
+ # - Initialize state, which is either: `awake`, `asleep`, `waking`,
+ # `sleeping`; respectively show, hide.
@_setState 'asleep'
+ # - The tip should remain visible and `awake` as long as there is a high
+ # enough frequency of relevant mouse activity. In addition to using
+ # `hoverIntent`, this is achieved with a simple base implementation around
+ # timers `_sleepCountdown` and `_wakeCountdown` and an extra reference to
+ # `_$currentTrigger`.
@_wakeCountdown = null
@_sleepCountdown = null
- # `_$currentTrigger` helps manage trigger-related state.
@_$currentTrigger = null
- # Tip instances start off rendered and bound.
+ # - Initialize tip. Note the initial render.
@_render()
@_bind()
- # Process `$triggers` and setup content, event, and positioning aspects.
- processTrigger = ($trigger) =>
- return no if not $trigger.length
- $trigger.addClass @classNames.trigger
- @_saveTriggerContent $trigger
- @_updateDirectionByTrigger $trigger
- if @shouldDelegate is no then @_bindTrigger $trigger
- # Do this for initially provided triggers.
- @$triggers.each (i, el) => processTrigger $(el)
- # If `doLiveUpdate` is inferred to be true, process triggers added in the
- # future. Make sure to ignore mutations related to the tip.
- @doLiveUpdate = window.MutationObserver?
- if @doLiveUpdate
- selector = @$triggers.selector
- onMutations = (mutations) =>
- for mutation in mutations
- $target = $ mutation.target
- continue if $target.hasClass(@classNames.content) # TODO: Limited.
- if mutation.addedNodes.length
- $triggers = $(mutation.addedNodes).find('[title],[alt]') # TODO: Limited.
- $triggers.each (i, el) => processTrigger $(el)
- @$triggers = @$triggers.add $triggers
- @_mutationObserver = new MutationObserver onMutations
- @_mutationObserver.observe @$context[0],
- childList: yes
- subtree: yes
- if @shouldDelegate then @_bindTrigger()
-
- # `_defaultHtml` provides a basic html structure for tip content. It can be
+ # - Initialize context.
+ @_bindContext()
+ # - Initialize triggers. Note the initial processing.
+ @_processTriggers()
+ @_bindTriggers()
+
+ # ### Accessors
+
+ # 𝒇 `_defaultHtml` provides a basic html structure for tip content. It can be
# customized via the `tipTemplate` external option, or by subclasses using
# the `htmlOnRender` hook.
_defaultHtml: ->
@@ -222,10 +217,147 @@ Written with jQuery 1.7.2
containerClass = $.trim [@classNames.tip, @classNames.follow, directionClass].join ' '
html = @tipTemplate containerClass
- # `_saveTriggerContent` comes with a very simple base implementation that's
+ # 𝒇 `_isDirection` is a helper to deduce if `$tip` currently has the given
+ # `directionComponent`. The tip is considered to have the same direction as
+ # the given `$trigger` if it has the classes or if there is no trigger or
+ # saved direction value and the directionComponent is part of
+ # `defaultDirection`. Note that this latter check is placed last for
+ # performance savings.
+ _isDirection: (directionComponent, $trigger) ->
+ @$tip.hasClass(@classNames[directionComponent]) or (
+ (not $trigger? or not $trigger.data(@attr('direction'))) and
+ _.include(@defaultDirection, directionComponent)
+ )
+
+ # 𝒇 `_setState` is a simple setter that returns false if state doesn't change.
+ _setState: (state) ->
+ return no if state is @_state
+ @_state = state
+ @debugLog @_state
+
+ # 𝒇 `_setTip` aliases the conventional `$el` property to `$tip` for clarity.
+ _setTip: ($tip) => @$tip = @$el = $tip
+
+ # 𝒇 `_sizeForTrigger` does a stealth render via `_wrapStealthRender` to find tip
+ # size. It will return saved data if possible before doing a measure. The
+ # measures, used by `_updateDirectionByTrigger`, are stored on the trigger
+ # as namespaced, `width` and `height` jQuery data values. If on,
+ # `contentOnly` will factor in content padding into the size value for the
+ # current size.
+ _sizeForTrigger: ($trigger, contentOnly=no) ->
+ size =
+ width: $trigger.data 'width'
+ height: $trigger.data 'height'
+
+ $content = @selectByClass('content')
+ if not (size.width? and size.height?)
+ $content.text $trigger.data @attr('content')
+ wrapped = @_wrapStealthRender ->
+ $trigger.data 'width', (size.width = @$tip.outerWidth())
+ $trigger.data 'height', (size.height = @$tip.outerHeight())
+ wrapped()
+
+ if contentOnly is yes
+ padding = $content.css('padding').split(' ')
+ [top, right, bottom, left] = (parseInt side, 10 for side in padding)
+ bottom ?= top
+ left ?= right
+ size.width -= left + right
+ size.height -= top + bottom + @selectByClass('stem').height() # TODO: This isn't always true.
+
+ size
+
+ # 𝒇 `_stemSize` does a stealth render via `_wrapStealthRender` to find stem
+ # size. The stem layout styles will add offset to the tip content based on
+ # the tip direction. Knowing the size helps operations like overall tip
+ # positioning.
+ _stemSize: ->
+ key = @attr 'stem-size'
+ size = @$tip.data key
+ return size if size?
+
+ $content = @selectByClass 'content'
+ wrapped = @_wrapStealthRender =>
+ for direction, offset in $content.position()
+ if offset > 0
+ size = Math.abs offset
+ @$tip.data key, size
+ 0
+ return wrapped()
+
+ # ### Appearance
+
+ # 𝒇 `wakeByTrigger` is the main toggler and a `_state` updater. It takes an
+ # `onWake` callback, which is usually to update position. The toggling and
+ # main changes only happen if the delay is passed. It will return a bool for
+ # success.
+ wakeByTrigger: ($trigger, event, onWake) ->
+ # - Store current trigger info.
+ triggerChanged = not $trigger.is @_$currentTrigger
+ if triggerChanged
+ @_inflateByTrigger $trigger
+ @_$currentTrigger = $trigger
+ # - Go directly to the position updating if no toggling is needed.
+ if @_state is 'awake'
+ @_positionToTrigger $trigger, event
+ @onShow triggerChanged, event
+ if onWake? then onWake()
+ @debugLog 'quick update'
+ return yes
+ if event? then @debugLog event.type
+ # - Don't toggle if awake or waking, or if event isn't `truemouseenter`.
+ return no if @_state in ['awake', 'waking']
+ # - Get delay and initial duration.
+ delay = @animations.show.delay
+ duration = @animations.show.duration
+ # - Our `wake` subroutine runs the timed-out logic, which includes the fade
+ # transition. The latter is also affected by `safeToggle`. The `onShow`
+ # and `afterShow` hook methods are also run.
+ wake = =>
+ @_positionToTrigger $trigger, event
+ @onShow triggerChanged, event
+ @$tip.stop().fadeIn duration, =>
+ if triggerChanged
+ onWake() if onWake?
+ if @safeToggle is on then @$tip.siblings(@classNames.tip).fadeOut()
+ @afterShow triggerChanged, event
+ @_setState 'awake'
+ # - Wake up depending on current state. If we are in the middle of
+ # sleeping, stop sleeping by updating `_sleepCountdown` and wake up
+ # sooner.
+ if @_state is 'sleeping'
+ @debugLog 'clear sleep'
+ clearTimeout @_sleepCountdown
+ duration = 0
+ wake()
+ # - Start the normal wakeup and update `_wakeCountdown`.
+ else if event? and event.type is 'truemouseenter'
+ triggerChanged = yes
+ @_setState 'waking'
+ @_wakeCountdown = setTimeout wake, delay
+ yes
+
+ # 𝒇 `sleepByTrigger` is a much simpler toggler compared to its counterpart
+ # `wakeByTrigger`. It also updates `_state` and returns a bool for success.
+ # As long as the tip isn't truly visible, or sleeping is redundant, it bails.
+ sleepByTrigger: ($trigger) ->
+ return no if @_state in ['asleep', 'sleeping']
+ @_setState 'sleeping'
+ clearTimeout @_wakeCountdown
+ @_sleepCountdown = setTimeout =>
+ @onHide()
+ @$tip.stop().fadeOut @animations.hide.duration, =>
+ @_setState 'asleep'
+ @afterHide()
+ , @animations.hide.delay
+ yes
+
+ # ### Content
+
+ # 𝒇 `_saveTriggerContent` comes with a very simple base implementation that
# supports the common `title` and `alt` meta content for an element. Support
# is also provided for the `triggerContent` option. We take that content and
- # store it into a `content` jQuery data value on the trigger.
+ # store it into a namespaced `content` jQuery data value on the trigger.
_saveTriggerContent: ($trigger) ->
content = null
attr = null
@@ -245,25 +377,58 @@ Written with jQuery 1.7.2
if content?
$trigger.data @attr('content'), content
- # `_bindTrigger` links each trigger to the tip for: 1) possible appearance
- # changes during mouseenter, mouseleave (uses special events) and 2)
- # following on mousemove only if `doFollow` is on. Also note for our
- # `onMouseMove` handler, it's throttled by `requestAnimationFrame` when
- # available, otherwise manually at hopefully 60fps. It does direct binding
- # by default, can also do delegation (preferred) if `$trigger` isn't given
- # but `$triggerContext` is.
- _bindTrigger: ($trigger) ->
- $bindTarget = $trigger
- if not $bindTarget?
- if @$context
- $bindTarget = @$context
- selector = ".#{@classNames.trigger}"
- else
- @debugLog 'invalid argument(s)'
- return no
- selector ?= null
- # Base bindings.
- $bindTarget.on [
+ # ### Events
+
+ # 𝒇 `_bind` adds event handlers to `$tip` mostly, so state can be updated such
+ # that the handlers on `_$currentTrigger` make an exception. So that cursor
+ # leaving the trigger for the tip doesn't cause the tip to dismiss.
+ #
+ # Additionally, track viewport `_bounds` at a reasonable rate, so that
+ # `_updateDirectionByTrigger` can work properly.
+ _bind: ->
+ @$tip.on
+ mouseenter: (event) =>
+ @debugLog 'enter tip'
+ if @_$currentTrigger?
+ @_$currentTrigger.data @attr('is-active'), yes
+ @wakeByTrigger @_$currentTrigger
+ mouseleave: (event) =>
+ @debugLog 'leave tip'
+ if @_$currentTrigger?
+ @_$currentTrigger.data @attr('is-active'), no
+ @sleepByTrigger @_$currentTrigger
+ if @autoDirection is on
+ $(window).resize _.debounce @_setBounds, 300
+
+ # 𝒇 `_bindContext` uses MutationObserver. If `doLiveUpdate` is inferred to be
+ # true, process triggers added in the future. Make sure to ignore mutations
+ # related to the tip.
+ _bindContext: ->
+ return false unless window.MutationObserver?
+
+ selector = @$triggers.selector
+ @_mutationObserver = new MutationObserver (mutations) =>
+ for mutation in mutations
+ $target = $ mutation.target
+ continue if $target.hasClass(@classNames.content) # TODO: Limited.
+ if mutation.addedNodes.length
+ $triggers = $(mutation.addedNodes).find('[title],[alt]') # TODO: Limited.
+ @_processTriggers $triggers
+ @$triggers = @$triggers.add $triggers
+ @_mutationObserver.observe @$context[0],
+ childList: yes
+ subtree: yes
+
+ # 𝒇 `_bindTriggers` links each trigger to the tip for:
+ # 1. Possible appearance changes during mouseenter, mouseleave (uses special
+ # events).
+ # 2. Following on mousemove only if `doFollow` is on.
+ #
+ # Also note for our `onMouseMove` handler, it's throttled by `requestAnimationFrame`
+ # when available, otherwise manually at hopefully 60fps.
+ _bindTriggers: ->
+ selector = ".#{@classNames.trigger}"
+ @$context.on [
@evt('truemouseenter')
@evt('truemouseleave')
].join(' '),
@@ -276,7 +441,7 @@ Written with jQuery 1.7.2
when 'truemouseleave' then @sleepByTrigger $(event.target)
else @debugLog 'unknown event type', event.type
event.stopPropagation()
- # Follow binding.
+
if @doFollow is on
if window.requestAnimationFrame?
onMouseMove = (event) =>
@@ -284,132 +449,124 @@ Written with jQuery 1.7.2
@_onTriggerMouseMove event
else
onMouseMove = _.throttle @_onTriggerMouseMove, 16
- $bindTarget.on 'mousemove', selector, onMouseMove
-
- # `_bind` adds event handlers to `$tip`, mostly so state can be updated such
- # that the handlers on `_$currentTrigger` make an exception. The desired
- # behavior is the cursor leaving the trigger for the tip doesn't cause the
- # tip to dismiss.
- _bind: ->
- @$tip
- .on 'mouseenter', (event) =>
- @debugLog 'enter tip'
- if @_$currentTrigger?
- @_$currentTrigger.data @attr('is-active'), yes
- @wakeByTrigger @_$currentTrigger
- .on 'mouseleave', (event) =>
- @debugLog 'leave tip'
- if @_$currentTrigger?
- @_$currentTrigger.data @attr('is-active'), no
- @sleepByTrigger @_$currentTrigger
- # Additionally, track viewport `_bounds` at a reasonable rate, so that
- # `_updateDirectionByTrigger` can work properly.
- if @autoDirection is on
- $(window).resize _.debounce @_setBounds, 300
+ @$context.on 'mousemove', selector, onMouseMove
- # `_render` comes with a base implementation that fills in and attaches
- # `$tip` to the DOM, specifically at the beginning of `$viewport`. It uses
- # the result of `htmlOnRender` and falls back to that of `_defaultHtml`.
- # Render also sets up any animations per the `shouldAnimate` option.
- _render: ->
- return no if @$tip.html().length
- html = @htmlOnRender()
- if not (html? and html.length) then html = @_defaultHtml()
- $tip = $(html).addClass @classNames.follow
- # Animation setup.
- transitionStyle = []
- if @shouldAnimate.resize
- duration = @ms.duration.resize / 1000.0 + 's'
- easing = @easing.resize
- easing ?= @easing.base
- transitionStyle.push "width #{duration} #{easing}", "height #{duration} #{easing}"
- transitionStyle = transitionStyle.join(',')
- # /Animation setup.
- @_setTip $tip
- @selectByClass('content').css 'transition', transitionStyle
- @$tip.prependTo @$viewport
+ # ### Positioning
- # `_inflateByTrigger` will reset and update `$tip` for the given trigger, so
- # that it is ready to present, i.e. it is 'inflated'. Mostly it's just the
- # content element and class list that get updated. If the `resize` animation
- # is desired, we need to also specify the content element's dimensions for
- # respective transitions to take effect.
- _inflateByTrigger: ($trigger) ->
- compoundDirection = if $trigger.data(@attr('direction')) then $trigger.data(@attr('direction')).split(' ') else @defaultDirection
- @debugLog 'update direction class', compoundDirection
- $content = @selectByClass 'content'
- $content.text $trigger.data @attr('content')
- if @shouldAnimate.resize
- contentSize = @sizeForTrigger $trigger, (contentOnly = yes)
- $content
- .width contentSize.width
- .height contentSize.height
- @$tip
- .removeClass [
- @classNames.top
- @classNames.bottom
- @classNames.right
- @classNames.left
- ].join ' '
- .addClass $.trim(
- _.reduce compoundDirection, (classListMemo, directionComponent) =>
- "#{classListMemo} #{@classNames[directionComponent]}"
- , ''
- )
-
- # `_onTriggerMouseMove` is actually the main tip toggling handler. To
+ # 𝒇 `_onTriggerMouseMove` is actually the main tip toggling handler. To
# explain, first we take into account of child elements triggering the mouse
# event by deducing the event's actual `$trigger` element. Then we
# `wakeByTrigger` if needed.
_onTriggerMouseMove: (event) ->
return no if not event.pageX?
- $trigger = if (
- ($trigger = $(event.currentTarget)) and
- $trigger.hasClass @classNames.trigger
- ) then $trigger else $trigger.closest(@classNames.trigger)
+
+ $trigger =
+ if ($trigger = $(event.currentTarget)) and $trigger.hasClass @classNames.trigger then $trigger
+ else $trigger.closest(@classNames.trigger)
return no if not $trigger.length
+
@wakeByTrigger $trigger, event
- # `_positionToTrigger` will properly update the tip offset per
- # `offsetOnTriggerMouseMove` and `isDirection`. Also note that `stemSize`
+ # 𝒇 `_positionToTrigger` will properly update the tip offset per
+ # `offsetOnTriggerMouseMove` and `_isDirection`. Also note that `_stemSize`
# gets factored in.
_positionToTrigger: ($trigger, mouseEvent, cursorHeight=@cursorHeight) ->
return no if not mouseEvent?
+
offset =
top: mouseEvent.pageY
left: mouseEvent.pageX
offset = @offsetOnTriggerMouseMove(mouseEvent, offset, $trigger) or offset
- if @isDirection('top', $trigger)
- offset.top -= @$tip.outerHeight() + @stemSize()
- else if @isDirection('bottom', $trigger)
- offset.top += @stemSize() + cursorHeight
- if @isDirection('left', $trigger)
+
+ if @_isDirection('top', $trigger)
+ offset.top -= @$tip.outerHeight() + @_stemSize()
+ else if @_isDirection('bottom', $trigger)
+ offset.top += @_stemSize() + cursorHeight
+
+ if @_isDirection('left', $trigger)
tipWidth = @$tip.outerWidth()
triggerWidth = $trigger.outerWidth()
offset.left -= tipWidth
- # If direction changed due to tip being wider than trigger.
+ #- If direction changed due to tip being wider than trigger.
if tipWidth > triggerWidth
offset.left += triggerWidth
+
@$tip.css offset
- # `stemSize` does a stealth render via `_wrapStealthRender` to find stem
- # `size. The stem layout styles will add offset to the tip content based on
- # `the tip direction. Knowing the size helps operations like overall tip
- # positioning.
- stemSize: ->
- key = @attr 'stem-size'
- size = @$tip.data key
- return size if size?
+ # 𝒇 `_setBounds` updates `_bounds` per `$viewport`'s inner bounds, and those
+ # measures get used by `_updateDirectionByTrigger`.
+ _setBounds: ->
+ $viewport = if @$viewport.is('body') then $(window) else @$viewport
+ @_bounds =
+ top: $.css @$viewport[0], 'padding-top', yes
+ left: $.css @$viewport[0], 'padding-left', yes
+ bottom: $viewport.innerHeight()
+ right: $viewport.innerWidth()
+
+ # ### Rendering
+
+ # 𝒇 `_inflateByTrigger` will reset and update `$tip` and its content element for the given trigger, so
+ # that it is ready to present, ie. it is 'inflated'. If the `resize` animation
+ # is desired, we need to also specify the content element's dimensions for
+ # respective transitions to take effect.
+ _inflateByTrigger: ($trigger) ->
$content = @selectByClass 'content'
- wrapped = @_wrapStealthRender =>
- for direction, offset in $content.position()
- if offset > 0
- size = Math.abs offset
- @$tip.data key, size
- 0
- return wrapped()
+ $content.text $trigger.data @attr('content')
+
+ #- NOTE: A transition style is in place, so this causes animation.
+ if @animations.resize.enabled
+ contentSize = @_sizeForTrigger $trigger, (contentOnly = yes)
+ $content
+ .width contentSize.width
+ .height contentSize.height
+
+ compoundDirection =
+ if $trigger.data(@attr('direction')) then $trigger.data(@attr('direction')).split(' ')
+ else @defaultDirection
+ @debugLog 'update direction class', compoundDirection
+ @$tip
+ .removeClass _.chain(@classNames).pick('top', 'bottom', 'right', 'left').values().join(' ').value()
+ .addClass $.trim(
+ _.reduce compoundDirection, (classListMemo, directionComponent) =>
+ "#{classListMemo} #{@classNames[directionComponent]}"
+ , ''
+ )
+
+ # 𝒇 `_render` comes with a base implementation that fills in and attaches
+ # `$tip` to the DOM, specifically at the beginning of `$viewport`. It uses
+ # the result of `htmlOnRender` and falls back to that of `_defaultHtml`.
+ # Render also sets up any animations per the `shouldAnimate` option.
+ _render: ->
+ return no if @$tip.html().length
+ html = @htmlOnRender()
+ if not (html? and html.length) then html = @_defaultHtml()
+ $tip = $(html).addClass @classNames.follow
+
+ transitionStyle = []
+ if @animations.resize.enabled
+ duration = @animations.resize.duration / 1000.0 + 's'
+ easing = @animations.resize.easing
+ transitionStyle.push "width #{duration} #{easing}", "height #{duration} #{easing}"
+ transitionStyle = transitionStyle.join(',')
+
+ @_setTip $tip
+ @selectByClass('content').css 'transition', transitionStyle
+ @$tip.prependTo @$viewport
- # `_updateDirectionByTrigger` is the main provider of auto-direction
+ # ### Subroutines
+
+ # 𝒇 `_processTriggers` does just that and sets up content, event, and
+ # positioning aspects.
+ _processTriggers: ($triggers) ->
+ $triggers ?= @$triggers
+ $triggers.each (i, el) =>
+ $trigger = $ el
+ return no unless $trigger.length
+ $trigger.addClass @classNames.trigger
+ @_saveTriggerContent $trigger
+ @_updateDirectionByTrigger $trigger
+
+ # 𝒇 `_updateDirectionByTrigger` is the main provider of auto-direction
# support. Given the `$viewport`'s `_bounds`, it changes to the best
# direction as needed. The current `direction` is stored as jQuery data with
# trigger.
@@ -418,7 +575,7 @@ Written with jQuery 1.7.2
triggerPosition = $trigger.position()
triggerWidth = $trigger.outerWidth()
triggerHeight = $trigger.outerHeight()
- tipSize = @sizeForTrigger $trigger
+ tipSize = @_sizeForTrigger $trigger
newDirection = _.clone @defaultDirection
@debugLog { triggerPosition, triggerWidth, triggerHeight, tipSize }
for component in @defaultDirection
@@ -438,51 +595,7 @@ Written with jQuery 1.7.2
when 'left' then newDirection[1] = 'right'
$trigger.data @attr('direction'), newDirection.join ' '
- # `_setBounds` updates `_bounds` per `$viewport`'s inner bounds, and those
- # measures get used by `_updateDirectionByTrigger`.
- _setBounds: ->
- $viewport = if @$viewport.is('body') then $(window) else @$viewport
- @_bounds =
- top: $.css @$viewport[0], 'padding-top', yes
- left: $.css @$viewport[0], 'padding-left', yes
- bottom: $viewport.innerHeight()
- right: $viewport.innerWidth()
-
- _setState: (state) ->
- return no if state is @_state
- @_state = state
- @debugLog @_state
-
- # `sizeForTrigger` does a stealth render via `_wrapStealthRender` to find tip
- # size. It will return saved data if possible before doing a measure. The
- # measures, used by `_updateDirectionByTrigger`, are stored on the trigger
- # as namespaced, `width` and `height` jQuery data values. If on,
- # `contentOnly` will factor in content padding into the size value for the
- # current size.
- sizeForTrigger: ($trigger, contentOnly=no) ->
- # Short on existing data.
- size =
- width: $trigger.data 'width'
- height: $trigger.data 'height'
- # Get size.
- $content = @selectByClass('content')
- if not (size.width? and size.height?)
- $content.text $trigger.data @attr('content')
- wrapped = @_wrapStealthRender ->
- $trigger.data 'width', (size.width = @$tip.outerWidth())
- $trigger.data 'height', (size.height = @$tip.outerHeight())
- wrapped()
- # Get content size.
- if contentOnly is yes
- padding = $content.css('padding').split(' ')
- [top, right, bottom, left] = (parseInt side, 10 for side in padding)
- bottom ?= top
- left ?= right
- size.width -= left + right
- size.height -= top + bottom + @selectByClass('stem').height() # TODO: This isn't always true.
- size
-
- # `_wrapStealthRender` is a helper mostly for size detection on tips and
+ # 𝒇 `_wrapStealthRender` is a helper mostly for size detection on tips and
# triggers. Without stealth rendering the elements by temporarily un-hiding
# and making invisible, we can't do `getComputedStyle` on them.
_wrapStealthRender: (func) ->
@@ -497,85 +610,10 @@ Written with jQuery 1.7.2
visibility: 'visible'
return result
- # `isDirection` is a helper to deduce if `$tip` currently has the given
- # `directionComponent`. The tip is considered to have the same direction as
- # the given `$trigger` if it has the classes or if there is no trigger or
- # saved direction value and the directionComponent is part of
- # `defaultDirection`. Note that this latter check is placed last for
- # performance savings.
- isDirection: (directionComponent, $trigger) ->
- @$tip.hasClass(@classNames[directionComponent]) or (
- (not $trigger? or not $trigger.data(@attr('direction'))) and
- _.include(@defaultDirection, directionComponent)
- )
-
- # `wakeByTrigger` is the main toggler and a `_state` updater. It takes an
- # `onWake` callback, which is usually to update position. The toggling and
- # main changes only happen if the delay is passed. It will return a bool for
- # success.
- wakeByTrigger: ($trigger, event, onWake) ->
- # Store current trigger info.
- triggerChanged = not $trigger.is @_$currentTrigger
- if triggerChanged
- @_inflateByTrigger $trigger
- @_$currentTrigger = $trigger
- # Go directly to the position updating if no toggling is needed.
- if @_state is 'awake'
- @_positionToTrigger $trigger, event
- @onShow triggerChanged, event
- if onWake? then onWake()
- @debugLog 'quick update'
- return yes
- if event? then @debugLog event.type
- # Don't toggle if awake or waking, or if event isn't `truemouseenter`.
- return no if @_state in ['awake', 'waking']
- delay = @ms.delay.in
- duration = @ms.duration.in
- # Our `wake` subroutine runs the timed-out logic, which includes the fade
- # transition. The latter is also affected by `safeToggle`. The `onShow`
- # and `afterShow` hook methods are also run.
- wake = =>
- @_positionToTrigger $trigger, event
- @onShow triggerChanged, event
- @$tip.stop().fadeIn duration, =>
- if triggerChanged
- onWake() if onWake?
- if @safeToggle is on then @$tip.siblings(@classNames.tip).fadeOut()
- @afterShow triggerChanged, event
- @_setState 'awake'
- # Wake up depending on current state.
- # If we are in the middle of sleeping, stop sleeping by updating
- # `_sleepCountdown` and wake up sooner.
- if @_state is 'sleeping'
- @debugLog 'clear sleep'
- clearTimeout @_sleepCountdown
- duration = 0
- wake()
- # Start the normal wakeup and update `_wakeCountdown`.
- else if event? and event.type is 'truemouseenter'
- triggerChanged = yes
- @_setState 'waking'
- @_wakeCountdown = setTimeout wake, delay
- yes
-
- # `sleepByTrigger` is a much simpler toggler compared to its counterpart
- # `wakeByTrigger`. It also updates `_state` and returns a bool for success.
- # As long as the tip isn't truly visible, sleep is unneeded.
- sleepByTrigger: ($trigger) ->
- # Don't toggle if asleep or sleeping.
- return no if @_state in ['asleep', 'sleeping']
- @_setState 'sleeping'
- clearTimeout @_wakeCountdown
- @_sleepCountdown = setTimeout =>
- @onHide()
- @$tip.stop().fadeOut @ms.duration.out, =>
- @_setState 'asleep'
- @afterHide()
- , @ms.delay.out
- yes
+ # ### Delegation to Subclass
- # These methods are hooks for custom functionality from subclasses. Some are
- # set to no-ops becase they are given no arguments.
+ # These methods are hooks for custom functionality from subclasses. (Some are
+ # set to no-ops becase they are given no arguments.)
onShow: (triggerChanged, event) -> undefined
onHide: $.noop
afterShow: (triggerChanged, event) -> undefined
@@ -583,70 +621,72 @@ Written with jQuery 1.7.2
htmlOnRender: $.noop
offsetOnTriggerMouseMove: (event, offset, $trigger) -> no
- # SnapTip API
- # -----------
+ # SnapTip Implementation
+ # ----------------------
# With such a complete base API, extending it with an implementation with
# snapping becomes almost trivial.
class SnapTip extends Tip
- # Continue setting up `$tip` and other properties.
+ # 𝒇 `init` continues setting up `$tip` and other properties.
init: ->
super()
- # Infer `snap.toTrigger`.
+ # - Infer `snap.toTrigger`.
if @snap.toTrigger is off
@snap.toTrigger = @snap.toXAxis is on or @snap.toYAxis is on
- # `_offsetStart` stores the original offset, which is used for snapping.
+ # - `_offsetStart` stores the original offset, which is used for snapping.
@_offsetStart = null
- # Add snapping config as classes.
+ # - Add snapping config as classes.
@$tip.addClass(@classNames.snap[key]) for own key, active of @snap when active
- # `_moveToTrigger` is the main positioner. The `baseOffset` given is expected
- # to be the trigger offset.
+ # ### Events
+
+ # 𝒇 `_bindTriggers` extend its super to get initial position for snapping.
+ # This is only for snapping without snapping to the trigger, which is only
+ # what's currently supported. See `afterShow` hook.
+ _bindTriggers: ->
+ super()
+ selector = ".#{@classNames.trigger}"
+ #- Modify base binding.
+ @$context.on @evt('truemouseleave'), selector, { selector },
+ (event) => @_offsetStart = null
+
+
+ # ### Positioning
+
+ # 𝒇 `_moveToTrigger` is the main positioner. The `baseOffset` given is
+ # expected to be the trigger offset.
_moveToTrigger: ($trigger, baseOffset) -> # TODO: Still needs to support all the directions.
- #@debugLog baseOffset
+ #- @debugLog baseOffset
offset = $trigger.position()
toTriggerOnly = @snap.toTrigger is on and @snap.toXAxis is off and @snap.toYAxis is off
if @snap.toXAxis is on
- if @isDirection 'bottom', $trigger
+ if @_isDirection 'bottom', $trigger
offset.top += $trigger.outerHeight()
if @snap.toYAxis is off
- # Note arbitrary buffer offset.
+ #- Note arbitrary buffer offset.
offset.left = baseOffset.left - @$tip.outerWidth() / 2
if @snap.toYAxis is on
- if @isDirection 'right', $trigger
+ if @_isDirection 'right', $trigger
offset.left += $trigger.outerWidth()
if @snap.toXAxis is off
offset.top = baseOffset.top - @$tip.outerHeight() / 2
if toTriggerOnly is on
- if @isDirection 'bottom', $trigger
+ if @_isDirection 'bottom', $trigger
offset.top += $trigger.outerHeight()
offset
- # Extend `_bindTrigger` to get initial position for snapping. This is only
- # for snapping without snapping to the trigger, which is only what's
- # currently supported. See `afterShow` hook.
- _bindTrigger: ($trigger) ->
- didBind = super $trigger
- return no if didBind is no
- $bindTarget = $trigger
- if not $bindTarget? and @$context
- $bindTarget = @$context
- selector = ".#{@classNames.trigger}"
- selector ?= null
- # Modify base binding.
- $bindTarget.on @evt('truemouseleave'), selector, { selector },
- (event) => @_offsetStart = null
-
- # Extend `_positionToTrigger` to set `cursorHeight` to 0, since it won't
- # need to be factored in if we're snapping.
+ # 𝒇 `_positionToTrigger` extends its super to set `cursorHeight` to 0, since
+ # it won't need to be factored in if we're snapping.
_positionToTrigger: ($trigger, mouseEvent, cursorHeight=@cursorHeight) ->
super $trigger, mouseEvent, 0
- # `onShow` and `afterShow` are implemented such that they make the tip
- # invisible while it's being positioned and then reveal it.
+ # ### Tip Delegation
+
+ # 𝒇 Implement `onShow` and `afterShow` delegate methods such that they make
+ # the tip invisible while it's being positioned and then reveal it.
onShow: (triggerChanged, event) ->
- if triggerChanged is yes
- @$tip.css 'visibility', 'hidden'
+ @$tip.css 'visibility', 'hidden' if triggerChanged is yes
+
afterShow: (triggerChanged, event) ->
if triggerChanged is yes
@$tip.css 'visibility', 'visible'
@@ -654,13 +694,13 @@ Written with jQuery 1.7.2
top: event.pageY
left: event.pageX
- # `offsetOnTriggerMouseMove` is implemented as the main snapping positioning
- # handler. Instead of returning false, we return our custom, snapping
- # offset, so it gets used in lieu of the base `offset`.
+ # 𝒇 Implement `offsetOnTriggerMouseMove` delegate method as the main snapping
+ # positioning handler. Instead of returning false, we return our custom,
+ # snapping offset, so it gets used in lieu of the base `offset`.
offsetOnTriggerMouseMove: (event, offset, $trigger) ->
- newOffset = _.clone offset
- newOffset = @_moveToTrigger $trigger, newOffset
- newOffset
+ newOffset = @_moveToTrigger $trigger, _.clone(offset)
+
+ # ❧
# Export
# ------
@@ -680,4 +720,6 @@ Written with jQuery 1.7.2
baseMixins: ['selection']
compactOptions: yes
+ yes
+
)
diff --git a/tests/css/base.scss b/tests/css/_base.scss
similarity index 93%
rename from tests/css/base.scss
rename to tests/css/_base.scss
index f26123c..f5c0a73 100644
--- a/tests/css/base.scss
+++ b/tests/css/_base.scss
@@ -1,4 +1,12 @@
-/* This is only styling for visual test pages. */
+/*
+ Base Visual Test
+ ================
+ Provides the styling foundation for visual test pages. Import into a test
+ stylesheet.
+*/
+
+// Variables
+// ---------
$bg-color: #dadad6;
$fill-color: rgba(#fff, 0.95);
@@ -9,6 +17,9 @@ $line-size: 3px;
$radius-size: 10px;
$rem: 14px; // Until we drop IE 8, rem can't replace this.
+// Placeholders
+// ------------
+
%clearfix {
&::before, &::after {
content: " ";
@@ -43,9 +54,9 @@ $rem: 14px; // Until we drop IE 8, rem can't replace this.
margin: { left: -$rem; right: -$rem; }
}
-// Start output:
+// Base CSS Output
+// ---------------
-/* base styling */
html {
font-size: $rem;
}
@@ -138,6 +149,28 @@ section.visual-test { /* main test styles */
}
}
+.edge { /* alternative test container styles */
+ position: absolute;
+ &.affixed { position: fixed; }
+ &.top { top: $rem; }
+ &.right { right: $rem; }
+ &.bottom { bottom: $rem; }
+ &.left { left: $rem; }
+}
+.edge-call {
+ &:not(body) {
+ position: relative;
+ }
+ > .visual-test-fragment {
+ @extend .edge, %panel-skin;
+ border-radius: $radius-size;
+ padding: $rem;
+ }
+}
+
+// Other CSS Output
+// ----------------
+
.box {
@extend %panel-skin;
border-radius: $radius-size;
@@ -220,25 +253,6 @@ section.visual-test { /* main test styles */
}
}
-.edge {
- position: absolute;
- &.affixed { position: fixed; }
- &.top { top: $rem; }
- &.right { right: $rem; }
- &.bottom { bottom: $rem; }
- &.left { left: $rem; }
-}
-.edge-call {
- &:not(body) {
- position: relative;
- }
- > .visual-test-fragment {
- @extend .edge, %panel-skin;
- border-radius: $radius-size;
- padding: $rem;
- }
-}
-
.grid-item {
float: left;
line-height: 0;
diff --git a/tests/css/editable.scss b/tests/css/editable.scss
index ee74b89..492a974 100644
--- a/tests/css/editable.scss
+++ b/tests/css/editable.scss
@@ -1,4 +1,12 @@
-@import "../../dist/jquery.hlf.editable.scss";
+/*
+ HLF Editable Visual Tests
+ =========================
+*/
+
+// See [tests](../../../tests/tip.editable.html)
+
+@import '../../dist/jquery.hlf.editable';
+@import 'base';
#{map-get($editable-inline-selectors, container)} {
@include editable-inline-skin;
diff --git a/tests/css/tip.scss b/tests/css/tip.scss
index e273fe9..dbcaa97 100644
--- a/tests/css/tip.scss
+++ b/tests/css/tip.scss
@@ -1,6 +1,16 @@
-@import "../../dist/jquery.hlf.tip.scss";
+/*
+ HLF Tip Visual Tests
+ ====================
+*/
+// See [tests](../../../tests/tip.visual.html)
+
+@import '../../dist/jquery.hlf.tip';
+@import 'base';
+
+// A basic example of how to use the tip plugin's style-building mixins.
.js-tip {
- @include tip-skin(#666, #fff);
- @include tip-layout;
+ @include tip-base;
+ @include tip-layout($em-size: $rem);
+ @include tip-skin($fill-color: #666, $text-color: #fff);
}
diff --git a/tests/editable.visual.html b/tests/editable.visual.html
index b14d5f5..4c05aa0 100644
--- a/tests/editable.visual.html
+++ b/tests/editable.visual.html
@@ -4,11 +4,10 @@
HLF Editable Test
-
-
+
HLF Editable Test
(see docs for code)
diff --git a/tests/js/base-visual.coffee b/tests/js/base-visual.coffee
index 4cff30e..81ecfa0 100644
--- a/tests/js/base-visual.coffee
+++ b/tests/js/base-visual.coffee
@@ -1,15 +1,17 @@
+###
+Visual Test Helpers
+===================
+This is a developer-level API for writing UI tests. Plugin users need not read
+further.
+###
+
define [
'jquery'
'underscore'
], ($, _) ->
- # Visual Test Helpers
- # -------------------
- # This is a developer-level API for writing UI tests. Plugin users need not
- # read further.
-
# $.visualTest
- # ============
+ # ------------
# The test function generator. Invoke tests on document ready.
# Configuration:
@@ -31,6 +33,7 @@ define [
opts = { template: '<%= html %>' } if config.asFragments
$test = $container.renderVisualTest vars, opts
if config.asFragments
+ $container.addClass config.className if config.className?
$test = $test.addClass 'visual-test-fragment'
.filter '.visual-test-fragment'
# - Run tests.
@@ -72,7 +75,7 @@ define [
"""
# $.loremIpsum
- # ============
+ # ------------
loremIpsum =
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor
diff --git a/tests/js/base.coffee b/tests/js/base.coffee
index 8b0e4b9..550d6a8 100644
--- a/tests/js/base.coffee
+++ b/tests/js/base.coffee
@@ -1,11 +1,15 @@
+###
+Unit Test Helpers
+=================
+This is a developer-level extension for writing unit tests. Plugin users need
+not read further.
+###
+
define [
'jquery'
'underscore'
], ($, _) ->
- # Unit Test Helpers
- # -----------------
-
QUnit.extend QUnit.assert,
hasFunctions: (object, names, message) ->
diff --git a/tests/js/core.coffee b/tests/js/core.coffee
index d56225e..ff482f1 100644
--- a/tests/js/core.coffee
+++ b/tests/js/core.coffee
@@ -1,3 +1,9 @@
+###
+HLF Core Unit Tests
+===================
+Offloads testing larger corer components to other test modules.
+###
+
require.config
baseUrl: '../lib'
paths:
diff --git a/tests/js/core.mixin.coffee b/tests/js/core.mixin.coffee
index e4f0122..63ab78f 100644
--- a/tests/js/core.mixin.coffee
+++ b/tests/js/core.mixin.coffee
@@ -1,3 +1,8 @@
+###
+HLF Core Mixin Unit Tests
+=========================
+###
+
define [
'jquery'
'underscore'
diff --git a/tests/js/core.plugin.coffee b/tests/js/core.plugin.coffee
index 5a175bb..13b24f8 100644
--- a/tests/js/core.plugin.coffee
+++ b/tests/js/core.plugin.coffee
@@ -1,3 +1,8 @@
+###
+HLF Core Plugin Unit Tests
+==========================
+###
+
define [
'jquery'
'underscore'
diff --git a/tests/js/editable.coffee b/tests/js/editable.coffee
index 39c2382..7d890bc 100644
--- a/tests/js/editable.coffee
+++ b/tests/js/editable.coffee
@@ -1,3 +1,10 @@
+###
+HLF Editable Visual Tests
+=========================
+###
+
+# See [tests](../../../tests/editable.visual.html)
+
require.config
baseUrl: '../lib'
paths:
diff --git a/tests/js/tip.coffee b/tests/js/tip.coffee
index 8b13085..d863a65 100644
--- a/tests/js/tip.coffee
+++ b/tests/js/tip.coffee
@@ -1,3 +1,10 @@
+###
+HLF Tip Visual Tests
+====================
+###
+
+# See [tests](../../../tests/tip.visual.html)
+
require.config
baseUrl: '../lib'
paths:
@@ -15,8 +22,6 @@ require [
return false unless shouldRunVisualTests
tests = []
- # See [tests](../../docs/tests/js/tip.html)
-
# Default
# -------
# Basic test with the default settings. Basic tooltips are created and
@@ -75,7 +80,7 @@ require [
"""
test: ($context) ->
- $context.find('[title]').snapTip { snap: { toYAxis: true } }, $context
+ $context.find('[title]').snapTip { snap: { toYAxis: on } }, $context
anchorName: 'snapping-vertically'
className: 'list-call'
@@ -105,7 +110,7 @@ require [
"""
test: ($context) ->
- $context.find('[title]').snapTip { snap: { toXAxis: true } }, $context
+ $context.find('[title]').snapTip { snap: { toXAxis: on } }, $context
anchorName: 'snapping-horizontally'
className: 'bar-call'
@@ -131,7 +136,7 @@ require [
"""
test: ($context) ->
- $context.find('[alt]').snapTip { snap: { toXAxis: true } }, $context
+ $context.find('[alt]').snapTip { snap: { toXAxis: on } }, $context
anchorName: 'a-model-use-case'
className: 'grid-call'
@@ -160,6 +165,7 @@ require [
anchorName: 'corner-cases'
asFragments: yes
+ className: 'edge-call'
vars: _.pick $, 'loremIpsum'
$ -> test() for test in tests
diff --git a/tests/tip.visual.html b/tests/tip.visual.html
index dba507f..067f52a 100644
--- a/tests/tip.visual.html
+++ b/tests/tip.visual.html
@@ -4,11 +4,10 @@
HLF Tip Test
-
-
+