From 339aeb4c0b0a67e75373bcaa4b4fed642535298f Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Mon, 11 May 2015 00:10:02 -0700 Subject: [PATCH 01/25] Add UMD support for existing plugins and update readme. --- README.md | 15 +++++++++------ src/js/jquery.hlf.editable.coffee | 22 ++++++++++++++++------ src/js/jquery.hlf.tip.coffee | 26 ++++++++++++++++++++------ 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index cead2c0..edf8ca2 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ # HLF jQuery Library [![Build Status](https://travis-ci.org/hlfcoding/hlf-jquery.svg?branch=master)](https://travis-ci.org/hlfcoding/hlf-jquery) ![Bower Version](https://img.shields.io/bower/v/hlf-jquery.svg) -jQuery extensions and plugins for quality UI. All modules have scoped debug -flags, jQuery namespaces, and no-conflict support with jQuery. All modules have -AMD-compatible versions, so you can pick and choose what to use. The only other -hard dependency is UnderscoreJS. RequireJS is suggested. Other dependencies -(see Bower file) are for tests and demos. +jQuery extensions and plugins for quality UI and implemented following best +practices. The [annotated source code][] is also available and include +documented examples. -The [annotated source code][] is also available. +All modules have scoped debug flags, jQuery namespaces, and no-conflict support +with jQuery. They are exported using [UMD]() and work with AMD, Browserify, or +plain. Only other dependency is UnderscoreJS. Other Bower dependencies are for +tests and demos. ## Extensions @@ -90,6 +91,7 @@ Start off with `npm install`. [ "dist/*", "docs/*", + "release/*", "tests/*.css", "tests/*.js" ] @@ -122,6 +124,7 @@ The MIT License (MIT) FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +[UMD]: https://github.com/umdjs/umd [annotated source code]: http://hlfcoding.github.io/hlf-jquery/docs/index.html [visual test for Tip]: http://hlfcoding.github.io/hlf-jquery/tests/tip.visual.html [(WIP) visual test for Editable]: http://hlfcoding.github.io/hlf-jquery/tests/editable.visual.html diff --git a/src/js/jquery.hlf.editable.coffee b/src/js/jquery.hlf.editable.coffee index dc65b73..b99401e 100644 --- a/src/js/jquery.hlf.editable.coffee +++ b/src/js/jquery.hlf.editable.coffee @@ -5,17 +5,27 @@ 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 diff --git a/src/js/jquery.hlf.tip.coffee b/src/js/jquery.hlf.tip.coffee index dbb173b..3f86416 100644 --- a/src/js/jquery.hlf.tip.coffee +++ b/src/js/jquery.hlf.tip.coffee @@ -27,17 +27,29 @@ Written with jQuery 1.7.2 # `$('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? +# 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) -> hlf.tip = debug: off @@ -680,4 +692,6 @@ Written with jQuery 1.7.2 baseMixins: ['selection'] compactOptions: yes + yes + ) From efa540056c7d14f8f3e9f94049c3407f34f98e86 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Mon, 11 May 2015 00:14:44 -0700 Subject: [PATCH 02/25] Tip should always use event delegation... Forbid not using it. Make it easier for plugin consumer to fall into 'pit of success.' --- src/js/jquery.hlf.tip.coffee | 43 ++++++++++-------------------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/src/js/jquery.hlf.tip.coffee b/src/js/jquery.hlf.tip.coffee index 3f86416..0d76117 100644 --- a/src/js/jquery.hlf.tip.coffee +++ b/src/js/jquery.hlf.tip.coffee @@ -71,9 +71,6 @@ 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 - # - `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: @@ -197,10 +194,10 @@ Written with jQuery 1.7.2 # Process `$triggers` and setup content, event, and positioning aspects. processTrigger = ($trigger) => return no if not $trigger.length + @_bindTriggers() $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 @@ -220,7 +217,6 @@ Written with jQuery 1.7.2 @_mutationObserver.observe @$context[0], childList: yes subtree: yes - if @shouldDelegate then @_bindTrigger() # `_defaultHtml` provides a basic html structure for tip content. It can be # customized via the `tipTemplate` external option, or by subclasses using @@ -257,25 +253,15 @@ Written with jQuery 1.7.2 if content? $trigger.data @attr('content'), content - # `_bindTrigger` links each trigger to the tip for: 1) possible appearance + # `_bindTriggers` 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 + # available, otherwise manually at hopefully 60fps. + _bindTriggers: -> + selector = ".#{@classNames.trigger}" # Base bindings. - $bindTarget.on [ + @$context.on [ @evt('truemouseenter') @evt('truemouseleave') ].join(' '), @@ -296,7 +282,7 @@ Written with jQuery 1.7.2 @_onTriggerMouseMove event else onMouseMove = _.throttle @_onTriggerMouseMove, 16 - $bindTarget.on 'mousemove', selector, onMouseMove + @$context.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 @@ -634,19 +620,14 @@ Written with jQuery 1.7.2 offset.top += $trigger.outerHeight() offset - # Extend `_bindTrigger` to get initial position for snapping. This is only + # Extend `_bindTriggers` 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 + _bindTriggers: -> + super() + selector = ".#{@classNames.trigger}" # Modify base binding. - $bindTarget.on @evt('truemouseleave'), selector, { selector }, + @$context.on @evt('truemouseleave'), selector, { selector }, (event) => @_offsetStart = null # Extend `_positionToTrigger` to set `cursorHeight` to 0, since it won't From a2d0e5600cf1f792e4b11ac23d0adedf30211f25 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Mon, 11 May 2015 00:58:39 -0700 Subject: [PATCH 03/25] Clean and power up tip's animation options. :zap: --- src/js/jquery.hlf.tip.coffee | 53 +++++++++++++++++------------------- 1 file changed, 25 insertions(+), 28 deletions(-) diff --git a/src/js/jquery.hlf.tip.coffee b/src/js/jquery.hlf.tip.coffee index 0d76117..af42ffe 100644 --- a/src/js/jquery.hlf.tip.coffee +++ b/src/js/jquery.hlf.tip.coffee @@ -71,25 +71,6 @@ 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 - # - `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 # - `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 @@ -121,6 +102,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 @@ -167,6 +162,8 @@ Written with jQuery 1.7.2 # binding event listeners to triggers, which can improve performance and # allow dynamic binding. constructor: (@$triggers, options, @$context) -> + for name, animation of options.animations when name isnt 'base' + _.defaults animation, options.animations.base init: -> # Bind handler methods here after class setup completes. @@ -316,10 +313,9 @@ Written with jQuery 1.7.2 $tip = $(html).addClass @classNames.follow # Animation setup. transitionStyle = [] - if @shouldAnimate.resize - duration = @ms.duration.resize / 1000.0 + 's' - easing = @easing.resize - easing ?= @easing.base + 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(',') # /Animation setup. @@ -337,7 +333,7 @@ Written with jQuery 1.7.2 @debugLog 'update direction class', compoundDirection $content = @selectByClass 'content' $content.text $trigger.data @attr('content') - if @shouldAnimate.resize + if @animations.resize.enabled contentSize = @sizeForTrigger $trigger, (contentOnly = yes) $content .width contentSize.width @@ -527,8 +523,9 @@ Written with jQuery 1.7.2 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 + # 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. @@ -566,10 +563,10 @@ Written with jQuery 1.7.2 clearTimeout @_wakeCountdown @_sleepCountdown = setTimeout => @onHide() - @$tip.stop().fadeOut @ms.duration.out, => + @$tip.stop().fadeOut @animations.hide.duration, => @_setState 'asleep' @afterHide() - , @ms.delay.out + , @animations.hide.delay yes # These methods are hooks for custom functionality from subclasses. Some are From edb1b84975df86dbce7057ea2eaaa18e1c56f595 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Mon, 11 May 2015 01:25:13 -0700 Subject: [PATCH 04/25] Refactor tip initialization into simpler methods. :cake: --- src/js/jquery.hlf.tip.coffee | 51 ++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 22 deletions(-) diff --git a/src/js/jquery.hlf.tip.coffee b/src/js/jquery.hlf.tip.coffee index af42ffe..dfff33a 100644 --- a/src/js/jquery.hlf.tip.coffee +++ b/src/js/jquery.hlf.tip.coffee @@ -188,32 +188,21 @@ Written with jQuery 1.7.2 # Tip instances start off rendered and bound. @_render() @_bind() - # Process `$triggers` and setup content, event, and positioning aspects. - processTrigger = ($trigger) => - return no if not $trigger.length + # Also setup any context bindings. + @_bindContext() + # Do this for initially provided triggers. + @_processTriggers() @_bindTriggers() + + # Process `$triggers` and setup 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 - # 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 # `_defaultHtml` provides a basic html structure for tip content. It can be # customized via the `tipTemplate` external option, or by subclasses using @@ -250,6 +239,24 @@ Written with jQuery 1.7.2 if content? $trigger.data @attr('content'), content + # `_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) and 2) # following on mousemove only if `doFollow` is on. Also note for our From 95e1b34df242b350d85bb2e9d3f513ff17863097 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Mon, 11 May 2015 01:33:54 -0700 Subject: [PATCH 05/25] Minor test url fixup. [ci skip] --- tests/js/tip.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/js/tip.coffee b/tests/js/tip.coffee index 8b13085..b20a98a 100644 --- a/tests/js/tip.coffee +++ b/tests/js/tip.coffee @@ -15,7 +15,7 @@ require [ return false unless shouldRunVisualTests tests = [] - # See [tests](../../docs/tests/js/tip.html) + # See [tests](../../../tests/tip.visual.html) # Default # ------- From 4a816253ea75d624932f33035c1ce51172960d48 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Tue, 12 May 2015 01:43:33 -0700 Subject: [PATCH 06/25] Add UMD support for existing extensions. --- src/js/jquery.extension.hlf.core.coffee | 22 +++++++++++++++------ src/js/jquery.extension.hlf.event.coffee | 25 +++++++++++++++++------- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/src/js/jquery.extension.hlf.core.coffee b/src/js/jquery.extension.hlf.core.coffee index 1e564bf..880ae53 100644 --- a/src/js/jquery.extension.hlf.core.coffee +++ b/src/js/jquery.extension.hlf.core.coffee @@ -17,15 +17,25 @@ Written with jQuery 1.7.2 # 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 +)(@, ($, _) -> hlf = debug: on # Turn this off when going to production. diff --git a/src/js/jquery.extension.hlf.event.coffee b/src/js/jquery.extension.hlf.event.coffee index ce4ec96..a40725b 100644 --- a/src/js/jquery.extension.hlf.event.coffee +++ b/src/js/jquery.extension.hlf.event.coffee @@ -14,17 +14,28 @@ 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) -> + ], 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) -> + # I. Hover-Intent # --------------- From 6b673dabff5f9a0c690d2963e1fb8371dcc71eb7 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Tue, 12 May 2015 01:43:59 -0700 Subject: [PATCH 07/25] Add initial Grunt watch:docs task. --- Gruntfile.coffee | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Gruntfile.coffee b/Gruntfile.coffee index d1fca7f..c23a24c 100644 --- a/Gruntfile.coffee +++ b/Gruntfile.coffee @@ -56,6 +56,9 @@ module.exports = (grunt) -> css: files: '{src,tests}/**/*.scss' tasks: ['copy:dist', 'sass', 'autoprefixer'] + docs: + files: aspects.docs.groc.all.src + tasks: ['docs'] js: files: '{src,tests}/**/*.coffee' tasks: ['coffee'] From a71f6755b1082eba6bc6111542fdef786fa8ca25 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Tue, 12 May 2015 01:45:44 -0700 Subject: [PATCH 08/25] Start editing tip documentation for readability and brevity. --- src/js/jquery.hlf.tip.coffee | 103 +++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 48 deletions(-) diff --git a/src/js/jquery.hlf.tip.coffee b/src/js/jquery.hlf.tip.coffee index dfff33a..5c35417 100644 --- a/src/js/jquery.hlf.tip.coffee +++ b/src/js/jquery.hlf.tip.coffee @@ -5,27 +5,24 @@ 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. +# ❧ + +# 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) -> @@ -52,6 +49,10 @@ Written with jQuery 1.7.2 )(@, ($, _, hlf) -> hlf.tip = + # It takes some boilerplate to write the plugins. To namespace when + # extending any jQuery API, we use custom `toString` helpers. Plugin + # namespaces (under the root`$.hlf`) each have a `debug` flag for granular + # logging. debug: off toString: _.memoize (context) -> switch context @@ -60,13 +61,16 @@ Written with jQuery 1.7.2 when 'class' then 'js-tips' else 'hlf.tip' + # ❧ + # 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. @@ -74,7 +78,8 @@ Written with jQuery 1.7.2 # - `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. @@ -94,6 +99,8 @@ Written with jQuery 1.7.2 """ + # NOTE: The majority of presentation state logic is in the plugin + # stylesheet. We update the presentation state by using `classNames`. # - `classNames.stem` - Empty string to remove the stem. # - `classNames.follow` - Empty string to disable cursor following. classNames: do -> @@ -134,7 +141,7 @@ Written with jQuery 1.7.2 # - `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 - # axis-snapping. On by default. + # axis-snapping. On by default. snap: toTrigger: on toXAxis: off @@ -151,49 +158,49 @@ Written with jQuery 1.7.2 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 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. @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() - # Also setup any context bindings. + # Initialize context. @_bindContext() - # Do this for initially provided triggers. + # Initialize triggers. Note the initial processing. @_processTriggers() @_bindTriggers() + _setTip: ($tip) => @$tip = @$el = $tip + # Process `$triggers` and setup content, event, and positioning aspects. _processTriggers: ($triggers) -> $triggers ?= @$triggers From a076ac93619c9c664e3163d7d2017f3b15b041e0 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Tue, 12 May 2015 11:43:16 -0700 Subject: [PATCH 09/25] Minor CoffeeScript usage cleanup. --- src/js/jquery.hlf.tip.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/jquery.hlf.tip.coffee b/src/js/jquery.hlf.tip.coffee index 5c35417..6cc9d23 100644 --- a/src/js/jquery.hlf.tip.coffee +++ b/src/js/jquery.hlf.tip.coffee @@ -168,7 +168,7 @@ Written with jQuery 1.7.2 # ❦ `constructor` keeps `$triggers` and `$context` as properties. `options` # is further normalized. constructor: (@$triggers, options, @$context) -> - for name, animation of options.animations when name isnt 'base' + for own name, animation of options.animations when name isnt 'base' _.defaults animation, options.animations.base # ❦ `init` offloads non-trivial setup to other subroutines From e064b129d8e81ecac198c29dd9262fc870354567 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Tue, 12 May 2015 13:39:10 -0700 Subject: [PATCH 10/25] Rework tip documentation and organization. Methods are grouped and more obvious. Lessen and make obvious method-specific comments. Simplify some methods syntax-wise where possible. Cleanup some mistakenly 'exposed' API. --- src/js/jquery.hlf.tip.coffee | 640 +++++++++++++++++++---------------- 1 file changed, 339 insertions(+), 301 deletions(-) diff --git a/src/js/jquery.hlf.tip.coffee b/src/js/jquery.hlf.tip.coffee index 6cc9d23..2385e23 100644 --- a/src/js/jquery.hlf.tip.coffee +++ b/src/js/jquery.hlf.tip.coffee @@ -48,33 +48,34 @@ Written with jQuery 1.7.2 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 = - # It takes some boilerplate to write the plugins. To namespace when - # extending any jQuery API, we use custom `toString` helpers. Plugin - # namespaces (under the root`$.hlf`) each have a `debug` flag for granular - # logging. + # To toggle debug logging for all instances of a plguin, 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: $ '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 + # - `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 @@ -84,9 +85,6 @@ Written with jQuery 1.7.2 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) -> @@ -99,8 +97,13 @@ Written with jQuery 1.7.2
""" - # NOTE: The majority of presentation state logic is in the plugin - # stylesheet. We update the presentation state by using `classNames`. + # - `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 -> @@ -135,13 +138,19 @@ 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 - # axis-snapping. On by default. + # 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 toXAxis: off @@ -154,7 +163,6 @@ 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 @@ -165,13 +173,13 @@ Written with jQuery 1.7.2 # Read on to learn about implementation details. class Tip - # ❦ `constructor` keeps `$triggers` and `$context` as properties. `options` + # 𝒇 `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` offloads non-trivial setup to other subroutines init: -> _.bindAll @, '_onTriggerMouseMove', '_setBounds' # Initialize tip element. @@ -199,19 +207,9 @@ Written with jQuery 1.7.2 @_processTriggers() @_bindTriggers() - _setTip: ($tip) => @$tip = @$el = $tip - - # Process `$triggers` and setup 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 + # ### Accessors - # `_defaultHtml` provides a basic html structure for tip content. It can be + # 𝒇 `_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: -> @@ -223,10 +221,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 @@ -246,11 +381,35 @@ Written with jQuery 1.7.2 if content? $trigger.data @attr('content'), content - # `_bindContext` uses MutationObserver. If `doLiveUpdate` is inferred to be + # ### 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 @@ -264,14 +423,15 @@ Written with jQuery 1.7.2 childList: yes subtree: yes - # `_bindTriggers` 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. + # 𝒇 `_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}" - # Base bindings. @$context.on [ @evt('truemouseenter') @evt('truemouseleave') @@ -285,7 +445,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) => @@ -295,129 +455,122 @@ Written with jQuery 1.7.2 onMouseMove = _.throttle @_onTriggerMouseMove, 16 @$context.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 - - # `_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 @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(',') - # /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 @animations.resize.enabled - 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(' ') + .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 + + # ### 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 + # 𝒇 `_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. @@ -426,7 +579,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 @@ -446,51 +599,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) -> @@ -505,86 +614,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) - ) + # ### Delegation to Subclass - # `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, 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 @animations.hide.duration, => - @_setState 'asleep' - @afterHide() - , @animations.hide.delay - yes - - # 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 @@ -592,65 +625,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 `_bindTriggers` 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 - - # 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' @@ -658,13 +698,11 @@ 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 # ------ From 6d7ee546dce9fc2612e00473db4d75dd6664c77f Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Tue, 12 May 2015 20:05:50 -0700 Subject: [PATCH 11/25] Fixup e064b12, Tip documentation. --- src/js/jquery.hlf.tip.coffee | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/js/jquery.hlf.tip.coffee b/src/js/jquery.hlf.tip.coffee index 2385e23..8a84c0b 100644 --- a/src/js/jquery.hlf.tip.coffee +++ b/src/js/jquery.hlf.tip.coffee @@ -179,18 +179,18 @@ Written with jQuery 1.7.2 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` offloads non-trivial setup to other subroutines. init: -> _.bindAll @, '_onTriggerMouseMove', '_setBounds' - # Initialize tip element. + # - 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 '' - # Initialize state, which is either: `awake`, `asleep`, `waking`, + # - 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 + # - 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 @@ -198,12 +198,12 @@ Written with jQuery 1.7.2 @_wakeCountdown = null @_sleepCountdown = null @_$currentTrigger = null - # Initialize tip. Note the initial render. + # - Initialize tip. Note the initial render. @_render() @_bind() - # Initialize context. + # - Initialize context. @_bindContext() - # Initialize triggers. Note the initial processing. + # - Initialize triggers. Note the initial processing. @_processTriggers() @_bindTriggers() From 08beaa2681eb689e04949cd32ecf29486acdf318 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Tue, 12 May 2015 20:23:51 -0700 Subject: [PATCH 12/25] Rework readme wording and order to be shorter and clearer. --- README.md | 69 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index edf8ca2..70dfb5b 100644 --- a/README.md +++ b/README.md @@ -9,59 +9,56 @@ with jQuery. They are exported using [UMD]() and work with AMD, Browserify, or plain. Only other dependency is UnderscoreJS. Other Bower dependencies are for tests and demos. -## Extensions +## Plugins -All extensions should have test pages. +All plugins should have test pages with documented source. Please use them as +usage examples. Plugins should also have sample styles, and some have required +styles. When possible, styles are made customizeable as SCSS mixins. -### Core +### [HLF Tip][] -Main features: +Main features summary: -- Generate jQuery plugin methods from plugin definitions. -- Helpers to create mixins that can be used for plugin API. -- Provide no-conflict support. +- Based on hover 'intent' and prevents redundant toggling or DOM thrashing. +- Re-use the same tip for a group of triggers to keep DOM light. +- Aware of available space surrounding to triggering element. +- Has an extended, 'snapping' version that only follows the mouse on one axis. + The tip will snaps to the trigger's edge on the other. -### Event +Short examples: -Main features: +```javascript +$('.article-1').find('a[title]').tip(); // Tips will follow cursor. +$('.article-2').find('a[title]').snapTip({ snap: { toXAxis: true } }); // Tips will only follow along x axis. +``` -- Hover-intent provides rate-limited versions of mouseenter and mouseleave - events through a customizable delay. +See [Tip's visual tests][] for more examples. -## Plugins - -All plugins should have test pages and sample styles. Some plugins may have -required styles. When possible, styles are made customizeable as SCSS mixins. +## Extensions -### HLF Tip +All extensions should be covered by QUnit tests. -A [visual test for Tip][] is available. +### [HLF Core][] Main features: -- Uses custom hover intent events that allow for custom delays. -- Re-use the same tip for a group of triggers. -- Has a snapping extension that allows snapping to the trigger or tracking in - either direction. +- Generate jQuery plugin methods from plugin definitions. +- Helpers to create mixins that can be used for plugin API. +- Provide no-conflict support. -Additional features: +See [Core's unit tests][] for examples. -- Sample styling that draws tip stems with CSS borders. -- Detailed API. +### HLF Event -Example: +Main features: -```javascript -$('.article-1').find('a[title]').tip(); // Tips will follow cursor. -$('.article-2').find('a[title]').snapTip(); // Tips will remain affixed. -``` +- Hover-intent provides rate-limited versions of mouseenter and mouseleave + events through a customizable delay. -See visual tests for more examples. +## Plugins Coming Soon ### HLF Editable -A [(WIP) visual test for Editable][] is available. - Main features: - Uses mixins for encapsulate editing behaviors, so plugin instances can be @@ -126,5 +123,9 @@ The MIT License (MIT) [UMD]: https://github.com/umdjs/umd [annotated source code]: http://hlfcoding.github.io/hlf-jquery/docs/index.html -[visual test for Tip]: http://hlfcoding.github.io/hlf-jquery/tests/tip.visual.html -[(WIP) visual test for Editable]: http://hlfcoding.github.io/hlf-jquery/tests/editable.visual.html +[HLF Tip]: http://hlfcoding.github.io/hlf-jquery/docs/src/js/jquery.hlf.tip.html +[Tip's visual tests]: http://hlfcoding.github.io/hlf-jquery/tests/tip.visual.html +[HLF Core]: http://hlfcoding.github.io/hlf-jquery/docs/src/js/jquery.extension.hlf.core.html +[Core's unit tests]: http://localhost/hlf-jquery/tests/core.unit.html +[HLF Editable]: http://hlfcoding.github.io/hlf-jquery/docs/src/js/jquery.hlf.editable.html +[Editable's visual tests]: http://hlfcoding.github.io/hlf-jquery/tests/editable.visual.html From e5a1ef8c8c2fa40a5a35f851c7ec15dd467a99cc Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Tue, 12 May 2015 20:24:06 -0700 Subject: [PATCH 13/25] Minor CoffeeScript usage cleanup. --- tests/js/tip.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/js/tip.coffee b/tests/js/tip.coffee index b20a98a..e63a4f6 100644 --- a/tests/js/tip.coffee +++ b/tests/js/tip.coffee @@ -75,7 +75,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 +105,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 +131,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' From ddf1cb337f77b01a3c6eb0de16b618f9cd9802ed Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Tue, 12 May 2015 23:25:12 -0700 Subject: [PATCH 14/25] Fixup e064b12, Underscore logic error in refactor. :sweat: --- src/js/jquery.hlf.tip.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/jquery.hlf.tip.coffee b/src/js/jquery.hlf.tip.coffee index 8a84c0b..c9a2dd1 100644 --- a/src/js/jquery.hlf.tip.coffee +++ b/src/js/jquery.hlf.tip.coffee @@ -529,7 +529,7 @@ Written with jQuery 1.7.2 else @defaultDirection @debugLog 'update direction class', compoundDirection @$tip - .removeClass _.chain(@classNames).pick('top', 'bottom', 'right', 'left').values().join(' ') + .removeClass _.chain(@classNames).pick('top', 'bottom', 'right', 'left').values().join(' ').value() .addClass $.trim( _.reduce compoundDirection, (classListMemo, directionComponent) => "#{classListMemo} #{@classNames[directionComponent]}" From 38f7cc895b6d4574e2d4f9ec60d13dc71506c3ea Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Tue, 12 May 2015 22:20:09 -0700 Subject: [PATCH 15/25] Fixup 08beaa2, readme grammar, code sample. --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 70dfb5b..48c2e21 100644 --- a/README.md +++ b/README.md @@ -21,15 +21,18 @@ Main features summary: - Based on hover 'intent' and prevents redundant toggling or DOM thrashing. - Re-use the same tip for a group of triggers to keep DOM light. -- Aware of available space surrounding to triggering element. +- Aware of available space surrounding the triggering element. - Has an extended, 'snapping' version that only follows the mouse on one axis. - The tip will snaps to the trigger's edge on the other. + The tip snaps to the trigger's edge on the other axis. Short examples: -```javascript -$('.article-1').find('a[title]').tip(); // Tips will follow cursor. -$('.article-2').find('a[title]').snapTip({ snap: { toXAxis: true } }); // Tips will only follow along x axis. +```js +$('.avatars').find('img[alt]').tip(); // Tips will follow cursor. +$('nav.bar').find('a[title]').snapTip({ + snap: { toXAxis: true } // Tips will only follow along x axis. +}); +$('article').find('a[title]').snapTip() // Tip will not follow. ``` See [Tip's visual tests][] for more examples. From 548650e9922914883a7d2e2053ab41684ec0f1f2 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Tue, 12 May 2015 23:16:00 -0700 Subject: [PATCH 16/25] Refactor tip scss and Sass usage. --- src/css/jquery.hlf.tip.scss | 167 ++++++++++++++---------------------- tests/css/tip.scss | 3 +- 2 files changed, 68 insertions(+), 102 deletions(-) diff --git a/src/css/jquery.hlf.tip.scss b/src/css/jquery.hlf.tip.scss index 985e9e6..405e73c 100644 --- a/src/css/jquery.hlf.tip.scss +++ b/src/css/jquery.hlf.tip.scss @@ -1,134 +1,99 @@ -@mixin tip-skin ($fill-color, $text-color, $stem-color:null) +$_side-snap-side-map: (right: y, left: y, bottom: x, top: x); +$_side-inverse-map: (right: left, left: right, bottom: top, top: bottom); + +@mixin tip-base { + display: none; + position: absolute; + > .js-tip-inner { + position: relative; + } + .js-tip-stem { + border: 0 solid transparent; width: 0; height: 0; // Initial values. + background: none; + position: absolute; + } + .js-tip-content { + overflow: hidden; + position: relative; + } +} + +@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); } -} - -@mixin _stem-skin ($side, $stem-color) { - border-color: transparent; // Reset. - border-#{$side}-color: $stem-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; + } + }}} + } } // 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) +@mixin tip-layout($baseline: 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) { $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; + $content-padding: $content-padding + ($em * $baseline); + $i: $i + 1; @if $i < length($content-padding-em) { - $content-padding: $content-padding+' '; + $content-padding: $content-padding + ' '; } } + z-index: $z-index; .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; + @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; + }}} } -} - -@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; } - } - } -} - -// Base -// ---- - -.js-tip { - display: none; - position: absolute; - z-index: 9999; - > .js-tip-inner { - position: relative; - } -} -.js-tip-inner { - > .js-tip-stem { + &.js-snap-tip-x-side .js-tip-stem { border: { - width: 0; - style: solid; - color: transparent; + color: transparent; // Partial reset. + width: 0 ($stem-wide-base / 2); } - background: none; - position: absolute; - width: 0; - height: 0; - } - > .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; - } - .js-tip-left.js-snap-tip-y-side &, // Reapply. - .js-tip-left & { - right: 0; - } - // 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-top.js-snap-tip-x-side &, // Reapply. - .js-tip-top & { - bottom: 0; - } - .js-snap-tip-x-side & { left: 50%; - border-color: transparent; // Partial reset. + margin-left: -$stem-wide-base / 2; // Center. } - .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; // Center. top: 50%; - border-color: transparent; // Partial reset. } } diff --git a/tests/css/tip.scss b/tests/css/tip.scss index e273fe9..6034500 100644 --- a/tests/css/tip.scss +++ b/tests/css/tip.scss @@ -1,6 +1,7 @@ @import "../../dist/jquery.hlf.tip.scss"; .js-tip { - @include tip-skin(#666, #fff); + @include tip-base; @include tip-layout; + @include tip-skin(#666, #fff); } From e128a18c475adad4fabbc77cd59563c603e921c8 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Wed, 13 May 2015 00:19:43 -0700 Subject: [PATCH 17/25] Further reduce visual test html boilerplate. --- tests/css/{base.scss => _base.scss} | 0 tests/css/editable.scss | 3 ++- tests/css/tip.scss | 3 ++- tests/editable.visual.html | 3 +-- tests/js/base-visual.coffee | 1 + tests/js/tip.coffee | 1 + tests/tip.visual.html | 3 +-- 7 files changed, 8 insertions(+), 6 deletions(-) rename tests/css/{base.scss => _base.scss} (100%) diff --git a/tests/css/base.scss b/tests/css/_base.scss similarity index 100% rename from tests/css/base.scss rename to tests/css/_base.scss diff --git a/tests/css/editable.scss b/tests/css/editable.scss index ee74b89..12fbd5d 100644 --- a/tests/css/editable.scss +++ b/tests/css/editable.scss @@ -1,4 +1,5 @@ -@import "../../dist/jquery.hlf.editable.scss"; +@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 6034500..e62f5b7 100644 --- a/tests/css/tip.scss +++ b/tests/css/tip.scss @@ -1,4 +1,5 @@ -@import "../../dist/jquery.hlf.tip.scss"; +@import '../../dist/jquery.hlf.tip'; +@import 'base'; .js-tip { @include tip-base; 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..bda8c0f 100644 --- a/tests/js/base-visual.coffee +++ b/tests/js/base-visual.coffee @@ -31,6 +31,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. diff --git a/tests/js/tip.coffee b/tests/js/tip.coffee index e63a4f6..c4fdc6b 100644 --- a/tests/js/tip.coffee +++ b/tests/js/tip.coffee @@ -160,6 +160,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 - - +

HLF Tip Test

(see docs for code) From ffd118937c700cbb0df0b9d70861841fa26df335 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Wed, 13 May 2015 00:50:06 -0700 Subject: [PATCH 18/25] Minor style corrections for tip visual test. --- tests/css/tip.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/css/tip.scss b/tests/css/tip.scss index e62f5b7..94f207e 100644 --- a/tests/css/tip.scss +++ b/tests/css/tip.scss @@ -3,6 +3,6 @@ .js-tip { @include tip-base; - @include tip-layout; - @include tip-skin(#666, #fff); + @include tip-layout($em-size: $rem); + @include tip-skin($fill-color: #666, $text-color: #fff); } From 3ae5cf14a7eaedc192fb2cf168b31429a7c72c62 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Wed, 13 May 2015 01:33:21 -0700 Subject: [PATCH 19/25] Add initial documentation for tip style builder. --- src/css/jquery.hlf.tip.scss | 102 +++++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 31 deletions(-) diff --git a/src/css/jquery.hlf.tip.scss b/src/css/jquery.hlf.tip.scss index 405e73c..c4f6a23 100644 --- a/src/css/jquery.hlf.tip.scss +++ b/src/css/jquery.hlf.tip.scss @@ -1,6 +1,39 @@ +/* +HLF Tip jQuery Plugin +===================== +There should be nothing in this stylesheet because it's only mixins. +*/ + +// 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. + +// Internal +// -------- + +// 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); +// ❧ + +// Mixins +// ------ + +// 𝒇 `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; @@ -18,43 +51,27 @@ $_side-inverse-map: (right: left, left: right, bottom: top, top: bottom); } } -@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; - } - }}} - } -} - -// Include in root. -@mixin tip-layout($baseline: 12px, +// 𝒇 `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) { - $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); - $content-padding: ''; - $i: 0; + // - 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 * $baseline); - $i: $i + 1; - @if $i < length($content-padding-em) { - $content-padding: $content-padding + ' '; - } + $content-padding: $content-padding ($em * $em-size); } + // - Then for some basic layout styles. z-index: $z-index; .js-tip-inner { max-width: $max-width; @@ -62,6 +79,8 @@ $_side-inverse-map: (right: left, left: right, bottom: top, top: bottom); .js-tip-content { padding: #{$content-padding}; } + // - 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; @@ -80,20 +99,41 @@ $_side-inverse-map: (right: left, left: right, bottom: top, top: bottom); #{map-get($_side-inverse-map, $side)}: 0; }}} } + // - 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%; - margin-left: -$stem-wide-base / 2; // Center. + margin-left: -$stem-wide-base / 2; } &.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; // Center. + margin-top: -$stem-wide-base / 2; top: 50%; } } + +// 𝒇 `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; + } + }}} + } +} From 7ae97421e14a39d8e41ef186edf056630c0f8975 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Wed, 13 May 2015 01:51:26 -0700 Subject: [PATCH 20/25] Remove outdated or redundant info from js header docs. --- src/js/jquery.extension.hlf.core.coffee | 2 -- src/js/jquery.extension.hlf.event.coffee | 2 -- src/js/jquery.hlf.editable.coffee | 2 -- src/js/jquery.hlf.tip.coffee | 4 ---- 4 files changed, 10 deletions(-) diff --git a/src/js/jquery.extension.hlf.core.coffee b/src/js/jquery.extension.hlf.core.coffee index 880ae53..7760f82 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. diff --git a/src/js/jquery.extension.hlf.event.coffee b/src/js/jquery.extension.hlf.event.coffee index a40725b..8ee6713 100644 --- a/src/js/jquery.extension.hlf.event.coffee +++ b/src/js/jquery.extension.hlf.event.coffee @@ -1,8 +1,6 @@ ### HLF Event jQuery Extension ========================== -Released under the MIT License -Written with jQuery 1.7.2 ### #!For working docs generation, disable automatic trailing whitespace trimming. diff --git a/src/js/jquery.hlf.editable.coffee b/src/js/jquery.hlf.editable.coffee index b99401e..1a9d63b 100644 --- a/src/js/jquery.hlf.editable.coffee +++ b/src/js/jquery.hlf.editable.coffee @@ -1,8 +1,6 @@ ### HLF Editable jQuery Plugin ========================== -Released under the MIT License -Written with jQuery 1.7.2 ### # Export. Support AMD, CommonJS (Browserify), and browser globals. diff --git a/src/js/jquery.hlf.tip.coffee b/src/js/jquery.hlf.tip.coffee index c9a2dd1..eb01c68 100644 --- a/src/js/jquery.hlf.tip.coffee +++ b/src/js/jquery.hlf.tip.coffee @@ -1,12 +1,8 @@ ### HLF Tip jQuery Plugin ===================== -Released under the MIT License -Written with jQuery 1.7.2 ### -# ❧ - # 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 From 3e1b19d462b59823c378b02b19a0b836f8cd6caa Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Wed, 13 May 2015 02:10:03 -0700 Subject: [PATCH 21/25] Minor code cleanup and documentation. --- src/css/jquery.hlf.editable.scss | 14 +++++---- tests/css/_base.scss | 51 +++++++++++++++++++------------- tests/css/editable.scss | 3 ++ tests/css/tip.scss | 4 +++ tests/js/base-visual.coffee | 4 +-- tests/js/editable.coffee | 3 ++ tests/js/tip.coffee | 5 ++-- 7 files changed, 53 insertions(+), 31 deletions(-) diff --git a/src/css/jquery.hlf.editable.scss b/src/css/jquery.hlf.editable.scss index 779444e..6a219f7 100644 --- a/src/css/jquery.hlf.editable.scss +++ b/src/css/jquery.hlf.editable.scss @@ -6,6 +6,7 @@ outline-style: none; } } + %spacing-none { margin: 0; padding: 0; @@ -16,10 +17,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 +40,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/tests/css/_base.scss b/tests/css/_base.scss index f26123c..b3f07ca 100644 --- a/tests/css/_base.scss +++ b/tests/css/_base.scss @@ -1,5 +1,8 @@ /* This is only styling for visual test pages. */ +// Variables +// --------- + $bg-color: #dadad6; $fill-color: rgba(#fff, 0.95); $line-color: rgba(#000, 0.1); @@ -9,6 +12,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 +49,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 +144,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 +248,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 12fbd5d..d56bb6b 100644 --- a/tests/css/editable.scss +++ b/tests/css/editable.scss @@ -1,3 +1,6 @@ + +// See [tests](../../../tests/tip.editable.html) + @import '../../dist/jquery.hlf.editable'; @import 'base'; diff --git a/tests/css/tip.scss b/tests/css/tip.scss index 94f207e..f5fb79d 100644 --- a/tests/css/tip.scss +++ b/tests/css/tip.scss @@ -1,6 +1,10 @@ + +// 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-base; @include tip-layout($em-size: $rem); diff --git a/tests/js/base-visual.coffee b/tests/js/base-visual.coffee index bda8c0f..72b732f 100644 --- a/tests/js/base-visual.coffee +++ b/tests/js/base-visual.coffee @@ -9,7 +9,7 @@ define [ # read further. # $.visualTest - # ============ + # ------------ # The test function generator. Invoke tests on document ready. # Configuration: @@ -73,7 +73,7 @@ define [ """ # $.loremIpsum - # ============ + # ------------ loremIpsum = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor diff --git a/tests/js/editable.coffee b/tests/js/editable.coffee index 39c2382..1e3e011 100644 --- a/tests/js/editable.coffee +++ b/tests/js/editable.coffee @@ -1,3 +1,6 @@ + +# See [tests](../../../tests/editable.visual.html) + require.config baseUrl: '../lib' paths: diff --git a/tests/js/tip.coffee b/tests/js/tip.coffee index c4fdc6b..9ca2bfa 100644 --- a/tests/js/tip.coffee +++ b/tests/js/tip.coffee @@ -1,3 +1,6 @@ + +# See [tests](../../../tests/tip.visual.html) + require.config baseUrl: '../lib' paths: @@ -15,8 +18,6 @@ require [ return false unless shouldRunVisualTests tests = [] - # See [tests](../../../tests/tip.visual.html) - # Default # ------- # Basic test with the default settings. Basic tooltips are created and From c39f7e418d5d4a563c35334377d427cf389b076f Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Wed, 13 May 2015 02:10:55 -0700 Subject: [PATCH 22/25] All items in groc doc browser's ToC should be titled. --- src/css/jquery.hlf.editable.scss | 5 +++++ src/css/jquery.hlf.tip.scss | 6 +++--- tests/css/_base.scss | 7 ++++++- tests/css/editable.scss | 4 ++++ tests/css/tip.scss | 4 ++++ tests/js/base-visual.coffee | 12 +++++++----- tests/js/base.coffee | 10 +++++++--- tests/js/core.coffee | 6 ++++++ tests/js/core.mixin.coffee | 5 +++++ tests/js/core.plugin.coffee | 5 +++++ tests/js/editable.coffee | 4 ++++ tests/js/tip.coffee | 4 ++++ 12 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/css/jquery.hlf.editable.scss b/src/css/jquery.hlf.editable.scss index 6a219f7..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; diff --git a/src/css/jquery.hlf.tip.scss b/src/css/jquery.hlf.tip.scss index c4f6a23..1e12579 100644 --- a/src/css/jquery.hlf.tip.scss +++ b/src/css/jquery.hlf.tip.scss @@ -1,7 +1,7 @@ /* -HLF Tip jQuery Plugin -===================== -There should be nothing in this stylesheet because it's only mixins. + HLF Tip jQuery Plugin + ===================== + There should be no css output from this file. */ // The main responsibility for the required plugin styling is to provide diff --git a/tests/css/_base.scss b/tests/css/_base.scss index b3f07ca..f5c0a73 100644 --- a/tests/css/_base.scss +++ b/tests/css/_base.scss @@ -1,4 +1,9 @@ -/* 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 // --------- diff --git a/tests/css/editable.scss b/tests/css/editable.scss index d56bb6b..492a974 100644 --- a/tests/css/editable.scss +++ b/tests/css/editable.scss @@ -1,3 +1,7 @@ +/* + HLF Editable Visual Tests + ========================= +*/ // See [tests](../../../tests/tip.editable.html) diff --git a/tests/css/tip.scss b/tests/css/tip.scss index f5fb79d..dbcaa97 100644 --- a/tests/css/tip.scss +++ b/tests/css/tip.scss @@ -1,3 +1,7 @@ +/* + HLF Tip Visual Tests + ==================== +*/ // See [tests](../../../tests/tip.visual.html) diff --git a/tests/js/base-visual.coffee b/tests/js/base-visual.coffee index 72b732f..81ecfa0 100644 --- a/tests/js/base-visual.coffee +++ b/tests/js/base-visual.coffee @@ -1,13 +1,15 @@ +### +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 # ------------ 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 1e3e011..7d890bc 100644 --- a/tests/js/editable.coffee +++ b/tests/js/editable.coffee @@ -1,3 +1,7 @@ +### +HLF Editable Visual Tests +========================= +### # See [tests](../../../tests/editable.visual.html) diff --git a/tests/js/tip.coffee b/tests/js/tip.coffee index 9ca2bfa..d863a65 100644 --- a/tests/js/tip.coffee +++ b/tests/js/tip.coffee @@ -1,3 +1,7 @@ +### +HLF Tip Visual Tests +==================== +### # See [tests](../../../tests/tip.visual.html) From e753bd6a97f2dca8e6ac84016a6e36585b08dd39 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Wed, 13 May 2015 03:15:34 -0700 Subject: [PATCH 23/25] Minor plugin documentation fixups. --- src/js/jquery.hlf.editable.coffee | 2 ++ src/js/jquery.hlf.tip.coffee | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/js/jquery.hlf.editable.coffee b/src/js/jquery.hlf.editable.coffee index 1a9d63b..a968a42 100644 --- a/src/js/jquery.hlf.editable.coffee +++ b/src/js/jquery.hlf.editable.coffee @@ -257,6 +257,8 @@ HLF Editable jQuery Plugin bindFileUploader: -> + # ❧ + # Export # ------ diff --git a/src/js/jquery.hlf.tip.coffee b/src/js/jquery.hlf.tip.coffee index eb01c68..abc34ea 100644 --- a/src/js/jquery.hlf.tip.coffee +++ b/src/js/jquery.hlf.tip.coffee @@ -47,7 +47,7 @@ HLF Tip jQuery Plugin # 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 plguin, use the `debug` flag. + # 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) -> @@ -700,6 +700,8 @@ HLF Tip jQuery Plugin offsetOnTriggerMouseMove: (event, offset, $trigger) -> newOffset = @_moveToTrigger $trigger, _.clone(offset) + # ❧ + # Export # ------ # Both are exported with the `asSharedInstance` flag set to true. From 26196fdfaedbe7d8b899cb9f88e0618e3c99671f Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Wed, 13 May 2015 03:16:07 -0700 Subject: [PATCH 24/25] Clean up and reorganize extension documentation. --- build/docs.coffee | 2 + src/js/jquery.extension.hlf.core.coffee | 349 ++++++++++++----------- src/js/jquery.extension.hlf.event.coffee | 37 +-- 3 files changed, 206 insertions(+), 182 deletions(-) diff --git a/build/docs.coffee b/build/docs.coffee index a040be9..12e746c 100644 --- a/build/docs.coffee +++ b/build/docs.coffee @@ -1,5 +1,7 @@ grunt = require 'grunt' +# For working docs generation, disable automatic trailing whitespace trimming. + module.exports = clean: [ diff --git a/src/js/jquery.extension.hlf.core.coffee b/src/js/jquery.extension.hlf.core.coffee index 7760f82..58f0307 100644 --- a/src/js/jquery.extension.hlf.core.coffee +++ b/src/js/jquery.extension.hlf.core.coffee @@ -10,7 +10,8 @@ HLF Core jQuery Extension # - 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. @@ -35,25 +36,40 @@ HLF Core jQuery Extension 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 # -------------- @@ -66,36 +82,128 @@ HLF Core jQuery Extension # 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? @@ -107,33 +215,33 @@ HLF Core jQuery Extension .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. @@ -153,97 +261,7 @@ HLF Core jQuery Extension 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 # ------------- @@ -254,38 +272,38 @@ HLF Core jQuery Extension # 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'. @@ -299,23 +317,23 @@ HLF Core jQuery Extension # 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 @@ -327,10 +345,10 @@ HLF Core jQuery Extension (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? @@ -341,9 +359,9 @@ HLF Core jQuery Extension 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 @@ -354,20 +372,23 @@ HLF Core jQuery Extension 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 8ee6713..0742cb0 100644 --- a/src/js/jquery.extension.hlf.event.coffee +++ b/src/js/jquery.extension.hlf.event.coffee @@ -3,8 +3,6 @@ HLF Event jQuery Extension ========================== ### -#!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: @@ -12,6 +10,8 @@ HLF Event jQuery Extension # 2. Private functions to implement certain behaviors. # 3. Adapting the behaviors to custom events. +# ❧ + # Export. Support AMD, CommonJS (Browserify), and browser globals. ((root, factory) -> if typeof define is 'function' and define.amd? @@ -34,9 +34,10 @@ HLF Event jQuery Extension factory jQuery, _, jQuery.hlf )(@, ($, _, hlf) -> + # ❧ - # I. Hover-Intent - # --------------- + # 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- @@ -46,9 +47,9 @@ HLF Event jQuery Extension # `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 @@ -56,11 +57,11 @@ HLF Event jQuery Extension 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' @@ -69,11 +70,11 @@ HLF Event jQuery Extension # 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... @@ -92,7 +93,7 @@ HLF Event jQuery Extension 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) -> @@ -102,7 +103,7 @@ HLF Event jQuery Extension 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 = $ @ @@ -111,7 +112,7 @@ HLF Event jQuery Extension 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? @@ -124,7 +125,7 @@ HLF Event jQuery Extension $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) -> @@ -138,7 +139,7 @@ HLF Event jQuery Extension 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 @@ -156,13 +157,13 @@ HLF Event jQuery Extension $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 From 1e10dc4c87726143b361623400db7cf9ba0a1899 Mon Sep 17 00:00:00 2001 From: Peng Wang Date: Wed, 13 May 2015 03:25:14 -0700 Subject: [PATCH 25/25] Build for release. :ship: [ci skip] --- release/jquery.extension.hlf.core.js | 186 ++++--- release/jquery.extension.hlf.event.js | 14 +- release/jquery.hlf.editable.css | 5 + release/jquery.hlf.editable.js | 14 +- release/jquery.hlf.editable.scss | 19 +- release/jquery.hlf.tip.css | 38 +- release/jquery.hlf.tip.js | 706 +++++++++++++------------- release/jquery.hlf.tip.scss | 223 ++++---- 8 files changed, 589 insertions(+), 616 deletions(-) diff --git a/release/jquery.extension.hlf.core.js b/release/jquery.extension.hlf.core.js index cec69ef..a39db06 100644 --- a/release/jquery.extension.hlf.core.js +++ b/release/jquery.extension.hlf.core.js @@ -2,8 +2,6 @@ /* HLF Core jQuery Extension ========================= -Released under the MIT License -Written with jQuery 1.7.2 */ (function() { @@ -11,22 +9,21 @@ Written with jQuery 1.7.2 hasProp = {}.hasOwnProperty, indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; - (function(extension) { - if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) { - return define(['jquery', 'underscore'], extension); + (function(root, factory) { + if (typeof define === 'function' && (define.amd != null)) { + return define(['jquery', 'underscore'], factory); + } else if (typeof exports === 'object') { + return module.exports = factory(require('jquery', require('underscore'))); } else { - return extension(jQuery, _); + return factory(jQuery, _, jQuery.hlf); } - })(function($, _) { - var _createPluginAPIAdditions, _createPluginInstance, _noConflicts, hlf, safeSet; + })(this, function($, _) { + var _createPluginAPIAdditions, _createPluginInstance, _noConflicts, _safeSet, hlf; hlf = { debug: true, toString: _.memoize(function(context) { return 'hlf'; - }) - }; - _noConflicts = []; - _.extend(hlf, { + }), noConflict: function() { var fn; return ((function() { @@ -38,83 +35,10 @@ Written with jQuery 1.7.2 } return results; })()).length; - }, - debugLog: hlf.debug === false ? $.noop : (console.log.bind ? console.log.bind(console) : console.log) - }); - _createPluginInstance = function($el, options, $context, namespace, apiClass, apiMixins, mixinFilter, createOptions) { - var $root, data, deep, finalOptions, instance, otherMixins; - data = $el.data(namespace.toString('data')); - finalOptions = options; - if ($.isPlainObject(data)) { - finalOptions = $.extend((deep = true), {}, options, data); - $root = $el; - } else if (createOptions.asSharedInstance) { - $root = $context; - } else { - $root = $el; - } - if (apiClass != null) { - instance = new apiClass($el, finalOptions, $context); - if (createOptions.baseMixins != null) { - hlf.applyMixins.apply(hlf, [instance, namespace].concat(slice.call(createOptions.baseMixins))); - } - if (createOptions.apiMixins != null) { - hlf.applyMixins.apply(hlf, [instance, namespace].concat(slice.call(createOptions.apiMixins))); - } - } else if (apiMixins != null) { - instance = { - $el: $el, - options: finalOptions - }; - if (createOptions.baseMixins != null) { - hlf.applyMixins.apply(hlf, [instance, namespace].concat(slice.call(createOptions.baseMixins))); - } - hlf.applyMixin(instance, namespace, apiMixins.base); - otherMixins = _.chain(apiMixins).filter(mixinFilter, instance).values().without(apiMixins.base).value(); - hlf.applyMixins.apply(hlf, [instance, namespace].concat(slice.call(otherMixins))); - } - if (createOptions.compactOptions === true) { - $.extend((deep = true), instance, finalOptions); - delete instance.options; - } else { - if (finalOptions.selectors != null) { - instance.selectors = finalOptions.selectors; - } - if (finalOptions.classNames != null) { - instance.classNames = finalOptions.classNames; - } - } - if (createOptions.autoSelect === true && _.isFunction(instance.select)) { - instance.select(); - } - if (instance.cls !== $.noop) { - $root.addClass(instance.cls()); - } - if (_.isFunction(instance.init)) { - instance.init(); - } else if (apiClass == null) { - hlf.debugLog('ERROR: No `init` method on instance.', instance); } - return $root.data(instance.attr(), instance); - }; - _createPluginAPIAdditions = function(name, namespace) { - return { - evt: _.memoize(function(name) { - return "" + name + (namespace.toString('event')); - }), - attr: _.memoize(function(name) { - name = name != null ? "-" + name : ''; - return namespace.toString('data') + name; - }), - cls: namespace.toString('class') === namespace.toString() ? $.noop : _.memoize(function(name) { - name = name != null ? "-" + name : ''; - return namespace.toString('class') + name; - }), - debugLog: namespace.debug === false ? $.noop : function() { - return hlf.debugLog.apply(hlf, [namespace.toString('log')].concat(slice.call(arguments))); - } - }; }; + hlf.debugLog = hlf.debug === false ? $.noop : (console.log.bind ? console.log.bind(console) : console.log); + _noConflicts = []; _.extend(hlf, { createPlugin: function(createOptions) { var _noConflict, _plugin, apiAdditions, apiClass, apiMixins, deep, mixinFilter, name, namespace, plugin, safeName; @@ -201,6 +125,80 @@ Written with jQuery 1.7.2 } }); _.bindAll(hlf, 'createPlugin'); + _createPluginInstance = function($el, options, $context, namespace, apiClass, apiMixins, mixinFilter, createOptions) { + var $root, data, deep, finalOptions, instance, otherMixins; + data = $el.data(namespace.toString('data')); + finalOptions = options; + if ($.isPlainObject(data)) { + finalOptions = $.extend((deep = true), {}, options, data); + $root = $el; + } else if (createOptions.asSharedInstance) { + $root = $context; + } else { + $root = $el; + } + if (apiClass != null) { + instance = new apiClass($el, finalOptions, $context); + if (createOptions.baseMixins != null) { + hlf.applyMixins.apply(hlf, [instance, namespace].concat(slice.call(createOptions.baseMixins))); + } + if (createOptions.apiMixins != null) { + hlf.applyMixins.apply(hlf, [instance, namespace].concat(slice.call(createOptions.apiMixins))); + } + } else if (apiMixins != null) { + instance = { + $el: $el, + options: finalOptions + }; + if (createOptions.baseMixins != null) { + hlf.applyMixins.apply(hlf, [instance, namespace].concat(slice.call(createOptions.baseMixins))); + } + hlf.applyMixin(instance, namespace, apiMixins.base); + otherMixins = _.chain(apiMixins).filter(mixinFilter, instance).values().without(apiMixins.base).value(); + hlf.applyMixins.apply(hlf, [instance, namespace].concat(slice.call(otherMixins))); + } + if (createOptions.compactOptions === true) { + $.extend((deep = true), instance, finalOptions); + delete instance.options; + } else { + if (finalOptions.selectors != null) { + instance.selectors = finalOptions.selectors; + } + if (finalOptions.classNames != null) { + instance.classNames = finalOptions.classNames; + } + } + if (createOptions.autoSelect === true && _.isFunction(instance.select)) { + instance.select(); + } + if (instance.cls !== $.noop) { + $root.addClass(instance.cls()); + } + if (_.isFunction(instance.init)) { + instance.init(); + } else if (apiClass == null) { + hlf.debugLog('ERROR: No `init` method on instance.', instance); + } + return $root.data(instance.attr(), instance); + }; + _createPluginAPIAdditions = function(name, namespace) { + return { + evt: _.memoize(function(name) { + return "" + name + (namespace.toString('event')); + }), + attr: _.memoize(function(name) { + name = name != null ? "-" + name : ''; + return namespace.toString('data') + name; + }), + cls: namespace.toString('class') === namespace.toString() ? $.noop : _.memoize(function(name) { + name = name != null ? "-" + name : ''; + return namespace.toString('class') + name; + }), + debugLog: namespace.debug === false ? $.noop : function() { + return hlf.debugLog.apply(hlf, [namespace.toString('log')].concat(slice.call(arguments))); + } + }; + }; _.extend(hlf, { applyMixin: function(context, dependencies, mixin) { var handlerNames, i, len, method, mixinToApply, name, onceMethods, prop; @@ -345,7 +343,7 @@ Written with jQuery 1.7.2 } } }); - safeSet = function(key, toContext, fromContext) { + _safeSet = function(key, toContext, fromContext) { var _oldValue; if (toContext == null) { toContext = $; @@ -359,12 +357,12 @@ Written with jQuery 1.7.2 return 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; return $.hlf; }); diff --git a/release/jquery.extension.hlf.event.js b/release/jquery.extension.hlf.event.js index d1f1680..a4a9aa7 100644 --- a/release/jquery.extension.hlf.event.js +++ b/release/jquery.extension.hlf.event.js @@ -2,21 +2,21 @@ /* HLF Event jQuery Extension ========================== -Released under the MIT License -Written with jQuery 1.7.2 */ (function() { var slice = [].slice, hasProp = {}.hasOwnProperty; - (function(extension) { - if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) { - return define(['jquery', 'underscore', 'hlf/jquery.extension.hlf.core'], extension); + (function(root, factory) { + if (typeof define === 'function' && (define.amd != null)) { + return define(['jquery', 'underscore', 'hlf/jquery.extension.hlf.core'], factory); + } else if (typeof exports === 'object') { + return module.exports = factory(require('jquery', require('underscore', require('hlf/jquery.extension.hlf.core')))); } else { - return extension(jQuery, _, jQuery.hlf); + return factory(jQuery, _, jQuery.hlf); } - })(function($, _, hlf) { + })(this, function($, _, hlf) { $.extend(true, hlf, { hoverIntent: { debug: false, diff --git a/release/jquery.hlf.editable.css b/release/jquery.hlf.editable.css index e69de29..2f550e3 100644 --- a/release/jquery.hlf.editable.css +++ b/release/jquery.hlf.editable.css @@ -0,0 +1,5 @@ +/* + HLF Editable jQuery Plugin + ========================== + There should be no css output from this file. +*/ diff --git a/release/jquery.hlf.editable.js b/release/jquery.hlf.editable.js index 6249fb9..ab15e1c 100644 --- a/release/jquery.hlf.editable.js +++ b/release/jquery.hlf.editable.js @@ -2,20 +2,20 @@ /* HLF Editable jQuery Plugin ========================== -Released under the MIT License -Written with jQuery 1.7.2 */ (function() { var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; - (function(plugin) { - if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) { - return define(['jquery', 'underscore', 'hlf/jquery.extension.hlf.core'], plugin); + (function(root, factory) { + if (typeof define === 'function' && (define.amd != null)) { + return define(['jquery', 'underscore', 'hlf/jquery.extension.hlf.core'], factory); + } else if (typeof exports === 'object') { + return module.exports = factory(require('jquery', require('underscore', require('hlf/jquery.extension.hlf.core')))); } else { - return plugin(jQuery, _, jQuery.hlf); + return factory(jQuery, _, jQuery.hlf); } - })(function($, _, hlf) { + })(this, function($, _, hlf) { var mixins; hlf.editable = { debug: true, diff --git a/release/jquery.hlf.editable.scss b/release/jquery.hlf.editable.scss index 779444e..3b1de32 100644 --- a/release/jquery.hlf.editable.scss +++ b/release/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/release/jquery.hlf.tip.css b/release/jquery.hlf.tip.css index d1e68e9..ba1de33 100644 --- a/release/jquery.hlf.tip.css +++ b/release/jquery.hlf.tip.css @@ -1,33 +1,5 @@ -.js-tip { - display: none; - position: absolute; - z-index: 9999; } - .js-tip > .js-tip-inner { - position: relative; } - -.js-tip-inner > .js-tip-stem { - border-width: 0; - border-style: solid; - border-color: transparent; - background: none; - position: absolute; - width: 0; - height: 0; } -.js-tip-inner > .js-tip-content { - overflow: hidden; - position: relative; } - -.js-tip-right.js-snap-tip-y-side .js-tip-stem, .js-tip-right .js-tip-stem { - left: 0; } -.js-tip-left.js-snap-tip-y-side .js-tip-stem, .js-tip-left .js-tip-stem { - right: 0; } -.js-tip-bottom.js-snap-tip-x-side .js-tip-stem, .js-tip-bottom .js-tip-stem { - top: 0; } -.js-tip-top.js-snap-tip-x-side .js-tip-stem, .js-tip-top .js-tip-stem { - bottom: 0; } -.js-snap-tip-x-side .js-tip-stem { - left: 50%; - border-color: transparent; } -.js-snap-tip-y-side .js-tip-stem { - top: 50%; - border-color: transparent; } +/* + HLF Tip jQuery Plugin + ===================== + There should be no css output from this file. +*/ diff --git a/release/jquery.hlf.tip.js b/release/jquery.hlf.tip.js index e59516f..5a0ac5b 100644 --- a/release/jquery.hlf.tip.js +++ b/release/jquery.hlf.tip.js @@ -2,21 +2,22 @@ /* HLF Tip jQuery Plugin ===================== -Released under the MIT License -Written with jQuery 1.7.2 */ (function() { var hasProp = {}.hasOwnProperty, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; - (function(plugin) { - if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) { - return define(['jquery', 'underscore', 'hlf/jquery.extension.hlf.core', 'hlf/jquery.extension.hlf.event'], plugin); + (function(root, factory) { + if (typeof define === 'function' && (define.amd != null)) { + return define(['jquery', 'underscore', 'hlf/jquery.extension.hlf.core', 'hlf/jquery.extension.hlf.event'], factory); + } else if (typeof exports === 'object') { + return module.exports = factory(require('jquery', require('underscore', require('hlf/jquery.extension.hlf.core', require('hlf/jquery.extension.hlf.event'))))); } else { - return plugin(jQuery, _, jQuery.hlf); + return factory(jQuery, _, jQuery.hlf); } - })(function($, _, hlf) { + })(this, function($, _, hlf) { var SnapTip, Tip; hlf.tip = { debug: false, @@ -35,29 +36,10 @@ Written with jQuery 1.7.2 defaults: (function(pre) { return { $viewport: $('body'), - triggerContent: null, - shouldDelegate: true, - ms: { - duration: { - "in": 200, - out: 200, - resize: 300 - }, - delay: { - "in": 300, - out: 300 - } - }, - easing: { - base: 'ease-in-out' - }, - shouldAnimate: { - resize: true - }, + autoDirection: true, cursorHeight: 12, defaultDirection: ['bottom', 'right'], safeToggle: true, - autoDirection: true, tipTemplate: function(containerClass) { var stemHtml; if (this.doStem === true) { @@ -65,6 +47,7 @@ Written with jQuery 1.7.2 } return "
\n
\n " + stemHtml + "\n
\n
\n
"; }, + triggerContent: null, classNames: (function() { var classNames, j, key, keys, len; classNames = {}; @@ -75,7 +58,24 @@ Written with jQuery 1.7.2 } classNames.tip = 'js-tip'; return classNames; - })() + })(), + animations: { + base: { + delay: 0, + duration: 200, + easing: 'ease-in-out', + enabled: true + }, + show: { + delay: 200 + }, + hide: { + delay: 200 + }, + resize: { + delay: 300 + } + } }; })('js-tip-') }; @@ -124,18 +124,22 @@ Written with jQuery 1.7.2 }; Tip = (function() { function Tip($triggers1, options, $context) { + var animation, name, ref; this.$triggers = $triggers1; this.$context = $context; + this._setTip = bind(this._setTip, this); + ref = options.animations; + for (name in ref) { + if (!hasProp.call(ref, name)) continue; + animation = ref[name]; + if (name !== 'base') { + _.defaults(animation, options.animations.base); + } + } } Tip.prototype.init = function() { - var onMutations, processTrigger, selector; _.bindAll(this, '_onTriggerMouseMove', '_setBounds'); - this._setTip = (function(_this) { - return function($tip) { - return _this.$tip = _this.$el = $tip; - }; - })(this); this._setTip($('
')); this.doStem = this.classNames.stem !== ''; this.doFollow = this.classNames.follow !== ''; @@ -145,59 +149,9 @@ Written with jQuery 1.7.2 this._$currentTrigger = null; this._render(); this._bind(); - processTrigger = (function(_this) { - return function($trigger) { - if (!$trigger.length) { - return false; - } - $trigger.addClass(_this.classNames.trigger); - _this._saveTriggerContent($trigger); - _this._updateDirectionByTrigger($trigger); - if (_this.shouldDelegate === false) { - return _this._bindTrigger($trigger); - } - }; - })(this); - this.$triggers.each((function(_this) { - return function(i, el) { - return processTrigger($(el)); - }; - })(this)); - this.doLiveUpdate = window.MutationObserver != null; - if (this.doLiveUpdate) { - selector = this.$triggers.selector; - onMutations = (function(_this) { - return function(mutations) { - var $target, $triggers, j, len, mutation, results; - results = []; - for (j = 0, len = mutations.length; j < len; j++) { - mutation = mutations[j]; - $target = $(mutation.target); - if ($target.hasClass(_this.classNames.content)) { - continue; - } - if (mutation.addedNodes.length) { - $triggers = $(mutation.addedNodes).find('[title],[alt]'); - $triggers.each(function(i, el) { - return processTrigger($(el)); - }); - results.push(_this.$triggers = _this.$triggers.add($triggers)); - } else { - results.push(void 0); - } - } - return results; - }; - })(this); - this._mutationObserver = new MutationObserver(onMutations); - this._mutationObserver.observe(this.$context[0], { - childList: true, - subtree: true - }); - } - if (this.shouldDelegate) { - return this._bindTrigger(); - } + this._bindContext(); + this._processTriggers(); + return this._bindTriggers(); }; Tip.prototype._defaultHtml = function() { @@ -211,6 +165,162 @@ Written with jQuery 1.7.2 return html = this.tipTemplate(containerClass); }; + Tip.prototype._isDirection = function(directionComponent, $trigger) { + return this.$tip.hasClass(this.classNames[directionComponent]) || ((($trigger == null) || !$trigger.data(this.attr('direction'))) && _.include(this.defaultDirection, directionComponent)); + }; + + Tip.prototype._setState = function(state) { + if (state === this._state) { + return false; + } + this._state = state; + return this.debugLog(this._state); + }; + + Tip.prototype._setTip = function($tip) { + return this.$tip = this.$el = $tip; + }; + + Tip.prototype._sizeForTrigger = function($trigger, contentOnly) { + var $content, bottom, left, padding, ref, right, side, size, top, wrapped; + if (contentOnly == null) { + contentOnly = false; + } + size = { + width: $trigger.data('width'), + height: $trigger.data('height') + }; + $content = this.selectByClass('content'); + if (!((size.width != null) && (size.height != null))) { + $content.text($trigger.data(this.attr('content'))); + wrapped = this._wrapStealthRender(function() { + $trigger.data('width', (size.width = this.$tip.outerWidth())); + return $trigger.data('height', (size.height = this.$tip.outerHeight())); + }); + wrapped(); + } + if (contentOnly === true) { + padding = $content.css('padding').split(' '); + ref = (function() { + var j, len, results; + results = []; + for (j = 0, len = padding.length; j < len; j++) { + side = padding[j]; + results.push(parseInt(side, 10)); + } + return results; + })(), top = ref[0], right = ref[1], bottom = ref[2], left = ref[3]; + if (bottom == null) { + bottom = top; + } + if (left == null) { + left = right; + } + size.width -= left + right; + size.height -= top + bottom + this.selectByClass('stem').height(); + } + return size; + }; + + Tip.prototype._stemSize = function() { + var $content, key, size, wrapped; + key = this.attr('stem-size'); + size = this.$tip.data(key); + if (size != null) { + return size; + } + $content = this.selectByClass('content'); + wrapped = this._wrapStealthRender((function(_this) { + return function() { + var direction, j, len, offset, ref; + ref = $content.position(); + for (offset = j = 0, len = ref.length; j < len; offset = ++j) { + direction = ref[offset]; + if (offset > 0) { + size = Math.abs(offset); + _this.$tip.data(key, size); + } + } + return 0; + }; + })(this)); + return wrapped(); + }; + + Tip.prototype.wakeByTrigger = function($trigger, event, onWake) { + var delay, duration, ref, triggerChanged, wake; + triggerChanged = !$trigger.is(this._$currentTrigger); + if (triggerChanged) { + this._inflateByTrigger($trigger); + this._$currentTrigger = $trigger; + } + if (this._state === 'awake') { + this._positionToTrigger($trigger, event); + this.onShow(triggerChanged, event); + if (onWake != null) { + onWake(); + } + this.debugLog('quick update'); + return true; + } + if (event != null) { + this.debugLog(event.type); + } + if ((ref = this._state) === 'awake' || ref === 'waking') { + return false; + } + delay = this.animations.show.delay; + duration = this.animations.show.duration; + wake = (function(_this) { + return function() { + _this._positionToTrigger($trigger, event); + _this.onShow(triggerChanged, event); + return _this.$tip.stop().fadeIn(duration, function() { + if (triggerChanged) { + if (onWake != null) { + onWake(); + } + } + if (_this.safeToggle === true) { + _this.$tip.siblings(_this.classNames.tip).fadeOut(); + } + _this.afterShow(triggerChanged, event); + return _this._setState('awake'); + }); + }; + })(this); + if (this._state === 'sleeping') { + this.debugLog('clear sleep'); + clearTimeout(this._sleepCountdown); + duration = 0; + wake(); + } else if ((event != null) && event.type === 'truemouseenter') { + triggerChanged = true; + this._setState('waking'); + this._wakeCountdown = setTimeout(wake, delay); + } + return true; + }; + + Tip.prototype.sleepByTrigger = function($trigger) { + var ref; + if ((ref = this._state) === 'asleep' || ref === 'sleeping') { + return false; + } + this._setState('sleeping'); + clearTimeout(this._wakeCountdown); + this._sleepCountdown = setTimeout((function(_this) { + return function() { + _this.onHide(); + return _this.$tip.stop().fadeOut(_this.animations.hide.duration, function() { + _this._setState('asleep'); + return _this.afterHide(); + }); + }; + })(this), this.animations.hide.delay); + return true; + }; + Tip.prototype._saveTriggerContent = function($trigger) { var attr, canRemoveAttr, content; content = null; @@ -241,22 +351,69 @@ Written with jQuery 1.7.2 } }; - Tip.prototype._bindTrigger = function($trigger) { - var $bindTarget, onMouseMove, selector; - $bindTarget = $trigger; - if ($bindTarget == null) { - if (this.$context) { - $bindTarget = this.$context; - selector = "." + this.classNames.trigger; - } else { - this.debugLog('invalid argument(s)'); - return false; - } + Tip.prototype._bind = function() { + this.$tip.on({ + mouseenter: (function(_this) { + return function(event) { + _this.debugLog('enter tip'); + if (_this._$currentTrigger != null) { + _this._$currentTrigger.data(_this.attr('is-active'), true); + return _this.wakeByTrigger(_this._$currentTrigger); + } + }; + })(this), + mouseleave: (function(_this) { + return function(event) { + _this.debugLog('leave tip'); + if (_this._$currentTrigger != null) { + _this._$currentTrigger.data(_this.attr('is-active'), false); + return _this.sleepByTrigger(_this._$currentTrigger); + } + }; + })(this) + }); + if (this.autoDirection === true) { + return $(window).resize(_.debounce(this._setBounds, 300)); } - if (selector == null) { - selector = null; + }; + + Tip.prototype._bindContext = function() { + var selector; + if (window.MutationObserver == null) { + return false; } - $bindTarget.on([this.evt('truemouseenter'), this.evt('truemouseleave')].join(' '), selector, { + selector = this.$triggers.selector; + this._mutationObserver = new MutationObserver((function(_this) { + return function(mutations) { + var $target, $triggers, j, len, mutation, results; + results = []; + for (j = 0, len = mutations.length; j < len; j++) { + mutation = mutations[j]; + $target = $(mutation.target); + if ($target.hasClass(_this.classNames.content)) { + continue; + } + if (mutation.addedNodes.length) { + $triggers = $(mutation.addedNodes).find('[title],[alt]'); + _this._processTriggers($triggers); + results.push(_this.$triggers = _this.$triggers.add($triggers)); + } else { + results.push(void 0); + } + } + return results; + }; + })(this)); + return this._mutationObserver.observe(this.$context[0], { + childList: true, + subtree: true + }); + }; + + Tip.prototype._bindTriggers = function() { + var onMouseMove, selector; + selector = "." + this.classNames.trigger; + this.$context.on([this.evt('truemouseenter'), this.evt('truemouseleave')].join(' '), selector, { selector: selector }, (function(_this) { return function(event) { @@ -286,73 +443,8 @@ Written with jQuery 1.7.2 } else { onMouseMove = _.throttle(this._onTriggerMouseMove, 16); } - return $bindTarget.on('mousemove', selector, onMouseMove); - } - }; - - Tip.prototype._bind = function() { - this.$tip.on('mouseenter', (function(_this) { - return function(event) { - _this.debugLog('enter tip'); - if (_this._$currentTrigger != null) { - _this._$currentTrigger.data(_this.attr('is-active'), true); - return _this.wakeByTrigger(_this._$currentTrigger); - } - }; - })(this)).on('mouseleave', (function(_this) { - return function(event) { - _this.debugLog('leave tip'); - if (_this._$currentTrigger != null) { - _this._$currentTrigger.data(_this.attr('is-active'), false); - return _this.sleepByTrigger(_this._$currentTrigger); - } - }; - })(this)); - if (this.autoDirection === true) { - return $(window).resize(_.debounce(this._setBounds, 300)); - } - }; - - Tip.prototype._render = function() { - var $tip, duration, easing, html, transitionStyle; - if (this.$tip.html().length) { - return false; - } - html = this.htmlOnRender(); - if (!((html != null) && html.length)) { - html = this._defaultHtml(); - } - $tip = $(html).addClass(this.classNames.follow); - transitionStyle = []; - if (this.shouldAnimate.resize) { - duration = this.ms.duration.resize / 1000.0 + 's'; - easing = this.easing.resize; - if (easing == null) { - easing = this.easing.base; - } - transitionStyle.push("width " + duration + " " + easing, "height " + duration + " " + easing); - } - transitionStyle = transitionStyle.join(','); - this._setTip($tip); - this.selectByClass('content').css('transition', transitionStyle); - return this.$tip.prependTo(this.$viewport); - }; - - Tip.prototype._inflateByTrigger = function($trigger) { - var $content, compoundDirection, contentOnly, contentSize; - compoundDirection = $trigger.data(this.attr('direction')) ? $trigger.data(this.attr('direction')).split(' ') : this.defaultDirection; - this.debugLog('update direction class', compoundDirection); - $content = this.selectByClass('content'); - $content.text($trigger.data(this.attr('content'))); - if (this.shouldAnimate.resize) { - contentSize = this.sizeForTrigger($trigger, (contentOnly = true)); - $content.width(contentSize.width).height(contentSize.height); + return this.$context.on('mousemove', selector, onMouseMove); } - return this.$tip.removeClass([this.classNames.top, this.classNames.bottom, this.classNames.right, this.classNames.left].join(' ')).addClass($.trim(_.reduce(compoundDirection, (function(_this) { - return function(classListMemo, directionComponent) { - return classListMemo + " " + _this.classNames[directionComponent]; - }; - })(this), ''))); }; Tip.prototype._onTriggerMouseMove = function(event) { @@ -380,12 +472,12 @@ Written with jQuery 1.7.2 left: mouseEvent.pageX }; offset = this.offsetOnTriggerMouseMove(mouseEvent, offset, $trigger) || offset; - if (this.isDirection('top', $trigger)) { - offset.top -= this.$tip.outerHeight() + this.stemSize(); - } else if (this.isDirection('bottom', $trigger)) { - offset.top += this.stemSize() + cursorHeight; + if (this._isDirection('top', $trigger)) { + offset.top -= this.$tip.outerHeight() + this._stemSize(); + } else if (this._isDirection('bottom', $trigger)) { + offset.top += this._stemSize() + cursorHeight; } - if (this.isDirection('left', $trigger)) { + if (this._isDirection('left', $trigger)) { tipWidth = this.$tip.outerWidth(); triggerWidth = $trigger.outerWidth(); offset.left -= tipWidth; @@ -396,29 +488,72 @@ Written with jQuery 1.7.2 return this.$tip.css(offset); }; - Tip.prototype.stemSize = function() { - var $content, key, size, wrapped; - key = this.attr('stem-size'); - size = this.$tip.data(key); - if (size != null) { - return size; - } + Tip.prototype._setBounds = function() { + var $viewport; + $viewport = this.$viewport.is('body') ? $(window) : this.$viewport; + return this._bounds = { + top: $.css(this.$viewport[0], 'padding-top', true), + left: $.css(this.$viewport[0], 'padding-left', true), + bottom: $viewport.innerHeight(), + right: $viewport.innerWidth() + }; + }; + + Tip.prototype._inflateByTrigger = function($trigger) { + var $content, compoundDirection, contentOnly, contentSize; $content = this.selectByClass('content'); - wrapped = this._wrapStealthRender((function(_this) { - return function() { - var direction, j, len, offset, ref; - ref = $content.position(); - for (offset = j = 0, len = ref.length; j < len; offset = ++j) { - direction = ref[offset]; - if (offset > 0) { - size = Math.abs(offset); - _this.$tip.data(key, size); - } + $content.text($trigger.data(this.attr('content'))); + if (this.animations.resize.enabled) { + contentSize = this._sizeForTrigger($trigger, (contentOnly = true)); + $content.width(contentSize.width).height(contentSize.height); + } + compoundDirection = $trigger.data(this.attr('direction')) ? $trigger.data(this.attr('direction')).split(' ') : this.defaultDirection; + this.debugLog('update direction class', compoundDirection); + return this.$tip.removeClass(_.chain(this.classNames).pick('top', 'bottom', 'right', 'left').values().join(' ').value()).addClass($.trim(_.reduce(compoundDirection, (function(_this) { + return function(classListMemo, directionComponent) { + return classListMemo + " " + _this.classNames[directionComponent]; + }; + })(this), ''))); + }; + + Tip.prototype._render = function() { + var $tip, duration, easing, html, transitionStyle; + if (this.$tip.html().length) { + return false; + } + html = this.htmlOnRender(); + if (!((html != null) && html.length)) { + html = this._defaultHtml(); + } + $tip = $(html).addClass(this.classNames.follow); + transitionStyle = []; + if (this.animations.resize.enabled) { + duration = this.animations.resize.duration / 1000.0 + 's'; + easing = this.animations.resize.easing; + transitionStyle.push("width " + duration + " " + easing, "height " + duration + " " + easing); + } + transitionStyle = transitionStyle.join(','); + this._setTip($tip); + this.selectByClass('content').css('transition', transitionStyle); + return this.$tip.prependTo(this.$viewport); + }; + + Tip.prototype._processTriggers = function($triggers) { + if ($triggers == null) { + $triggers = this.$triggers; + } + return $triggers.each((function(_this) { + return function(i, el) { + var $trigger; + $trigger = $(el); + if (!$trigger.length) { + return false; } - return 0; + $trigger.addClass(_this.classNames.trigger); + _this._saveTriggerContent($trigger); + return _this._updateDirectionByTrigger($trigger); }; })(this)); - return wrapped(); }; Tip.prototype._updateDirectionByTrigger = function($trigger) { @@ -429,7 +564,7 @@ Written with jQuery 1.7.2 triggerPosition = $trigger.position(); triggerWidth = $trigger.outerWidth(); triggerHeight = $trigger.outerHeight(); - tipSize = this.sizeForTrigger($trigger); + tipSize = this._sizeForTrigger($trigger); newDirection = _.clone(this.defaultDirection); this.debugLog({ triggerPosition: triggerPosition, @@ -484,66 +619,6 @@ Written with jQuery 1.7.2 return results; }; - Tip.prototype._setBounds = function() { - var $viewport; - $viewport = this.$viewport.is('body') ? $(window) : this.$viewport; - return this._bounds = { - top: $.css(this.$viewport[0], 'padding-top', true), - left: $.css(this.$viewport[0], 'padding-left', true), - bottom: $viewport.innerHeight(), - right: $viewport.innerWidth() - }; - }; - - Tip.prototype._setState = function(state) { - if (state === this._state) { - return false; - } - this._state = state; - return this.debugLog(this._state); - }; - - Tip.prototype.sizeForTrigger = function($trigger, contentOnly) { - var $content, bottom, left, padding, ref, right, side, size, top, wrapped; - if (contentOnly == null) { - contentOnly = false; - } - size = { - width: $trigger.data('width'), - height: $trigger.data('height') - }; - $content = this.selectByClass('content'); - if (!((size.width != null) && (size.height != null))) { - $content.text($trigger.data(this.attr('content'))); - wrapped = this._wrapStealthRender(function() { - $trigger.data('width', (size.width = this.$tip.outerWidth())); - return $trigger.data('height', (size.height = this.$tip.outerHeight())); - }); - wrapped(); - } - if (contentOnly === true) { - padding = $content.css('padding').split(' '); - ref = (function() { - var j, len, results; - results = []; - for (j = 0, len = padding.length; j < len; j++) { - side = padding[j]; - results.push(parseInt(side, 10)); - } - return results; - })(), top = ref[0], right = ref[1], bottom = ref[2], left = ref[3]; - if (bottom == null) { - bottom = top; - } - if (left == null) { - left = right; - } - size.width -= left + right; - size.height -= top + bottom + this.selectByClass('stem').height(); - } - return size; - }; - Tip.prototype._wrapStealthRender = function(func) { return (function(_this) { return function() { @@ -565,84 +640,6 @@ Written with jQuery 1.7.2 })(this); }; - Tip.prototype.isDirection = function(directionComponent, $trigger) { - return this.$tip.hasClass(this.classNames[directionComponent]) || ((($trigger == null) || !$trigger.data(this.attr('direction'))) && _.include(this.defaultDirection, directionComponent)); - }; - - Tip.prototype.wakeByTrigger = function($trigger, event, onWake) { - var delay, duration, ref, triggerChanged, wake; - triggerChanged = !$trigger.is(this._$currentTrigger); - if (triggerChanged) { - this._inflateByTrigger($trigger); - this._$currentTrigger = $trigger; - } - if (this._state === 'awake') { - this._positionToTrigger($trigger, event); - this.onShow(triggerChanged, event); - if (onWake != null) { - onWake(); - } - this.debugLog('quick update'); - return true; - } - if (event != null) { - this.debugLog(event.type); - } - if ((ref = this._state) === 'awake' || ref === 'waking') { - return false; - } - delay = this.ms.delay["in"]; - duration = this.ms.duration["in"]; - wake = (function(_this) { - return function() { - _this._positionToTrigger($trigger, event); - _this.onShow(triggerChanged, event); - return _this.$tip.stop().fadeIn(duration, function() { - if (triggerChanged) { - if (onWake != null) { - onWake(); - } - } - if (_this.safeToggle === true) { - _this.$tip.siblings(_this.classNames.tip).fadeOut(); - } - _this.afterShow(triggerChanged, event); - return _this._setState('awake'); - }); - }; - })(this); - if (this._state === 'sleeping') { - this.debugLog('clear sleep'); - clearTimeout(this._sleepCountdown); - duration = 0; - wake(); - } else if ((event != null) && event.type === 'truemouseenter') { - triggerChanged = true; - this._setState('waking'); - this._wakeCountdown = setTimeout(wake, delay); - } - return true; - }; - - Tip.prototype.sleepByTrigger = function($trigger) { - var ref; - if ((ref = this._state) === 'asleep' || ref === 'sleeping') { - return false; - } - this._setState('sleeping'); - clearTimeout(this._wakeCountdown); - this._sleepCountdown = setTimeout((function(_this) { - return function() { - _this.onHide(); - return _this.$tip.stop().fadeOut(_this.ms.duration.out, function() { - _this._setState('asleep'); - return _this.afterHide(); - }); - }; - })(this), this.ms.delay.out); - return true; - }; - Tip.prototype.onShow = function(triggerChanged, event) { return void 0; }; @@ -690,12 +687,25 @@ Written with jQuery 1.7.2 return results; }; + SnapTip.prototype._bindTriggers = function() { + var selector; + SnapTip.__super__._bindTriggers.call(this); + selector = "." + this.classNames.trigger; + return this.$context.on(this.evt('truemouseleave'), selector, { + selector: selector + }, (function(_this) { + return function(event) { + return _this._offsetStart = null; + }; + })(this)); + }; + SnapTip.prototype._moveToTrigger = function($trigger, baseOffset) { var offset, toTriggerOnly; offset = $trigger.position(); toTriggerOnly = this.snap.toTrigger === true && this.snap.toXAxis === false && this.snap.toYAxis === false; if (this.snap.toXAxis === true) { - if (this.isDirection('bottom', $trigger)) { + if (this._isDirection('bottom', $trigger)) { offset.top += $trigger.outerHeight(); } if (this.snap.toYAxis === false) { @@ -703,7 +713,7 @@ Written with jQuery 1.7.2 } } if (this.snap.toYAxis === true) { - if (this.isDirection('right', $trigger)) { + if (this._isDirection('right', $trigger)) { offset.left += $trigger.outerWidth(); } if (this.snap.toXAxis === false) { @@ -711,36 +721,13 @@ Written with jQuery 1.7.2 } } if (toTriggerOnly === true) { - if (this.isDirection('bottom', $trigger)) { + if (this._isDirection('bottom', $trigger)) { offset.top += $trigger.outerHeight(); } } return offset; }; - SnapTip.prototype._bindTrigger = function($trigger) { - var $bindTarget, didBind, selector; - didBind = SnapTip.__super__._bindTrigger.call(this, $trigger); - if (didBind === false) { - return false; - } - $bindTarget = $trigger; - if (($bindTarget == null) && this.$context) { - $bindTarget = this.$context; - selector = "." + this.classNames.trigger; - } - if (selector == null) { - selector = null; - } - return $bindTarget.on(this.evt('truemouseleave'), selector, { - selector: selector - }, (function(_this) { - return function(event) { - return _this._offsetStart = null; - }; - })(this)); - }; - SnapTip.prototype._positionToTrigger = function($trigger, mouseEvent, cursorHeight) { if (cursorHeight == null) { cursorHeight = this.cursorHeight; @@ -766,9 +753,7 @@ Written with jQuery 1.7.2 SnapTip.prototype.offsetOnTriggerMouseMove = function(event, offset, $trigger) { var newOffset; - newOffset = _.clone(offset); - newOffset = this._moveToTrigger($trigger, newOffset); - return newOffset; + return newOffset = this._moveToTrigger($trigger, _.clone(offset)); }; return SnapTip; @@ -782,7 +767,7 @@ Written with jQuery 1.7.2 baseMixins: ['selection'], compactOptions: true }); - return hlf.createPlugin({ + hlf.createPlugin({ name: 'snapTip', namespace: hlf.tip.snap, apiClass: SnapTip, @@ -790,6 +775,7 @@ Written with jQuery 1.7.2 baseMixins: ['selection'], compactOptions: true }); + return true; }); }).call(this); diff --git a/release/jquery.hlf.tip.scss b/release/jquery.hlf.tip.scss index 985e9e6..1e12579 100644 --- a/release/jquery.hlf.tip.scss +++ b/release/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; + } + }}} } }