├── .gitignore ├── package.json ├── LICENSE.txt ├── gruntfile.js ├── README.md ├── src ├── mapify.scss └── mapify.js └── example ├── index.html └── map.svg /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .sass-cache 3 | .idea 4 | node_modules -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Mapify", 3 | "version" : "1.0.0", 4 | "author" : "Etienne Martin", 5 | "private" : false, 6 | "devDependencies" : { 7 | "autoprefixer": "^6.0.3", 8 | "grunt" : "~0.4.0", 9 | "grunt-contrib-cssmin": "*", 10 | "grunt-contrib-sass": "*", 11 | "grunt-postcss": "*", 12 | "grunt-contrib-uglify": "*", 13 | "grunt-contrib-watch": "*", 14 | "grunt-contrib-concat": "*", 15 | "grunt-cssc": "*", 16 | "matchdep": "*" 17 | } 18 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2014 etienne-martin 6 | Contributions by Miro Hudak 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | 26 | */ -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt){ 2 | 3 | require("matchdep").filterDev("grunt-*").forEach(grunt.loadNpmTasks); 4 | 5 | grunt.initConfig({ 6 | pkg: grunt.file.readJSON('package.json'), 7 | concat: { 8 | options: { separator: '\n\n' }, 9 | core : { 10 | src: ['LICENSE.txt','build/jquery.mapify.js'], 11 | dest: 'build/jquery.mapify.js' 12 | }, 13 | css : { 14 | src: ['LICENSE.txt','build/jquery.mapify.css'], 15 | dest: 'build/jquery.mapify.css' 16 | } 17 | }, 18 | uglify: { 19 | build: { 20 | files: { 21 | 'build/jquery.mapify.js': ['src/mapify.js'] 22 | } 23 | } 24 | }, 25 | postcss: { 26 | options: { 27 | map: false, 28 | processors: [ 29 | require('autoprefixer')({browsers: ['last 3 version']}) 30 | ] 31 | }, 32 | dist: { 33 | src: 'build/jquery.mapify.css' 34 | } 35 | }, 36 | cssmin: { 37 | build: { 38 | src: 'build/jquery.mapify.css', 39 | dest: 'build/jquery.mapify.css' 40 | } 41 | }, 42 | sass: { 43 | options: { 44 | sourcemap: 'none' 45 | }, 46 | build: { 47 | files: { 48 | 'build/jquery.mapify.css': 'src/mapify.scss' 49 | } 50 | } 51 | }, 52 | watch: { 53 | js: { 54 | files: ['src/mapify.js'], 55 | tasks: ['buildjs'] 56 | }, 57 | css: { 58 | files: ['src/mapify.scss'], 59 | tasks: ['buildcss'] 60 | } 61 | } 62 | }); 63 | 64 | grunt.registerTask('default', ['buildall','watch']); 65 | grunt.registerTask('buildall', ['buildcss','buildjs']); 66 | 67 | grunt.registerTask('buildcss', ['sass','postcss','cssmin','concat:css']); 68 | grunt.registerTask('buildjs', ['uglify:build','concat:core']); 69 | 70 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mapify plugin 2 | 3 | Responsive and stylable image maps using jQuery, SVG and CSS3 4 | 5 | Project website: http://etiennemartin.ca/mapify/ 6 | 7 | ## Basic usage 8 | 9 | Embed [jquery.mapify.css](https://github.com/etienne-martin/mapify/blob/master/build/jquery.mapify.css) and [jquery.mapify.js](https://github.com/etienne-martin/mapify/blob/master/build/jquery.mapify.js) in your page and call the plugin with the following function: 10 | 11 | ```javascript 12 | $("img[usemap]").mapify(); 13 | ``` 14 | 15 | ## Popovers 16 | 17 | ```javascript 18 | $("img[usemap]").mapify({ 19 | popOver: { 20 | content: function(zone){ 21 | return ""+zone.attr("data-title")+""+zone.attr("data-nbmembre")+" Members"; 22 | }, 23 | delay: 0.7, 24 | margin: "15px", 25 | height: "130px", 26 | width: "260px" 27 | } 28 | }); 29 | ``` 30 | Custom class for a specific popOver 31 | ```html 32 | 33 | ``` 34 | 35 | ## Hover effects 36 | Custom hover class for all areas 37 | 38 | ```javascript 39 | $("img[usemap]").mapify({ 40 | hoverClass: "custom-hover" 41 | }); 42 | ``` 43 | Custom hover class for a specific area 44 | ```html 45 | 46 | ``` 47 | 48 | Group multiple areas together 49 | 50 | ```html 51 | 52 | 53 | ``` 54 | 55 | ## Stylable with css 56 | 57 | ```css 58 | 59 | .custom-popover{ 60 | background: #09f; 61 | } 62 | 63 | .mapify-hover{ 64 | fill:rgba(0,0,0,0.15); 65 | stroke: #fff; 66 | stroke-width: 2; 67 | } 68 | 69 | .custom-hover{ 70 | fill:rgba(0,0,0,0.15); 71 | stroke: #fff; 72 | stroke-width: 2; 73 | } 74 | 75 | .custom-hover-2{ 76 | fill: #09f; 77 | stroke: #fff; 78 | stroke-width: 2; 79 | } 80 | ``` 81 | 82 | ## Examples 83 | 84 | See http://etiennemartin.ca/mapify/ for live examples. 85 | 86 | ## Built With 87 | 88 | * [Grunt](https://gruntjs.com/) - The JavaScript Task Runner 89 | * [jQuery](https://jquery.com/) - A fast, small, and feature-rich JavaScript library 90 | * [Sass](http://sass-lang.com/) - Syntactically Awesome Style Sheets 91 | 92 | ## Contributing 93 | 94 | When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. 95 | 96 | Update the README.md with details of changes to the plugin. 97 | 98 | Update the [demo](https://github.com/etienne-martin/Mapify/blob/master/example/index.html) with examples demonstrating the changes to the plugin. 99 | 100 | Build the project & test all the features before submitting your pull request. 101 | 102 | ## Authors 103 | 104 | * **Etienne Martin** - *Initial work* - [etiennemartin.ca](http://etiennemartin.ca/) 105 | * **Yehor Konoval** - *Improvements* - [@ekonoval](https://github.com/ekonoval) 106 | * **Brock Fanning** - *Improvements* - [@brockfanning](https://github.com/brockfanning) 107 | * **enscope, s.r.o.** - *Improvements* - [enscope.com](https://www.enscope.com/) 108 | 109 | ## License 110 | 111 | This project is licensed under the MIT License - see the [LICENSE.txt](https://github.com/etienne-martin/Mapify/blob/master/LICENSE.txt) file for details. 112 | -------------------------------------------------------------------------------- /src/mapify.scss: -------------------------------------------------------------------------------- 1 | .mapify-holder{ 2 | position: relative; 3 | display: inline-block; 4 | font-size: 0px; 5 | max-width: 100%;/* Make the image responsive */ 6 | z-index:1; 7 | 8 | -webkit-touch-callout: none; 9 | 10 | -webkit-user-select: none; /* Chrome all / Safari all */ 11 | -moz-user-select: none; /* Firefox all */ 12 | -ms-user-select: none; /* IE 10+ */ 13 | } 14 | 15 | .mapify-imgHolder{ 16 | position:relative; 17 | z-index:1; 18 | } 19 | 20 | .mapify-holder *, 21 | .mapify-GPU{ 22 | -webkit-transform: translate3d(0,0,0); 23 | -moz-transform: translate3d(0,0,0); 24 | -ms-transform: translate3d(0,0,0); 25 | -o-transform: translate3d(0,0,0); 26 | transform: translate3d(0,0,0); 27 | } 28 | 29 | /* Make the image responsive */ 30 | .mapify-imgHolder .mapify{ 31 | max-width: 100%; 32 | height: auto; 33 | } 34 | 35 | .mapify-img{ 36 | position: absolute; 37 | top:0px; 38 | left: 0px; 39 | width: 100%; 40 | height: 100%; 41 | z-index: -2; 42 | } 43 | .mapify-svg{ 44 | position: absolute; 45 | top:0px; 46 | left: 0px; 47 | width: 100%; 48 | height: 100%; 49 | z-index: -2; 50 | } 51 | 52 | /* Styles for the hilight effect */ 53 | 54 | .mapify-polygon{ 55 | transition: all 0.5s; 56 | fill:transparent; 57 | stroke: transparent; 58 | stroke-width: 0; 59 | 60 | /* do not use css transform translate3d, otherwise it starts lagging in iOs when panning the map*/ 61 | } 62 | .mapify-hover{ 63 | fill:#09f; 64 | } 65 | 66 | /**/ 67 | 68 | .mapify-popOver{ 69 | 70 | color: #000; 71 | position: absolute; 72 | top:0px; 73 | left: 0px; 74 | 75 | padding:20px 30px; 76 | width: 260px; 77 | background: #fff; 78 | box-shadow: rgba(0,0,0,0.15) 0 0 0 2px; 79 | z-index: 999; 80 | -webkit-transform: translateY(-15px); 81 | transform: translateY(-15px); 82 | border-radius: 5px; 83 | text-align: center; 84 | height: auto; 85 | 86 | box-sizing: border-box; 87 | -moz-box-sizing: border-box; 88 | 89 | font-size: 14px; 90 | 91 | z-index: -1; 92 | 93 | opacity: 0; 94 | 95 | /* now declared in the plugin options 96 | transition: all 0.5s; 97 | */ 98 | } 99 | .mapify-popOver .mapify-popOver-arrow{ 100 | content: ""; 101 | width: 15px; 102 | height: 15px; 103 | 104 | /* now declared in the plugin options 105 | transition: margin 0.5s; 106 | */ 107 | 108 | z-index: -2; 109 | 110 | margin-top: -3px; 111 | 112 | box-shadow: inset #fff 0 0 0 100px; 113 | 114 | border-top: solid transparent 2px; 115 | border-left: solid transparent 2px; 116 | 117 | border-right: solid rgba(0,0,0,0.15) 2px; 118 | border-bottom: solid rgba(0,0,0,0.15) 2px; 119 | 120 | position: absolute; 121 | top:100%; 122 | left: 50%; 123 | -webkit-transform: translateX(-15px) rotate(45deg) translateY(-50%); 124 | transform: translateX(-15px) rotate(45deg) translateY(-50%); 125 | } 126 | .mapify-popOver.mapify-bottom .mapify-popOver-arrow{ 127 | top:auto; 128 | bottom: 100%; 129 | margin-top: auto; 130 | margin-bottom: -3px; 131 | 132 | border-bottom: solid transparent 2px; 133 | border-right: solid transparent 2px; 134 | 135 | border-top: solid rgba(0,0,0,0.15) 2px; 136 | border-left: solid rgba(0,0,0,0.15) 2px; 137 | 138 | -webkit-transform: rotate(45deg) translateY(50%); 139 | transform: rotate(45deg) translateY(50%); 140 | } 141 | 142 | .mapify-popOver.mapify-visible{ 143 | 144 | /* Showing the popover */ 145 | opacity: 1; 146 | -webkit-transform: translateY(0px); 147 | transform: translateY(0px); 148 | } -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Mapify - test 8 | 9 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | Saint-Louis 75 | 76 | Saint-Marcel-de-Richelieu 80 | 81 | Saint-Hugues 85 | 86 | Sainte-Hélène-de-Bagot 88 | 89 | Saint-Bernard-de-Michaudville 94 | 95 | Saint-Jude 98 | 99 | Saint-Barnabé-Sud 103 | 104 | Saint-Simon 108 | 109 | Saint-Liboire 112 | 113 | La Présentation 117 | 118 | Saint-Hyacinthe 124 | 125 | Saint-Dominique 128 | 129 | Saint-Valérien-de-Milton 132 | 133 | Sainte-Marie-Madeleine 139 | 140 | Sainte-Madeleine 143 | 144 | Saint-Damase 150 | 151 | Saint-Pie 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | Saint-Louis 170 | 171 | Saint-Marcel-de-Richelieu 175 | 176 | Saint-Hugues 180 | 181 | Sainte-Hélène-de-Bagot 183 | 184 | Saint-Bernard-de-Michaudville 189 | 190 | Saint-Jude 193 | 194 | Saint-Barnabé-Sud 198 | 199 | Saint-Simon 203 | 204 | Saint-Liboire 207 | 208 | La Présentation 212 | 213 | Saint-Hyacinthe 219 | 220 | Saint-Dominique 223 | 224 | Saint-Valérien-de-Milton 227 | 228 | Sainte-Marie-Madeleine 234 | 235 | Sainte-Madeleine 238 | 239 | Saint-Damase 245 | 246 | Saint-Pie 254 | 255 | 256 | 257 | 258 | 259 | 260 | 291 | 292 | -------------------------------------------------------------------------------- /src/mapify.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) 2014 etienne-martin 6 | Contributions by Miro Hudak 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | 26 | */ 27 | 28 | ;(function ($, window, document, undefined) { 29 | 30 | var defaults = { 31 | hoverClass: false, 32 | popOver: false 33 | }; 34 | 35 | // All available options for the plugin 36 | // noinspection JSUnusedLocalSymbols 37 | var availableOptions = { 38 | hoverClass: false, 39 | popOver: { 40 | content: function (zone, imageMap) { 41 | return ''; 42 | }, 43 | customPopOver: { 44 | selector: false, 45 | contentSelector: '.mapify-popOver-content', 46 | visibleClass: 'mapify-visible', 47 | alwaysVisible: false 48 | }, 49 | delay: 0.8, 50 | margin: '10px', 51 | width: false, 52 | height: false 53 | }, 54 | 55 | onAreaHighlight: false, 56 | onMapClear: false, 57 | 58 | instantClickOnMobile: false 59 | }; 60 | 61 | //region --- Internal Mapify Implementation --- 62 | 63 | var userAgent = navigator.userAgent.toLowerCase(); 64 | var iOS = /(ipad|iphone|ipod)/g.test(userAgent); 65 | var android = /(android)/g.test(userAgent); 66 | var isMobile = iOS || android; 67 | 68 | Mapify.prototype._initPopOver = function () { 69 | var $imageMap = $(this.element); 70 | this.options.popOver.margin = parseInt(this.options.popOver.margin); 71 | 72 | this._timer = null; 73 | this._popOverTransition = ''; 74 | this._popOverArrowTransition = ''; 75 | this._popOverTimeout = null; 76 | 77 | if (!isNaN(this.options.popOver.delay)) { 78 | // if delay is a number, use it as seconds 79 | this._popOverTransition = "all " + this.options.popOver.delay + "s"; 80 | this._popOverArrowTransition = "margin " + this.options.popOver.delay + "s"; 81 | } 82 | 83 | this.popOver = false; 84 | this.popOverArrow = false; 85 | if (this.isPopOverEnabled) { 86 | if (this.isCustomPopOver) { 87 | // if custom pop-over is defined, add defaults 88 | if ((typeof this.options.popOver.customPopOver == 'string')) { 89 | this.options.popOver.customPopOver.selector = this.options.popOver.customPopOver; 90 | } 91 | this.options.popOver.customPopOver = $.extend(true, {}, 92 | availableOptions.popOver.customPopOver, this.options.popOver.customPopOver); 93 | this.popOver = $(this.options.popOver.customPopOver.selector); 94 | this.popOver.css({'transition': this._popOverTransition}); 95 | } else { 96 | $imageMap.after( 97 | '
' + 98 | '
' + 99 | '
' + 100 | '
'); 101 | 102 | this.popOver = $imageMap.next('.mapify-popOver'); 103 | this.popOverArrow = this.popOver.find('.mapify-popOver-arrow'); 104 | this.popOver.css({ 105 | width: this.options.popOver.width, 106 | height: this.options.popOver.height 107 | }); 108 | } 109 | } 110 | }; 111 | 112 | Mapify.prototype._initImageMap = function () { 113 | var _this = this, 114 | $imageMap = $(this.element); 115 | 116 | this.map = $imageMap.attr('usemap'); 117 | this.zones = $(this.map).find('area'); 118 | 119 | if (!$imageMap.hasClass('mapify')) { 120 | $imageMap.addClass('mapify'); 121 | 122 | this._mapWidth = parseInt($imageMap.attr('width')); 123 | this._mapHeight = parseInt($imageMap.attr('height')); 124 | if (!this._mapWidth || !this._mapHeight) { 125 | window.alert('ERROR: The width and height attributes must be specified on your image.'); 126 | return (false); 127 | } 128 | 129 | $imageMap.wrap(function () { 130 | return ('
'); 131 | }); 132 | 133 | this._mapHolder = $imageMap.parent(); 134 | $(this.map).appendTo(this._mapHolder); 135 | $imageMap.before(''); 136 | 137 | $imageMap.before(''); 138 | this.svgMap = $imageMap.prev('.mapify-svg'); 139 | 140 | // perform zones initialization 141 | this.zones.each(function () { 142 | _this._initSingleZone(this); 143 | }); 144 | 145 | $imageMap.wrap(function () { 146 | return '
'; 147 | }).css('opacity', 0); 148 | } 149 | }; 150 | 151 | Mapify.prototype._initSingleZone = function (zone) { 152 | switch ($(zone).attr('shape')) { 153 | case 'rect': 154 | var rectCoords = $(zone).attr("coords").split(','), 155 | fixedCoords = []; 156 | 157 | // From the top/left and bottom/right coordinates of the rect, we 158 | // can infer top/left, bottom/left, bottom/right, and top/right 159 | // in order to make a proper polygonal shape. 160 | $.each([0, 1, 0, 3, 2, 3, 2, 1], function (index, value) { 161 | fixedCoords.push(rectCoords[value]); 162 | }); 163 | $(zone).attr('coords', fixedCoords.join(',')); 164 | $(zone).attr('shape', 'poly'); 165 | break; 166 | case 'poly': 167 | // supported, passthru 168 | break; 169 | default: 170 | console.log('ERROR: Area shape type "' + $(zone).attr('shape') + '" is not supported.'); 171 | return (false); 172 | } 173 | 174 | // store default pixel coordinates for later use 175 | $(zone).attr('data-coords-default', $(zone).attr('coords')); 176 | 177 | if (this.isPopOverEnabled) { 178 | // Prevent that little anoying bubble from 179 | // poping up when the popover is activated 180 | $(zone).removeAttr('alt') 181 | .attr('data-title', $(zone).attr('title')) 182 | .removeAttr('title'); 183 | } 184 | 185 | var coords = $(zone).attr('coords').split(','); 186 | for (var key in coords) { // convert the pixel coordinates to percentage 187 | if (key % 2 == 0) { // X 188 | coords[key] = coords[key] * 100 / this._mapWidth; 189 | } else { // Y 190 | coords[key] = coords[key] * 100 / this._mapHeight; 191 | } 192 | } 193 | $(zone).attr('data-coords', coords.toString()); // store the percentage coordinates for later use 194 | 195 | var polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon'); 196 | polygon.className = 'mapify-polygon'; 197 | polygon.setAttribute('fill', 'none'); 198 | 199 | this.svgMap.append(polygon); 200 | }; 201 | 202 | Mapify.prototype._bindEvents = function () { 203 | var _this = this, 204 | $imageMap = $(this.element); 205 | 206 | this._hasScrolled = false; 207 | 208 | $(document).bind('touchend.mapify', function () { 209 | if (!_this._hasScrolled) { 210 | _this._clearMap(); 211 | } 212 | _this._hasScrolled = false; 213 | }).bind('touchmove.mapify', function () { 214 | _this._hasScrolled = true; 215 | }); 216 | 217 | $imageMap.bind('touchmove.mapify', function (e) { 218 | // adding a touchmove event (even empty) makes the touchstart 219 | // event below work on map areas in iOS8, don't know why. 220 | }); 221 | 222 | this._bindZoneEvents(); 223 | this._bindWindowEvents(); 224 | this._bindScrollParentEvents(); 225 | }; 226 | 227 | Mapify.prototype._bindZoneEvents = function () { 228 | var _this = this; 229 | 230 | this.zones.css({outline: 'none'}); 231 | this.zones.bind('touchend.mapify', function (e) { // fast-click on iOS 232 | if ($(this).hasClass('mapify-clickable')) { 233 | $(this).trigger('click'); 234 | _this.zones.removeClass('mapify-clickable'); 235 | } 236 | _this.hasScrolled = false; 237 | e.stopPropagation(); // Prevent event bubbling, prevent triggering a document touchend, wich would clear the map. 238 | }).bind('click.mapify', function (e) { 239 | // Preventing the click event from being triggered by a human 240 | // The click event must be triggered on touchend for the fast-click 241 | if ((e.originalEvent !== undefined) && isMobile) { 242 | return (false); 243 | } 244 | }).bind('touchstart.mapify', function () { 245 | _this.zones.removeClass('mapify-clickable'); 246 | var polygon = _this.svgMap.find('polygon:eq(' + $(this).index() + ')')[0]; 247 | 248 | // DO NOT USE hasClass on SVGs, it won't work. Use .classList.contains() instead. 249 | // Issue #25: https://github.com/etienne-martin/mapify/issues/25 250 | 251 | if( polygon.classList.contains("mapify-hover") ){ 252 | $(this).addClass('mapify-clickable'); 253 | } else { 254 | if (isMobile && _this.options.instantClickOnMobile) { 255 | console.log('Triggering instantClickOnMobile after touchstart'); 256 | $(this).addClass('mapify-clickable'); 257 | } else { 258 | $(this).addClass('mapify-hilightable'); 259 | } 260 | } 261 | }).bind('touchmove.mapify', function () { 262 | // prevent from triggering a click when a touch-move event occurred 263 | _this.zones.removeClass('mapify-clickable mapify-hilightable'); 264 | }).bind('mouseenter.mapify focus.mapify touchend.mapify', function (e) { 265 | var zone = this; 266 | if (!$(this).hasClass('mapify-hilightable') && isMobile) { 267 | return (false); 268 | } 269 | 270 | _this._clearMap(); 271 | if (_this.isPopOverEnabled) { 272 | _this._renderPopOver(zone); 273 | } 274 | 275 | // Now it's time to draw our SVG with the area coordinates 276 | _this._drawHighlight(zone); 277 | 278 | e.preventDefault(); 279 | }).bind("mouseout.mapify", function () { 280 | _this._clearMap(); 281 | }); 282 | 283 | if (!isMobile) { 284 | this.zones.bind('blur.mapify', function () { 285 | _this._clearMap(); 286 | }); 287 | } 288 | }; 289 | 290 | Mapify.prototype._bindWindowEvents = function () { 291 | var _this = this; 292 | $(window).bind('resize.mapify', function () { // on resizeStop event 293 | 294 | _this._timer && clearTimeout(_this._timer); 295 | _this._timer = setTimeout(function () { 296 | 297 | if (_this.isPopOverEnabled) { 298 | if (!_this.popOver.hasClass('mapify-visible')) { 299 | if (!_this.isCustomPopOver) { 300 | // prevent the popover from overflowing when resizing the window 301 | _this.popOver.css({left: 0, top: 0}); 302 | } 303 | } 304 | } 305 | 306 | // We must reset the points in Internet explorer, otherwise it overflow the viewport 307 | _this.svgMap.find('polygon').attr('points', ''); 308 | 309 | _this._remapZones(); 310 | 311 | var hlZone = _this.zones[_this.svgMap.find('polygon.mapify-hover').index()]; 312 | if (hlZone) { 313 | if (_this.isPopOverEnabled) { 314 | _this._renderPopOver(hlZone); 315 | } 316 | _this._drawHighlight(hlZone); 317 | } 318 | 319 | }, 100); 320 | }); 321 | }; 322 | 323 | Mapify.prototype._bindScrollParentEvents = function () { 324 | var _this = this; 325 | 326 | // noinspection JSUnresolvedFunction 327 | this.scrollParent = $(this.element).mapify_scrollParent(); 328 | 329 | if (this.scrollParent.is(document)) { 330 | this.scrollParent = $(window); 331 | } 332 | 333 | this.scrollParent 334 | .addClass('mapify-GPU') 335 | .bind('scroll.mapify', function () { // on scrollStop 336 | if (isMobile) { 337 | _this.zones.removeClass('mapify-clickable mapify-hilightable'); 338 | } 339 | 340 | if (_this.isPopOverEnabled) { 341 | if (!_this.isCustomPopOver && isMobile) { 342 | _this.popOver.css({ 343 | top: _this.popOver.css('top'), 344 | left: _this.popOver.css('left'), 345 | transition: 'none' 346 | }); 347 | _this.popOverArrow.css({ 348 | marginLeft: _this.popOverArrow.css('margin-left'), 349 | transition: 'none' 350 | }); 351 | } 352 | 353 | clearTimeout($.data(this, 'scrollTimer')); 354 | $.data(this, 'scrollTimer', setTimeout(function () { 355 | var hlZone = _this.zones[_this.svgMap.find('polygon.mapify-hover').index()]; 356 | if (hlZone) { 357 | // Trigger re-render of the popOver when the user stop scrolling 358 | _this._renderPopOver(hlZone); 359 | 360 | var _tmp = _this._computePopOverCompensation(hlZone), 361 | arrowCompensation = _tmp[1], 362 | corners = _tmp[2]; 363 | 364 | if (!_this.isCustomPopOver && isMobile) { 365 | _this.popOver.css({ 366 | top: corners[1], 367 | left: corners[0], 368 | transition: _this._popOverTransition 369 | }); 370 | _this.popOverArrow.css({ 371 | marginLeft: arrowCompensation, 372 | transition: _this._popOverArrowTransition 373 | }); 374 | } 375 | } 376 | }, 100)); 377 | } 378 | }); 379 | }; 380 | 381 | Mapify.prototype._drawHighlight = function (zone) { 382 | var _this = this, 383 | groupIdValue = $(zone).attr('data-group-id'), 384 | hoverClass = $(zone).attr('data-hover-class'); // Use .attr instead of .data https://github.com/etienne-martin/Mapify/issues/27 385 | 386 | // Combine hover classes 387 | hoverClass = hoverClass ? this.options.hoverClass + " " + hoverClass : this.options.hoverClass; 388 | 389 | if (!groupIdValue) { 390 | this._highlightSingleArea(zone, hoverClass); 391 | } else { 392 | // Highlight areas of the same map id which have the same groupId 393 | $(zone).siblings('area[data-group-id=' + groupIdValue + ']').addBack().each(function () { 394 | _this._highlightSingleArea(this, hoverClass); 395 | }); 396 | } 397 | 398 | // call area highlight event if assigned 399 | if (this.options.onAreaHighlight) { 400 | this.options.onAreaHighlight(this, zone); 401 | } 402 | }; 403 | 404 | Mapify.prototype._highlightSingleArea = function (zone, hoverClass) { 405 | var coords = $(zone).attr('data-coords').split(','); 406 | var zonePoints = ''; 407 | 408 | // Generating our points map based on the csv coordinates 409 | for (var key in coords) { // Convert percentage coordinates back to pixel coordinates relative to the image size 410 | if (key % 2 == 0) { // X 411 | zonePoints += ($(this.element).width() * (coords[key] / 100)); 412 | } else { // Y 413 | zonePoints += ',' + ($(this.element).height() * (coords[key] / 100)) + ' '; 414 | } 415 | } 416 | 417 | var polygon = this.svgMap.find('polygon:eq(' + $(zone).index() + ')')[0]; 418 | $(polygon) 419 | .attr('points', zonePoints) 420 | .attr('class', function (index, classNames) { 421 | var result = classNames; 422 | if (!$(polygon).hasClass('mapify-hover')) { 423 | result += ' mapify-hover'; 424 | if (hoverClass) { 425 | result += ' ' + hoverClass; 426 | } 427 | } 428 | return (result); 429 | }); 430 | }; 431 | 432 | Mapify.prototype._remapZones = function () { 433 | var _this = this; 434 | this.zones.each(function () { 435 | var coords = $(this).attr('data-coords').split(','); 436 | for (var key in coords) { // Convert percentage coordinates back to pixel coordinates relative to the image size 437 | if (key % 2 == 0) { // X 438 | coords[key] = ($(_this.element).width() * (coords[key] / 100)); 439 | } else { // Y 440 | coords[key] = ($(_this.element).height() * (coords[key] / 100)); 441 | } 442 | } 443 | $(this).attr('coords', coords.toString()); 444 | }); 445 | }; 446 | 447 | Mapify.prototype._renderPopOver = function (zone) { 448 | if (!this.isCustomPopOver) { 449 | this._renderDefaultPopOver(zone); 450 | } else { 451 | this._renderCustomPopOver(zone); 452 | } 453 | }; 454 | 455 | Mapify.prototype._renderCustomPopOver = function (zone) { 456 | var _this = this, 457 | customPopOver = this.options.popOver.customPopOver, 458 | $popOver = this.popOver; 459 | 460 | clearTimeout(this._popOverTimeout); 461 | this._popOverTimeout = setTimeout(function () { 462 | var content = _this.options.popOver.content($(zone), _this.element); 463 | $popOver.find(customPopOver.contentSelector).html(content); 464 | 465 | setTimeout(function () { 466 | $popOver.css({ 467 | transition: _this._popOverTransition 468 | }).addClass(customPopOver.visibleClass); 469 | }, 10); // prevent transitions when set to none 470 | }, 100); 471 | }; 472 | 473 | Mapify.prototype._renderDefaultPopOver = function (zone) { 474 | var _this = this, 475 | popOverWidth = this.popOver.outerWidth(), 476 | borderOffset = this.options.popOver.margin, 477 | 478 | $popOver = this.popOver, 479 | $popOverArrow = this.popOverArrow; 480 | 481 | // remove current pop-over class if some specified 482 | var currentCustomPopOverClass = $popOver.attr('data-popOver-class'); 483 | if (currentCustomPopOverClass != '') { 484 | $popOver.removeClass(currentCustomPopOverClass); 485 | $popOver.attr('data-popOver-class', ''); 486 | } 487 | 488 | // set popOver max-width based on the scrollparent width if it exceeds the scrollparent width 489 | if (this.scrollParent.width() - (borderOffset * 2) <= popOverWidth) { 490 | popOverWidth = this.scrollParent.width() - (borderOffset * 2); 491 | $popOver.css({ 492 | maxWidth: popOverWidth 493 | }); 494 | } else if (this._mapHolder.width() - (borderOffset * 2) <= popOverWidth) { 495 | // set popOver max-width based on the current map width if it exceeds the map width 496 | popOverWidth = this._mapHolder.width() - (borderOffset * 2); 497 | $popOver.css({ 498 | maxWidth: popOverWidth 499 | }); 500 | } else { 501 | $popOver.css({ 502 | maxWidth: '' 503 | }); 504 | } 505 | 506 | $popOver.css({ 507 | marginLeft: -(popOverWidth / 2) 508 | }); 509 | 510 | var _tmp = this._computePopOverCompensation(zone), 511 | compensation = _tmp[0], 512 | arrowCompensation = _tmp[1], 513 | corners = _tmp[2]; 514 | 515 | // prevent the popOver from sliding in of nowhere 516 | if (!$popOver.hasClass('mapify-visible')) { 517 | $popOver.css({ 518 | top: corners[1], 519 | left: corners[0], 520 | transition: 'none' 521 | }); 522 | $popOverArrow.css({ 523 | marginLeft: arrowCompensation, 524 | transition: 'none' 525 | }); 526 | } 527 | 528 | clearTimeout(this._popOverTimeout); 529 | this._popOverTimeout = setTimeout(function () { 530 | // We use a delay otherwise the popOver go crazy on mousemove 531 | var content = _this.options.popOver.content($(zone), _this.element); 532 | 533 | // allow for custom pop-over class specified in area element 534 | var customPopOverClass = $(zone).attr('data-pop-over-class'); 535 | if (customPopOverClass != '') { 536 | $popOver.addClass(customPopOverClass); 537 | $popOver.attr('data-popOver-class', customPopOverClass); 538 | } 539 | 540 | $popOver.find('.mapify-popOver-content').html(content); 541 | if ($popOver.hasClass('mapify-to-bottom')) { 542 | $popOver.css({marginTop: ''}); 543 | if (!$popOver.hasClass('mapify-bottom')) { 544 | $popOverArrow.css({ 545 | marginLeft: compensation, 546 | transition: 'none' 547 | }); 548 | } 549 | $popOver.addClass('mapify-bottom'); 550 | $popOver.removeClass('mapify-to-bottom'); 551 | } else { 552 | if ($popOver.hasClass('mapify-bottom')) { 553 | $popOverArrow.css({ 554 | marginLeft: compensation, 555 | transition: 'none' 556 | }); 557 | } 558 | $popOver.removeClass('mapify-bottom'); 559 | $popOver.css({ 560 | marginTop: -$popOver.outerHeight() 561 | }); 562 | } 563 | 564 | setTimeout(function () { 565 | $popOver.css({ 566 | top: corners[1], 567 | left: corners[0], 568 | transition: _this._popOverTransition 569 | }).addClass('mapify-visible'); 570 | $popOverArrow.css({ 571 | marginLeft: arrowCompensation, 572 | transition: _this._popOverArrowTransition 573 | }); 574 | }, 10); // prevent transitions when set to none 575 | }, 100); 576 | }; 577 | 578 | Mapify.prototype._computePopOverCompensation = function (zone) { 579 | var compensation = 0, 580 | positionLeft = 0, 581 | $popOver = this.popOver, 582 | cornersArray = this._getAreaCorners(zone), 583 | corners = cornersArray['center top'], 584 | popOverWidth = $popOver.outerWidth(), 585 | borderOffset = this.options.popOver.margin; 586 | 587 | if (this._mapHolder.width() < this.scrollParent.width()) { // if the map is smaller than the viewport 588 | positionLeft = (corners[0] - (popOverWidth / 2)) - this.scrollParent.scrollLeft(); 589 | if (positionLeft + popOverWidth > (this._mapHolder.width() - borderOffset)) { 590 | compensation = ((positionLeft + popOverWidth) - this._mapHolder.width()) + borderOffset; 591 | } else if (positionLeft < borderOffset) { 592 | compensation = positionLeft - borderOffset; 593 | } 594 | } else { 595 | positionLeft = (corners[0] - (popOverWidth / 2)) - this.scrollParent.scrollLeft(); 596 | if (positionLeft + popOverWidth > (this.scrollParent.outerWidth() - borderOffset)) { 597 | compensation = ((positionLeft + popOverWidth) - this.scrollParent.outerWidth()) + borderOffset; 598 | } else if (positionLeft < borderOffset) { 599 | compensation = positionLeft - borderOffset; 600 | } 601 | } 602 | 603 | if (corners[1] - $popOver.outerHeight() - borderOffset < 0) { 604 | corners = cornersArray['center bottom']; 605 | $popOver.addClass('mapify-to-bottom'); 606 | } else if ($popOver.hasClass('mapify-to-bottom')) { 607 | $popOver.removeClass('mapify-to-bottom'); 608 | } 609 | 610 | corners[0] -= compensation; 611 | var arrowCompensation = compensation; 612 | 613 | if (compensation > ((popOverWidth / 2) - (borderOffset * 2))) { 614 | arrowCompensation = (popOverWidth / 2) - (borderOffset * 2); 615 | } else if (compensation < -((popOverWidth / 2) - (borderOffset * 2))) { 616 | arrowCompensation = -(popOverWidth / 2) + (borderOffset * 2); 617 | } 618 | 619 | return [compensation, arrowCompensation, corners]; 620 | }; 621 | 622 | Mapify.prototype._getAreaCorners = function (zone) { 623 | var coords = zone.getAttribute('coords'); 624 | var coordsArray = coords.split(','); 625 | 626 | var coord, 627 | minX = parseInt(coordsArray[0], 10), 628 | maxX = minX, 629 | minY = parseInt(coordsArray[1], 10), 630 | maxY = minY; 631 | 632 | for (var i = 0, l = coordsArray.length; i < l; i++) { 633 | coord = parseInt(coordsArray[i], 10); 634 | if (i % 2 == 0) { 635 | if (coord < minX) { 636 | minX = coord; 637 | } else if (coord > maxX) { 638 | maxX = coord; 639 | } 640 | } else { 641 | if (coord < minY) { 642 | minY = coord; 643 | } else if (coord > maxY) { 644 | maxY = coord; 645 | } 646 | } 647 | } 648 | 649 | var centerX = parseInt((minX + maxX) / 2, 10); 650 | // noinspection JSUnusedLocalSymbols 651 | var centerY = parseInt((minY + maxY) / 2, 10); 652 | 653 | return { 654 | 'center top': {0: centerX, 1: minY}, 655 | 'center bottom': {0: centerX, 1: maxY} 656 | }; 657 | }; 658 | 659 | Mapify.prototype._clearMap = function () { 660 | var _this = this; 661 | if (this.isPopOverEnabled) { 662 | var shouldHide = true, 663 | visibleClass = 'mapify-visible'; 664 | if (this.isCustomPopOver) { 665 | var customPopOver = this.options.popOver.customPopOver; 666 | visibleClass = customPopOver.visibleClass; 667 | shouldHide = !customPopOver.alwaysVisible; 668 | } 669 | clearTimeout(this._popOverTimeout); 670 | if (shouldHide) { 671 | this._popOverTimeout = setTimeout(function () { 672 | _this.popOver.removeClass(visibleClass); 673 | }, 300); 674 | } 675 | } 676 | 677 | // removeClass and addClass seems to fail on svg 678 | this.svgMap.find('polygon').attr('class', 'mapify-polygon'); 679 | 680 | // if event is assigned, call the handler 681 | if (this.options.onMapClear) { 682 | this.options.onMapClear(this); 683 | } 684 | }; 685 | 686 | function Mapify(element, options) { 687 | var _this = this; 688 | 689 | this.element = element; 690 | this.options = options; 691 | 692 | this.isPopOverEnabled = (this.options.popOver != false); 693 | this.isCustomPopOver = (this.options.popOver.customPopOver != false) 694 | && (this.options.popOver.customPopOver != undefined); 695 | 696 | this._initImageMap(); 697 | this._initPopOver(); 698 | this._bindEvents(); 699 | 700 | this._remapZones(); 701 | $(this.element).bind('load.mapify', function () { 702 | // We must wait for the image to load in safari 703 | _this._remapZones(); 704 | }); 705 | } 706 | 707 | //endregion 708 | 709 | $.fn.mapify = function (options) { 710 | return this.each(function () { 711 | if (!$.data(this, 'plugin_mapify')) { 712 | $.data(this, 'plugin_mapify', 713 | new Mapify(this, $.extend(true, {}, defaults, options))); 714 | } 715 | }); 716 | }; 717 | 718 | $.fn.mapify_scrollParent = function () { 719 | var position = this.css('position'), 720 | isExcludeStaticParent = (position === 'absolute'), 721 | scrollParent = this.parents().filter(function () { 722 | var parent = $(this); 723 | if (isExcludeStaticParent 724 | && parent.css('position') === 'static') { 725 | return (false); 726 | } 727 | return (/(auto|scroll)/).test(parent.css('overflow') + parent.css('overflow-y') + parent.css('overflow-x')); 728 | }).eq(0); 729 | 730 | return (position === 'fixed') || !scrollParent.length 731 | ? $(this[0].ownerDocument || document) 732 | : scrollParent; 733 | }; 734 | 735 | })(jQuery, window, document); 736 | -------------------------------------------------------------------------------- /example/map.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 13 | 16 | 21 | 26 | 29 | 36 | 39 | 41 | 43 | 50 | 54 | 56 | 59 | 64 | 68 | 70 | 74 | 75 | 84 | 85 | 86 | 87 | 89 | 91 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 115 | 116 | 117 | 118 | 119 | 125 | 133 | 134 | 140 | 141 | 142 | 143 | 153 | 154 | 155 | 156 | 158 | 159 | 161 | 167 | 168 | 169 | 170 | 171 | 176 | 177 | 178 | 180 | 181 | 182 | 183 | 191 | 192 | 193 | 194 | 203 | 204 | 205 | 210 | 211 | 212 | 213 | 214 | 223 | 224 | 225 | 230 | 232 | 234 | 236 | 245 | 246 | 247 | 248 | 258 | 259 | 260 | 270 | 271 | 273 | 281 | 282 | 283 | 284 | 293 | 294 | 295 | 296 | 297 | 299 | 300 | 302 | 303 | 304 | 305 | 310 | 311 | 312 | 318 | 320 | 327 | 335 | 336 | 337 | 338 | 348 | 349 | 350 | 355 | 363 | 365 | 366 | 367 | 368 | 377 | 381 | 382 | 383 | 384 | 394 | 395 | 396 | 397 | 401 | 408 | 412 | 413 | 423 | 424 | 425 | 434 | 435 | 436 | 440 | 444 | 449 | 450 | 451 | 452 | 453 | 455 | 460 | 466 | 468 | 477 | 478 | 479 | 480 | 482 | 483 | 484 | 492 | 493 | 494 | 495 | 504 | 505 | 506 | 508 | 510 | 516 | 523 | 524 | 525 | 526 | 531 | 532 | 533 | 539 | 540 | 547 | 548 | 549 | 550 | 551 | 552 | 556 | 557 | 558 | 568 | 569 | 570 | 576 | 577 | 582 | 583 | 585 | 590 | 595 | 596 | 601 | 602 | 603 | 605 | 606 | 612 | 613 | 615 | 619 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 640 | 641 | 642 | 643 | 651 | 655 | 656 | 665 | 666 | 667 | 676 | 677 | 678 | 684 | 686 | 692 | 693 | 695 | 701 | 703 | 704 | 713 | 717 | 722 | 723 | 724 | 733 | 734 | 735 | 736 | 738 | 740 | 746 | 747 | 748 | 749 | 751 | 753 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 775 | 776 | 777 | 778 | 780 | 782 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | --------------------------------------------------------------------------------