├── .gitignore ├── stylesheets └── style.css ├── karma.conf.js ├── package.json ├── spec.html ├── license.txt ├── README.md ├── example └── example.html ├── Gruntfile.js ├── spec.js └── es2015-src └── jqtipnav.es6.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .sass-cache/ 3 | *.map 4 | js-compiled/ 5 | node_modules/ 6 | vendor/ -------------------------------------------------------------------------------- /stylesheets/style.css: -------------------------------------------------------------------------------- 1 | [data-jqtipnav] { 2 | background: #fff; 3 | padding: 0.4em; 4 | border: 1px solid black; 5 | } 6 | [data-jqtipnav] ul { 7 | list-style-type: none; 8 | } 9 | [data-jqtipnav] ul, [data-jqtipnav] ul li { 10 | margin: 0; 11 | padding: 0; 12 | display: inline; 13 | } 14 | [data-jqtipnav] ul li { 15 | font-size: 0.8em; 16 | padding: 3px 2px; 17 | } 18 | [data-jqtipnav] ul li a { 19 | text-decoration: none; 20 | color: inherit; 21 | } 22 | [data-jqtipnav] ul li a:visited { 23 | color: inherit; 24 | } 25 | [data-jqtipnav] ul li a:hover { 26 | text-decoration: underline; 27 | } 28 | 29 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | autoWatch: false, 4 | basePath: '', 5 | 'frameworks': ["jasmine"], 6 | 'files': [ 7 | {pattern: 'vendor/jquery/jquery.js', watched: true, served: true, included: true}, 8 | {pattern: 'node_modules/jasmine-jquery/lib/jasmine-jquery.js', watched: true, served: true, included: true}, 9 | {pattern: 'vendor/lodash/lodash.js', watched: true, served: true, included: true}, 10 | {pattern: 'js-compiled/jqtipnav.es6-compiled.js', watched: true, served: true, included: true}, 11 | {pattern: 'spec.html', watched: true, served: true, included: true}, 12 | {pattern: 'spec.js', watched: true, served: true, included: true} 13 | ] 14 | }); 15 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jQTipnav", 3 | "version": "1.0.1", 4 | "description": "jQuery plugin which enables showing small navigation bars on interaction with action element", 5 | "homepage": "http://tvrtkom.github.io/jQTipnav/", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/TvrtkoM/jQTipnav.git" 9 | }, 10 | "bugs": "https://github.com/TvrtkoM/jQTipnav/issues", 11 | "main": "build/jqtipnav.min.js", 12 | "license": "BSD-3-Clause", 13 | "author": { 14 | "email": "tvrtkom@gmail.com", 15 | "name": "TvrtkoM" 16 | }, 17 | "devDependencies": { 18 | "babel-cli": "~6.2.0", 19 | "babel-preset-es2015": "~6.1.18", 20 | "grunt": "0.4.5", 21 | "grunt-babel": "~6.0.0", 22 | "grunt-contrib-cssmin": "~0.14.0", 23 | "grunt-contrib-uglify": "~0.11.0", 24 | "grunt-contrib-watch": "^0.6.1", 25 | "grunt-karma": "0.12.1", 26 | "grunt-lodash": "~0.5.0", 27 | "jasmine": "2.3.2", 28 | "jasmine-jquery": "2.1.1", 29 | "jquery": "^2.2.0", 30 | "karma": "^0.13.19", 31 | "karma-firefox-launcher": "0.1.6", 32 | "karma-jasmine": "0.3.6", 33 | "lodash-cli": "~3.10.1", 34 | "skeleton-css": "^2.0.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /spec.html: -------------------------------------------------------------------------------- 1 | 26 | 29 | 30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 | 44 | 49 | 53 |
54 | Hover here to show tipnav 01 55 |
56 |
57 | Action element for multiple tipnavs 58 |
59 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Tvrtko Majstorović 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of the nor the 12 | names of its contributors may be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [jQTipnav](http://tvrtkom.github.io/jQTipnav) 2 | 3 | ## what is this? 4 | 5 | > A jQuery plugin which enables showing small navigation bars on interaction with action element. 6 | 7 | ## usage 8 | 9 | Write your HTML: 10 | 11 | 12 | 13 | 14 | 19 | 20 | Hover over! 21 | 22 | 23 | 24 | 25 | 26 | Initialize the plugin with: 27 | 28 | $('#subnav').tipnav(options); 29 | 30 | where options is an object containing at least *action_element* property. 31 | If multiple navigation lists are matched inside jquery selector, plugin will show additional arrows for navigation. 32 | Clicking on arrow will show you next/previous navigation list. 33 | 34 | ## options 35 | 36 | * action_element - jQuery selector object (required) 37 | * fade_time - set fade-out time in ms 38 | * trigger_event - set to 'click' to trigger showing and hiding tipnav on mouse click instead hovering 39 | 40 | ## clean method 41 | 42 | Plugin can be called with string 'clean' as first argument. In this case it will try to revert back to state before plugin is initialized, 43 | removing all created html elements. 44 | 45 | e.g 46 | 47 | $(document).tipnav('clean') // removes all jqtipnav elements from page and shows hidden ones 48 | $('#subnav').tipnav('clean') // removes all jqtipnv elements but only for matched element (#subnav) 49 | 50 | ## contributing 51 | 52 | Have some ideas to make this plugin work better or want to add a feature? Following commands and grunt tasks will get you going: 53 | 54 | * npm install - install npm development dependencies 55 | * grunt build - run build-dep task, compile es2015 code, copy minified js and css files to *build* directory 56 | * grunt build-dep - build and copy dependencies 57 | * grunt build-bin - compile es2015 code and copy it minified to *build* directory 58 | * grunt babel - compile es2015 code to es5 inside *js-compiled* directory 59 | * grunt karma - run tests 60 | * grunt watch - auto compile and test code when files change 61 | 62 | After running *grunt build*, example/example.html can be used to see plugin in action. 63 | Test cases in spec.js are done by using Jasmine. If contributing please write test cases for your additions. 64 | -------------------------------------------------------------------------------- /example/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | jQTipNav examples 6 | 7 | 8 | 9 | 13 | 14 | 15 |
16 |

jQTipnav

17 |

18 | A jQuery plugin which enables showing small navigation bars on interaction with action element. 19 |

20 |
21 | Usage: 22 |
23 |
$element.tipnav(options);
24 |

25 | where $element is jquery object pointing to unordered list elements - 'ul' tags. Options must contain 'action_element' jquery object. 26 |

27 |

28 | jQTipnav applied to single element: 29 |

30 |
31 |
32 |
$('ul#subnav').tipnav({ 'action_el': 'a#link1' });
33 |
34 |
35 |

36 | hover to show 37 |

38 |
39 |
40 |

41 | jQTipnav applied to multiple elements: 42 |

43 |
44 |
45 |
$('ul.subnav').tipnav({ 'action_el': 'a#link1' });
46 |
47 |
48 |

49 | hover to show 50 |

51 |
52 |
53 |
Additional options:
54 |
55 |
fade_time ->
56 |
fade out time in milliseconds
57 |
trigger_event ->
58 |
use 'click' event to trigger navigation bar visibility
59 |
60 |

61 | 66 | 71 | 76 | 80 |
81 | 82 | 83 | 91 | 92 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = function(grunt) { 4 | 'use strict'; 5 | 6 | // copy recursively files from 'src' directory to 'dest' dir under vendor 7 | var copyToVendor = function(src, dest) { 8 | grunt.file.recurse(src, function(abs, root, sub, filename) { 9 | var d = path.join('vendor', dest, sub || '', filename); 10 | grunt.file.copy(abs, d, { 11 | process: function(file, src) { 12 | if(grunt.file.exists(d)) { 13 | return false; 14 | } 15 | grunt.log.writeln('copying: ' + src + ' to ' + d); 16 | return file; 17 | } 18 | }); 19 | }); 20 | }; 21 | 22 | grunt.initConfig({ 23 | watch: { 24 | js: { 25 | options: { 26 | interrupt: true 27 | }, 28 | files: ['es2015-src/*'], 29 | tasks: ['build-dep', 'babel', 'build-bin', 'karma:dev'] 30 | } 31 | }, 32 | karma: { 33 | options: { 34 | configFile: 'karma.conf.js', 35 | browsers: ['Firefox'], 36 | singleRun: true 37 | }, 38 | dev: { 39 | reporters: 'dots' 40 | } 41 | }, 42 | lodash: { 43 | build: { 44 | dest: 'vendor/lodash/lodash.js', 45 | options: { 46 | category: ['collection', 'function'] 47 | } 48 | } 49 | }, 50 | babel: { 51 | build: { 52 | options: { 53 | presets: 'es2015' 54 | }, 55 | files: [{ 56 | expand: true, 57 | cwd: 'es2015-src', 58 | src: ['*'], 59 | dest: 'js-compiled', 60 | ext: '.es6-compiled.js' 61 | }] 62 | } 63 | }, 64 | uglify: { 65 | options: { 66 | mangle: true 67 | }, 68 | jqtipnav: { 69 | files: { 70 | 'build/jqtipnav.min.js': ['js-compiled/jqtipnav.es6-compiled.js'] 71 | } 72 | } 73 | }, 74 | cssmin: { 75 | target: { 76 | files: { 77 | 'build/jqtipnav.min.css': ['stylesheets/style.css'] 78 | } 79 | } 80 | } 81 | }); 82 | grunt.loadNpmTasks('grunt-contrib-uglify'); 83 | grunt.loadNpmTasks('grunt-contrib-cssmin'); 84 | grunt.loadNpmTasks('grunt-contrib-watch'); 85 | grunt.loadNpmTasks('grunt-karma'); 86 | grunt.loadNpmTasks('grunt-babel'); 87 | grunt.loadNpmTasks('grunt-lodash'); 88 | 89 | // prepares project dependencies - copy jquery and lodash to vendor directory 90 | grunt.registerTask('build-dep', 'prepare development dependencies', function() { 91 | if(!grunt.file.exists('vendor/lodash/lodash.js') || !grunt.file.exists('vendor/lodash/lodash.min.js')) { 92 | grunt.task.run('lodash'); 93 | } 94 | 95 | copyToVendor('node_modules/skeleton-css/css', 'skeleton'); 96 | copyToVendor('node_modules/jquery/dist', 'jquery'); 97 | }); 98 | 99 | // build javascript binary in build/ directory 100 | grunt.registerTask('build-bin', ['babel', 'uglify:jqtipnav']); 101 | 102 | // build all the development and binary files (js & css) 103 | grunt.registerTask('build', ['build-dep', 'babel', 'uglify:jqtipnav', 'cssmin']); 104 | }; -------------------------------------------------------------------------------- /spec.js: -------------------------------------------------------------------------------- 1 | jasmine.getFixtures().fixturesPath = '/base/'; 2 | 3 | var get_id = function($selector) { 4 | var id; 5 | _.forEach(['', '-main'], function(val) { 6 | id = $selector.attr('data-jqtipnav' + val); 7 | }); 8 | return id; 9 | }; 10 | 11 | describe('main application', function() { 12 | var $test_container_01, $notes, tipnav_func = $.fn.tipnav, 13 | default_options; 14 | 15 | beforeEach(function() { 16 | loadFixtures('spec.html'); 17 | default_options = { 18 | 'action_element': $('#link-01') 19 | }; 20 | $test_container_01 = $('#test-container-01'); 21 | $notes = $('.note'); 22 | $test_container_01.tipnav(default_options); 23 | }); 24 | 25 | it('should be present as jquery function', function() { 26 | spyOn($.fn, 'tipnav'); 27 | $('
').tipnav(default_options); 28 | expect($.fn.tipnav).toHaveBeenCalled(); 29 | }); 30 | 31 | it('should wrap multiple tipnavs', function() { 32 | $notes.tipnav(default_options); 33 | expect($('.jqtipnav-wrap')).toExist(); 34 | expect($('.jqtipnav-wrap')).toBeInDOM(); 35 | expect($('.jqtipnav-wrap').find('*').length).toBeGreaterThan(0); 36 | }); 37 | 38 | it('should turn original elements unvisible', function() { 39 | expect($test_container_01).toBeHidden(); 40 | $notes.tipnav(default_options); 41 | expect($notes).toBeHidden(); 42 | }); 43 | 44 | it('should put div[data-jqtipbar] element into body when initialized', function() { 45 | expect($('body > div[data-jqtipnav]')).toExist(); 46 | expect($('body > div[data-jqtipnav]')).toBeInDOM(); 47 | }); 48 | 49 | 50 | it('should have cleanup mechanism for all created elements', function() { 51 | // calling 'clean' on document to delete all created elements 52 | $(document).tipnav('clean'); 53 | expect($('body > div[data-jqtipnav]').length).toBe(0); 54 | $notes.tipnav(default_options); 55 | $test_container_01.tipnav(default_options); 56 | expect($('.jqtipnav-wrap > div[data-jqtipnav]').length).toBe(4); 57 | expect($('body > div[data-jqtipnav]').length).toBe(1); 58 | $(document).tipnav('clean'); 59 | }); 60 | 61 | it('should create each element with different data-jqtipnav value', function() { 62 | var ids = [], id; 63 | $(document).tipnav('clean'); 64 | $notes.tipnav(default_options); 65 | $test_container_01.tipnav(default_options); 66 | for(var i=0; i<$('body > div[data-jqtipnav]').length; i++) { 67 | id = $('body > div[data-jqtipnav]').eq(i).data('jqtipnav'); 68 | expect(_.contains(ids, id)).toBe(false); 69 | ids.push(id); 70 | } 71 | }); 72 | 73 | it('should add data-jqtipnav-main attribute on elements plugin is applied', function() { 74 | expect($test_container_01.data('jqtipnav-main')).toBeDefined(); 75 | }); 76 | 77 | it('should delete created elements for particular selector when calling "clean"', function() { 78 | var ids = [], $test_els; 79 | $(document).tipnav('clean'); 80 | $notes.tipnav(default_options); 81 | $test_els = $('div[data-jqtipnav-main]'); 82 | for(var i=0; i<$test_els.length; i++) { 83 | ids.push($test_els.eq(i).data('jqtipnav-main')); 84 | } 85 | $test_container_01.tipnav(default_options); 86 | $notes.tipnav('clean'); 87 | for(var i=0; i div[data-jqtipnav]').data('jqtipnav')).toBeDefined(); 125 | }); 126 | 127 | it('should hide created elements as well', function() { 128 | $(document).tipnav('clean'); 129 | $test_container_01.tipnav(default_options); 130 | $notes.tipnav(default_options); 131 | expect($('div[data-jqtipnav]')).toBeHidden(); 132 | $notes.tipnav('clean'); 133 | expect($notes).toBeVisible(); 134 | $(document).tipnav('clean'); 135 | }); 136 | 137 | it('should copy main elements content to tipnav elements', function() { 138 | var id = $test_container_01.data('jqtipnavMain'); 139 | expect($('div[data-jqtipnav=' + id + ']').find('ul').html()).toEqual($test_container_01.html().trim()); 140 | }); 141 | }); 142 | 143 | describe('tipnav', function() { 144 | var $test_container_01, $notes, 145 | default_options, $action_el; 146 | 147 | beforeEach(function() { 148 | loadFixtures('spec.html'); 149 | $action_el = $('#link-01'); 150 | default_options = { 151 | 'action_element': $action_el 152 | }; 153 | $(document).tipnav('clean'); 154 | $test_container_01 = $('#test-container-01'); 155 | $notes = $('.note'); 156 | $test_container_01.tipnav(default_options); 157 | }); 158 | 159 | afterEach(function() { 160 | }); 161 | 162 | describe('positioning', function() { 163 | var $test_container_01, 164 | default_options, id, 165 | tipnav, 166 | $action1, 167 | dims, 168 | get_dimensions = function(tn, ac) { 169 | return { 170 | 'tn': { 171 | 'off': tn.offset(), 172 | 'height': tn.outerHeight(), 173 | 'width': tn.outerWidth() 174 | }, 175 | 'ac': { 176 | 'off': ac.offset(), 177 | 'height': ac.outerHeight(), 178 | 'width': ac.outerWidth() 179 | } 180 | }; 181 | }; 182 | 183 | beforeEach(function() { 184 | loadFixtures('spec.html'); 185 | $(document).tipnav('clean'); 186 | $action1 = $('#link-01'); 187 | $test_container_01 = $('#test-container-01'); 188 | }); 189 | 190 | it('should show in middle if the action element if smaller than it', function() { 191 | default_options = { 192 | 'action_element': $action1 193 | }; 194 | $test_container_01.tipnav(default_options); 195 | id = get_id($test_container_01); 196 | tipnav = $('[data-jqtipnav=' + id + ']'); 197 | $action1.trigger('mouseenter'); 198 | dims = get_dimensions(tipnav, $action1); 199 | expect(tipnav).toBeVisible(); 200 | expect(dims.ac.width).toBeGreaterThan(dims.tn.width); 201 | expect(Math.floor(dims.tn.off.left)).toEqual(Math.floor(dims.ac.off.left + dims.ac.width/2 - dims.tn.width/2)); 202 | }); 203 | 204 | }); 205 | describe('containers', function() { 206 | describe('hovering over action', function () { 207 | var $action, $tipnavs, 208 | ids 209 | ; 210 | beforeEach(function () { 211 | loadFixtures('spec.html'); 212 | ids = []; 213 | jQuery.fx.off = true; 214 | $action = $('#link-02'); 215 | $tipnavs = $('.tipnav'); 216 | $(document).tipnav('clean'); 217 | $tipnavs.tipnav({ 218 | 'action_element': $action 219 | }); 220 | $('div[data-jqtipnav]').each(function () { 221 | ids.push($(this).attr('data-jqtipnav')); 222 | }); 223 | }); 224 | 225 | it('should show only first tipnav when activated', function () { 226 | $action.trigger('mouseenter'); 227 | expect($('div[data-jqtipnav=' + ids[0] + ']')).toBeVisible(); 228 | expect($('div[data-jqtipnav=' + ids[1] + ']')).toBeHidden(); 229 | expect($('div[data-jqtipnav=' + ids[2] + ']')).toBeHidden(); 230 | $action.trigger('mouseleave'); 231 | }); 232 | 233 | it('should cycle navs on arrow clicks', function() { 234 | var tipnav1 = $('div[data-jqtipnav=' + ids[0] + ']'), 235 | tipnav2 = $('div[data-jqtipnav=' + ids[1] + ']'), 236 | tipnav3 = $('div[data-jqtipnav=' + ids[2] + ']'), 237 | next_arr = tipnav1.find('.jqtipnav-next'), 238 | prev_arr = tipnav1.find('.jqtipnav-prev') 239 | ; 240 | $action.trigger('mouseenter'); 241 | next_arr.click(); 242 | expect(tipnav1).toBeHidden(); 243 | expect(tipnav2).toBeVisible(); 244 | expect(tipnav3).toBeHidden(); 245 | next_arr = $(tipnav2).find('.jqtipnav-next'); 246 | next_arr.trigger('click'); 247 | expect(tipnav1).toBeHidden(); 248 | expect(tipnav2).toBeHidden(); 249 | expect(tipnav3).toBeVisible(); 250 | next_arr = $(tipnav3).find('.jqtipnav-next'); 251 | next_arr.trigger('click'); 252 | expect(tipnav1).toBeVisible(); 253 | expect(tipnav2).toBeHidden(); 254 | expect(tipnav3).toBeHidden(); 255 | prev_arr.click(); 256 | expect(tipnav1).toBeHidden(); 257 | expect(tipnav2).toBeHidden(); 258 | expect(tipnav3).toBeVisible(); 259 | $action.trigger('mouseleave'); 260 | }); 261 | 262 | }); 263 | }); 264 | }); 265 | 266 | -------------------------------------------------------------------------------- /es2015-src/jqtipnav.es6.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | if(typeof define === 'function' && define.amd) { 3 | define(['jquery'], factory); 4 | } else { 5 | factory(window.jQuery); 6 | } 7 | })(function ($) { 8 | var jQTipnav, _module, 9 | unique = (function () { 10 | // returns incremented numbers 11 | var id; 12 | return { 13 | generate: function () { 14 | id = typeof id === 'undefined' ? 1 : id + 1; 15 | return id; 16 | } 17 | }; 18 | }()) 19 | ; 20 | 21 | _module = class { 22 | define (name, _obj) { 23 | this[name] = _obj 24 | } 25 | }; 26 | 27 | jQTipnav = new _module(); 28 | 29 | jQTipnav.define('Action', class { 30 | constructor($action_el, options) { 31 | this.$el = $action_el; 32 | this.trigger_event = options.trigger_event; 33 | this.fade_time = options.fade_time; 34 | } 35 | 36 | get_geometry(container) { 37 | // returns container's and action element's dimensions and offset point 38 | return { 39 | 'tn': { 40 | 'off': container.offset(), 41 | 'height': container.outerHeight(), 42 | 'width': container.outerWidth() 43 | }, 44 | 'ac': { 45 | 'off': this.$el.offset(), 46 | 'height': this.$el.outerHeight(), 47 | 'width': this.$el.outerWidth() 48 | } 49 | }; 50 | } 51 | 52 | get_container_position(container) { 53 | // return position of the navigation container on the screen 54 | var geometry = this.get_geometry(container), 55 | full_width = $(window).width(), 56 | room = (geometry.tn.width - geometry.ac.width) / 2, 57 | ac_mid = geometry.ac.off.left + geometry.ac.width / 2, 58 | top = geometry.ac.off.top - geometry.tn.height, 59 | left 60 | ; 61 | if (geometry.tn.width <= geometry.ac.width || geometry.ac.off.left > room 62 | && full_width - (geometry.ac.off.left + geometry.ac.width) > room) { 63 | left = ac_mid - geometry.tn.width / 2; 64 | } else { 65 | if(full_width - (geometry.ac.off.left + geometry.ac.width) < room) { 66 | left = full_width - geometry.tn.width; 67 | } else { 68 | left = 0; 69 | } 70 | } 71 | if (geometry.ac.off.top < geometry.tn.height) { 72 | top = geometry.ac.off.top + geometry.ac.height; 73 | } 74 | return { 75 | 'left': left, 76 | 'top': top 77 | }; 78 | } 79 | 80 | bind_container(container) { 81 | // sets up action element with basic events for displaying containers 82 | var set_hide_timeout = () => { 83 | // stores timeout to element's data attribute after 84 | // container fades out after timeout of 500 85 | container.data('hoverTimeout', setTimeout(() => { 86 | container.fadeOut(this.fade_time || 500, () => { 87 | container.data('open', false); 88 | }) 89 | }, 200)); 90 | } 91 | ; 92 | this.$el.bind(this.trigger_event, (e) => { 93 | // on specified trigger event container should appear 94 | e.preventDefault(); 95 | container.show().css('left', 0); 96 | let css = this.get_container_position(container); 97 | container.data('open', true).css(css); 98 | // clear hover timeout each time element is triggered 99 | if (typeof container.data('hoverTimeout') !== 'undefined') 100 | clearTimeout(container.data('hoverTimeout')); 101 | 102 | if (this.trigger_event === 'click') { 103 | // start timeout if container visible and only on 'click' 104 | if (container.data('open') === true) { 105 | set_hide_timeout(); 106 | } 107 | } 108 | }); 109 | if (this.trigger_event !== 'click') { 110 | // runs only on 'hover' 111 | this.$el.mouseleave(function () { 112 | set_hide_timeout(); 113 | }); 114 | container.mouseenter(function () { 115 | if(container.data('fade') == true) { 116 | container.stop(); 117 | container.fadeIn(100); 118 | container.data('fade', false); 119 | } 120 | clearTimeout($(this).data('hoverTimeout')); 121 | }); 122 | container.mouseleave(function () { 123 | container.data('fade', true); 124 | set_hide_timeout(); 125 | }); 126 | } 127 | } 128 | 129 | setup_container(container, wrap) { 130 | // sets up container with additional events for navigation and positioning 131 | var css, 132 | arrows = container.find('.jqtipnav-prev, .jqtipnav-next') 133 | ; 134 | if(arrows.length > 0) { 135 | // show next visible element when arrows are clicked 136 | arrows.bind('click', (e) => { 137 | var $el, arrow = $(e.target); 138 | e.preventDefault(); 139 | if (arrow.hasClass('jqtipnav-prev')) { 140 | $el = container.prev(); 141 | if ($el.length == 0) { 142 | $el = container.siblings(':last'); 143 | } 144 | } 145 | if (arrow.hasClass('jqtipnav-next')) { 146 | $el = container.next(); 147 | if ($el.length == 0) { 148 | $el = container.siblings(':first'); 149 | } 150 | } 151 | // hide last visible container, show next one and set tipnav container position 152 | wrap.css('left', 0); 153 | container.hide(); 154 | $el.show(); 155 | css = this.get_container_position($el); 156 | wrap.css(css); 157 | }); 158 | } 159 | }; 160 | }); 161 | 162 | jQTipnav.define('Container', class { 163 | constructor($action_el, $copy, options, wrap) { 164 | var default_options = { 165 | 'trigger_event': 'mouseenter', 166 | 'fade_time': 500 167 | }, 168 | options = $.extend({}, default_options, options || {}), 169 | wrap = wrap || false // wrap element? boolean or jQuery object 170 | ; 171 | this.action = new jQTipnav.Action($action_el, options); 172 | this.container = jQTipnav.Container.build($copy, wrap); 173 | 174 | this.$wrap = this.container.$wrap; 175 | 176 | if(wrap === true) { 177 | this.action.bind_container(this.$wrap); 178 | } else if(!wrap) { 179 | this.action.bind_container(this.container.$el); 180 | } 181 | this.action.setup_container(this.container.$el, this.$wrap); 182 | } 183 | 184 | static build_simple($copy, multi) { 185 | // bulds only container 186 | var id = unique.generate(), 187 | $el = $('
    ') 188 | ; 189 | $el.find('ul').append($copy.children().clone()); 190 | $copy.attr('data-jqtipnav-main', id); 191 | if(!!multi) { 192 | let arrow_p = $('
  • <
  • '), 193 | arrow_n = $('
  • >
  • ') 194 | ; 195 | $el.find('ul').prepend(arrow_p).append(arrow_n); 196 | } 197 | return $el; 198 | } 199 | 200 | static build($copy, wrap=false) { 201 | // build a container element markup and return it 202 | // adds $copy contents and arrows if content should be wrapped (multiple matched) 203 | var $el, $wrap; 204 | if(wrap === true) { 205 | $wrap = $('
    '); 206 | $wrap.css('position', 'absolute'); 207 | $el = jQTipnav.Container.build_simple($copy, wrap); 208 | $el.show().appendTo($wrap); 209 | $wrap.appendTo('body'); 210 | $wrap.hide(); 211 | } 212 | else { 213 | $el = jQTipnav.Container.build_simple($copy, wrap); 214 | if(!!wrap) { 215 | $wrap = wrap; 216 | $el.appendTo($wrap); 217 | $el.hide(); 218 | } 219 | else { 220 | $el.css('position', 'absolute'); 221 | $el.appendTo('body'); 222 | $el.hide(); 223 | } 224 | } 225 | return { 226 | $wrap: $wrap, 227 | $el: $el, 228 | id: $el.attr('data-jqtipnav') 229 | }; 230 | } 231 | }); 232 | 233 | $.fn.tipnav = function (options) { 234 | // plugin definition 235 | var matchedEl = this, 236 | action, 237 | multi = matchedEl.length > 1, 238 | actions = { 239 | 'clean': function () { 240 | // can be called on element - $jqtipnav.tipnav('clean') 241 | // or on whole document $(document).tipnav('clean') 242 | // removes all the mess plugin has done under matched element 243 | var id, wrap_el, el; 244 | if (matchedEl.length === 1 && matchedEl.prop('nodeName').toLowerCase() === 'body' 245 | || matchedEl.prop('nodeName') === '#document') { 246 | // if matched element is document or body 247 | $('body').find('[data-jqtipnav]').remove(); 248 | $('body').find('.jqtipnav-wrap').remove(); 249 | $('[data-jqtipnav-main]').show().removeAttr('data-jqtipnav-main'); 250 | } 251 | else { 252 | matchedEl.each(function () { 253 | // else remove all jqtpnav mess for only matched elements 254 | id = parseInt($(this).attr('data-jqtipnav-main')); 255 | if (typeof id !== 'undefined') { 256 | el = $('body').find('[data-jqtipnav=' + id + ']'); 257 | wrap_el = el.parent('.jqtipnav-wrap'); 258 | el.remove(); 259 | if(wrap_el.length !== 0 && wrap_el.children().length == 0) { 260 | wrap_el.remove(); 261 | } 262 | } 263 | $(this).show(); 264 | $(this).removeAttr('data-jqtipnav-main'); 265 | }); 266 | } 267 | } 268 | }, 269 | args = Array.prototype.slice.call(arguments), 270 | opts, 271 | wrap 272 | ; 273 | if(typeof(args[0]) !== 'string') { 274 | opts = options; 275 | action = opts.action_element; 276 | if(multi) { 277 | wrap = true; 278 | matchedEl.each(function() { 279 | let container; 280 | container = new jQTipnav.Container(action, $(this), opts, wrap); 281 | if(wrap === true) wrap = container.$wrap; 282 | }); 283 | } else { 284 | new jQTipnav.Container(action, $(this), opts); 285 | } 286 | return matchedEl.hide(); 287 | } else { 288 | actions[args[0]](); 289 | } 290 | return matchedEl; 291 | }; 292 | }); 293 | 294 | --------------------------------------------------------------------------------