├── .gitignore ├── .jshintrc ├── css ├── popover.tablet.css ├── popover.css └── popover.ie.css ├── src ├── fly.functions.js ├── fly.js ├── mixin.rect.js ├── jquery.fly.js ├── popover.css ├── mixin.position.js ├── component.dropdown.js ├── component.tooltip.js └── component.base.js ├── bower.json ├── package.json ├── Gruntfile.js ├── README.md └── fly.full.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "es3": true 3 | } 4 | -------------------------------------------------------------------------------- /css/popover.tablet.css: -------------------------------------------------------------------------------- 1 | .fly-popover { 2 | box-shadow: 0 1px 5px rgba(0, 0, 0, .8); 3 | } 4 | -------------------------------------------------------------------------------- /src/fly.functions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Hide all fly instances 3 | */ 4 | fly.hideAll = function () { 5 | for (var i = 0, ln = fly._instances.length; i < ln; i++) { 6 | fly._instances[i].hide(); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fly", 3 | "version": "0.0.19", 4 | "homepage": "https://github.com/artjock/fly", 5 | "authors": [ 6 | "Artur Burtsev " 7 | ], 8 | "license": "MIT", 9 | "ignore": [ 10 | "**/.*", 11 | "node_modules", 12 | "bower_components", 13 | "test", 14 | "tests" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fly", 3 | "version": "0.1.0", 4 | "author": "Artur Burtsev ", 5 | "private": true, 6 | "devDependencies": { 7 | "autoprefixer": "^6.3.6", 8 | "grunt": "~0.4.2", 9 | "grunt-contrib-concat": "~0.3.0", 10 | "grunt-contrib-jshint": "~0.8.0", 11 | "grunt-contrib-watch": "~0.5.3", 12 | "grunt-postcss": "^0.7.1" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/fly.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Wrapper 4 | */ 5 | var fly = { 6 | /** 7 | * Instances count 8 | * @private 9 | * @type Number 10 | */ 11 | _count: 0, 12 | 13 | /** 14 | * Mixins collection 15 | * @private 16 | * @type Object 17 | */ 18 | _mixin: {}, 19 | 20 | /** 21 | * Instances 22 | * @private 23 | * @type Array 24 | */ 25 | _instances: [] 26 | }; 27 | -------------------------------------------------------------------------------- /src/mixin.rect.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Calculates dimentions of DOM element 4 | * 5 | * @mixin 6 | * 7 | * @type Object 8 | * @property {Number} top 9 | * @property {Number} left 10 | * @property {Number} right 11 | * @property {Number} bottom 12 | * @property {Number} width 13 | * @property {Number} height 14 | */ 15 | fly._mixin.rect = function($el) { 16 | var rect = $el[0].getBoundingClientRect(); 17 | 18 | // IE 19 | if ( !('width' in rect) ) { 20 | rect = $.extend({}, rect); 21 | rect.width = $el.outerWidth(); 22 | rect.height = $el.outerHeight(); 23 | } 24 | 25 | return rect; 26 | }; 27 | -------------------------------------------------------------------------------- /src/jquery.fly.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * jQuery extension for dropdown 4 | * 5 | * @requires _base.js 6 | */ 7 | 8 | /** 9 | * Registers fly component, as jquery plugin 10 | * @static 11 | * @param {String} name 12 | * @param {Fly} component 13 | */ 14 | fly.register$ = function(type, component) { 15 | if ( component === fly._base || !(component instanceof fly._base._ctor) || $.fn[type] ) { 16 | return; 17 | } 18 | 19 | var expando = 'fly_' + type + '_' + (+new Date()); 20 | 21 | $.fn[ type ] = function(options) { 22 | 23 | var retVal; 24 | 25 | this.each(function(i) { 26 | if (retVal) { return false; } 27 | 28 | var $el = $(this); 29 | var old = $el.data(expando); 30 | 31 | switch (options) { 32 | 33 | case 'instance': retVal = old; break; 34 | case 'destroy': destroy(old); break; 35 | default: 36 | destroy(old); 37 | $el.data(expando, component.create($el, options)); 38 | } 39 | }); 40 | 41 | return retVal || this; 42 | 43 | function destroy(component) { 44 | if (component) { 45 | component.handle().removeData(expando); 46 | component._destroy(); 47 | } 48 | } 49 | }; 50 | }; 51 | 52 | $.each(fly, fly.register$); 53 | -------------------------------------------------------------------------------- /src/popover.css: -------------------------------------------------------------------------------- 1 | .fly-popover { 2 | position: absolute; 3 | background: #FFF; 4 | box-shadow: 0 10px 20px -4px rgba(0, 0, 0, .4), 0 0 0 1px rgba(0, 0, 0, .1); 5 | transition: opacity .2s, visibility .2s, transform .2s; 6 | transform: translateY(0); 7 | z-index: 999; 8 | } 9 | .fly-popover::before { 10 | content: ''; 11 | position: absolute; 12 | width: 20px; 13 | height: 20px; 14 | border: inherit; 15 | background: inherit; 16 | box-shadow: inherit; 17 | z-index: -1; 18 | } 19 | .fly-popover_body_bottom::before { 20 | top: -5px; 21 | left: 50%; 22 | margin-left: -10px; 23 | transform: scaleX(0.9) rotate(45deg); 24 | } 25 | .fly-popover_body_top::before { 26 | bottom: -5px; 27 | left: 50%; 28 | margin-left: -10px; 29 | transform: scaleX(0.9) rotate(45deg); 30 | } 31 | .fly-popover_body_left::before { 32 | top: 50%; 33 | right: -5px; 34 | margin-top: -10px; 35 | transform: scaleY(0.9) rotate(45deg); 36 | } 37 | .fly-popover_body_right::before { 38 | top: 50%; 39 | left: -5px; 40 | margin-top: -10px; 41 | transform: scaleY(0.9) rotate(45deg); 42 | } 43 | .fly-popover_arrow_top::before { 44 | top: 15px; 45 | } 46 | .fly-popover_arrow_left::before { 47 | left: 15px; 48 | } 49 | .fly-popover_arrow_right::before { 50 | left: auto; 51 | right: 5px; 52 | } 53 | .fly-popover_arrow_bottom::before { 54 | top: auto; 55 | bottom: 5px; 56 | } 57 | 58 | .fly-popover::after { 59 | content: ''; 60 | position: absolute; 61 | top: 0; 62 | left: 0; 63 | right: 0; 64 | bottom: 0; 65 | background: inherit; 66 | border-radius: inherit; 67 | z-index: -1; 68 | } 69 | .fly-popover--hidden { 70 | opacity: 0; 71 | visibility: hidden; 72 | transform: translateY(10px); 73 | } 74 | -------------------------------------------------------------------------------- /src/mixin.position.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Calculates fly position depending on handle 4 | * 5 | * @requires mixin.rect 6 | * @mixin 7 | * @type Object 8 | * @property {Number} top 9 | * @property {Number} left 10 | */ 11 | fly._mixin.position = function() { 12 | var css = {}; 13 | 14 | var $w = $(window); 15 | var pos = this.options.position.split(' '); 16 | var ars = this.options.arrowSize; 17 | var aro = this.options.arrowOffset; 18 | 19 | var body = pos.shift(); 20 | var arrow = pos.shift(); 21 | 22 | var a = {}; 23 | var p = this._rect( this.root() ); 24 | var h = this._rect( this.handle() ); 25 | var f = this.root().css('position') === 'fixed'; 26 | var s = f ? {top: 0, left: 0} : {top: $w.scrollTop(), left: $w.scrollLeft()}; 27 | 28 | switch (arrow) { 29 | case 'top': a.top = h.height / 2 - aro; break; 30 | case 'left': a.left = h.width / 2 - aro; break; 31 | case 'right': a.left = h.width / 2 - p.width + aro; break; 32 | case 'bottom': a.top = h.height / 2 - p.height + aro; break; 33 | default /*center*/: 34 | a.top = (h.height - p.height) / 2; 35 | a.left = (h.width - p.width) / 2; 36 | break; 37 | } 38 | 39 | switch (body) { 40 | case 'left': 41 | css.top = s.top + h.top + a.top; 42 | css.left = s.left + h.left - p.width - ars; 43 | break; 44 | case 'right': 45 | css.top = s.top + h.top + a.top; 46 | css.left = s.left + h.left + h.width + ars; 47 | break; 48 | case 'top': 49 | css.top = s.top + h.top - p.height - ars; 50 | css.left = s.left + h.left + a.left; 51 | break; 52 | default /*bottom*/: 53 | css.top = s.top + h.top + h.height + ars; 54 | css.left = s.left + h.left + a.left; 55 | } 56 | 57 | return css; 58 | }; 59 | -------------------------------------------------------------------------------- /css/popover.css: -------------------------------------------------------------------------------- 1 | .fly-popover { 2 | position: absolute; 3 | background: #FFF; 4 | box-shadow: 0 10px 20px -4px rgba(0, 0, 0, .4), 0 0 0 1px rgba(0, 0, 0, .1); 5 | transition: opacity .2s, visibility .2s, -webkit-transform .2s; 6 | transition: opacity .2s, visibility .2s, transform .2s; 7 | transition: opacity .2s, visibility .2s, transform .2s, -webkit-transform .2s; 8 | -webkit-transform: translateY(0); 9 | transform: translateY(0); 10 | z-index: 999; 11 | } 12 | .fly-popover::before { 13 | content: ''; 14 | position: absolute; 15 | width: 20px; 16 | height: 20px; 17 | border: inherit; 18 | background: inherit; 19 | box-shadow: inherit; 20 | z-index: -1; 21 | } 22 | .fly-popover_body_bottom::before { 23 | top: -5px; 24 | left: 50%; 25 | margin-left: -10px; 26 | -webkit-transform: scaleX(0.9) rotate(45deg); 27 | transform: scaleX(0.9) rotate(45deg); 28 | } 29 | .fly-popover_body_top::before { 30 | bottom: -5px; 31 | left: 50%; 32 | margin-left: -10px; 33 | -webkit-transform: scaleX(0.9) rotate(45deg); 34 | transform: scaleX(0.9) rotate(45deg); 35 | } 36 | .fly-popover_body_left::before { 37 | top: 50%; 38 | right: -5px; 39 | margin-top: -10px; 40 | -webkit-transform: scaleY(0.9) rotate(45deg); 41 | transform: scaleY(0.9) rotate(45deg); 42 | } 43 | .fly-popover_body_right::before { 44 | top: 50%; 45 | left: -5px; 46 | margin-top: -10px; 47 | -webkit-transform: scaleY(0.9) rotate(45deg); 48 | transform: scaleY(0.9) rotate(45deg); 49 | } 50 | .fly-popover_arrow_top::before { 51 | top: 15px; 52 | } 53 | .fly-popover_arrow_left::before { 54 | left: 15px; 55 | } 56 | .fly-popover_arrow_right::before { 57 | left: auto; 58 | right: 5px; 59 | } 60 | .fly-popover_arrow_bottom::before { 61 | top: auto; 62 | bottom: 5px; 63 | } 64 | 65 | .fly-popover::after { 66 | content: ''; 67 | position: absolute; 68 | top: 0; 69 | left: 0; 70 | right: 0; 71 | bottom: 0; 72 | background: inherit; 73 | border-radius: inherit; 74 | z-index: -1; 75 | } 76 | .fly-popover--hidden { 77 | opacity: 0; 78 | visibility: hidden; 79 | -webkit-transform: translateY(10px); 80 | transform: translateY(10px); 81 | } 82 | -------------------------------------------------------------------------------- /src/component.dropdown.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Dropdown 4 | * 5 | * @requires fly._base 6 | * @requires mixin.position 7 | * 8 | * @extends fly._base 9 | */ 10 | fly.dropdown = fly._base.extend({ 11 | 12 | actions: { 13 | 'click': 'onclick' 14 | }, 15 | 16 | /** 17 | * Default click action for dropdown 18 | */ 19 | onclick: function() { 20 | this.toggle(); 21 | }, 22 | 23 | /** 24 | * Default window.onresize handler for dropdown 25 | */ 26 | onresize: function() { 27 | if ( this.hidden() ) { return; } 28 | 29 | this.root().css( this._position() ); 30 | }, 31 | 32 | /** 33 | * Base _action + window resize handler 34 | * 35 | * @private 36 | * @param {Boolean} mode 37 | */ 38 | _action: function(mode) { 39 | 40 | fly._base._action.apply(this, arguments); 41 | fly._base._action.call(this, 42 | mode, {'resize': 'onresize'}, $(window)); 43 | 44 | this._autohide(mode); 45 | }, 46 | 47 | /** 48 | * Dropdown can be closed by clicking outside or pressing ESC 49 | * @private 50 | * @param {boolean} mode 51 | */ 52 | _autohide: function(mode) { 53 | var that = this; 54 | var events = 'click' + that.ens + ' keydown' + that.ens + ' touchstart' + that.ens; 55 | 56 | if (!mode) { return; } 57 | 58 | this 59 | .bind(this.events.show, function() { setTimeout(onshow, 0); }) 60 | .bind(this.events.hide, function() { $(document).unbind(events); }); 61 | 62 | function onshow() { 63 | $(document).bind(events, function(evt) { 64 | var el = evt.target, root = that.root()[0], handle = that.handle()[0]; 65 | if ( 66 | evt.type === 'keydown' && evt.which === 27 || 67 | (evt.type === 'click' || evt.type === 'touchstart' ) && 68 | el !== root && !$.contains(root, el) && 69 | el !== handle && !$.contains(handle, el) 70 | ) { 71 | that.hide(); 72 | } 73 | }); 74 | } 75 | }, 76 | 77 | /** 78 | * Deafult settings for dropdown 79 | * @type Object 80 | */ 81 | defaults: {}, 82 | 83 | _rect: fly._mixin.rect, 84 | _position: fly._mixin.position 85 | }); 86 | 87 | -------------------------------------------------------------------------------- /src/component.tooltip.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Tooltip 4 | * 5 | * @requires fly._base 6 | * @requires mixin.position 7 | * 8 | * @extends fly._base 9 | */ 10 | fly.tooltip = fly._base.extend({ 11 | 12 | actions: { 13 | 'mouseenter': 'onmouseenter', 14 | 'mouseleave': 'onmouseleave' 15 | }, 16 | 17 | /** 18 | * Show delay timeout reference 19 | * @private 20 | */ 21 | _showTimeout: null, 22 | 23 | /** 24 | * Hide delay timeout reference 25 | * @private 26 | */ 27 | _hideTimeout: null, 28 | 29 | /** 30 | * Default mouseenter action for tooltip 31 | */ 32 | onmouseenter: function() { 33 | var that = this; 34 | 35 | if (this._hideTimeout) { 36 | clearTimeout(this._hideTimeout); 37 | this._hideTimeout = null; 38 | } 39 | 40 | if (this.hidden()) { 41 | this._showTimeout = setTimeout(function() { 42 | that.show(); 43 | }, this.options.showDelay); 44 | } 45 | }, 46 | 47 | /** 48 | * Default mouseleave action for tooltip 49 | */ 50 | onmouseleave: function() { 51 | var that = this; 52 | 53 | if (this._showTimeout) { 54 | clearTimeout(this._showTimeout); 55 | this._showTimeout = null; 56 | } 57 | 58 | this._hideTimeout = setTimeout(function() { 59 | that.hide(); 60 | }, this.options.hideDelay); 61 | }, 62 | 63 | _action: function(mode) { 64 | fly._base._action.apply(this, arguments); 65 | 66 | if (this.options.keepOnContent) { 67 | this._keepOnContent(mode); 68 | } 69 | 70 | }, 71 | 72 | _keepOnContent: function(mode) { 73 | var that = this; 74 | var rrevent = this.events.rootready + '._keepOnContent'; 75 | 76 | if (mode) { 77 | this.bind(rrevent, function() { 78 | fly._base._action.call(that, mode, that.actions, that.root()); 79 | }); 80 | } else { 81 | this.unbind(rrevent); 82 | fly._base._action.call(this, mode, this.actions, this.root()); 83 | } 84 | }, 85 | 86 | /** 87 | * Deafult settings for tooltip 88 | * @type Object 89 | */ 90 | defaults: { 91 | showDelay: 300, 92 | hideDelay: 300 93 | }, 94 | 95 | _rect: fly._mixin.rect, 96 | _position: fly._mixin.position 97 | }); 98 | -------------------------------------------------------------------------------- /css/popover.ie.css: -------------------------------------------------------------------------------- 1 | .fly-popover { 2 | border: 1px solid #CCC; 3 | background: #FFF; 4 | box-shadow: none; 5 | } 6 | 7 | .fly-popover:before, 8 | .fly-popover:after { 9 | content: ''; 10 | position: absolute; 11 | 12 | 13 | width: 0; 14 | height: 0; 15 | 16 | background: none; 17 | border-style: solid; 18 | border-color: transparent; 19 | } 20 | 21 | .fly-popover_body_bottom:before { 22 | left: 50%; 23 | bottom: 100%; 24 | margin-left: -10px; 25 | border-width: 10px 10px; 26 | border-bottom-color: inherit; 27 | } 28 | .fly-popover_body_bottom:after { 29 | left: 50%; 30 | bottom: 100%; 31 | margin-left: -8px; 32 | border-width: 8px 8px; 33 | border-bottom-color: #FFF; 34 | } 35 | 36 | .fly-popover_body_top:before { 37 | top: 100%; 38 | left: 50%; 39 | margin-left: -10px; 40 | border-width: 10px 10px; 41 | border-top-color: inherit; 42 | } 43 | .fly-popover_body_top:after { 44 | top: 100%; 45 | left: 50%; 46 | margin-left: -8px; 47 | border-width: 8px 8px; 48 | border-top-color: #FFF; 49 | } 50 | 51 | .fly-popover_body_left:before { 52 | top: 50%; 53 | left: 100%; 54 | margin-top: -10px; 55 | border-width: 10px 10px; 56 | border-left-color: inherit; 57 | } 58 | .fly-popover_body_left:after { 59 | top: 50%; 60 | left: 100%; 61 | margin-top: -8px; 62 | border-width: 8px 8px; 63 | border-left-color: #FFF; 64 | } 65 | 66 | .fly-popover_body_right:before { 67 | top: 50%; 68 | right: 100%; 69 | margin-top: -10px; 70 | border-width: 10px 10px; 71 | border-right-color: inherit; 72 | } 73 | .fly-popover_body_right:after { 74 | top: 50%; 75 | right: 100%; 76 | margin-top: -8px; 77 | border-width: 8px 8px; 78 | border-right-color: #FFF; 79 | } 80 | 81 | .fly-popover_arrow_top:after, 82 | .fly-popover_arrow_top:before { 83 | top: 14px; 84 | } 85 | .fly-popover_arrow_left:after, 86 | .fly-popover_arrow_left:before { 87 | left: 14px; 88 | } 89 | .fly-popover_arrow_right:before { 90 | left: auto; 91 | right: 4px; 92 | } 93 | .fly-popover_arrow_right:after { 94 | left: auto; 95 | right: 6px; 96 | } 97 | .fly-popover_arrow_bottom:before { 98 | top: auto; 99 | bottom: 4px; 100 | } 101 | .fly-popover_arrow_bottom:after { 102 | top: auto; 103 | bottom: 6px; 104 | } 105 | 106 | .fly-popover--hidden:before, 107 | .fly-popover--hidden:after { 108 | content: ' '; 109 | } 110 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.initConfig({ 4 | bower: grunt.file.readJSON('bower.json'), 5 | concat: { 6 | options: { 7 | banner: '/*!\n' + 8 | ' * @name <%= bower.name %>\n' + 9 | ' * @version v<%= bower.version %>\n' + 10 | ' * @author <%= bower.authors[0] %>\n' + 11 | ' * @see <%= bower.homepage %>\n' + 12 | ' */\n' + 13 | ';(function(root, factory) {\n' + 14 | ' if (typeof module === \'object\' && typeof module.exports === \'object\') {\n' + 15 | ' module.exports = factory(require(\'jquery\'));\n' + 16 | ' } else {\n' + 17 | ' root.jQuery.fly = factory(root.jQuery);\n' + 18 | ' }\n' + 19 | '})(this, function($) {\n', 20 | footer: ' return fly;\n' + 21 | '});' 22 | }, 23 | full: { 24 | src: [ 25 | 'src/fly.js', 26 | 'src/component.base.js', 27 | 'src/mixin.rect.js', 28 | 'src/mixin.position.js', 29 | 'src/component.tooltip.js', 30 | 'src/component.dropdown.js', 31 | 'src/fly.functions.js', 32 | 'src/jquery.fly.js' 33 | ], 34 | dest: 'fly.full.js' 35 | } 36 | }, 37 | watch: { 38 | src: { 39 | files: ['Gruntfile.js', 'src/*.js', 'src/*.css'], 40 | tasks: ['concat', 'jshint', 'postcss:dist'] 41 | } 42 | }, 43 | jshint: { 44 | options: { 45 | jshintrc: true 46 | }, 47 | all: ['Gruntfile.js', 'src/*.js', '*.js'] 48 | }, 49 | postcss: { 50 | options: { 51 | processors: [ 52 | require('autoprefixer')({ 53 | browsers: ['last 2 versions'] 54 | }) 55 | ] 56 | }, 57 | dist: { 58 | src: 'src/popover.css', 59 | dest: 'css/popover.css' 60 | } 61 | } 62 | }); 63 | 64 | grunt.loadNpmTasks('grunt-postcss'); 65 | grunt.loadNpmTasks('grunt-contrib-jshint'); 66 | grunt.loadNpmTasks('grunt-contrib-concat'); 67 | grunt.loadNpmTasks('grunt-contrib-watch'); 68 | 69 | grunt.registerTask('default', ['concat', 'jshint', 'postcss:dist']); 70 | }; 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FLY 2 | =============== 3 | Popover/dropdown/tooltip/popup/whateveryouwant library.
4 | Requires jQuery 1.4+. Works for modern browsers and IE8+.
5 | Contains [_base](#base) component you can inherit from, ready to use [dropdown](#dropdown) and [tooltip](#tooltip) components, [mixins](#mixins), [jQuery plugin](#jqueryplugin) and some css styling.
6 | 7 | See [Examples](http://artjock.github.io/fly/). 8 | 9 | ## Base 10 | Is an abstract class you can extend to build your own popover type. You can find it at `fly._base` 11 | 12 | - [extend(extra)](#extendextra) 13 | - [create(options)](#createoptions) 14 | - [delegate(selector, options)](#delegateselectoroptions) 15 | - [fly.register$](#flyregister) 16 | - [defaults](#defaults) 17 | - [options.content](#optionscontent) 18 | - [show(content)](#show) 19 | - [hide()](#hide) 20 | - [toggle(mode)](#togglemode) 21 | - [redraw(done)](#redraw) 22 | - [hidden()](#hidden) 23 | - [root()](#root) 24 | - [handle()](#handle) 25 | - [init()](#init) 26 | - [destroy()](#destroy) 27 | - [events](#events) 28 | - [bind()](#events) 29 | - [unbind()](#events) 30 | - [one()](#events) 31 | - [trigger()](#events) 32 | 33 | ### extend(extra) 34 | Creates new class, based on context's class: 35 | ```js 36 | var titletip = fly.tooltip.extend({ 37 | defaults: { 38 | content: function() { 39 | return this.handle().attr('title'); 40 | } 41 | } 42 | }); 43 | ``` 44 | 45 | ### create(options) 46 | Creates an instance of class with a given options: 47 | ```js 48 | $('.js-titletip').each(function() { 49 | titletip.create(this, {position: $(this).data('position')); 50 | }); 51 | ``` 52 | 53 | ### delegate(selector, options) 54 | Delegates class on `selector`, creates instance with `options` on every first event from `actions` 55 | ```js 56 | fly.tooltip 57 | .extend({}) 58 | .delegate('.js-live-tooltip', { content: 'live' }) 59 | ; 60 | ``` 61 | 62 | ### defaults 63 | Objects with defaults options: 64 | ``` 65 | { 66 | baseClass: '...', // class to be added to created root 67 | hideClass: '...', // class to be added on .hide() and removed on .show() 68 | extraClass: '...', // any additional classes 69 | 70 | content: '', // defines how to get content for the popover 71 | redrawOnShow: true, // recalculate content on every/first show event 72 | register$: 'titletip' // will register titletip as jQuery plugin 73 | } 74 | ``` 75 | 76 | ### options.content 77 | Defines rules to get popover's content, can be: 78 | - `string`, so `root` will be filled with this string 79 | - `function() { return '...'; }` sync function, `root` will be filled with the returned value 80 | - `function(done) { ... done(result); }` async function, `root` will be filled with the `result` of `done`, when it will be ready. 81 | 82 | ```js 83 | // static string 84 | { 85 | content: '

Contacts

0000' 86 | } 87 | // sync function 88 | { 89 | content: function() { 90 | return this.handle().attr('data-title'); 91 | } 92 | } 93 | // async function 94 | { 95 | content: function(done) { 96 | $.get('/api/list', function(html) { 97 | done(html); 98 | }); 99 | } 100 | } 101 | ``` 102 | ### show(content) 103 | Shows popover and triggers appropriate events. If `content` passed it will be shown, otherwise `options.content` will be called to calculate content. 104 | 105 | ### hide()/toggle(mode) 106 | Hides/toggles popover 107 | 108 | ### hidden() 109 | Returns `true` if popover is visible and `false` otherwise 110 | 111 | ### root() 112 | Lazy `root` initiation, will return the `root` and ensure it was created and appropriate evens triggered. By default it creates root on first `show()` 113 | 114 | ### handle() 115 | jQuery object, representing `handle` which should trigger popover's show/hide actions. 116 | 117 | ### ens 118 | Event namespace, unique for every instance, use it to add global events and easily detach them. 119 | 120 | ### init() 121 | Will be called during instance creation, you can bind your events or whatever you want 122 | 123 | ### destroy() 124 | Will be called during popover destroying, you can clean it up if you added something special 125 | 126 | ### bind/unbind/one/trigger 127 | Just a jQuery eventing method attached to each instance, so each instance can be an event emitter. 128 | 129 | ### Events: 130 | - `show` on popover show 131 | - `hide` on popover hide 132 | - `rootready` on root element created (first `root()` call) 133 | 134 | You can use it as: 135 | ``` 136 | var popoverus = fly._base.extend({ 137 | init: function() { 138 | this.on(this.EVENTS.ROOTREADY, function() { 139 | // bind something on root 140 | }) 141 | } 142 | }); 143 | ``` 144 | 145 | ## dropdown 146 | Extends `defaults` with 147 | ``` 148 | { 149 | baseClass: 'fly-dropdown', 150 | hideClass: 'fly-dropdown_hidden', 151 | extraClass: '', 152 | 153 | position: 'bottom center', // where to open dropdown and where to show tail 154 | arrowSize: 10 // offset between dropdown and handle 155 | } 156 | ``` 157 | Adds `handle`'s `action`, which trigger dropdown on `handle` click and closes on click outside dropdown and `ESC` 158 | 159 | ## tooltip 160 | Extends `defaults` with 161 | ``` 162 | { 163 | baseClass: 'fly-tooltip', 164 | hideClass: 'fly-tooltip_hidden', 165 | extraClass: '', 166 | 167 | position: 'bottom center', // where to open tooltip and where to show tail 168 | arrowSize: 10 // offset between tooltip and handle 169 | } 170 | ``` 171 | Adds `handle`'s `action`, which show tooltip on `handle`'s mouseenter with 300ms delay and hides it on mouseleave 172 | 173 | ## Mixins 174 | 175 | ### rect($el) 176 | Returns offsets and dimensions of the `$el` (top/left/right/bottom/width/height) 177 | 178 | ### position() 179 | Calculates popover position relative to `handle` according to `options.position` 180 | 181 | ## jQuery plugin 182 | For every non-base component in `fly.*` (`tooltip`, `dropdown`) creates `$.fn` function, which accepts `options` or command, you can use it as: 183 | ``` 184 | $('.js-handle').dropdown({content: 'dropdown'}); 185 | $('.js-handle').dropdown('instance').hidden(); 186 | $('.js-handle').dropdown('destroy'); 187 | ``` 188 | 189 | ### fly.register$ 190 | You can register your fly class as a jQuery plugin: 191 | ```js 192 | fly.register$('titletip', titletip); 193 | // then 194 | $('.js-titletip').titletip({position: $(this).data('position'); 195 | ``` 196 | 197 | ## Global functions 198 | 199 | ### fly.hideAll 200 | Hides all current instances of fly. -------------------------------------------------------------------------------- /src/component.base.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | fly._base = { 4 | 5 | /** 6 | * Avaliable events 7 | * @enum 8 | */ 9 | events: { 10 | hide: 'hide', 11 | show: 'show', 12 | rootready: 'rootready' 13 | }, 14 | 15 | _cuid: fly._count++, 16 | 17 | /** 18 | * Shared constructor 19 | * @private 20 | * @type Function 21 | */ 22 | _ctor: function fly_ctor() {}, 23 | 24 | /** 25 | * DOM root 26 | * @type jQuery 27 | * @private 28 | */ 29 | _$root: null, 30 | 31 | /** 32 | * Open/close trigger 33 | * @type jQuery 34 | * @private 35 | */ 36 | _$handle: null, 37 | 38 | /** 39 | * Event emmiter 40 | * @type jQuery 41 | */ 42 | _emitter: null, 43 | 44 | /** 45 | * Default class options 46 | * @param Object 47 | * @private 48 | * @static 49 | */ 50 | defaults: { 51 | content: '', 52 | position: 'bottom center', 53 | baseClass: 'fly-popover', 54 | hideClass: 'fly-popover--hidden', 55 | extraClass: '', 56 | redrawOnShow: true, 57 | arrowOffset: 15, 58 | arrowSize: 10 59 | }, 60 | 61 | /** 62 | * Events namespace 63 | * @type String 64 | */ 65 | ens: '', 66 | 67 | /** 68 | * Options 69 | * @param Object 70 | */ 71 | options: null, 72 | 73 | 74 | 75 | /** 76 | * Creates instance of fly 77 | * @param {jQuery} handle 78 | * @param {Object} options 79 | * @return fly 80 | */ 81 | create: function(handle, options) { 82 | 83 | var inst = this.extend({ 84 | ens: '.ns' + fly._count++, 85 | _$handle: $(handle), 86 | _emitter: $({}) 87 | }); 88 | 89 | inst.options = $.extend({}, inst.defaults, options); 90 | 91 | fly._instances.push(inst); 92 | 93 | return inst._init(); 94 | }, 95 | 96 | /** 97 | * Extends fly's class and returns new one 98 | * @param {Object} extra 99 | * @return fly 100 | */ 101 | extend: function(extra) { 102 | this._ctor.prototype = this; 103 | 104 | if (extra && 'defaults' in extra) { 105 | extra.defaults = 106 | $.extend({}, this.defaults, extra.defaults); 107 | } 108 | 109 | var component = $.extend(new this._ctor(), extra); 110 | 111 | component._cuid = fly._count++; 112 | 113 | if (extra.register$) { 114 | fly.register$(extra.register$, component); 115 | } 116 | 117 | return component; 118 | }, 119 | 120 | /** 121 | * Adds listeners for component's action list 122 | * @param {string} selector 123 | * @param {Object} options 124 | */ 125 | delegate: function(selector, options) { 126 | var that = this; 127 | var expando = 'fly_delegated_' + that._cuid; 128 | 129 | $.each(that.actions, function(action) { 130 | $(document.body).delegate(selector, action, function() { 131 | if ($(this).data(expando)) { return; } 132 | 133 | var inst = that.create(this, options); 134 | inst.handle().data(expando, 1); 135 | inst._actionHandler(inst.actions[action]).apply(inst, arguments); 136 | }); 137 | }); 138 | }, 139 | 140 | /** 141 | * Rootrady callback 142 | * @virtual 143 | */ 144 | onrootready: function() {}, 145 | 146 | /** 147 | * Lazy fly's getter 148 | * @return jQuery 149 | */ 150 | root: function() { 151 | 152 | if (!this._$root) { 153 | var opt = this.options; 154 | 155 | this._$root = $('
') 156 | .addClass(opt.baseClass) 157 | .addClass(opt.extraClass) 158 | .addClass(opt.hideClass) 159 | .appendTo('body'); 160 | 161 | this.trigger(this.events.rootready); 162 | } 163 | 164 | return this._$root; 165 | }, 166 | 167 | /** 168 | * Handle getter 169 | * @return jQuery 170 | */ 171 | handle: function() { 172 | return this._$handle; 173 | }, 174 | 175 | /** 176 | * Initializes fly 177 | * @private 178 | * @return fly 179 | */ 180 | _init: function() { 181 | this._action(true); 182 | this.bind(this.events.rootready, $.proxy(this.onrootready, this)); 183 | this.init(); 184 | return this; 185 | }, 186 | 187 | /** 188 | * Cleans up fly 189 | */ 190 | _destroy: function() { 191 | this.destroy(); 192 | 193 | if (this._$root) { 194 | this._$root.remove(); 195 | this._$root = null; 196 | } 197 | this._action(false); 198 | 199 | for (var i = 0, ln = fly._instances.length; i < ln; i++) { 200 | if (fly._instances[i] === this) { 201 | fly._instances.splice(i, 1); 202 | break; 203 | } 204 | } 205 | }, 206 | 207 | /** 208 | * Binds action to handle 209 | * @private 210 | * @param {Boolean} mode 211 | */ 212 | _action: function(mode, actions, handle) { 213 | handle = handle || this.handle(); 214 | actions = actions || this.actions; 215 | 216 | for (var type in actions) { 217 | 218 | if (mode) { 219 | handle.bind(type + this.ens, this._actionHandler( actions[type] )); 220 | } else { 221 | handle.unbind(type + this.ens); 222 | } 223 | 224 | } 225 | }, 226 | 227 | _actionHandler: function(action) { 228 | return typeof action === 'string' ? 229 | $.proxy(this[action], this) : $.proxy(action, this); 230 | }, 231 | 232 | /** 233 | * Calculates content and run callback * 234 | * @private 235 | * @param {Function} done 236 | */ 237 | _content: function(done) { 238 | var content = this.options.content; 239 | 240 | if (typeof content === 'function') { 241 | if (content.length) { 242 | content.call(this, done); 243 | } else { 244 | done( content.call(this) ); 245 | } 246 | } else { 247 | done( content ); 248 | } 249 | }, 250 | 251 | /** 252 | * Fills root element with content 253 | * 254 | * @param {String} content 255 | */ 256 | _render: function(content) { 257 | this.root() 258 | .html(content || ''); 259 | this._rendered = true; 260 | }, 261 | 262 | /** 263 | * Generates CSS classes for current position options 264 | */ 265 | _modCss: function() { 266 | var mod = this.options.position.split(' '); 267 | var base = this.options.baseClass; 268 | 269 | return [base + '_body_' + mod[0], base + '_arrow_' + mod[1]].join(' '); 270 | }, 271 | 272 | /** 273 | * Calculates fly's position 274 | * @abstract 275 | * @private 276 | */ 277 | _position: function() {}, 278 | 279 | /** 280 | * Public init, should be overwritten 281 | * @abstract 282 | */ 283 | init: function() {}, 284 | 285 | /** 286 | * Public destroy, Should be overwritten 287 | * @abstract 288 | */ 289 | destroy: function() {}, 290 | 291 | /** 292 | * Shows fly 293 | * 294 | * If you will pass a content, it will force rendering 295 | * 296 | * @param {HTMLElement|String} content 297 | */ 298 | show: function(content) { 299 | var redraw = this.options.redrawOnShow || !this._rendered; 300 | 301 | if (redraw && !arguments.length) { 302 | return this._content( $.proxy(this.show, this) ); 303 | } 304 | 305 | if (arguments.length) { 306 | this._render(content); 307 | } 308 | 309 | this.trigger(this.events.show); 310 | 311 | this.root() 312 | .css( this._position() ) 313 | .addClass( this._modCss() ) 314 | .removeClass( this.options.hideClass ); 315 | }, 316 | 317 | /** 318 | * Hides fly 319 | */ 320 | hide: function() { 321 | if (this.hidden()) { return; } 322 | 323 | this.trigger(this.events.hide); 324 | 325 | this.root() 326 | .addClass(this.options.hideClass); 327 | }, 328 | 329 | /** 330 | * Force content rendering 331 | * @param {function} done 332 | */ 333 | redraw: function(done) { 334 | var that = this; 335 | 336 | this._content(function(content) { 337 | that._render(content); 338 | if (typeof done === 'function') { 339 | done(content); 340 | } 341 | }); 342 | }, 343 | 344 | /** 345 | * Toggles visibility of the fly 346 | * @param {Boolean} mode 347 | */ 348 | toggle: function(mode) { 349 | mode = arguments.length ? mode : this.hidden(); 350 | this[mode ? 'show' : 'hide'](); 351 | }, 352 | 353 | /** 354 | * Returns true if element is hidden 355 | * @return Boolean 356 | */ 357 | hidden: function() { 358 | return !this._$root || this.root().hasClass(this.options.hideClass); 359 | } 360 | }; 361 | 362 | 363 | /** 364 | * Adds jquery events support 365 | */ 366 | $.each(['bind', 'unbind', 'one', 'trigger'], function(i, type) { 367 | fly._base[type] = function() { 368 | this._emitter[type].apply(this._emitter, arguments); 369 | return this; 370 | }; 371 | }); 372 | 373 | -------------------------------------------------------------------------------- /fly.full.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @name fly 3 | * @version v0.0.19 4 | * @author Artur Burtsev 5 | * @see https://github.com/artjock/fly 6 | */ 7 | ;(function(root, factory) { 8 | if (typeof module === 'object' && typeof module.exports === 'object') { 9 | module.exports = factory(require('jquery')); 10 | } else { 11 | root.jQuery.fly = factory(root.jQuery); 12 | } 13 | })(this, function($) { 14 | 15 | /** 16 | * Wrapper 17 | */ 18 | var fly = { 19 | /** 20 | * Instances count 21 | * @private 22 | * @type Number 23 | */ 24 | _count: 0, 25 | 26 | /** 27 | * Mixins collection 28 | * @private 29 | * @type Object 30 | */ 31 | _mixin: {}, 32 | 33 | /** 34 | * Instances 35 | * @private 36 | * @type Array 37 | */ 38 | _instances: [] 39 | }; 40 | 41 | 42 | 43 | fly._base = { 44 | 45 | /** 46 | * Avaliable events 47 | * @enum 48 | */ 49 | events: { 50 | hide: 'hide', 51 | show: 'show', 52 | rootready: 'rootready' 53 | }, 54 | 55 | _cuid: fly._count++, 56 | 57 | /** 58 | * Shared constructor 59 | * @private 60 | * @type Function 61 | */ 62 | _ctor: function fly_ctor() {}, 63 | 64 | /** 65 | * DOM root 66 | * @type jQuery 67 | * @private 68 | */ 69 | _$root: null, 70 | 71 | /** 72 | * Open/close trigger 73 | * @type jQuery 74 | * @private 75 | */ 76 | _$handle: null, 77 | 78 | /** 79 | * Event emmiter 80 | * @type jQuery 81 | */ 82 | _emitter: null, 83 | 84 | /** 85 | * Default class options 86 | * @param Object 87 | * @private 88 | * @static 89 | */ 90 | defaults: { 91 | content: '', 92 | position: 'bottom center', 93 | baseClass: 'fly-popover', 94 | hideClass: 'fly-popover--hidden', 95 | extraClass: '', 96 | redrawOnShow: true, 97 | arrowOffset: 15, 98 | arrowSize: 10 99 | }, 100 | 101 | /** 102 | * Events namespace 103 | * @type String 104 | */ 105 | ens: '', 106 | 107 | /** 108 | * Options 109 | * @param Object 110 | */ 111 | options: null, 112 | 113 | 114 | 115 | /** 116 | * Creates instance of fly 117 | * @param {jQuery} handle 118 | * @param {Object} options 119 | * @return fly 120 | */ 121 | create: function(handle, options) { 122 | 123 | var inst = this.extend({ 124 | ens: '.ns' + fly._count++, 125 | _$handle: $(handle), 126 | _emitter: $({}) 127 | }); 128 | 129 | inst.options = $.extend({}, inst.defaults, options); 130 | 131 | fly._instances.push(inst); 132 | 133 | return inst._init(); 134 | }, 135 | 136 | /** 137 | * Extends fly's class and returns new one 138 | * @param {Object} extra 139 | * @return fly 140 | */ 141 | extend: function(extra) { 142 | this._ctor.prototype = this; 143 | 144 | if (extra && 'defaults' in extra) { 145 | extra.defaults = 146 | $.extend({}, this.defaults, extra.defaults); 147 | } 148 | 149 | var component = $.extend(new this._ctor(), extra); 150 | 151 | component._cuid = fly._count++; 152 | 153 | if (extra.register$) { 154 | fly.register$(extra.register$, component); 155 | } 156 | 157 | return component; 158 | }, 159 | 160 | /** 161 | * Adds listeners for component's action list 162 | * @param {string} selector 163 | * @param {Object} options 164 | */ 165 | delegate: function(selector, options) { 166 | var that = this; 167 | var expando = 'fly_delegated_' + that._cuid; 168 | 169 | $.each(that.actions, function(action) { 170 | $(document.body).delegate(selector, action, function() { 171 | if ($(this).data(expando)) { return; } 172 | 173 | var inst = that.create(this, options); 174 | inst.handle().data(expando, 1); 175 | inst._actionHandler(inst.actions[action]).apply(inst, arguments); 176 | }); 177 | }); 178 | }, 179 | 180 | /** 181 | * Rootrady callback 182 | * @virtual 183 | */ 184 | onrootready: function() {}, 185 | 186 | /** 187 | * Lazy fly's getter 188 | * @return jQuery 189 | */ 190 | root: function() { 191 | 192 | if (!this._$root) { 193 | var opt = this.options; 194 | 195 | this._$root = $('
') 196 | .addClass(opt.baseClass) 197 | .addClass(opt.extraClass) 198 | .addClass(opt.hideClass) 199 | .appendTo('body'); 200 | 201 | this.trigger(this.events.rootready); 202 | } 203 | 204 | return this._$root; 205 | }, 206 | 207 | /** 208 | * Handle getter 209 | * @return jQuery 210 | */ 211 | handle: function() { 212 | return this._$handle; 213 | }, 214 | 215 | /** 216 | * Initializes fly 217 | * @private 218 | * @return fly 219 | */ 220 | _init: function() { 221 | this._action(true); 222 | this.bind(this.events.rootready, $.proxy(this.onrootready, this)); 223 | this.init(); 224 | return this; 225 | }, 226 | 227 | /** 228 | * Cleans up fly 229 | */ 230 | _destroy: function() { 231 | this.destroy(); 232 | 233 | if (this._$root) { 234 | this._$root.remove(); 235 | this._$root = null; 236 | } 237 | this._action(false); 238 | 239 | for (var i = 0, ln = fly._instances.length; i < ln; i++) { 240 | if (fly._instances[i] === this) { 241 | fly._instances.splice(i, 1); 242 | break; 243 | } 244 | } 245 | }, 246 | 247 | /** 248 | * Binds action to handle 249 | * @private 250 | * @param {Boolean} mode 251 | */ 252 | _action: function(mode, actions, handle) { 253 | handle = handle || this.handle(); 254 | actions = actions || this.actions; 255 | 256 | for (var type in actions) { 257 | 258 | if (mode) { 259 | handle.bind(type + this.ens, this._actionHandler( actions[type] )); 260 | } else { 261 | handle.unbind(type + this.ens); 262 | } 263 | 264 | } 265 | }, 266 | 267 | _actionHandler: function(action) { 268 | return typeof action === 'string' ? 269 | $.proxy(this[action], this) : $.proxy(action, this); 270 | }, 271 | 272 | /** 273 | * Calculates content and run callback * 274 | * @private 275 | * @param {Function} done 276 | */ 277 | _content: function(done) { 278 | var content = this.options.content; 279 | 280 | if (typeof content === 'function') { 281 | if (content.length) { 282 | content.call(this, done); 283 | } else { 284 | done( content.call(this) ); 285 | } 286 | } else { 287 | done( content ); 288 | } 289 | }, 290 | 291 | /** 292 | * Fills root element with content 293 | * 294 | * @param {String} content 295 | */ 296 | _render: function(content) { 297 | this.root() 298 | .html(content || ''); 299 | this._rendered = true; 300 | }, 301 | 302 | /** 303 | * Generates CSS classes for current position options 304 | */ 305 | _modCss: function() { 306 | var mod = this.options.position.split(' '); 307 | var base = this.options.baseClass; 308 | 309 | return [base + '_body_' + mod[0], base + '_arrow_' + mod[1]].join(' '); 310 | }, 311 | 312 | /** 313 | * Calculates fly's position 314 | * @abstract 315 | * @private 316 | */ 317 | _position: function() {}, 318 | 319 | /** 320 | * Public init, should be overwritten 321 | * @abstract 322 | */ 323 | init: function() {}, 324 | 325 | /** 326 | * Public destroy, Should be overwritten 327 | * @abstract 328 | */ 329 | destroy: function() {}, 330 | 331 | /** 332 | * Shows fly 333 | * 334 | * If you will pass a content, it will force rendering 335 | * 336 | * @param {HTMLElement|String} content 337 | */ 338 | show: function(content) { 339 | var redraw = this.options.redrawOnShow || !this._rendered; 340 | 341 | if (redraw && !arguments.length) { 342 | return this._content( $.proxy(this.show, this) ); 343 | } 344 | 345 | if (arguments.length) { 346 | this._render(content); 347 | } 348 | 349 | this.trigger(this.events.show); 350 | 351 | this.root() 352 | .css( this._position() ) 353 | .addClass( this._modCss() ) 354 | .removeClass( this.options.hideClass ); 355 | }, 356 | 357 | /** 358 | * Hides fly 359 | */ 360 | hide: function() { 361 | if (this.hidden()) { return; } 362 | 363 | this.trigger(this.events.hide); 364 | 365 | this.root() 366 | .addClass(this.options.hideClass); 367 | }, 368 | 369 | /** 370 | * Force content rendering 371 | * @param {function} done 372 | */ 373 | redraw: function(done) { 374 | var that = this; 375 | 376 | this._content(function(content) { 377 | that._render(content); 378 | if (typeof done === 'function') { 379 | done(content); 380 | } 381 | }); 382 | }, 383 | 384 | /** 385 | * Toggles visibility of the fly 386 | * @param {Boolean} mode 387 | */ 388 | toggle: function(mode) { 389 | mode = arguments.length ? mode : this.hidden(); 390 | this[mode ? 'show' : 'hide'](); 391 | }, 392 | 393 | /** 394 | * Returns true if element is hidden 395 | * @return Boolean 396 | */ 397 | hidden: function() { 398 | return !this._$root || this.root().hasClass(this.options.hideClass); 399 | } 400 | }; 401 | 402 | 403 | /** 404 | * Adds jquery events support 405 | */ 406 | $.each(['bind', 'unbind', 'one', 'trigger'], function(i, type) { 407 | fly._base[type] = function() { 408 | this._emitter[type].apply(this._emitter, arguments); 409 | return this; 410 | }; 411 | }); 412 | 413 | 414 | 415 | /** 416 | * Calculates dimentions of DOM element 417 | * 418 | * @mixin 419 | * 420 | * @type Object 421 | * @property {Number} top 422 | * @property {Number} left 423 | * @property {Number} right 424 | * @property {Number} bottom 425 | * @property {Number} width 426 | * @property {Number} height 427 | */ 428 | fly._mixin.rect = function($el) { 429 | var rect = $el[0].getBoundingClientRect(); 430 | 431 | // IE 432 | if ( !('width' in rect) ) { 433 | rect = $.extend({}, rect); 434 | rect.width = $el.outerWidth(); 435 | rect.height = $el.outerHeight(); 436 | } 437 | 438 | return rect; 439 | }; 440 | 441 | 442 | /** 443 | * Calculates fly position depending on handle 444 | * 445 | * @requires mixin.rect 446 | * @mixin 447 | * @type Object 448 | * @property {Number} top 449 | * @property {Number} left 450 | */ 451 | fly._mixin.position = function() { 452 | var css = {}; 453 | 454 | var $w = $(window); 455 | var pos = this.options.position.split(' '); 456 | var ars = this.options.arrowSize; 457 | var aro = this.options.arrowOffset; 458 | 459 | var body = pos.shift(); 460 | var arrow = pos.shift(); 461 | 462 | var a = {}; 463 | var p = this._rect( this.root() ); 464 | var h = this._rect( this.handle() ); 465 | var f = this.root().css('position') === 'fixed'; 466 | var s = f ? {top: 0, left: 0} : {top: $w.scrollTop(), left: $w.scrollLeft()}; 467 | 468 | switch (arrow) { 469 | case 'top': a.top = h.height / 2 - aro; break; 470 | case 'left': a.left = h.width / 2 - aro; break; 471 | case 'right': a.left = h.width / 2 - p.width + aro; break; 472 | case 'bottom': a.top = h.height / 2 - p.height + aro; break; 473 | default /*center*/: 474 | a.top = (h.height - p.height) / 2; 475 | a.left = (h.width - p.width) / 2; 476 | break; 477 | } 478 | 479 | switch (body) { 480 | case 'left': 481 | css.top = s.top + h.top + a.top; 482 | css.left = s.left + h.left - p.width - ars; 483 | break; 484 | case 'right': 485 | css.top = s.top + h.top + a.top; 486 | css.left = s.left + h.left + h.width + ars; 487 | break; 488 | case 'top': 489 | css.top = s.top + h.top - p.height - ars; 490 | css.left = s.left + h.left + a.left; 491 | break; 492 | default /*bottom*/: 493 | css.top = s.top + h.top + h.height + ars; 494 | css.left = s.left + h.left + a.left; 495 | } 496 | 497 | return css; 498 | }; 499 | 500 | 501 | /** 502 | * Tooltip 503 | * 504 | * @requires fly._base 505 | * @requires mixin.position 506 | * 507 | * @extends fly._base 508 | */ 509 | fly.tooltip = fly._base.extend({ 510 | 511 | actions: { 512 | 'mouseenter': 'onmouseenter', 513 | 'mouseleave': 'onmouseleave' 514 | }, 515 | 516 | /** 517 | * Show delay timeout reference 518 | * @private 519 | */ 520 | _showTimeout: null, 521 | 522 | /** 523 | * Hide delay timeout reference 524 | * @private 525 | */ 526 | _hideTimeout: null, 527 | 528 | /** 529 | * Default mouseenter action for tooltip 530 | */ 531 | onmouseenter: function() { 532 | var that = this; 533 | 534 | if (this._hideTimeout) { 535 | clearTimeout(this._hideTimeout); 536 | this._hideTimeout = null; 537 | } 538 | 539 | if (this.hidden()) { 540 | this._showTimeout = setTimeout(function() { 541 | that.show(); 542 | }, this.options.showDelay); 543 | } 544 | }, 545 | 546 | /** 547 | * Default mouseleave action for tooltip 548 | */ 549 | onmouseleave: function() { 550 | var that = this; 551 | 552 | if (this._showTimeout) { 553 | clearTimeout(this._showTimeout); 554 | this._showTimeout = null; 555 | } 556 | 557 | this._hideTimeout = setTimeout(function() { 558 | that.hide(); 559 | }, this.options.hideDelay); 560 | }, 561 | 562 | _action: function(mode) { 563 | fly._base._action.apply(this, arguments); 564 | 565 | if (this.options.keepOnContent) { 566 | this._keepOnContent(mode); 567 | } 568 | 569 | }, 570 | 571 | _keepOnContent: function(mode) { 572 | var that = this; 573 | var rrevent = this.events.rootready + '._keepOnContent'; 574 | 575 | if (mode) { 576 | this.bind(rrevent, function() { 577 | fly._base._action.call(that, mode, that.actions, that.root()); 578 | }); 579 | } else { 580 | this.unbind(rrevent); 581 | fly._base._action.call(this, mode, this.actions, this.root()); 582 | } 583 | }, 584 | 585 | /** 586 | * Deafult settings for tooltip 587 | * @type Object 588 | */ 589 | defaults: { 590 | showDelay: 300, 591 | hideDelay: 300 592 | }, 593 | 594 | _rect: fly._mixin.rect, 595 | _position: fly._mixin.position 596 | }); 597 | 598 | 599 | /** 600 | * Dropdown 601 | * 602 | * @requires fly._base 603 | * @requires mixin.position 604 | * 605 | * @extends fly._base 606 | */ 607 | fly.dropdown = fly._base.extend({ 608 | 609 | actions: { 610 | 'click': 'onclick' 611 | }, 612 | 613 | /** 614 | * Default click action for dropdown 615 | */ 616 | onclick: function() { 617 | this.toggle(); 618 | }, 619 | 620 | /** 621 | * Default window.onresize handler for dropdown 622 | */ 623 | onresize: function() { 624 | if ( this.hidden() ) { return; } 625 | 626 | this.root().css( this._position() ); 627 | }, 628 | 629 | /** 630 | * Base _action + window resize handler 631 | * 632 | * @private 633 | * @param {Boolean} mode 634 | */ 635 | _action: function(mode) { 636 | 637 | fly._base._action.apply(this, arguments); 638 | fly._base._action.call(this, 639 | mode, {'resize': 'onresize'}, $(window)); 640 | 641 | this._autohide(mode); 642 | }, 643 | 644 | /** 645 | * Dropdown can be closed by clicking outside or pressing ESC 646 | * @private 647 | * @param {boolean} mode 648 | */ 649 | _autohide: function(mode) { 650 | var that = this; 651 | var events = 'click' + that.ens + ' keydown' + that.ens + ' touchstart' + that.ens; 652 | 653 | if (!mode) { return; } 654 | 655 | this 656 | .bind(this.events.show, function() { setTimeout(onshow, 0); }) 657 | .bind(this.events.hide, function() { $(document).unbind(events); }); 658 | 659 | function onshow() { 660 | $(document).bind(events, function(evt) { 661 | var el = evt.target, root = that.root()[0], handle = that.handle()[0]; 662 | if ( 663 | evt.type === 'keydown' && evt.which === 27 || 664 | (evt.type === 'click' || evt.type === 'touchstart' ) && 665 | el !== root && !$.contains(root, el) && 666 | el !== handle && !$.contains(handle, el) 667 | ) { 668 | that.hide(); 669 | } 670 | }); 671 | } 672 | }, 673 | 674 | /** 675 | * Deafult settings for dropdown 676 | * @type Object 677 | */ 678 | defaults: {}, 679 | 680 | _rect: fly._mixin.rect, 681 | _position: fly._mixin.position 682 | }); 683 | 684 | 685 | /** 686 | * Hide all fly instances 687 | */ 688 | fly.hideAll = function () { 689 | for (var i = 0, ln = fly._instances.length; i < ln; i++) { 690 | fly._instances[i].hide(); 691 | } 692 | }; 693 | 694 | 695 | /** 696 | * jQuery extension for dropdown 697 | * 698 | * @requires _base.js 699 | */ 700 | 701 | /** 702 | * Registers fly component, as jquery plugin 703 | * @static 704 | * @param {String} name 705 | * @param {Fly} component 706 | */ 707 | fly.register$ = function(type, component) { 708 | if ( component === fly._base || !(component instanceof fly._base._ctor) || $.fn[type] ) { 709 | return; 710 | } 711 | 712 | var expando = 'fly_' + type + '_' + (+new Date()); 713 | 714 | $.fn[ type ] = function(options) { 715 | 716 | var retVal; 717 | 718 | this.each(function(i) { 719 | if (retVal) { return false; } 720 | 721 | var $el = $(this); 722 | var old = $el.data(expando); 723 | 724 | switch (options) { 725 | 726 | case 'instance': retVal = old; break; 727 | case 'destroy': destroy(old); break; 728 | default: 729 | destroy(old); 730 | $el.data(expando, component.create($el, options)); 731 | } 732 | }); 733 | 734 | return retVal || this; 735 | 736 | function destroy(component) { 737 | if (component) { 738 | component.handle().removeData(expando); 739 | component._destroy(); 740 | } 741 | } 742 | }; 743 | }; 744 | 745 | $.each(fly, fly.register$); 746 | return fly; 747 | }); --------------------------------------------------------------------------------