├── .gitignore ├── CHANGELOG.md ├── .editorconfig ├── meta.json ├── component.json ├── package.json ├── docs ├── README.md ├── Examples.md └── API.md ├── .jshintrc ├── test ├── test.css ├── test.js └── index.html ├── CONTRIBUTING.md ├── README.md ├── Gruntfile.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | components 2 | node_modules 3 | build 4 | dist -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | **0.1.0** :: *20th Dec 2013* 4 | 5 | - Initial release. -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false -------------------------------------------------------------------------------- /meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tooltips", 3 | "description": "Tooltips for DOM elements.", 4 | "version": "0.1.0", 5 | "date": "2013-12-20 18:23:43 +00:00", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/darsain/tooltip.git", 9 | "homepage": "https://github.com/darsain/tooltip" 10 | }, 11 | "licenses": [ 12 | { 13 | "type": "MIT", 14 | "url": "http://opensource.org/licenses/MIT" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tooltips", 3 | "repo": "darsain/tooltips", 4 | "description": "Tooltips for DOM elements.", 5 | "version": "0.1.0", 6 | "keywords": [ 7 | "tooltip", 8 | "tip", 9 | "ui" 10 | ], 11 | "dependencies": { 12 | "darsain/tooltip": "*", 13 | "darsain/event": "*", 14 | "component/indexof": "*", 15 | "code42day/dataset": "*" 16 | }, 17 | "development": { 18 | "components/jquery": "1.10.2" 19 | }, 20 | "scripts": [ 21 | "index.js" 22 | ], 23 | "license": "MIT" 24 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tooltips", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "grunt-contrib-compress": "~0.5.3", 6 | "grunt-contrib-uglify": "~0.2.7", 7 | "grunt-contrib-jshint": "~0.7.2", 8 | "grunt-contrib-concat": "~0.3.0", 9 | "grunt-contrib-clean": "~0.5.0", 10 | "grunt-contrib-watch": "~0.5.3", 11 | "grunt-contrib-copy": "~0.4.1", 12 | "grunt-component-build": "~0.4.2", 13 | "grunt-tagrelease": "~0.3.0", 14 | "grunt-bumpup": "~0.4.2", 15 | "grunt": "~0.4.2" 16 | } 17 | } -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Documentation 2 | 3 | - **[Tooltips](API.md)** 4 | - **[Examples](Examples.md)** 5 | 6 | ## [Tooltip](https://github.com/darsain/tooltip) 7 | 8 | `Tooltips` is a wrapper around [`Tooltip`](https://github.com/darsain/tooltip), so you should also read the [`Tooltip documentation`](https://github.com/darsain/tooltip/tree/master/docs). 9 | 10 | `Tooltips` also exposes the `Tooltip` library via a static property: 11 | 12 | ```js 13 | var tip = new Tooltips.Tooltip('This is a Tooltip instance!'); 14 | ``` -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef" : [ 3 | "module", 4 | "require" 5 | ], 6 | 7 | "bitwise": false, 8 | "camelcase": false, 9 | "curly": true, 10 | "eqeqeq": true, 11 | "forin": false, 12 | "immed": true, 13 | "latedef": false, 14 | "newcap": true, 15 | "noarg": true, 16 | "noempty": true, 17 | "nonew": false, 18 | "plusplus": false, 19 | "quotmark": false, 20 | "regexp": false, 21 | "undef": true, 22 | "unused": true, 23 | "strict": true, 24 | "trailing": true, 25 | 26 | "asi": false, 27 | "boss": false, 28 | "debug": false, 29 | "eqnull": true, 30 | "es5": false, 31 | "esnext": false, 32 | "evil": false, 33 | "expr": false, 34 | "funcscope": false, 35 | "globalstrict": true, 36 | "iterator": false, 37 | "lastsemic": false, 38 | "laxbreak": false, 39 | "laxcomma": true, 40 | "loopfunc": false, 41 | "multistr": false, 42 | "onecase": true, 43 | "proto": false, 44 | "regexdash": false, 45 | "scripturl": false, 46 | "smarttabs": true, 47 | "shadow": false, 48 | "sub": false, 49 | "supernew": false, 50 | 51 | "browser": true 52 | } -------------------------------------------------------------------------------- /test/test.css: -------------------------------------------------------------------------------- 1 | html, body { margin: 0; padding: 1px 0; color: #444; font-size: 1em; font-family: sans-serif; background: #ddd; } 2 | 3 | .target { 4 | position: relative; 5 | width: 100px; 6 | /*height: 50px;*/ 7 | line-height: 50px; 8 | margin: 10px 5px; 9 | text-align: center; 10 | text-shadow: 1px 1px rgba(255,255,255,.5); 11 | border: 1px solid rgba(255,255,255,.8); 12 | border-radius: 4px; 13 | background: inherit; 14 | } 15 | .target:after { 16 | content: ''; 17 | position: absolute; 18 | top: -2px; 19 | left: -2px; 20 | width: 100%; 21 | height: 100%; 22 | border: 1px solid #666; 23 | border-radius: 4px; 24 | } 25 | .target.movable { cursor: move; border-style: dashed; } 26 | .target.movable, 27 | .target.movable:after { border-style: dashed; } 28 | 29 | /* Lists */ 30 | .targets { 31 | list-style: none; 32 | margin: 0 auto; 33 | padding: 0; 34 | word-spacing: -0.3em; 35 | text-align: center; 36 | } 37 | .targets .target { display: inline-block; } 38 | .targets.vertical .target { display: block; } 39 | .targets.vertical.left { float: left; } 40 | .targets.vertical.right { float: right; } 41 | .targets.clear { clear: both; } 42 | 43 | .examples { 44 | width: 600px; 45 | margin: 120px auto; 46 | text-align: center; 47 | } 48 | 49 | .dynamic { 50 | margin: 20px 120px 0; 51 | text-align: center; 52 | } 53 | .dynamic button { 54 | width: 80px; 55 | height: 40px; 56 | font-size: 1em; 57 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Submitting an issue 2 | 3 | When reporting a bug, please describe it thoroughly, with attached code showcasing how are you using this library. The 4 | best way how to make it easy for developers, and ensure that your issue will be looked at, is to replicate it on 5 | [jsfiddle](http://jsfiddle.net/) or a similar service. 6 | 7 | ## Contributions 8 | 9 | Contributions are welcome! But please, follow these few simple rules: 10 | 11 | **Maintain the coding style** used throughout the project, and defined in the `.editorconfig` file. You can use the 12 | [Editorconfig](http://editorconfig.org) plugin for your editor of choice: 13 | 14 | - [Sublime Text 2](https://github.com/sindresorhus/editorconfig-sublime) 15 | - [Textmate](https://github.com/Mr0grog/editorconfig-textmate) 16 | - [Notepad++](https://github.com/editorconfig/editorconfig-notepad-plus-plus) 17 | - [Emacs](https://github.com/editorconfig/editorconfig-emacs) 18 | - [Vim](https://github.com/editorconfig/editorconfig-vim) 19 | - [Visual Studio](https://github.com/editorconfig/editorconfig-visualstudio) 20 | - [... other editors](http://editorconfig.org/#download) 21 | 22 | --- 23 | 24 | **Code has to pass JSHint** with options defined in the `.jshintrc` file. You can use `grunt jshint` task to lint 25 | manually, or again, there are amazing plugins for a lot of popular editors consuming this file and linting as you code: 26 | 27 | - [Sublim Text 2](https://github.com/SublimeLinter/SublimeLinter) 28 | - [TextMate](http://rondevera.github.com/jslintmate/), or [alternative](http://fgnass.posterous.com/jslint-in-textmate) 29 | - [Notepad++](http://sourceforge.net/projects/jslintnpp/) 30 | - [Emacs](https://github.com/daleharvey/jshint-mode) 31 | - [Vim](https://github.com/walm/jshint.vim) 32 | - [Visual Studio](https://github.com/jamietre/SharpLinter), or [alternative](http://jslint4vs2010.codeplex.com/) 33 | - [... other editors](http://www.jshint.com/platforms/) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Tooltips](http://darsa.in/tooltips) 2 | 3 | Wrapper for [`darsain/tooltip`](http://github.com/darsain/tooltip) library that provides simple bindings for DOM elements 4 | with `data-{key}` attributes. 5 | 6 | Tooltips can use mutation observers to handle dynamic elements, but also provides a fallback methods when you need to 7 | support older browsers. 8 | 9 | It works by attaching events defined in `showOn` & `hideOn` options to each element with `data-{key}` attribute. This is 10 | way better for performance than attaching these events to `container` and handling them for all elements on a page. 11 | Especially in case of events like `mouseenter` & `mouseleave`, which are the main focus of this library. 12 | 13 | To save memory, `Tooltip` instances are created only when first `showOn` event is fired. 14 | 15 | #### Compatibility 16 | 17 | Browser support starts at IE8+, with an exception of automatic binding to dynamic elements via Mutation Observers, which 18 | are used only when supported. Mutation Observers have been implemented in all modern browsers and IE11. 19 | 20 | If you want to support browsers without Mutation Observers, you can fall back to `.reload()` method, or manage dynamic 21 | elements as you add & remove them with `.add()` & `.remove()` methods. 22 | 23 | ## Install 24 | 25 | Tooltips is a [component](https://github.com/component/component): 26 | 27 | ```bash 28 | component install darsain/tooltips 29 | ``` 30 | 31 | ## Download 32 | 33 | Standalone build of a latest stable version: 34 | 35 | - [`tooltips.zip`](http://darsain.github.io/tooltips/dist/tooltips.zip) - combined archive 36 | - [`tooltips.js`](http://darsain.github.io/tooltips/dist/tooltips.js) - 38 KB *sourcemapped* 37 | - [`tooltips.min.js`](http://darsain.github.io/tooltips/dist/tooltips.min.js) - 14 KB, 2.8KB gzipped 38 | - [`tooltips.css`](http://darsain.github.io/tooltips/dist/tooltips.css) - 4.5 KB *including transitions & types* 39 | 40 | When isolating issues on jsfiddle, use the [`tooltips.js`](http://darsain.github.io/tooltips/dist/tooltips.js) URL above. 41 | 42 | ## Documentation 43 | 44 | Can be found in the **[docs](docs)** directory. 45 | 46 | [Changelog](CHANGELOG.md). 47 | 48 | ## Contributing 49 | 50 | Please, read the [Contributing Guidelines](CONTRIBUTING.md) for this project. 51 | 52 | ## License 53 | 54 | MIT -------------------------------------------------------------------------------- /docs/Examples.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Assuming: 4 | 5 | ```js 6 | var Tooltips = require('tooltips'); 7 | ``` 8 | 9 | Bind tooltips to all elements on a page with `data-tooltip` attribute: 10 | 11 | ```js 12 | var tips = new Tooltips(document.body); // Using all default options 13 | ``` 14 | 15 | Tooltips will be attached to elements like this: 16 | 17 | ```html 18 |
target
19 | ``` 20 | 21 | Tooltip text is used as tooltip's `innerHTML` content, so you can use HTML, but escape it beforehand. 22 | 23 | --- 24 | 25 | Use custom data key to create focus hints for form elements: 26 | 27 | ```js 28 | var hints = new Tooltips(document.body, { 29 | key: 'hint', 30 | showOn: 'focus', 31 | hideOn: 'blur' 32 | }); 33 | ``` 34 | 35 | The tooltips will than show on elements with `data-hint` attribute when they are focused: 36 | 37 | ```html 38 | 39 | ``` 40 | 41 | --- 42 | 43 | To make Tooltips automatically keep track of dynamically added/removed elements, enable the `observe` option: 44 | 45 | ```js 46 | var tips = new Tooltips(document.body, { 47 | observe: 1 48 | }); 49 | ``` 50 | 51 | When you need to support browsers without Mutation Observer support, you can either use `.reload()` method on each DOM manipulation: 52 | 53 | ```js 54 | document.body.appendChild(newElement); 55 | tips.reload(); 56 | ``` 57 | 58 | But that is a nuclear option that destroys everything and rebuilds it again. 59 | 60 | Way more efficient is to notify current Tooltips instance about added or removed elements: 61 | 62 | ```js 63 | document.body.appendChild(newElement); 64 | tips.add(newElement); 65 | document.body.removeChild(newElement); 66 | tips.remove(newElement); 67 | ``` 68 | 69 | This will tell tooltip to look for added/removed tooltips only within `newElement`. You need to notify about removed elements so Tooltips can clean up after them (unbind event listeners and destroy Tooltip instances). 70 | 71 | If `newElement` doesn't have `data-{key}` attribute, Tooltips will look on its children. This means that when you insert element like this into the DOM: 72 | 73 | ```html 74 | 79 | ``` 80 | 81 | You need to pass only the `ul#foo` element into the `.add()` or `.remove()` method. -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Tooltips = require('tooltips'); 4 | var $ = require('jquery'); 5 | 6 | // Main examples 7 | var tips = window.tips = new Tooltips(document, { 8 | tooltip: { 9 | effectClass: 'slide', 10 | auto: 1 11 | }, 12 | key: 'tip', 13 | observe: 1 14 | }); 15 | 16 | // Dynamic test 17 | (function () { 18 | var $dynamic = $('#dynamic'); 19 | var $targets = $dynamic.find('.targets'); 20 | 21 | $dynamic.on('click', 'button', function () { 22 | var action = $(this).data('action'); 23 | switch (action) { 24 | case 'add': 25 | $targets.append('
  • new
  • '); 26 | break; 27 | case 'remove': 28 | $targets.children().eq(-1).remove(); 29 | break; 30 | } 31 | }); 32 | }()); 33 | 34 | // Movable test 35 | (function () { 36 | $('.target.movable').each(function (i, target) { 37 | var tip = tips.get(target); 38 | 39 | // Dragging 40 | var dragger = new DragAndReset(target); 41 | dragger.onMove = function reposition() { 42 | tip.position(); 43 | }; 44 | }); 45 | }()); 46 | 47 | // Focusable test 48 | window.ftips = new Tooltips(document, { 49 | key: 'ftip', 50 | showOn: 'focus', 51 | hideOn: 'blur', 52 | tooltip: { 53 | place: 'right' 54 | } 55 | }); 56 | 57 | // Helpers 58 | var rAF = window.requestAnimationFrame || window.webkitRequestAnimationFrame || function (callback) { 59 | return setTimeout(callback, 17); 60 | }; 61 | var getProp = window.getComputedStyle ? function getProp(element, name) { 62 | return window.getComputedStyle(element, null)[name]; 63 | } : function getProp(element, name) { 64 | return element.currentStyle[name]; 65 | }; 66 | function parsePx(value) { 67 | return 0 | Math.round(String(value).replace(/[^\-0-9.]/g, '')); 68 | } 69 | 70 | /** 71 | * Dragging class 72 | * 73 | * @param {Element} element 74 | */ 75 | function DragAndReset(element) { 76 | if (!(this instanceof DragAndReset)) { 77 | return new DragAndReset(element); 78 | } 79 | 80 | var self = this; 81 | var $document = $(document); 82 | var frameID = 0; 83 | self.element = element; 84 | self.initialized = 0; 85 | self.path = { 86 | left: 0, 87 | top: 0 88 | }; 89 | 90 | function move(event) { 91 | self.path.left = event.pageX - self.origin.left; 92 | self.path.top = event.pageY - self.origin.top; 93 | if (!self.initialized && (Math.abs(self.path.left) > 10 || Math.abs(self.path.top) > 10)) { 94 | self.initialized = 1; 95 | } 96 | if (self.initialized) { 97 | requestReposition(); 98 | } 99 | return false; 100 | } 101 | 102 | function requestReposition() { 103 | if (!frameID) { 104 | frameID = rAF(reposition); 105 | } 106 | } 107 | 108 | function reposition() { 109 | frameID = 0; 110 | element.style.left = (self.originPos.left + self.path.left) + 'px'; 111 | element.style.top = (self.originPos.top + self.path.top) + 'px'; 112 | if (self.onMove) { 113 | self.onMove(); 114 | } 115 | } 116 | 117 | function init(event) { 118 | self.origin = { 119 | left: event.pageX, 120 | top: event.pageY 121 | }; 122 | self.originPos = { 123 | left: parsePx(getProp(element, 'left')), 124 | top: parsePx(getProp(element, 'top')) 125 | }; 126 | $document.on('mousemove', move); 127 | $document.on('mouseup', self.end); 128 | return false; 129 | } 130 | 131 | self.end = function () { 132 | self.initialized = 0; 133 | self.path.top = self.path.left = 0; 134 | requestReposition(); 135 | $document.off('mousemove', move); 136 | $document.off('mouseup', self.end); 137 | }; 138 | 139 | (function () { 140 | $(element).on('mousedown', init); 141 | }()); 142 | } 143 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tooltips test 6 | 7 | 8 | 9 | 10 | 11 |
    12 | 17 | 22 | 27 |
    28 | 29 | 30 | 31 |
    32 | 37 |
    38 | 39 |
    40 | 46 |
    47 | 48 |
    49 | 55 |
    56 | 57 |
    58 | 59 |
    60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/API.md: -------------------------------------------------------------------------------- 1 | # API 2 | 3 | ```js 4 | var tips = new Tooltips(container, [options]); 5 | ``` 6 | 7 | ### container 8 | 9 | Type: `Element` 10 | 11 | Tooltips container. Tooltips will be bound to `data-{key}` elements that are descendants of this element. 12 | 13 | ### [options] 14 | 15 | Type: `Object` 16 | 17 | Tooltips options object. 18 | 19 | Default options are stored in `Tooltips.defaults`, and look like this: 20 | 21 | ```js 22 | Tooltips.defaults = { 23 | tooltip: {}, // Options for individual Tooltip instances. 24 | key: 'tooltip', // Tooltips data attribute key. 25 | showOn: 'mouseenter', // Show tooltip event. 26 | hideOn: 'mouseleave', // Hide tooltip event. 27 | observe: 0 // Enable mutation observer (used only when supported). 28 | }; 29 | ``` 30 | 31 | ##### tooltip 32 | 33 | Type `Object` Default `{}` 34 | 35 | [`Tooltip`](https://github.com/darsain/tooltip) options that will be used for every Tooltip in this instance, unless overridden by element's `data-{key}-{optionName}` attribute. 36 | 37 | ##### key 38 | 39 | Type `String` Default `tooltip` 40 | 41 | Data key name used to recognize elements with tooltips, and retrieve the Tooltip content & options. 42 | 43 | ```html 44 |
    45 | ``` 46 | 47 | ##### showOn 48 | 49 | Type `String` Default `mouseenter` 50 | 51 | Event that - when triggered on an element with `data-{key}` attribute - should show a tooltip. 52 | 53 | ##### hideOn 54 | 55 | Type `String` Default `mouseleave` 56 | 57 | Event that - when triggered on an element with `data-{key}` attribute - should hide a tooltip. 58 | 59 | ##### observe 60 | 61 | Type `Boolean` Default `false` 62 | 63 | Whether to use Mutation Observer to keep track of dynamic elements inside `container`. 64 | 65 | ## Methods 66 | 67 | Unless stated otherwise, all methods return tooltips object, making them chainable. 68 | 69 | #### #show(element) 70 | 71 | `Element` **element** 72 | 73 | Show Tooltip associated to an `element`. If Tooltip doesn't exist yet, it will be created using current instance's options. 74 | 75 | #### #hide(element) 76 | 77 | `Element` **element** 78 | 79 | Hide Tooltip associated to an `element`. 80 | 81 | #### #toggle(element) 82 | 83 | `Element` **element** 84 | 85 | Hide/Show Tooltip associated to an `element`. 86 | 87 | #### #get(element) 88 | 89 | `Element` **element** 90 | 91 | Get Tooltip associated to an `element`. 92 | 93 | #### #add(element) 94 | 95 | `Element` **element** 96 | 97 | When not using Mutation Observer, you can use this method to notify Tooltips instance that element has been added. Tooltips will than look at the element and its children for elements with `data-{key}` attributes and attach tooltips to them. 98 | 99 | #### #remove(element) 100 | 101 | `Element` **element** 102 | 103 | When not using Mutation Observer, you can use this method to notify Tooltips instance that element has been removed. Tooltips will than look at the element and its children for elements with `data-{key}` attributes and detach tooltips from them. 104 | 105 | #### #reload() 106 | 107 | Unbinds all current attached tooltips, clears event listeners, and than reapplies everything to elements with `data-{key}` attributes inside `container`. 108 | 109 | Use this when you are changing DOM, not using Mutation Observer, and don't want to bother with `.add()` and `.remove()` methods. Using this instead of `.add()` & `.remove()` is less efficient. 110 | 111 | #### #destroy() 112 | 113 | Destroys the Tooltips instance. Clears all event listeners, destroys all tooltips, and clears objects. 114 | 115 | ## Properties 116 | 117 | #### #container 118 | 119 | Container element of a current instance. 120 | 121 | #### #elements 122 | 123 | Array of elements with `data-{key}` attributes the current instance is keeping track of. 124 | 125 | #### #options 126 | 127 | Tooltips options object. 128 | 129 | ## HTML 130 | 131 | Tooltips looks for elements with `data-{key}` attributes and creates tooltips for them. We will assume that key is a default value `tooltip`. 132 | 133 | Tooltip content is retrieved from main `data-tooltip` attribute: 134 | 135 | ```html 136 |
    target
    137 | ``` 138 | 139 | You can also override options for individual tooltips with additional attributes: 140 | 141 | ```html 142 |
    target
    143 | ``` -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*jshint node:true */ 2 | module.exports = function(grunt) { 3 | 'use strict'; 4 | 5 | // Override environment based line endings enforced by Grunt 6 | grunt.util.linefeed = '\n'; 7 | 8 | // Grunt configuration 9 | grunt.initConfig({ 10 | pkg: grunt.file.readJSON('meta.json'), 11 | meta: { 12 | banner: '/*!\n' + 13 | ' * <%= pkg.name %> <%= pkg.version %> - <%= grunt.template.today("dS mmm yyyy") %>\n' + 14 | ' * <%= pkg.repository.homepage %>\n' + 15 | ' *\n' + 16 | ' * Licensed under the <%= pkg.licenses[0].type %> license.\n' + 17 | ' * <%= pkg.licenses[0].url %>\n' + 18 | ' */\n', 19 | bannerLight: '/*! <%= pkg.name %> <%= pkg.version %>' + 20 | ' - <%= grunt.template.today("dS mmm yyyy") %> | <%= pkg.repository.homepage %> */\n', 21 | }, 22 | 23 | // JSHint the code. 24 | jshint: { 25 | options: { 26 | jshintrc: true, 27 | }, 28 | all: ['index.js'], 29 | }, 30 | 31 | // Clean folders. 32 | clean: { 33 | build: ['build/**'], 34 | tmp: ['tmp/**'], 35 | dist: ['dist/**'] 36 | }, 37 | 38 | // Concatenate files. 39 | concat: { 40 | development: { 41 | options: { 42 | banner: '<%= meta.banner %>' 43 | }, 44 | src: 'tmp/<%= pkg.name %>.js', 45 | dest: 'tmp/<%= pkg.name %>.js', 46 | }, 47 | style: { 48 | options: { 49 | banner: '<%= meta.bannerLight %>' 50 | }, 51 | src: 'tmp/<%= pkg.name %>.css', 52 | dest: 'tmp/<%= pkg.name %>.css', 53 | }, 54 | }, 55 | 56 | // Minify files. 57 | uglify: { 58 | production: { 59 | options: { 60 | banner: '<%= meta.bannerLight %>', 61 | report: 'gzip', 62 | }, 63 | src: 'tmp/<%= pkg.name %>.js', 64 | dest: 'tmp/<%= pkg.name %>.min.js' 65 | }, 66 | }, 67 | 68 | // Copy files. 69 | copy: { 70 | dist: { 71 | expand: true, 72 | cwd: 'tmp', 73 | src: ['*.js', '*.css'], 74 | dest: 'dist/', 75 | }, 76 | }, 77 | 78 | // Build components. 79 | componentbuild: { 80 | test: { 81 | options: { 82 | dev: true, 83 | sourceUrls: true, 84 | }, 85 | src: '.', 86 | dest: 'build', 87 | }, 88 | production: { 89 | options: { 90 | name: '<%= pkg.name %>', 91 | standalone: 'Tooltips', 92 | }, 93 | src: '.', 94 | dest: 'tmp', 95 | }, 96 | development: { 97 | options: { 98 | name: '<%= pkg.name %>', 99 | standalone: 'Tooltips', 100 | sourceUrls: true, 101 | }, 102 | src: '.', 103 | dest: 'tmp', 104 | }, 105 | }, 106 | 107 | // Create zipfiles. 108 | compress: { 109 | options: { 110 | level: 9, 111 | pretty: true, 112 | }, 113 | standalone: { 114 | options: { 115 | archive: 'dist/tooltips.zip', 116 | }, 117 | expand: true, 118 | cwd: 'tmp', 119 | src: ['*'], 120 | }, 121 | }, 122 | 123 | // Watch for changes and run tasks. 124 | watch: { 125 | component: { 126 | files: ['index.js', '*.css'], 127 | tasks: ['componentbuild:test'], 128 | options: { 129 | spawn: false, 130 | livereload: true, 131 | }, 132 | }, 133 | test: { 134 | files: ['test/*.js', 'test/*.css'], 135 | options: { 136 | spawn: false, 137 | livereload: true, 138 | }, 139 | }, 140 | }, 141 | 142 | // Bump up fields in JSON files. 143 | bumpup: { 144 | options: { 145 | updateProps: { 146 | pkg: 'meta.json', 147 | }, 148 | }, 149 | files: ['meta.json', 'component.json'], 150 | }, 151 | 152 | // Commit changes and tag the latest commit with a version from JSON file. 153 | tagrelease: '<%= pkg.version %>' 154 | }); 155 | 156 | // These plugins provide necessary tasks. 157 | grunt.loadNpmTasks('grunt-contrib-compress'); 158 | grunt.loadNpmTasks('grunt-contrib-uglify'); 159 | grunt.loadNpmTasks('grunt-contrib-jshint'); 160 | grunt.loadNpmTasks('grunt-contrib-concat'); 161 | grunt.loadNpmTasks('grunt-contrib-clean'); 162 | grunt.loadNpmTasks('grunt-contrib-watch'); 163 | grunt.loadNpmTasks('grunt-contrib-copy'); 164 | grunt.loadNpmTasks('grunt-component-build'); 165 | grunt.loadNpmTasks('grunt-tagrelease'); 166 | grunt.loadNpmTasks('grunt-bumpup'); 167 | 168 | // Dev task. 169 | grunt.registerTask('dev', function () { 170 | grunt.task.run('componentbuild:test'); 171 | }); 172 | 173 | // Build task. 174 | grunt.registerTask('dist', function () { 175 | grunt.task.run('jshint'); 176 | grunt.task.run('clean:tmp'); 177 | grunt.task.run('clean:dist'); 178 | // Production 179 | grunt.task.run('componentbuild:production'); 180 | grunt.task.run('uglify:production'); 181 | // Development 182 | grunt.task.run('componentbuild:development'); 183 | grunt.task.run('concat:development'); 184 | // Distribution 185 | grunt.task.run('concat:style'); 186 | grunt.task.run('copy:dist'); 187 | grunt.task.run('compress'); 188 | // Cleanup 189 | grunt.task.run('clean:tmp'); 190 | }); 191 | 192 | // Release task. 193 | grunt.registerTask('release', function (type) { 194 | type = type ? type : 'patch'; 195 | grunt.task.run('jshint'); 196 | grunt.task.run('bumpup:' + type); 197 | grunt.task.run('tagrelease'); 198 | }); 199 | 200 | // Default task. 201 | grunt.registerTask('default', ['jshint']); 202 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Dependencies. 5 | */ 6 | var evt = require('event'); 7 | var indexOf = require('indexof'); 8 | var Tooltip = require('tooltip'); 9 | var dataset = require('dataset'); 10 | 11 | /** 12 | * Transport. 13 | */ 14 | module.exports = Tooltips; 15 | 16 | /** 17 | * Globals. 18 | */ 19 | var MObserver = window.MutationObserver || window.WebkitMutationObserver; 20 | 21 | /** 22 | * Prototypal inheritance. 23 | * 24 | * @param {Object} o 25 | * 26 | * @return {Object} 27 | */ 28 | var objectCreate = Object.create || (function () { 29 | function F() {} 30 | return function (o) { 31 | F.prototype = o; 32 | return new F(); 33 | }; 34 | })(); 35 | 36 | /** 37 | * Poor man's shallow object extend. 38 | * 39 | * @param {Object} a 40 | * @param {Object} b 41 | * 42 | * @return {Object} 43 | */ 44 | function extend(a, b) { 45 | for (var key in b) { 46 | a[key] = b[key]; 47 | } 48 | return a; 49 | } 50 | 51 | /** 52 | * Capitalize the first letter of a string. 53 | * 54 | * @param {String} string 55 | * 56 | * @return {String} 57 | */ 58 | function ucFirst(string) { 59 | return string.charAt(0).toUpperCase() + string.slice(1); 60 | } 61 | 62 | /** 63 | * Tooltips constructor. 64 | * 65 | * @param {Element} container 66 | * @param {Object} options 67 | * 68 | * @return {Tooltips} 69 | */ 70 | function Tooltips(container, options) { 71 | if (!(this instanceof Tooltips)) { 72 | return new Tooltips(container, options); 73 | } 74 | 75 | var self = this; 76 | var observer, TID; 77 | 78 | /** 79 | * Show tooltip attached to an element. 80 | * 81 | * @param {Element} element 82 | * 83 | * @return {Tooltips} 84 | */ 85 | self.show = function (element) { 86 | return callTooltipMethod(element, 'show'); 87 | }; 88 | 89 | /** 90 | * Hide tooltip attached to an element. 91 | * 92 | * @param {Element} element 93 | * 94 | * @return {Tooltips} 95 | */ 96 | self.hide = function (element) { 97 | return callTooltipMethod(element, 'hide'); 98 | }; 99 | 100 | /** 101 | * Toggle tooltip attached to an element. 102 | * 103 | * @param {Element} element 104 | * 105 | * @return {Tooltips} 106 | */ 107 | self.toggle = function (element) { 108 | return callTooltipMethod(element, 'toggle'); 109 | }; 110 | 111 | /** 112 | * Retrieve tooltip attached to an element and call it's method. 113 | * 114 | * @param {Element} element 115 | * @param {String} method 116 | * 117 | * @return {Tooltips} 118 | */ 119 | function callTooltipMethod(element, method) { 120 | var tip = self.get(element); 121 | if (tip) { 122 | tip[method](); 123 | } 124 | return self; 125 | } 126 | 127 | /** 128 | * Return a tooltip attached to an element. Tooltip is created if it doesn't exist yet. 129 | * 130 | * @param {Element} element 131 | * 132 | * @return {Tooltip} 133 | */ 134 | self.get = function (element) { 135 | var tip = !!element && (element[TID] || createTip(element)); 136 | if (tip && !element[TID]) { 137 | element[TID] = tip; 138 | } 139 | return tip; 140 | }; 141 | 142 | /** 143 | * Add element(s) to Tooltips instance. 144 | * 145 | * @param {[type]} element Can be element, or container containing elements to be added. 146 | * 147 | * @return {Tooltips} 148 | */ 149 | self.add = function (element) { 150 | if (!element || element.nodeType !== 1) { 151 | return self; 152 | } 153 | if (dataset(element).get(options.key)) { 154 | bindElement(element); 155 | } else if (element.children) { 156 | bindElements(element.querySelectorAll(self.selector)); 157 | } 158 | return self; 159 | }; 160 | 161 | /** 162 | * Remove element(s) from Tooltips instance. 163 | * 164 | * @param {Element} element Can be element, or container containing elements to be removed. 165 | * 166 | * @return {Tooltips} 167 | */ 168 | self.remove = function (element) { 169 | if (!element || element.nodeType !== 1) { 170 | return self; 171 | } 172 | if (dataset(element).get(options.key)) { 173 | unbindElement(element); 174 | } else if (element.children) { 175 | unbindElements(element.querySelectorAll(self.selector)); 176 | } 177 | return self; 178 | }; 179 | 180 | /** 181 | * Reload Tooltips instance. 182 | * 183 | * Unbinds current tooltipped elements, than selects the 184 | * data-key elements from container and binds them again. 185 | * 186 | * @return {Tooltips} 187 | */ 188 | self.reload = function () { 189 | // Unbind old elements 190 | unbindElements(self.elements); 191 | // Bind new elements 192 | bindElements(self.container.querySelectorAll(self.selector)); 193 | return self; 194 | }; 195 | 196 | /** 197 | * Destroy Tooltips instance. 198 | * 199 | * @return {Void} 200 | */ 201 | self.destroy = function () { 202 | unbindElements(this.elements); 203 | if (observer) { 204 | observer.disconnect(); 205 | } 206 | this.container = this.elements = this.options = observer = null; 207 | }; 208 | 209 | /** 210 | * Create a tip from element data attributes. 211 | * 212 | * @param {Element} element 213 | * 214 | * @return {Tooltip} 215 | */ 216 | function createTip(element) { 217 | var data = dataset(element); 218 | var content = data.get(options.key); 219 | if (!content) { 220 | return false; 221 | } 222 | var tipOptions = objectCreate(options.tooltip); 223 | var keyData; 224 | for (var key in Tooltip.defaults) { 225 | keyData = data.get(options.key + ucFirst(key.replace(/Class$/, ''))); 226 | if (!keyData) { 227 | continue; 228 | } 229 | tipOptions[key] = keyData; 230 | } 231 | return new Tooltip(content, tipOptions).attach(element); 232 | } 233 | 234 | /** 235 | * Bind Tooltips events to Array/NodeList of elements. 236 | * 237 | * @param {Array} elements 238 | * 239 | * @return {Void} 240 | */ 241 | function bindElements(elements) { 242 | for (var i = 0, l = elements.length; i < l; i++) { 243 | bindElement(elements[i]); 244 | } 245 | } 246 | 247 | /** 248 | * Bind Tooltips events to element. 249 | * 250 | * @param {Element} element 251 | * 252 | * @return {Void} 253 | */ 254 | function bindElement(element) { 255 | if (element[TID] || ~indexOf(self.elements, element)) { 256 | return; 257 | } 258 | evt.bind(element, options.showOn, eventHandler); 259 | evt.bind(element, options.hideOn, eventHandler); 260 | self.elements.push(element); 261 | } 262 | 263 | /** 264 | * Unbind Tooltips events from Array/NodeList of elements. 265 | * 266 | * @param {Array} elements 267 | * 268 | * @return {Void} 269 | */ 270 | function unbindElements(elements) { 271 | if (self.elements === elements) { 272 | elements = elements.slice(); 273 | } 274 | for (var i = 0, l = elements.length; i < l; i++) { 275 | unbindElement(elements[i]); 276 | } 277 | } 278 | 279 | /** 280 | * Unbind Tooltips events from element. 281 | * 282 | * @param {Element} element 283 | * 284 | * @return {Void} 285 | */ 286 | function unbindElement(element) { 287 | var index = indexOf(self.elements, element); 288 | if (!~index) { 289 | return; 290 | } 291 | if (element[TID]) { 292 | element[TID].destroy(); 293 | delete element[TID]; 294 | } 295 | evt.unbind(element, options.showOn, eventHandler); 296 | evt.unbind(element, options.hideOn, eventHandler); 297 | self.elements.splice(index, 1); 298 | } 299 | 300 | /** 301 | * Tooltips events handler. 302 | * 303 | * @param {Event} event 304 | * 305 | * @return {Void} 306 | */ 307 | function eventHandler(event) { 308 | /*jshint validthis:true */ 309 | if (options.showOn === options.hideOn) { 310 | self.toggle(this); 311 | } else { 312 | self[event.type === options.showOn ? 'show' : 'hide'](this); 313 | } 314 | } 315 | 316 | /** 317 | * Mutations handler. 318 | * 319 | * @param {Array} mutations 320 | * 321 | * @return {Void} 322 | */ 323 | function mutationsHandler(mutations) { 324 | var added, removed, i, l; 325 | for (var m = 0, ml = mutations.length; m < ml; m++) { 326 | added = mutations[m].addedNodes; 327 | removed = mutations[m].removedNodes; 328 | for (i = 0, l = added.length; i < l; i++) { 329 | self.add(added[i]); 330 | } 331 | for (i = 0, l = removed.length; i < l; i++) { 332 | self.remove(removed[i]); 333 | } 334 | } 335 | } 336 | 337 | // Construct 338 | (function () { 339 | self.container = container; 340 | self.options = options = extend(objectCreate(Tooltips.defaults), options); 341 | self.ID = TID = options.key + Math.random().toString(36).slice(2); 342 | self.elements = []; 343 | 344 | // Create tips selector 345 | self.selector = '[data-' + options.key + ']'; 346 | 347 | // Load tips 348 | self.reload(); 349 | 350 | // Create mutations observer 351 | if (options.observe && MObserver) { 352 | observer = new MObserver(mutationsHandler); 353 | observer.observe(self.container, { 354 | childList: true, 355 | subtree: true 356 | }); 357 | } 358 | }()); 359 | } 360 | 361 | /** 362 | * Expose Tooltip. 363 | */ 364 | Tooltips.Tooltip = Tooltip; 365 | 366 | /** 367 | * Default Tooltips options. 368 | * 369 | * @type {Object} 370 | */ 371 | Tooltips.defaults = { 372 | tooltip: {}, // Options for individual Tooltip instances. 373 | key: 'tooltip', // Tooltips data attribute key. 374 | showOn: 'mouseenter', // Show tooltip event. 375 | hideOn: 'mouseleave', // Hide tooltip event. 376 | observe: 0 // Enable mutation observer (used only when supported). 377 | }; --------------------------------------------------------------------------------