├── test ├── .gitignore ├── readme.md ├── src │ ├── 210-adapter-native.coffee │ ├── 220-adapter-ender.coffee │ ├── 020-opentip-startup.coffee │ ├── 060-opentip-ajax.coffee │ ├── 100-utils.coffee │ ├── 040-opentip-positioning.coffee │ ├── 110-joint.coffee │ ├── 050-opentip-draw.coffee │ ├── 030-opentip-show.coffee │ ├── 010-opentip.coffee │ └── 200-adapters.coffee └── test.html ├── .travis.yml ├── files ├── tests.png ├── explanations.psd └── close-button-angle.png ├── .gitignore ├── bower.json ├── index.js ├── component.json ├── downloads └── readme.md ├── Makefile ├── package.json ├── CONTRIBUTING.md ├── css ├── stylus │ └── opentip.styl └── opentip.css ├── .tagconfig.json ├── Gruntfile.coffee ├── src ├── adapter-component.coffee ├── adapter-jquery.coffee ├── adapter-ender.coffee ├── adapter-prototype.coffee ├── adapter-native.coffee └── adapter-browserify.coffee ├── lib ├── adapter-component.js ├── adapter-jquery.js ├── adapter-ender.js ├── adapter-prototype.js ├── adapter-native.js └── adapter-browserify.js ├── docs ├── adapter.prototype.html └── docco.css └── README.md /test/.gitignore: -------------------------------------------------------------------------------- 1 | ender.min.js 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | -------------------------------------------------------------------------------- /files/tests.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pose/opentip/master/files/tests.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | components 2 | build 3 | .* 4 | !.gitignore 5 | !.travis.yml 6 | node_modules 7 | .tmp-* -------------------------------------------------------------------------------- /files/explanations.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pose/opentip/master/files/explanations.psd -------------------------------------------------------------------------------- /files/close-button-angle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pose/opentip/master/files/close-button-angle.png -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opentip", 3 | "version": "2.4.6", 4 | "main": [ 5 | "css/opentip.css", 6 | "lib/opentip.js", 7 | "lib/adapter-component.js" 8 | ], 9 | "dependencies": { 10 | "jquery": "~1.9.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/readme.md: -------------------------------------------------------------------------------- 1 | # Tests 2 | 3 | The tests are writte in coffeescript and are in the `src/` folder. 4 | 5 | To compile them, launch `grunt js` in the project root folder. 6 | 7 | To execute the tests, simply open the `test.html` file in a browser, or run 8 | `npm test` in the root folder. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Matias Meno 2 | 3 | 4 | // The index.js file for component 5 | var Opentip = require("./lib/opentip.js"); 6 | 7 | 8 | var Adapter = require("./lib/adapter-component.js"); 9 | 10 | // Add the adapter to the list 11 | Opentip.addAdapter(new Adapter()); 12 | 13 | 14 | // Exposing the Opentip class 15 | module.exports = Opentip; -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opentip", 3 | "repo": "enyo/opentip", 4 | "version": "2.4.6", 5 | "description": "Free opensource tooltip class.", 6 | "keywords": [ "tooltip" ], 7 | "dependencies": { 8 | "component/jquery": "*" 9 | }, 10 | "styles": [ "css/opentip.css" ], 11 | "scripts": [ "index.js", "lib/opentip.js", "lib/adapter-component.js" ] 12 | } 13 | -------------------------------------------------------------------------------- /downloads/readme.md: -------------------------------------------------------------------------------- 1 | # Downloads 2 | 3 | Choose the **one** file that suits your needs. If you don't intend to support IE8 4 | (or include excanvas yourself) you don't need the `-excanvas` version. 5 | 6 | You'll probably want the `.min.js` version unless you count on debugging Opentip. 7 | 8 | ## Generate downloads 9 | 10 | To generate downloads you need to run `npm install` once from within the 11 | root folder, and then `grunt js`. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: index.js components 2 | @component build 3 | 4 | rebuild: index.js components 5 | rm -fr build 6 | make build 7 | 8 | components: 9 | @component install 10 | 11 | clean: 12 | rm -fr build components 13 | 14 | downloads: 15 | ./downloads/generate.coffee 16 | 17 | release: 18 | cake build 19 | cake css 20 | make downloads 21 | 22 | all: 23 | clear 24 | make clean 25 | make build 26 | 27 | .PHONY: clean, downloads 28 | -------------------------------------------------------------------------------- /test/src/210-adapter-native.coffee: -------------------------------------------------------------------------------- 1 | 2 | 3 | unless Opentip.adapters.component? 4 | # Only test this adapter if not testing component 5 | describe "Native adapter", -> 6 | adapter = Opentip.adapter 7 | 8 | describe "DOM", -> 9 | describe "create()", -> 10 | it "should properly create DOM elements from string", -> 11 | elements = adapter.create """
HI
""" 12 | expect(elements).to.be.an "object" 13 | expect(elements.length).to.equal 1 14 | expect(elements[0].className).to.equal "test" 15 | it "the created elements should be wrapped", -> 16 | elements = adapter.create """
HI
""" 17 | wrapped = adapter.wrap elements 18 | expect(elements).to.equal wrapped 19 | 20 | describe "wrap()", -> 21 | it "should wrap the element in an array", -> 22 | element = document.createElement "div" 23 | wrapped = adapter.wrap element 24 | expect(element).to.equal wrapped[0] 25 | 26 | it "should properly wrap nodelists", -> 27 | wrapped = adapter.wrap document.body.childNodes 28 | expect(wrapped).to.not.be.a NodeList 29 | -------------------------------------------------------------------------------- /test/src/220-adapter-ender.coffee: -------------------------------------------------------------------------------- 1 | 2 | unless Opentip.adapters.component? 3 | # Only test this adapter if not testing component 4 | $ = jQuery 5 | 6 | describe "Ender adapter", -> 7 | adapter = Opentip.adapter 8 | 9 | describe "DOM", -> 10 | describe "create()", -> 11 | it "should properly create DOM elements from string", -> 12 | elements = adapter.create """
HI
""" 13 | expect(elements).to.be.an "object" 14 | expect(elements.length).to.equal 1 15 | expect(elements[0].className).to.equal "test" 16 | 17 | describe "wrap()", -> 18 | it "should return a bonzo element", -> 19 | element = document.createElement "div" 20 | wrapped = adapter.wrap element 21 | expect(element).to.not.equal wrapped 22 | expect(element).to.equal adapter.unwrap wrapped 23 | 24 | 25 | describe "tagName()", -> 26 | it "should return the tagName of passed ender element", -> 27 | element = $("div")[0] 28 | expect(adapter.tagName element).to.equal "DIV" 29 | 30 | describe "attr()", -> 31 | it "should return the attribute of passed ender element", -> 32 | element = $("""""")[0] 33 | expect(adapter.attr element, "href").to.equal "http://link" 34 | 35 | describe "observe()", -> 36 | it "should observe given event on ender element", (done) -> 37 | element = $("link")[0] 38 | adapter.observe element, "click", -> done() 39 | Test.clickElement element 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /test/src/020-opentip-startup.coffee: -------------------------------------------------------------------------------- 1 | 2 | $ = jQuery 3 | 4 | describe "Opentip - Startup", -> 5 | adapter = Opentip.adapter 6 | opentip = null 7 | 8 | afterEach -> 9 | opentip[prop]?.restore?() for own prop of opentip 10 | opentip?.deactivate?() 11 | $("[data-ot]").remove() 12 | $(".opentip-container").remove() 13 | 14 | it "should find all elements with data-ot()", -> 15 | trigger = $("""
""")[0] 16 | $(document.body).append trigger 17 | Opentip.findElements() 18 | expect(adapter.data(trigger, "opentips")).to.be.an Array 19 | expect(adapter.data(trigger, "opentips").length).to.equal 1 20 | expect(adapter.data(trigger, "opentips")[0]).to.be.an Opentip 21 | 22 | it "should take configuration from data- attributes", -> 23 | trigger = $("""
""")[0] 24 | $(document.body).append trigger 25 | Opentip.findElements() 26 | expect(adapter.data(trigger, "opentips")[0]).to.be.an Opentip 27 | opentip = adapter.data(trigger, "opentips")[0] 28 | expect(opentip.options.hideTrigger).to.eql "closeButton" 29 | expect(opentip.options.showOn).to.eql "click" 30 | 31 | it "should properly parse boolean data- attributes", -> 32 | trigger = $("""
""")[0] 33 | $(document.body).append trigger 34 | Opentip.findElements() 35 | opentip = adapter.data(trigger, "opentips")[0] 36 | expect(opentip.options.shadow).to.be yes 37 | expect(opentip.options.autoOffset).to.be no 38 | expect(opentip.options.containInViewport).to.be no 39 | 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opentip", 3 | "version": "2.4.6", 4 | "description": "Free opensource tooltip class.", 5 | "keywords": [ "tooltip" ], 6 | "homepage": "http://www.opentip.org", 7 | "main": [ 8 | "./lib/opentip.js", 9 | "./lib/adapter.ender.js" 10 | ], 11 | "maintainers": [ 12 | { 13 | "name": "Matias Meno", 14 | "email": "m@tias.me", 15 | "web": "http://www.matiasmeno.com" 16 | } 17 | ], 18 | "contributors": [ 19 | { 20 | "name": "Matias Meno", 21 | "email": "m@tias.me", 22 | "web": "http://www.matiasmeno.com" 23 | } 24 | ], 25 | "bugs": { 26 | "mail": "m@tias.me" 27 | }, 28 | "licenses": [ 29 | { 30 | "type": "MIT", 31 | "url": "http://www.opensource.org/licenses/MIT" 32 | } 33 | ], 34 | "repositories": [ 35 | { 36 | "type": "git", 37 | "url": "https://github.com/enyo/opentip.git" 38 | } 39 | ], 40 | "scripts": { 41 | "test": "node_modules/mocha-phantomjs/bin/mocha-phantomjs test/test.html" 42 | }, 43 | "dependencies": { 44 | "qwery": "3.x", 45 | "domready": "0.x", 46 | "bean": "0.x", 47 | "bonzo": "1.x", 48 | "reqwest": "0.x" 49 | }, 50 | "devDependencies": { 51 | "grunt": "~0.4.5", 52 | "grunt-contrib-stylus": "~0.17.0", 53 | "grunt-contrib-watch": "~0.6.1", 54 | "grunt-contrib-coffee": "~0.10.1", 55 | "grunt-contrib-concat": "~0.4.0", 56 | "grunt-contrib-uglify": "~0.5.0", 57 | "grunt-contrib-clean": "~0.5.0", 58 | "grunt-curl": "~2.0.2", 59 | "mocha": "~1.20.1", 60 | "expect.js": "~0.3.1", 61 | "mocha-phantomjs": "~3.4.1", 62 | "uglify-js2": "~2.1.11", 63 | "request": "~2.36.0" 64 | }, 65 | "ender": "noop" 66 | } 67 | -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 61 | 62 | 63 | 64 | 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contribute 2 | ========== 3 | 4 | The latest stable version is always in the **[master](https://github.com/enyo/opentip)** branch (which always 5 | points at the latest version tag). 6 | 7 | The latest development version is in the **[develop](https://github.com/enyo/opentip/tree/develop)** branch. 8 | 9 | > Use the develop branch if you want to contribute or test features. 10 | 11 | Please do also **send pull requests to the `develop` branch**. 12 | I will **not** merge pull requests to the `master` branch. 13 | 14 | 15 | Make sure that changes pass all [tests](#testing). 16 | 17 | 18 | ### Coffeescript & Stylus (-> Javascript & CSS) 19 | 20 | Opentip is written in [Coffeescript](http://coffeescript.org) and 21 | [Stylus](http://learnboost.github.com/stylus/) so *do not* make 22 | changes to the Javascript or CSS files 23 | 24 | **I will not merge requests written in Javascript or CSS.** 25 | 26 | Getting started 27 | --------------- 28 | 29 | You need node to compile and test Opentip. So [install node](http://nodejs.org) 30 | first if you haven't done so already. 31 | 32 | 33 | ### Building Opentip 34 | 35 | 36 | First you have to setup the node modules to build Opentip. Simply run this in 37 | the Opentip directory: 38 | 39 | ```bash 40 | $ npm install 41 | ``` 42 | 43 | This will setup [Grunt](http://gruntjs.com) so you can compile Coffeescript and 44 | Stylus and generate the download files. 45 | 46 | To get a list of available commands use `grunt -h`. 47 | 48 | The most important command is 49 | 50 | ```bash 51 | $ grunt watch 52 | ``` 53 | 54 | This will observe any change to a coffeescript or stylus file and compile it 55 | immediately. 56 | 57 | 58 | > Please only submit commits with changed `.coffee` and `.stylus` files and do 59 | > *not* include the compiled JS or CSS files. 60 | 61 | 62 | ### Testing 63 | 64 | To test the library make sure that the source has been compiled with `grunt js` 65 | (as mentioned before, use `grunt watch` to always stay up to date) and then 66 | either type `npm test` to run the tests on the command line, or open the 67 | file `test/test.html` in a browser. 68 | 69 | It should look like this: 70 | 71 | ![Tests screenshot](https://raw.github.com/enyo/opentip/develop/files/tests.png) 72 | 73 | All tests are located in `test/src` and are written in coffeescript. 74 | 75 | If you add a change, please make sure that all tests pass! 76 | 77 | -------------------------------------------------------------------------------- /css/stylus/opentip.styl: -------------------------------------------------------------------------------- 1 | @import "nib" 2 | 3 | 4 | @keyframes otloading 5 | from 6 | transform rotate(0deg) 7 | to 8 | transform rotate(360deg) 9 | 10 | 11 | .opentip-container, .opentip-container * 12 | box-sizing border-box 13 | 14 | .opentip-container 15 | position absolute 16 | max-width 300px 17 | z-index 100 18 | transition transform 1s ease-in-out 19 | pointer-events none 20 | transform translateX(0) translateY(0) 21 | 22 | 23 | 24 | &.ot-fixed 25 | &.ot-hidden 26 | &.ot-going-to-show 27 | &.ot-hiding 28 | &.stem-top.stem-center 29 | transform translateY(-5px) 30 | &.stem-top.stem-right 31 | transform translateY(-5px) translateX(5px) 32 | &.stem-middle.stem-right 33 | transform translateX(5px) 34 | &.stem-bottom.stem-right 35 | transform translateY(5px) translateX(5px) 36 | &.stem-bottom.stem-center 37 | transform translateY(5px) 38 | &.stem-bottom.stem-left 39 | transform translateY(5px) translateX(-5px) 40 | &.stem-middle.stem-left 41 | transform translateX(-5px) 42 | &.stem-top.stem-left 43 | transform translateY(-5px) translateX(-5px) 44 | 45 | &.ot-fixed 46 | // When it's fixed, the close button, and links inside the tooltip should 47 | // be clickable 48 | .opentip 49 | pointer-events auto 50 | 51 | &.ot-hidden 52 | display none 53 | 54 | .opentip 55 | position relative 56 | font-size 13px 57 | line-height 120% 58 | padding 9px 14px 59 | color #4F4B47 60 | text-shadow -1px -1px 0px rgba(255, 255, 255, 0.2) 61 | 62 | .header 63 | margin 0 64 | padding 0 65 | 66 | .ot-close 67 | pointer-events auto 68 | display block 69 | absolute top -12px left 60px 70 | color rgba(0, 0, 0, 0.5) 71 | background rgba(0, 0, 0, 0) // So IE9 renders this as clickable 72 | text-decoration none 73 | 74 | span 75 | display none 76 | 77 | .ot-loading-indicator 78 | display none 79 | 80 | &.ot-loading 81 | .ot-loading-indicator 82 | width 30px 83 | height @width 84 | font-size @width 85 | line-height @width 86 | font-weight bold 87 | display block 88 | span 89 | display block 90 | animation otloading 2s linear infinite 91 | text-align center 92 | 93 | 94 | 95 | // Styles 96 | // ====== 97 | &.style-dark 98 | &.style-alert 99 | .opentip 100 | color #f8f8f8 101 | text-shadow 1px 1px 0px rgba(0, 0, 0, 0.2) 102 | 103 | &.style-glass 104 | .opentip 105 | padding 15px 25px 106 | color #317CC5 107 | text-shadow 1px 1px 8px rgba(0, 94, 153, 0.3) 108 | 109 | 110 | // Effects 111 | // ======= 112 | &.ot-hide-effect-fade 113 | transition transform 0.5s ease-in-out, opacity 1s ease-in-out 114 | opacity 1 115 | &.ot-hiding 116 | opacity 0 117 | 118 | &.ot-show-effect-appear 119 | &.ot-going-to-show 120 | &.ot-showing 121 | transition transform 0.5s ease-in-out, opacity 1s ease-in-out 122 | &.ot-going-to-show 123 | opacity 0 124 | &.ot-showing 125 | opacity 1 126 | &.ot-visible 127 | opacity 1 128 | -------------------------------------------------------------------------------- /test/src/060-opentip-ajax.coffee: -------------------------------------------------------------------------------- 1 | 2 | $ = jQuery 3 | 4 | describe "Opentip - AJAX", -> 5 | adapter = Opentip.adapter 6 | opentip = null 7 | triggerElementExists = yes 8 | 9 | beforeEach -> 10 | triggerElementExists = yes 11 | 12 | afterEach -> 13 | opentip[prop]?.restore?() for own prop of opentip 14 | opentip.deactivate() 15 | $(".opentip-container").remove() 16 | 17 | describe "_loadAjax()", -> 18 | describe "on success", -> 19 | beforeEach -> 20 | sinon.stub adapter, "ajax", (options) -> 21 | options.onSuccess "response text" 22 | options.onComplete() 23 | afterEach -> 24 | adapter.ajax.restore() 25 | 26 | it "should use adapter.ajax", -> 27 | opentip = new Opentip adapter.create("
"), "Test", ajax: "http://www.test.com", ajaxMethod: "post" 28 | opentip._setup() 29 | opentip._loadAjax() 30 | expect(adapter.ajax.callCount).to.be 1 31 | expect(adapter.ajax.args[0][0].url).to.equal "http://www.test.com" 32 | expect(adapter.ajax.args[0][0].method).to.equal "post" 33 | 34 | 35 | it "should be called by show() and update the content (only once!)", -> 36 | opentip = new Opentip adapter.create("
"), "Test", ajax: "http://www.test.com", ajaxMethod: "post", cache: yes 37 | sinon.stub opentip, "_triggerElementExists", -> yes 38 | 39 | sinon.spy opentip, "setContent"#, (content) -> #expect(content).to.be "response text" 40 | 41 | opentip.show() 42 | opentip.hide() 43 | opentip.show() 44 | opentip.hide() 45 | opentip.show() 46 | opentip.hide() 47 | expect(adapter.ajax.callCount).to.be 1 48 | expect(opentip.setContent.callCount).to.be 2 # Every time AJAX gets loaded, it empties the content 49 | expect(opentip.content).to.be "response text" 50 | 51 | 52 | it "if cache: false, should be called by show() and update the content every time show is called", -> 53 | opentip = new Opentip adapter.create("
"), "Test", ajax: "http://www.test.com", ajaxMethod: "post", cache: no 54 | sinon.stub opentip, "_triggerElementExists", -> yes 55 | sinon.spy opentip, "setContent"#, (content) -> expect(content).to.be "response text" 56 | opentip.show() 57 | opentip.hide() 58 | opentip.show() 59 | opentip.hide() 60 | opentip.show() 61 | opentip.hide() 62 | expect(adapter.ajax.callCount).to.be 3 63 | expect(opentip.setContent.callCount).to.be 6 # Every time AJAX gets loaded, it empties the content 64 | 65 | describe "on error", -> 66 | beforeEach -> 67 | sinon.stub adapter, "ajax", (options) -> 68 | options.onError "Some error" 69 | afterEach -> 70 | adapter.ajax.restore() 71 | 72 | it "should use the options.ajaxErrorMessage on failure", -> 73 | opentip = new Opentip adapter.create("
"), "Test", ajax: "http://www.test.com", ajaxMethod: "post", ajaxErrorMessage: "No download dude." 74 | opentip._setup() 75 | expect(opentip.options.ajaxErrorMessage).to.be "No download dude." 76 | 77 | expect(opentip.content).to.be "Test" 78 | 79 | opentip._loadAjax() 80 | 81 | expect(opentip.content).to.be opentip.options.ajaxErrorMessage -------------------------------------------------------------------------------- /.tagconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | { 4 | "name": "README.md", 5 | "regexs": [ 6 | "Version ###" 7 | ] 8 | }, 9 | { 10 | "name": "src/opentip.coffee", 11 | "regexs": [ 12 | "Opentip v###", 13 | "Opentip\\.version = \"###\"" 14 | ] 15 | }, 16 | { 17 | "name": "lib/opentip.js", 18 | "regexs": [ 19 | "Opentip v###", 20 | "Opentip\\.version = \"###\"" 21 | ] 22 | }, 23 | { 24 | "name": "component.json", 25 | "regexs": [ 26 | "\"version\": \"###\"" 27 | ] 28 | }, 29 | { 30 | "name": "bower.json", 31 | "regexs": [ 32 | "\"version\": \"###\"" 33 | ] 34 | }, 35 | { 36 | "name": "package.json", 37 | "regexs": [ 38 | "\"version\": \"###\"" 39 | ] 40 | }, 41 | 42 | 43 | { 44 | "name": "downloads/opentip-jquery-excanvas.js", 45 | "regexs": [ 46 | "Opentip\\.version = \"###\"", 47 | "Opentip v###" 48 | ] 49 | }, 50 | { 51 | "name": "downloads/opentip-jquery.js", 52 | "regexs": [ 53 | "Opentip\\.version = \"###\"", 54 | "Opentip v###" 55 | ] 56 | }, 57 | { 58 | "name": "downloads/opentip-native-excanvas.js", 59 | "regexs": [ 60 | "Opentip\\.version = \"###\"", 61 | "Opentip v###" 62 | ] 63 | }, 64 | { 65 | "name": "downloads/opentip-native.js", 66 | "regexs": [ 67 | "Opentip\\.version = \"###\"", 68 | "Opentip v###" 69 | ] 70 | }, 71 | { 72 | "name": "downloads/opentip-prototype-excanvas.js", 73 | "regexs": [ 74 | "Opentip\\.version = \"###\"", 75 | "Opentip v###" 76 | ] 77 | }, 78 | { 79 | "name": "downloads/opentip-prototype.js", 80 | "regexs": [ 81 | "Opentip\\.version = \"###\"", 82 | "Opentip v###" 83 | ] 84 | }, 85 | 86 | { 87 | "name": "downloads/opentip-jquery-excanvas.min.js", 88 | "regexs": [ 89 | "Opentip\\.version=\"###\"", 90 | "Opentip v###" 91 | ] 92 | }, 93 | { 94 | "name": "downloads/opentip-jquery.min.js", 95 | "regexs": [ 96 | "Opentip\\.version=\"###\"", 97 | "Opentip v###" 98 | ] 99 | }, 100 | { 101 | "name": "downloads/opentip-native-excanvas.min.js", 102 | "regexs": [ 103 | "Opentip\\.version=\"###\"", 104 | "Opentip v###" 105 | ] 106 | }, 107 | { 108 | "name": "downloads/opentip-native.min.js", 109 | "regexs": [ 110 | "Opentip\\.version=\"###\"", 111 | "Opentip v###" 112 | ] 113 | }, 114 | { 115 | "name": "downloads/opentip-prototype-excanvas.min.js", 116 | "regexs": [ 117 | "Opentip\\.version=\"###\"", 118 | "Opentip v###" 119 | ] 120 | }, 121 | { 122 | "name": "downloads/opentip-prototype.min.js", 123 | "regexs": [ 124 | "Opentip\\.version=\"###\"", 125 | "Opentip v###" 126 | ] 127 | }, 128 | { 129 | "name": "Gruntfile.coffee", 130 | "regexs": [ 131 | "Opentip v###" 132 | ] 133 | } 134 | 135 | ] 136 | } 137 | -------------------------------------------------------------------------------- /test/src/100-utils.coffee: -------------------------------------------------------------------------------- 1 | 2 | describe "utils", -> 3 | adapter = Opentip.adapter 4 | 5 | describe "debug()", -> 6 | consoleDebug = console.debug 7 | beforeEach -> sinon.stub console, "debug" 8 | afterEach -> console.debug.restore() 9 | 10 | it "should only debug when Opentip.debug == true", -> 11 | Opentip.debug = off 12 | Opentip::debug "test" 13 | expect(console.debug.called).to.not.be.ok() 14 | Opentip.debug = on 15 | Opentip::debug "test" 16 | expect(console.debug.called).to.be.ok() 17 | 18 | it "should include the opentip id", -> 19 | Opentip.debug = on 20 | opentip = new Opentip document.createElement("span") 21 | # Automatically calls debug but to make sure: 22 | opentip.debug "test" 23 | expect(console.debug.getCall(0).args[0]).to.be "##{opentip.id} |" 24 | 25 | describe "ucfirst()", -> 26 | it "should transform the first character to uppercase", -> 27 | expect(Opentip::ucfirst "abc def").to.equal "Abc def" 28 | 29 | describe "dasherize()", -> 30 | it "should transform camelized words into dasherized", -> 31 | expect(Opentip::dasherize "testAbcHoiTEST").to.equal "test-abc-hoi-t-e-s-t" 32 | 33 | describe "_positionsEqual()", -> 34 | it "should properly compare positions", -> 35 | eq = Opentip::_positionsEqual 36 | expect(eq { left: 0, top: 0 }, { left: 0, top: 0 }).to.be.ok() 37 | expect(eq { left: 100, top: 20 }, { left: 100, top: 20 }).to.be.ok() 38 | expect(eq { left: 100, top: 20 }, { left: 101, top: 20 }).to.not.be.ok() 39 | expect(eq null, { left: 101, top: 20 }).to.not.be.ok() 40 | expect(eq null, null).to.not.be.ok() 41 | 42 | describe "_dimensionsEqual()", -> 43 | it "should properly compare dimensions", -> 44 | eq = Opentip::_dimensionsEqual 45 | expect(eq { width: 0, height: 0 }, { width: 0, height: 0 }).to.be.ok() 46 | expect(eq { width: 100, height: 20 }, { width: 100, height: 20 }).to.be.ok() 47 | expect(eq { width: 100, height: 20 }, { width: 101, height: 20 }).to.not.be.ok() 48 | expect(eq null, { width: 101, height: 20 }).to.not.be.ok() 49 | expect(eq null, null).to.not.be.ok() 50 | 51 | describe "setCss3Style()", -> 52 | opentip = new Opentip adapter.create("
"), "Test" 53 | it "should set the style for all vendors", -> 54 | element = document.createElement "div" 55 | opentip.setCss3Style element, { opacity: "0.5", transitionDuration: "1s" } 56 | transitionDuration = false 57 | opacity = false 58 | 59 | for vendor in [ 60 | "" 61 | "Khtml" 62 | "Ms" 63 | "O" 64 | "Moz" 65 | "Webkit" 66 | ] 67 | prop = if vendor then "#{vendor}TransitionDuration" else "transitionDuration" 68 | transitionDuration = true if element.style[prop] == "1s" 69 | prop = if vendor then "#{vendor}Opacity" else "opacity" 70 | opacity = true if element.style[prop] == "0.5" 71 | # expect(element.style["-moz-transition-duration"]).to.be "1s" 72 | # expect(element.style["-moz-opacity"]).to.be "0.5" 73 | expect(transitionDuration).to.be true 74 | expect(opacity).to.be true 75 | # expect(element.style["-o-transition-duration"]).to.be "1s" 76 | 77 | describe "defer()", -> 78 | it "should call the callback as soon as possible" 79 | 80 | describe "setTimeout()", -> 81 | it "should wrap window.setTimeout but with seconds" 82 | 83 | -------------------------------------------------------------------------------- /test/src/040-opentip-positioning.coffee: -------------------------------------------------------------------------------- 1 | 2 | $ = jQuery 3 | 4 | describe "Opentip - Positioning", -> 5 | adapter = Opentip.adapter 6 | opentip = null 7 | triggerElementExists = yes 8 | 9 | beforeEach -> 10 | triggerElementExists = yes 11 | 12 | afterEach -> 13 | opentip[prop]?.restore?() for prop of opentip 14 | $(".opentip-container").remove() 15 | 16 | describe "fixed", -> 17 | element = adapter.create("""
""") 18 | beforeEach -> 19 | adapter.append document.body, element 20 | afterEach -> 21 | $(adapter.unwrap element).remove() 22 | 23 | describe "without autoOffset", -> 24 | it "should correctly position opentip without border and stem", -> 25 | opentip = new Opentip element, "Test", delay: 0, target: yes, borderWidth: 0, stem: off, offset: [ 0, 0 ], autoOffset: false 26 | opentip._setup() 27 | elementOffset = adapter.offset element 28 | expect(elementOffset).to.eql left: 500, top: 500 29 | opentip.reposition() 30 | expect(opentip.currentPosition).to.eql left: 550, top: 550 31 | it "should correctly position opentip with", -> 32 | opentip = new Opentip element, "Test", delay: 0, target: yes, borderWidth: 10, stem: off, offset: [ 0, 0 ], autoOffset: false 33 | opentip._setup() 34 | elementOffset = adapter.offset element 35 | opentip.reposition() 36 | expect(opentip.currentPosition).to.eql left: 560, top: 560 37 | it "should correctly position opentip with stem on the left", -> 38 | opentip = new Opentip element, "Test", delay: 0, target: yes, borderWidth: 0, stem: yes, tipJoint: "top left", stemLength: 5, offset: [ 0, 0 ], autoOffset: false 39 | opentip._setup() 40 | elementOffset = adapter.offset element 41 | opentip.reposition() 42 | expect(opentip.currentPosition).to.eql left: 550, top: 550 43 | it "should correctly position opentip on the bottom right", -> 44 | opentip = new Opentip element, "Test", delay: 0, target: yes, borderWidth: 0, stem: off, tipJoint: "bottom right", offset: [ 0, 0 ], autoOffset: false 45 | opentip._setup() 46 | opentip.dimensions = width: 200, height: 200 47 | elementOffset = adapter.offset element 48 | elementDimensions = adapter.dimensions element 49 | expect(elementDimensions).to.eql width: 50, height: 50 50 | opentip.reposition() 51 | expect(opentip.currentPosition).to.eql left: 300, top: 300 52 | it "should correctly position opentip on the bottom right with stem", -> 53 | opentip = new Opentip element, "Test", delay: 0, target: yes, borderWidth: 0, stem: yes, tipJoint: "bottom right", stemLength: 10, offset: [ 0, 0 ], autoOffset: false 54 | opentip._setup() 55 | opentip.dimensions = width: 200, height: 200 56 | elementOffset = adapter.offset element 57 | elementDimensions = adapter.dimensions element 58 | expect(elementDimensions).to.eql width: 50, height: 50 59 | opentip.reposition() 60 | expect(opentip.currentPosition).to.eql left: 300, top: 300 # The autoOffset takes care of accounting for the stem 61 | 62 | 63 | describe "following mouse", -> 64 | it "should correctly position opentip when following mouse" 65 | 66 | describe "_ensureViewportContainment()", -> 67 | it "should put the tooltip on the other side when it's sticking out" 68 | it "shouldn't do anything if the viewport is smaller than the tooltip" 69 | it "should revert if the tooltip sticks out the other side as well" 70 | 71 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | grunt.initConfig 4 | pkg: grunt.file.readJSON "package.json" 5 | 6 | 7 | stylus: 8 | options: 9 | compress: false 10 | default: 11 | files: [ 12 | "css/opentip.css": "css/stylus/opentip.styl" 13 | ] 14 | 15 | coffee: 16 | default: 17 | expand: true 18 | options: 19 | bare: true 20 | cwd: "src/" 21 | src: [ "*.coffee" ] 22 | dest: "lib/" 23 | ext: ".js" 24 | 25 | test: 26 | files: 27 | "test/test.js": "test/src/*.coffee" 28 | 29 | watch: 30 | css: 31 | files: "css/stylus/*.styl" 32 | tasks: [ "css" ] 33 | options: nospawn: on 34 | js: 35 | files: "src/*.coffee" 36 | tasks: [ "js" ] 37 | options: nospawn: on 38 | test: 39 | files: [ 40 | "test/src/*.coffee" 41 | ] 42 | tasks: [ "coffee:test" ] 43 | options: nospawn: on 44 | 45 | 46 | curl: 47 | ".tmp-excanvas.js": "https://raw.github.com/enyo/excanvas/master/index.js" 48 | ".tmp-classlist.js": "https://raw.github.com/eligrey/classList.js/master/classList.js" 49 | ".tmp-addeventlistener.js": "https://gist.github.com/raw/4684216/c58a272ef9d9e0f55ea5e90ac313e3a3b2f2b7b3/eventListener.polyfill.js" 50 | 51 | clean: 52 | tmp: ".tmp-*" 53 | 54 | concat: 55 | js: 56 | files: 57 | "downloads/opentip-jquery.js": [ "lib/opentip.js", "lib/adapter-jquery.js" ] 58 | "downloads/opentip-jquery-excanvas.js": [ "downloads/opentip-jquery.js", ".tmp-excanvas.js" ] 59 | 60 | "downloads/opentip-prototype.js": [ "lib/opentip.js", "lib/adapter-prototype.js" ] 61 | "downloads/opentip-prototype-excanvas.js": [ "downloads/opentip-prototype.js", ".tmp-excanvas.js" ] 62 | 63 | "downloads/opentip-native.js": [ "lib/opentip.js", "lib/adapter-native.js", ".tmp-classlist.js", ".tmp-addeventlistener.js" ] 64 | "downloads/opentip-native-excanvas.js": [ "downloads/opentip-native.js", ".tmp-excanvas.js" ] 65 | 66 | 67 | uglify: 68 | options: 69 | banner: """ 70 | // Opentip v2.4.6 71 | // Copyright (c) 2009-2012 72 | // www.opentip.org 73 | // MIT Licensed 74 | 75 | """ 76 | js: 77 | files: [ 78 | "downloads/opentip-jquery.min.js": "downloads/opentip-jquery.js" 79 | "downloads/opentip-jquery-excanvas.min.js": "downloads/opentip-jquery-excanvas.js" 80 | "downloads/opentip-prototype.min.js": "downloads/opentip-prototype.js" 81 | "downloads/opentip-prototype-excanvas.min.js": "downloads/opentip-prototype-excanvas.js" 82 | "downloads/opentip-native.min.js": "downloads/opentip-native.js" 83 | "downloads/opentip-native-excanvas.min.js": "downloads/opentip-native-excanvas.js" 84 | ] 85 | 86 | 87 | grunt.loadNpmTasks "grunt-contrib-coffee" 88 | grunt.loadNpmTasks "grunt-contrib-stylus" 89 | grunt.loadNpmTasks "grunt-contrib-concat" 90 | grunt.loadNpmTasks "grunt-contrib-watch" 91 | grunt.loadNpmTasks "grunt-contrib-uglify" 92 | grunt.loadNpmTasks "grunt-contrib-clean" 93 | grunt.loadNpmTasks "grunt-curl" 94 | 95 | # Default tasks 96 | grunt.registerTask "default", [ "downloads" ] 97 | 98 | grunt.registerTask "css", "Compile the stylus files to css", [ "stylus" ] 99 | 100 | grunt.registerTask "js", "Compile coffeescript and create all download files", [ "coffee" ] 101 | 102 | grunt.registerTask "downloads", [ "css", "js", "curl", "concat", "clean", "uglify" ] 103 | -------------------------------------------------------------------------------- /test/src/110-joint.coffee: -------------------------------------------------------------------------------- 1 | 2 | 3 | extend = (target, sources...) -> 4 | for source in sources 5 | for own key, val of source 6 | target[key] = val 7 | target 8 | 9 | describe "Opentip.Joint", -> 10 | 11 | describe "constructor()", -> 12 | it "should forward to set()", -> 13 | sinon.stub Opentip.Joint::, "set" 14 | new Opentip.Joint "abc" 15 | expect(Opentip.Joint::set.args[0][0]).to.be "abc" 16 | Opentip.Joint::set.restore() 17 | it "should accept Pointer objects", -> 18 | p = new Opentip.Joint "top left" 19 | expect(p.toString()).to.be "top left" 20 | p2 = new Opentip.Joint p 21 | expect(p).to.not.be p2 22 | expect(p2.toString()).to.be "top left" 23 | 24 | describe "set()", -> 25 | it "should properly set the positions", -> 26 | p = new Opentip.Joint 27 | p.set "top-left" 28 | expect(p.toString()).to.eql "top left" 29 | 30 | p.set "top-Right" 31 | expect(p.toString()).to.eql "top right" 32 | 33 | p.set "BOTTOM left" 34 | expect(p.toString()).to.eql "bottom left" 35 | 36 | it "should handle any order of positions", -> 37 | p = new Opentip.Joint 38 | p.set "right bottom" 39 | expect(p.toString()).to.eql "bottom right" 40 | 41 | p.set "left left middle" 42 | expect(p.toString()).to.eql "left" 43 | 44 | p.set "left - top" 45 | expect(p.toString()).to.eql "top left" 46 | 47 | it "should add .bottom, .left etc... properties on the position", -> 48 | positions = 49 | top: no 50 | bottom: no 51 | middle: no 52 | left: no 53 | center: no 54 | right: no 55 | 56 | testCount = sinon.stub() 57 | testPointers = (position, thisPositions) -> 58 | thisPositions = extend { }, positions, thisPositions 59 | for positionName, shouldBeTrue of thisPositions 60 | testCount() 61 | if shouldBeTrue then expect(position[positionName]).to.be.ok() 62 | else expect(position[positionName]).to.not.be.ok() 63 | 64 | testPointers (new Opentip.Joint("top")), center: yes, top: yes 65 | testPointers (new Opentip.Joint("top right")), right: yes, top: yes 66 | testPointers (new Opentip.Joint("right")), right: yes, middle: yes 67 | testPointers (new Opentip.Joint("bottom right")), right: yes, bottom: yes 68 | testPointers (new Opentip.Joint("bottom")), center: yes, bottom: yes 69 | testPointers (new Opentip.Joint("bottom left")), left: yes, bottom: yes 70 | testPointers (new Opentip.Joint("left")), left: yes, middle: yes 71 | testPointers (new Opentip.Joint("top left")), left: yes, top: yes 72 | 73 | # Just making sure that the tests are actually called 74 | expect(testCount.callCount).to.be 6 * 8 75 | 76 | describe "setHorizontal()", -> 77 | it "should set the horizontal position", -> 78 | p = new Opentip.Joint "top left" 79 | expect(p.left).to.be.ok(); 80 | expect(p.top).to.be.ok(); 81 | p.setHorizontal "right" 82 | expect(p.left).to.not.be.ok(); 83 | expect(p.top).to.be.ok(); 84 | expect(p.right).to.be.ok(); 85 | 86 | describe "setVertical()", -> 87 | it "should set the vertical position", -> 88 | p = new Opentip.Joint "top left" 89 | expect(p.top).to.be.ok(); 90 | expect(p.left).to.be.ok(); 91 | p.setVertical "bottom" 92 | expect(p.top).to.not.be.ok(); 93 | expect(p.left).to.be.ok(); 94 | expect(p.bottom).to.be.ok(); 95 | 96 | describe "flip()", -> 97 | it "should return itself for chaining", -> 98 | p = new Opentip.Joint "top" 99 | p2 = p.flip() 100 | expect(p).to.be p2 101 | it "should properly flip the position", -> 102 | expect(new Opentip.Joint("top").flip().toString()).to.be "bottom" 103 | expect(new Opentip.Joint("bottomRight").flip().toString()).to.be "top left" 104 | expect(new Opentip.Joint("left top").flip().toString()).to.be "bottom right" 105 | expect(new Opentip.Joint("bottom").flip().toString()).to.be "top" 106 | 107 | -------------------------------------------------------------------------------- /src/adapter-component.coffee: -------------------------------------------------------------------------------- 1 | # Component Opentip Adapter 2 | # ====================== 3 | # 4 | # Uses github.com/component components 5 | 6 | $ = window.jQuery ? require "jquery" 7 | 8 | # The adapter class 9 | module.exports = class Adapter 10 | 11 | name: "component" 12 | 13 | # Simply using $.domReady 14 | domReady: (callback) -> $ callback 15 | 16 | 17 | # DOM 18 | # === 19 | 20 | # Using bonzo to create html 21 | create: (html) -> $ html 22 | 23 | 24 | # Element handling 25 | # ---------------- 26 | 27 | # Wraps the element in ender 28 | wrap: (element) -> 29 | element = $ element 30 | throw new Error "Multiple elements provided." if element.length > 1 31 | element 32 | 33 | # Returns the unwrapped element 34 | unwrap: (element) -> $(element)[0] 35 | 36 | # Returns the tag name of the element 37 | tagName: (element) -> @unwrap(element).tagName 38 | 39 | # Returns or sets the given attribute of element 40 | # 41 | # It's important not to simply forward name and value because the value 42 | # is set whether or not the value argument is present 43 | attr: (element, args...) -> $(element).attr args... 44 | 45 | # Returns or sets the given data of element 46 | # It's important not to simply forward name and value because the value 47 | # is set whether or not the value argument is present 48 | data: (element, args...) -> $(element).data args... 49 | 50 | # Finds elements by selector 51 | find: (element, selector) -> $(element).find(selector)[0] 52 | 53 | # Finds all elements by selector 54 | findAll: (element, selector) -> $(element).find selector 55 | 56 | # Updates the content of the element 57 | update: (element, content, escape) -> 58 | element = $ element 59 | if escape 60 | element.text content 61 | else 62 | element.html content 63 | 64 | # Appends given child to element 65 | append: (element, child) -> $(element).append child 66 | 67 | # Removes element 68 | remove: (element) -> $(element).remove() 69 | 70 | # Add a class 71 | addClass: (element, className) -> $(element).addClass className 72 | 73 | # Remove a class 74 | removeClass: (element, className) -> $(element).removeClass className 75 | 76 | # Set given css properties 77 | css: (element, properties) -> $(element).css properties 78 | 79 | # Returns an object with given dimensions 80 | dimensions: (element) -> 81 | { 82 | width: $(element).outerWidth() 83 | height: $(element).outerHeight() 84 | } 85 | 86 | # Returns the scroll offsets of current document 87 | scrollOffset: -> 88 | [ 89 | window.pageXOffset or document.documentElement.scrollLeft or document.body.scrollLeft 90 | window.pageYOffset or document.documentElement.scrollTop or document.body.scrollTop 91 | ] 92 | 93 | # Returns the dimensions of the viewport (currently visible browser area) 94 | viewportDimensions: -> 95 | { 96 | width: document.documentElement.clientWidth 97 | height: document.documentElement.clientHeight 98 | } 99 | 100 | # Returns an object with x and y 101 | mousePosition: (e) -> 102 | return null unless e? 103 | x: e.pageX, y: e.pageY 104 | 105 | 106 | # Returns the offset of the element 107 | offset: (element) -> 108 | offset = $(element).offset() 109 | { 110 | left: offset.left 111 | top: offset.top 112 | } 113 | 114 | # Observe given eventName 115 | observe: (element, eventName, observer) -> $(element).bind eventName, observer 116 | 117 | # Stop observing event 118 | stopObserving: (element, eventName, observer) -> $(element).unbind eventName, observer 119 | 120 | # Perform an AJAX request and call the appropriate callbacks. 121 | ajax: (options) -> 122 | throw new Error "No url provided" unless options.url? 123 | $.ajax( 124 | url: options.url 125 | type: options.method?.toUpperCase() ? "GET" 126 | ) 127 | .done((content) -> options.onSuccess? content) 128 | .fail((request) -> options.onError? "Server responded with status #{request.status}") 129 | .always(-> options.onComplete?()) 130 | 131 | 132 | # Utility functions 133 | # ================= 134 | 135 | # Creates a shallow copy of the object 136 | clone: (object) -> $.extend { }, object 137 | 138 | # Copies all properties from sources to target 139 | extend: (target, sources...) -> $.extend target, sources... 140 | 141 | 142 | -------------------------------------------------------------------------------- /src/adapter-jquery.coffee: -------------------------------------------------------------------------------- 1 | # jQuery Opentip Adapter 2 | # ====================== 3 | # 4 | # Uses jQuery 5 | 6 | # Because $ is my favorite character 7 | (($) -> 8 | 9 | 10 | # Augment jQuery 11 | $.fn.opentip = (content, title, options) -> 12 | new Opentip this, content, title, options 13 | 14 | 15 | # And now the class 16 | class Adapter 17 | 18 | name: "jquery" 19 | 20 | # Simply using $.domReady 21 | domReady: (callback) -> $ callback 22 | 23 | 24 | # DOM 25 | # === 26 | 27 | # Using bonzo to create html 28 | create: (html) -> $ html 29 | 30 | 31 | # Element handling 32 | # ---------------- 33 | 34 | # Wraps the element in ender 35 | wrap: (element) -> 36 | element = $ element 37 | throw new Error "Multiple elements provided." if element.length > 1 38 | element 39 | 40 | # Returns the unwrapped element 41 | unwrap: (element) -> $(element)[0] 42 | 43 | # Returns the tag name of the element 44 | tagName: (element) -> @unwrap(element).tagName 45 | 46 | # Returns or sets the given attribute of element 47 | # 48 | # It's important not to simply forward name and value because the value 49 | # is set whether or not the value argument is present 50 | attr: (element, args...) -> $(element).attr args... 51 | 52 | # Returns or sets the given data of element 53 | # It's important not to simply forward name and value because the value 54 | # is set whether or not the value argument is present 55 | data: (element, args...) -> $(element).data args... 56 | 57 | # Finds elements by selector 58 | find: (element, selector) -> $(element).find(selector).get(0) 59 | 60 | # Finds all elements by selector 61 | findAll: (element, selector) -> $(element).find selector 62 | 63 | # Updates the content of the element 64 | update: (element, content, escape) -> 65 | element = $ element 66 | if escape 67 | element.text content 68 | else 69 | element.html content 70 | 71 | # Appends given child to element 72 | append: (element, child) -> $(element).append child 73 | 74 | # Removes element 75 | remove: (element) -> $(element).remove() 76 | 77 | # Add a class 78 | addClass: (element, className) -> $(element).addClass className 79 | 80 | # Remove a class 81 | removeClass: (element, className) -> $(element).removeClass className 82 | 83 | # Set given css properties 84 | css: (element, properties) -> $(element).css properties 85 | 86 | # Returns an object with given dimensions 87 | dimensions: (element) -> 88 | { 89 | width: $(element).outerWidth() 90 | height: $(element).outerHeight() 91 | } 92 | 93 | # Returns the scroll offsets of current document 94 | scrollOffset: -> 95 | [ 96 | window.pageXOffset or document.documentElement.scrollLeft or document.body.scrollLeft 97 | window.pageYOffset or document.documentElement.scrollTop or document.body.scrollTop 98 | ] 99 | 100 | # Returns the dimensions of the viewport (currently visible browser area) 101 | viewportDimensions: -> 102 | { 103 | width: document.documentElement.clientWidth 104 | height: document.documentElement.clientHeight 105 | } 106 | 107 | # Returns an object with x and y 108 | mousePosition: (e) -> 109 | return null unless e? 110 | x: e.pageX, y: e.pageY 111 | 112 | 113 | # Returns the offset of the element 114 | offset: (element) -> 115 | offset = $(element).offset() 116 | { 117 | left: offset.left 118 | top: offset.top 119 | } 120 | 121 | # Observe given eventName 122 | observe: (element, eventName, observer) -> $(element).bind eventName, observer 123 | 124 | # Stop observing event 125 | stopObserving: (element, eventName, observer) -> $(element).unbind eventName, observer 126 | 127 | # Perform an AJAX request and call the appropriate callbacks. 128 | ajax: (options) -> 129 | throw new Error "No url provided" unless options.url? 130 | $.ajax( 131 | url: options.url 132 | type: options.method?.toUpperCase() ? "GET" 133 | ) 134 | .done((content) -> options.onSuccess? content) 135 | .fail((request) -> options.onError? "Server responded with status #{request.status}") 136 | .always(-> options.onComplete?()) 137 | 138 | 139 | # Utility functions 140 | # ================= 141 | 142 | # Creates a shallow copy of the object 143 | clone: (object) -> $.extend { }, object 144 | 145 | # Copies all properties from sources to target 146 | extend: (target, sources...) -> $.extend target, sources... 147 | 148 | # Add the adapter to the list 149 | Opentip.addAdapter new Adapter 150 | 151 | )(jQuery) 152 | -------------------------------------------------------------------------------- /lib/adapter-component.js: -------------------------------------------------------------------------------- 1 | var $, Adapter, _ref, 2 | __slice = [].slice; 3 | 4 | $ = (_ref = window.jQuery) != null ? _ref : require("jquery"); 5 | 6 | module.exports = Adapter = (function() { 7 | function Adapter() {} 8 | 9 | Adapter.prototype.name = "component"; 10 | 11 | Adapter.prototype.domReady = function(callback) { 12 | return $(callback); 13 | }; 14 | 15 | Adapter.prototype.create = function(html) { 16 | return $(html); 17 | }; 18 | 19 | Adapter.prototype.wrap = function(element) { 20 | element = $(element); 21 | if (element.length > 1) { 22 | throw new Error("Multiple elements provided."); 23 | } 24 | return element; 25 | }; 26 | 27 | Adapter.prototype.unwrap = function(element) { 28 | return $(element)[0]; 29 | }; 30 | 31 | Adapter.prototype.tagName = function(element) { 32 | return this.unwrap(element).tagName; 33 | }; 34 | 35 | Adapter.prototype.attr = function() { 36 | var args, element, _ref1; 37 | element = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 38 | return (_ref1 = $(element)).attr.apply(_ref1, args); 39 | }; 40 | 41 | Adapter.prototype.data = function() { 42 | var args, element, _ref1; 43 | element = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 44 | return (_ref1 = $(element)).data.apply(_ref1, args); 45 | }; 46 | 47 | Adapter.prototype.find = function(element, selector) { 48 | return $(element).find(selector)[0]; 49 | }; 50 | 51 | Adapter.prototype.findAll = function(element, selector) { 52 | return $(element).find(selector); 53 | }; 54 | 55 | Adapter.prototype.update = function(element, content, escape) { 56 | element = $(element); 57 | if (escape) { 58 | return element.text(content); 59 | } else { 60 | return element.html(content); 61 | } 62 | }; 63 | 64 | Adapter.prototype.append = function(element, child) { 65 | return $(element).append(child); 66 | }; 67 | 68 | Adapter.prototype.remove = function(element) { 69 | return $(element).remove(); 70 | }; 71 | 72 | Adapter.prototype.addClass = function(element, className) { 73 | return $(element).addClass(className); 74 | }; 75 | 76 | Adapter.prototype.removeClass = function(element, className) { 77 | return $(element).removeClass(className); 78 | }; 79 | 80 | Adapter.prototype.css = function(element, properties) { 81 | return $(element).css(properties); 82 | }; 83 | 84 | Adapter.prototype.dimensions = function(element) { 85 | return { 86 | width: $(element).outerWidth(), 87 | height: $(element).outerHeight() 88 | }; 89 | }; 90 | 91 | Adapter.prototype.scrollOffset = function() { 92 | return [window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop]; 93 | }; 94 | 95 | Adapter.prototype.viewportDimensions = function() { 96 | return { 97 | width: document.documentElement.clientWidth, 98 | height: document.documentElement.clientHeight 99 | }; 100 | }; 101 | 102 | Adapter.prototype.mousePosition = function(e) { 103 | if (e == null) { 104 | return null; 105 | } 106 | return { 107 | x: e.pageX, 108 | y: e.pageY 109 | }; 110 | }; 111 | 112 | Adapter.prototype.offset = function(element) { 113 | var offset; 114 | offset = $(element).offset(); 115 | return { 116 | left: offset.left, 117 | top: offset.top 118 | }; 119 | }; 120 | 121 | Adapter.prototype.observe = function(element, eventName, observer) { 122 | return $(element).bind(eventName, observer); 123 | }; 124 | 125 | Adapter.prototype.stopObserving = function(element, eventName, observer) { 126 | return $(element).unbind(eventName, observer); 127 | }; 128 | 129 | Adapter.prototype.ajax = function(options) { 130 | var _ref1, _ref2; 131 | if (options.url == null) { 132 | throw new Error("No url provided"); 133 | } 134 | return $.ajax({ 135 | url: options.url, 136 | type: (_ref1 = (_ref2 = options.method) != null ? _ref2.toUpperCase() : void 0) != null ? _ref1 : "GET" 137 | }).done(function(content) { 138 | return typeof options.onSuccess === "function" ? options.onSuccess(content) : void 0; 139 | }).fail(function(request) { 140 | return typeof options.onError === "function" ? options.onError("Server responded with status " + request.status) : void 0; 141 | }).always(function() { 142 | return typeof options.onComplete === "function" ? options.onComplete() : void 0; 143 | }); 144 | }; 145 | 146 | Adapter.prototype.clone = function(object) { 147 | return $.extend({}, object); 148 | }; 149 | 150 | Adapter.prototype.extend = function() { 151 | var sources, target; 152 | target = arguments[0], sources = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 153 | return $.extend.apply($, [target].concat(__slice.call(sources))); 154 | }; 155 | 156 | return Adapter; 157 | 158 | })(); 159 | -------------------------------------------------------------------------------- /lib/adapter-jquery.js: -------------------------------------------------------------------------------- 1 | var __slice = [].slice; 2 | 3 | (function($) { 4 | var Adapter; 5 | $.fn.opentip = function(content, title, options) { 6 | return new Opentip(this, content, title, options); 7 | }; 8 | Adapter = (function() { 9 | function Adapter() {} 10 | 11 | Adapter.prototype.name = "jquery"; 12 | 13 | Adapter.prototype.domReady = function(callback) { 14 | return $(callback); 15 | }; 16 | 17 | Adapter.prototype.create = function(html) { 18 | return $(html); 19 | }; 20 | 21 | Adapter.prototype.wrap = function(element) { 22 | element = $(element); 23 | if (element.length > 1) { 24 | throw new Error("Multiple elements provided."); 25 | } 26 | return element; 27 | }; 28 | 29 | Adapter.prototype.unwrap = function(element) { 30 | return $(element)[0]; 31 | }; 32 | 33 | Adapter.prototype.tagName = function(element) { 34 | return this.unwrap(element).tagName; 35 | }; 36 | 37 | Adapter.prototype.attr = function() { 38 | var args, element, _ref; 39 | element = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 40 | return (_ref = $(element)).attr.apply(_ref, args); 41 | }; 42 | 43 | Adapter.prototype.data = function() { 44 | var args, element, _ref; 45 | element = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 46 | return (_ref = $(element)).data.apply(_ref, args); 47 | }; 48 | 49 | Adapter.prototype.find = function(element, selector) { 50 | return $(element).find(selector).get(0); 51 | }; 52 | 53 | Adapter.prototype.findAll = function(element, selector) { 54 | return $(element).find(selector); 55 | }; 56 | 57 | Adapter.prototype.update = function(element, content, escape) { 58 | element = $(element); 59 | if (escape) { 60 | return element.text(content); 61 | } else { 62 | return element.html(content); 63 | } 64 | }; 65 | 66 | Adapter.prototype.append = function(element, child) { 67 | return $(element).append(child); 68 | }; 69 | 70 | Adapter.prototype.remove = function(element) { 71 | return $(element).remove(); 72 | }; 73 | 74 | Adapter.prototype.addClass = function(element, className) { 75 | return $(element).addClass(className); 76 | }; 77 | 78 | Adapter.prototype.removeClass = function(element, className) { 79 | return $(element).removeClass(className); 80 | }; 81 | 82 | Adapter.prototype.css = function(element, properties) { 83 | return $(element).css(properties); 84 | }; 85 | 86 | Adapter.prototype.dimensions = function(element) { 87 | return { 88 | width: $(element).outerWidth(), 89 | height: $(element).outerHeight() 90 | }; 91 | }; 92 | 93 | Adapter.prototype.scrollOffset = function() { 94 | return [window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop]; 95 | }; 96 | 97 | Adapter.prototype.viewportDimensions = function() { 98 | return { 99 | width: document.documentElement.clientWidth, 100 | height: document.documentElement.clientHeight 101 | }; 102 | }; 103 | 104 | Adapter.prototype.mousePosition = function(e) { 105 | if (e == null) { 106 | return null; 107 | } 108 | return { 109 | x: e.pageX, 110 | y: e.pageY 111 | }; 112 | }; 113 | 114 | Adapter.prototype.offset = function(element) { 115 | var offset; 116 | offset = $(element).offset(); 117 | return { 118 | left: offset.left, 119 | top: offset.top 120 | }; 121 | }; 122 | 123 | Adapter.prototype.observe = function(element, eventName, observer) { 124 | return $(element).bind(eventName, observer); 125 | }; 126 | 127 | Adapter.prototype.stopObserving = function(element, eventName, observer) { 128 | return $(element).unbind(eventName, observer); 129 | }; 130 | 131 | Adapter.prototype.ajax = function(options) { 132 | var _ref, _ref1; 133 | if (options.url == null) { 134 | throw new Error("No url provided"); 135 | } 136 | return $.ajax({ 137 | url: options.url, 138 | type: (_ref = (_ref1 = options.method) != null ? _ref1.toUpperCase() : void 0) != null ? _ref : "GET" 139 | }).done(function(content) { 140 | return typeof options.onSuccess === "function" ? options.onSuccess(content) : void 0; 141 | }).fail(function(request) { 142 | return typeof options.onError === "function" ? options.onError("Server responded with status " + request.status) : void 0; 143 | }).always(function() { 144 | return typeof options.onComplete === "function" ? options.onComplete() : void 0; 145 | }); 146 | }; 147 | 148 | Adapter.prototype.clone = function(object) { 149 | return $.extend({}, object); 150 | }; 151 | 152 | Adapter.prototype.extend = function() { 153 | var sources, target; 154 | target = arguments[0], sources = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 155 | return $.extend.apply($, [target].concat(__slice.call(sources))); 156 | }; 157 | 158 | return Adapter; 159 | 160 | })(); 161 | return Opentip.addAdapter(new Adapter); 162 | })(jQuery); 163 | -------------------------------------------------------------------------------- /src/adapter-ender.coffee: -------------------------------------------------------------------------------- 1 | # Ender Opentip Adapter 2 | # ===================== 3 | # 4 | # Uses ender packages 5 | 6 | # Because $ is my favorite character 7 | (($) -> 8 | 9 | # Using bean as event handler 10 | bean = require "bean" 11 | 12 | # Using reqwest as AJAX lib 13 | reqwest = require "reqwest" 14 | 15 | # Augment ender 16 | $.ender { 17 | opentip: (content, title, options) -> new Opentip this, content, title, options 18 | }, true 19 | 20 | 21 | # And now the class 22 | class Adapter 23 | 24 | name: "ender" 25 | 26 | # Simply using $.domReady 27 | domReady: (callback) -> $.domReady callback 28 | 29 | 30 | # DOM 31 | # === 32 | 33 | # Using bonzo to create html 34 | create: (html) -> $ html 35 | 36 | 37 | # Element handling 38 | # ---------------- 39 | 40 | # Wraps the element in ender 41 | wrap: (element) -> 42 | element = $ element 43 | throw new Error "Multiple elements provided." if element.length > 1 44 | element 45 | 46 | # Returns the unwrapped element 47 | unwrap: (element) -> $(element).get 0 48 | 49 | # Returns the tag name of the element 50 | tagName: (element) -> @unwrap(element).tagName 51 | 52 | # Returns or sets the given attribute of element 53 | # It's important not to simply forward name and value because the value 54 | # is set whether or not the value argument is present 55 | attr: (element, args...) -> $(element).attr args... 56 | 57 | # Returns or sets the given data of element 58 | # It's important not to simply forward name and value because the value 59 | # is set whether or not the value argument is present 60 | data: (element, args...) -> $(element).data args... 61 | 62 | # Finds elements by selector 63 | find: (element, selector) -> $(element).find(selector)[0] 64 | 65 | # Finds all elements by selector 66 | findAll: (element, selector) -> $(element).find selector 67 | 68 | # Updates the content of the element 69 | update: (element, content, escape) -> 70 | element = $ element 71 | if escape 72 | element.text content 73 | else 74 | element.html content 75 | 76 | # Appends given child to element 77 | append: (element, child) -> $(element).append child 78 | 79 | # Removes element 80 | remove: (element) -> $(element).remove() 81 | 82 | # Add a class 83 | addClass: (element, className) -> $(element).addClass className 84 | 85 | # Remove a class 86 | removeClass: (element, className) -> $(element).removeClass className 87 | 88 | # Set given css properties 89 | css: (element, properties) -> $(element).css properties 90 | 91 | # Returns an object with given dimensions 92 | dimensions: (element) -> $(element).dim() 93 | 94 | # Returns the scroll offsets of current document 95 | scrollOffset: -> 96 | [ 97 | window.pageXOffset or document.documentElement.scrollLeft or document.body.scrollLeft 98 | window.pageYOffset or document.documentElement.scrollTop or document.body.scrollTop 99 | ] 100 | 101 | # Returns the dimensions of the viewport (currently visible browser area) 102 | viewportDimensions: -> 103 | { 104 | width: document.documentElement.clientWidth 105 | height: document.documentElement.clientHeight 106 | } 107 | 108 | # Returns an object with x and y 109 | mousePosition: (e) -> 110 | pos = x: 0, y: 0 111 | 112 | e ?= window.event 113 | 114 | return unless e? 115 | 116 | if e.pageX or e.pageY 117 | pos.x = e.pageX 118 | pos.y = e.pageY 119 | else if e.clientX or e.clientY 120 | pos.x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft 121 | pos.y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop 122 | 123 | pos 124 | 125 | # Returns the offset of the element 126 | offset: (element) -> 127 | offset = $(element).offset() 128 | { 129 | top: offset.top 130 | left: offset.left 131 | } 132 | 133 | # Observe given eventName 134 | observe: (element, eventName, observer) -> 135 | $(element).on eventName, observer 136 | 137 | # Stop observing event 138 | stopObserving: (element, eventName, observer) -> $(element).unbind eventName, observer 139 | 140 | # Perform an AJAX request and call the appropriate callbacks. 141 | ajax: (options) -> 142 | throw new Error "No url provided" unless options.url? 143 | reqwest 144 | url: options.url 145 | type: 'html' 146 | method: options.method?.toUpperCase() ? "GET" 147 | error: (resp) -> options.onError? "Server responded with status #{resp.status}" 148 | success: (resp) -> options.onSuccess? resp 149 | complete: -> options.onComplete?() 150 | 151 | 152 | # Utility functions 153 | # ================= 154 | 155 | # Creates a shallow copy of the object 156 | clone: (object) -> 157 | newObject = { } 158 | for own key, val of object 159 | newObject[key] = val 160 | newObject 161 | 162 | # Copies all properties from sources to target 163 | extend: (target, sources...) -> 164 | for source in sources 165 | for own key, val of source 166 | target[key] = val 167 | target 168 | 169 | # Add the adapter to the list 170 | Opentip.addAdapter new Adapter 171 | 172 | )(ender) 173 | -------------------------------------------------------------------------------- /docs/adapter.prototype.html: -------------------------------------------------------------------------------- 1 | adapter.prototype.coffee

adapter.prototype.coffee

$ = ender
 2 | 
 3 | 
 4 | class Adapter
 5 | 
 6 |   name: "prototype"

Simply using $.domReady

  domReady: (callback) -> Event.observe window, "dom:loaded", callback

Using bonzo to create html

  create: (html) -> $ html

Mimics scriptaculous Builder.node behaviour 7 | element: (tagName, attributes, children) -> 8 | if Object.isArray(attributes) or Object.isString(attributes) or Object.isElement(attributes) 9 | children = attributes 10 | attributes = null 11 | element = new Element(tagName, attributes or {})

    

# This is a prototype 1.6 bug, that doesn't apply the className to IE8 elements. 12 | # Thanks to Alexander Shakhnovsky for finding the bug, and pinpointing the problem. 13 | if attributes and attributes["className"] 14 | attributes["className"].split(" ").each (classname) -> 15 | element.addClassName classname

if children 16 | if Object.isArray(children) 17 | children.each (child) -> 18 | element.insert bottom: child

else
19 |   element.insert bottom: children
20 | 
21 | 22 |

element

adapter = new Adapter
23 | 
24 | Opentip.addAdapter adapter
25 | 
26 | 
-------------------------------------------------------------------------------- /src/adapter-prototype.coffee: -------------------------------------------------------------------------------- 1 | # Prototype Opentip Adapter 2 | # ====================== 3 | # 4 | # Uses the prototype framework 5 | 6 | do -> 7 | 8 | Element.addMethods 9 | addTip: (element, content, title, options) -> 10 | new Opentip element, content, title, options 11 | 12 | 13 | # Needs this function because of IE8 14 | isArrayOrNodeList = (element) -> 15 | if (element instanceof Array) or (element? and typeof element.length == 'number' and typeof element.item == 'function' and typeof element.nextNode == 'function' and typeof element.reset == 'function') 16 | return yes 17 | return no 18 | 19 | # And now the class 20 | class Adapter 21 | 22 | name: "prototype" 23 | 24 | domReady: (callback) -> 25 | if document.loaded 26 | callback() 27 | else 28 | $(document).observe "dom:loaded", callback 29 | 30 | 31 | # DOM 32 | # === 33 | 34 | # Using bonzo to create html 35 | create: (html) -> new Element('div').update(html).childElements() 36 | 37 | 38 | # Element handling 39 | # ---------------- 40 | 41 | # Wraps the element 42 | wrap: (element) -> 43 | if isArrayOrNodeList element 44 | throw new Error "Multiple elements provided." if element.length > 1 45 | element = @unwrap element 46 | else if typeof element == "string" 47 | element = $$(element)[0] 48 | $ element 49 | 50 | # Returns the unwrapped element 51 | unwrap: (element) -> 52 | if isArrayOrNodeList element 53 | element[0] 54 | else 55 | element 56 | 57 | # Returns the tag name of the element 58 | tagName: (element) -> @unwrap(element).tagName 59 | 60 | # Returns or sets the given attribute of element 61 | # 62 | # It's important not to simply forward name and value because the value 63 | # is set whether or not the value argument is present 64 | attr: (element, args...) -> 65 | if args.length == 1 66 | @wrap(element).readAttribute args[0] 67 | else 68 | @wrap(element).writeAttribute args... 69 | 70 | # Returns or sets the given data of element 71 | # It's important not to simply forward name and value because the value 72 | # is set whether or not the value argument is present 73 | data: (element, name, value) -> 74 | @wrap(element) 75 | if arguments.length > 2 76 | element.store name, value 77 | else 78 | arg = element.readAttribute "data-#{name.underscore().dasherize()}" 79 | return arg if arg? 80 | element.retrieve name 81 | 82 | # Finds elements by selector 83 | find: (element, selector) -> @wrap(element).select(selector)[0] 84 | 85 | # Finds all elements by selector 86 | findAll: (element, selector) -> @wrap(element).select selector 87 | 88 | # Updates the content of the element 89 | update: (element, content, escape) -> 90 | @wrap(element).update if escape then content.escapeHTML() else content 91 | 92 | # Appends given child to element 93 | append: (element, child) -> @wrap(element).insert @wrap child 94 | 95 | # Removes element 96 | remove: (element) -> @wrap(element).remove() 97 | 98 | # Add a class 99 | addClass: (element, className) -> @wrap(element).addClassName className 100 | 101 | # Remove a class 102 | removeClass: (element, className) -> @wrap(element).removeClassName className 103 | 104 | # Set given css properties 105 | css: (element, properties) -> @wrap(element).setStyle properties 106 | 107 | # Returns an object with given dimensions 108 | dimensions: (element) -> @wrap(element).getDimensions() 109 | 110 | # Returns the scroll offsets of current document 111 | scrollOffset: -> 112 | offsets = document.viewport.getScrollOffsets() 113 | [ offsets.left, offsets.top ] 114 | 115 | # Returns the dimensions of the viewport (currently visible browser area) 116 | viewportDimensions: -> document.viewport.getDimensions() 117 | 118 | # Returns an object with x and y 119 | mousePosition: (e) -> 120 | return null unless e? 121 | x: Event.pointerX(e), y: Event.pointerY(e) 122 | 123 | 124 | # Returns the offset of the element 125 | offset: (element) -> 126 | offset = @wrap(element).cumulativeOffset() 127 | left: offset.left, top: offset.top 128 | 129 | # Observe given eventName 130 | observe: (element, eventName, observer) -> Event.observe @wrap(element), eventName, observer 131 | 132 | # Stop observing event 133 | stopObserving: (element, eventName, observer) -> Event.stopObserving @wrap(element), eventName, observer 134 | 135 | # Perform an AJAX request and call the appropriate callbacks. 136 | ajax: (options) -> 137 | throw new Error "No url provided" unless options.url? 138 | 139 | new Ajax.Request options.url, { 140 | method: options.method?.toUpperCase() ? "GET" 141 | onSuccess: (response) -> options.onSuccess? response.responseText 142 | onFailure: (response) -> options.onError? "Server responded with status #{response.status}" 143 | onComplete: -> options.onComplete?() 144 | } 145 | 146 | 147 | # Utility functions 148 | # ================= 149 | 150 | # Creates a shallow copy of the object 151 | clone: (object) -> Object.clone(object) 152 | 153 | # Copies all properties from sources to target 154 | extend: (target, sources...) -> 155 | for source in sources 156 | Object.extend target, source 157 | target 158 | 159 | # Add the adapter to the list 160 | Opentip.addAdapter new Adapter 161 | 162 | -------------------------------------------------------------------------------- /lib/adapter-ender.js: -------------------------------------------------------------------------------- 1 | var __slice = [].slice, 2 | __hasProp = {}.hasOwnProperty; 3 | 4 | (function($) { 5 | var Adapter, bean, reqwest; 6 | bean = require("bean"); 7 | reqwest = require("reqwest"); 8 | $.ender({ 9 | opentip: function(content, title, options) { 10 | return new Opentip(this, content, title, options); 11 | } 12 | }, true); 13 | Adapter = (function() { 14 | function Adapter() {} 15 | 16 | Adapter.prototype.name = "ender"; 17 | 18 | Adapter.prototype.domReady = function(callback) { 19 | return $.domReady(callback); 20 | }; 21 | 22 | Adapter.prototype.create = function(html) { 23 | return $(html); 24 | }; 25 | 26 | Adapter.prototype.wrap = function(element) { 27 | element = $(element); 28 | if (element.length > 1) { 29 | throw new Error("Multiple elements provided."); 30 | } 31 | return element; 32 | }; 33 | 34 | Adapter.prototype.unwrap = function(element) { 35 | return $(element).get(0); 36 | }; 37 | 38 | Adapter.prototype.tagName = function(element) { 39 | return this.unwrap(element).tagName; 40 | }; 41 | 42 | Adapter.prototype.attr = function() { 43 | var args, element, _ref; 44 | element = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 45 | return (_ref = $(element)).attr.apply(_ref, args); 46 | }; 47 | 48 | Adapter.prototype.data = function() { 49 | var args, element, _ref; 50 | element = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 51 | return (_ref = $(element)).data.apply(_ref, args); 52 | }; 53 | 54 | Adapter.prototype.find = function(element, selector) { 55 | return $(element).find(selector)[0]; 56 | }; 57 | 58 | Adapter.prototype.findAll = function(element, selector) { 59 | return $(element).find(selector); 60 | }; 61 | 62 | Adapter.prototype.update = function(element, content, escape) { 63 | element = $(element); 64 | if (escape) { 65 | return element.text(content); 66 | } else { 67 | return element.html(content); 68 | } 69 | }; 70 | 71 | Adapter.prototype.append = function(element, child) { 72 | return $(element).append(child); 73 | }; 74 | 75 | Adapter.prototype.remove = function(element) { 76 | return $(element).remove(); 77 | }; 78 | 79 | Adapter.prototype.addClass = function(element, className) { 80 | return $(element).addClass(className); 81 | }; 82 | 83 | Adapter.prototype.removeClass = function(element, className) { 84 | return $(element).removeClass(className); 85 | }; 86 | 87 | Adapter.prototype.css = function(element, properties) { 88 | return $(element).css(properties); 89 | }; 90 | 91 | Adapter.prototype.dimensions = function(element) { 92 | return $(element).dim(); 93 | }; 94 | 95 | Adapter.prototype.scrollOffset = function() { 96 | return [window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop]; 97 | }; 98 | 99 | Adapter.prototype.viewportDimensions = function() { 100 | return { 101 | width: document.documentElement.clientWidth, 102 | height: document.documentElement.clientHeight 103 | }; 104 | }; 105 | 106 | Adapter.prototype.mousePosition = function(e) { 107 | var pos; 108 | pos = { 109 | x: 0, 110 | y: 0 111 | }; 112 | if (e == null) { 113 | e = window.event; 114 | } 115 | if (e == null) { 116 | return; 117 | } 118 | if (e.pageX || e.pageY) { 119 | pos.x = e.pageX; 120 | pos.y = e.pageY; 121 | } else if (e.clientX || e.clientY) { 122 | pos.x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; 123 | pos.y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; 124 | } 125 | return pos; 126 | }; 127 | 128 | Adapter.prototype.offset = function(element) { 129 | var offset; 130 | offset = $(element).offset(); 131 | return { 132 | top: offset.top, 133 | left: offset.left 134 | }; 135 | }; 136 | 137 | Adapter.prototype.observe = function(element, eventName, observer) { 138 | return $(element).on(eventName, observer); 139 | }; 140 | 141 | Adapter.prototype.stopObserving = function(element, eventName, observer) { 142 | return $(element).unbind(eventName, observer); 143 | }; 144 | 145 | Adapter.prototype.ajax = function(options) { 146 | var _ref, _ref1; 147 | if (options.url == null) { 148 | throw new Error("No url provided"); 149 | } 150 | return reqwest({ 151 | url: options.url, 152 | type: 'html', 153 | method: (_ref = (_ref1 = options.method) != null ? _ref1.toUpperCase() : void 0) != null ? _ref : "GET", 154 | error: function(resp) { 155 | return typeof options.onError === "function" ? options.onError("Server responded with status " + resp.status) : void 0; 156 | }, 157 | success: function(resp) { 158 | return typeof options.onSuccess === "function" ? options.onSuccess(resp) : void 0; 159 | }, 160 | complete: function() { 161 | return typeof options.onComplete === "function" ? options.onComplete() : void 0; 162 | } 163 | }); 164 | }; 165 | 166 | Adapter.prototype.clone = function(object) { 167 | var key, newObject, val; 168 | newObject = {}; 169 | for (key in object) { 170 | if (!__hasProp.call(object, key)) continue; 171 | val = object[key]; 172 | newObject[key] = val; 173 | } 174 | return newObject; 175 | }; 176 | 177 | Adapter.prototype.extend = function() { 178 | var key, source, sources, target, val, _i, _len; 179 | target = arguments[0], sources = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 180 | for (_i = 0, _len = sources.length; _i < _len; _i++) { 181 | source = sources[_i]; 182 | for (key in source) { 183 | if (!__hasProp.call(source, key)) continue; 184 | val = source[key]; 185 | target[key] = val; 186 | } 187 | } 188 | return target; 189 | }; 190 | 191 | return Adapter; 192 | 193 | })(); 194 | return Opentip.addAdapter(new Adapter); 195 | })(ender); 196 | -------------------------------------------------------------------------------- /lib/adapter-prototype.js: -------------------------------------------------------------------------------- 1 | var __slice = [].slice; 2 | 3 | (function() { 4 | var Adapter, isArrayOrNodeList; 5 | Element.addMethods({ 6 | addTip: function(element, content, title, options) { 7 | return new Opentip(element, content, title, options); 8 | } 9 | }); 10 | isArrayOrNodeList = function(element) { 11 | if ((element instanceof Array) || ((element != null) && typeof element.length === 'number' && typeof element.item === 'function' && typeof element.nextNode === 'function' && typeof element.reset === 'function')) { 12 | return true; 13 | } 14 | return false; 15 | }; 16 | Adapter = (function() { 17 | function Adapter() {} 18 | 19 | Adapter.prototype.name = "prototype"; 20 | 21 | Adapter.prototype.domReady = function(callback) { 22 | if (document.loaded) { 23 | return callback(); 24 | } else { 25 | return $(document).observe("dom:loaded", callback); 26 | } 27 | }; 28 | 29 | Adapter.prototype.create = function(html) { 30 | return new Element('div').update(html).childElements(); 31 | }; 32 | 33 | Adapter.prototype.wrap = function(element) { 34 | if (isArrayOrNodeList(element)) { 35 | if (element.length > 1) { 36 | throw new Error("Multiple elements provided."); 37 | } 38 | element = this.unwrap(element); 39 | } else if (typeof element === "string") { 40 | element = $$(element)[0]; 41 | } 42 | return $(element); 43 | }; 44 | 45 | Adapter.prototype.unwrap = function(element) { 46 | if (isArrayOrNodeList(element)) { 47 | return element[0]; 48 | } else { 49 | return element; 50 | } 51 | }; 52 | 53 | Adapter.prototype.tagName = function(element) { 54 | return this.unwrap(element).tagName; 55 | }; 56 | 57 | Adapter.prototype.attr = function() { 58 | var args, element, _ref; 59 | element = arguments[0], args = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 60 | if (args.length === 1) { 61 | return this.wrap(element).readAttribute(args[0]); 62 | } else { 63 | return (_ref = this.wrap(element)).writeAttribute.apply(_ref, args); 64 | } 65 | }; 66 | 67 | Adapter.prototype.data = function(element, name, value) { 68 | var arg; 69 | this.wrap(element); 70 | if (arguments.length > 2) { 71 | return element.store(name, value); 72 | } else { 73 | arg = element.readAttribute("data-" + (name.underscore().dasherize())); 74 | if (arg != null) { 75 | return arg; 76 | } 77 | return element.retrieve(name); 78 | } 79 | }; 80 | 81 | Adapter.prototype.find = function(element, selector) { 82 | return this.wrap(element).select(selector)[0]; 83 | }; 84 | 85 | Adapter.prototype.findAll = function(element, selector) { 86 | return this.wrap(element).select(selector); 87 | }; 88 | 89 | Adapter.prototype.update = function(element, content, escape) { 90 | return this.wrap(element).update(escape ? content.escapeHTML() : content); 91 | }; 92 | 93 | Adapter.prototype.append = function(element, child) { 94 | return this.wrap(element).insert(this.wrap(child)); 95 | }; 96 | 97 | Adapter.prototype.remove = function(element) { 98 | return this.wrap(element).remove(); 99 | }; 100 | 101 | Adapter.prototype.addClass = function(element, className) { 102 | return this.wrap(element).addClassName(className); 103 | }; 104 | 105 | Adapter.prototype.removeClass = function(element, className) { 106 | return this.wrap(element).removeClassName(className); 107 | }; 108 | 109 | Adapter.prototype.css = function(element, properties) { 110 | return this.wrap(element).setStyle(properties); 111 | }; 112 | 113 | Adapter.prototype.dimensions = function(element) { 114 | return this.wrap(element).getDimensions(); 115 | }; 116 | 117 | Adapter.prototype.scrollOffset = function() { 118 | var offsets; 119 | offsets = document.viewport.getScrollOffsets(); 120 | return [offsets.left, offsets.top]; 121 | }; 122 | 123 | Adapter.prototype.viewportDimensions = function() { 124 | return document.viewport.getDimensions(); 125 | }; 126 | 127 | Adapter.prototype.mousePosition = function(e) { 128 | if (e == null) { 129 | return null; 130 | } 131 | return { 132 | x: Event.pointerX(e), 133 | y: Event.pointerY(e) 134 | }; 135 | }; 136 | 137 | Adapter.prototype.offset = function(element) { 138 | var offset; 139 | offset = this.wrap(element).cumulativeOffset(); 140 | return { 141 | left: offset.left, 142 | top: offset.top 143 | }; 144 | }; 145 | 146 | Adapter.prototype.observe = function(element, eventName, observer) { 147 | return Event.observe(this.wrap(element), eventName, observer); 148 | }; 149 | 150 | Adapter.prototype.stopObserving = function(element, eventName, observer) { 151 | return Event.stopObserving(this.wrap(element), eventName, observer); 152 | }; 153 | 154 | Adapter.prototype.ajax = function(options) { 155 | var _ref, _ref1; 156 | if (options.url == null) { 157 | throw new Error("No url provided"); 158 | } 159 | return new Ajax.Request(options.url, { 160 | method: (_ref = (_ref1 = options.method) != null ? _ref1.toUpperCase() : void 0) != null ? _ref : "GET", 161 | onSuccess: function(response) { 162 | return typeof options.onSuccess === "function" ? options.onSuccess(response.responseText) : void 0; 163 | }, 164 | onFailure: function(response) { 165 | return typeof options.onError === "function" ? options.onError("Server responded with status " + response.status) : void 0; 166 | }, 167 | onComplete: function() { 168 | return typeof options.onComplete === "function" ? options.onComplete() : void 0; 169 | } 170 | }); 171 | }; 172 | 173 | Adapter.prototype.clone = function(object) { 174 | return Object.clone(object); 175 | }; 176 | 177 | Adapter.prototype.extend = function() { 178 | var source, sources, target, _i, _len; 179 | target = arguments[0], sources = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 180 | for (_i = 0, _len = sources.length; _i < _len; _i++) { 181 | source = sources[_i]; 182 | Object.extend(target, source); 183 | } 184 | return target; 185 | }; 186 | 187 | return Adapter; 188 | 189 | })(); 190 | return Opentip.addAdapter(new Adapter); 191 | })(); 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Opentip 2 | ======= 3 | 4 | [Opentip][opentip] is a free opensource Java-Script tooltip class. 5 | 6 | 7 | Features 8 | -------- 9 | 10 | It supports: 11 | 12 | - Stems (little pointers) 13 | - Automatic content download with [AJAX][ajax] 14 | - Different styles 15 | - Automatic repositioning of the tooltip if it's not in the viewport of the browser anymore 16 | - All kind of triggers (The tooltip can be triggered by mouse over, click, form submit,... everything you can think of really) 17 | - CSS3 Animations 18 | - Well tested, with over 200 unit tests 19 | 20 | As of Version 2.0 Opentip does **no longer depend on [Prototype]**. You can choose 21 | *any* adapter you want so you can work with the framework of your choice. 22 | 23 | Supported frameworks are: 24 | 25 | - Native. You can use this one if you don't use any framework. 26 | - [Ender] 27 | - [Component] 28 | - [jQuery] 29 | - [Prototype] 30 | 31 | 32 | > If you want to contribute, please read on in the [contribute](https://github.com/enyo/opentip/blob/master/CONTRIBUTING.md) 33 | > file. If you are migrating from version **1.x** please refer to the 34 | > [migration section](#migrating-from-opentip-1x-to-2x) 35 | 36 | ### Build status 37 | 38 | Master [![Build Status](https://travis-ci.org/enyo/opentip.png?branch=master)](https://travis-ci.org/enyo/opentip) 39 | 40 | Develop [![Build Status](https://travis-ci.org/enyo/opentip.png?branch=develop)](https://travis-ci.org/enyo/opentip) 41 | 42 | 43 | Installation 44 | ------------ 45 | 46 | ### jQuery, Prototype, Native 47 | 48 | Just download `lib/opentip.js` and `lib/adapter.FRAMEWORK.js` and include them 49 | in this order. You can also take the already minified and combined files in the 50 | `downloads/` folder. 51 | 52 | ### Component 53 | 54 | The easiest and recommended way to install *opentip* is with [component]. Just 55 | add `enyo/opentip` as dependency in your `component.json` and rebuild it. 56 | 57 | Simply requiring opentip then activates the tooltips: `require "opentip";` 58 | 59 | 60 | ### Ender 61 | 62 | If you prefer [ender] as package manager just install it like this: 63 | 64 | ```bash 65 | $ ender build opentip 66 | ``` 67 | 68 | ### Bower 69 | 70 | Another package manager supported is [bower]: 71 | 72 | ```bash 73 | $ bower install opentip 74 | ``` 75 | 76 | * * * 77 | 78 | You should include opentip's CSS as well. It's in `css/opentip.css`. (Except 79 | for [component] of course which automatically bundles the css.) 80 | 81 | * * * 82 | 83 | If you want to work it with <=IE8, you have to include excanvas as well. Please 84 | refer to the [installation guide](http://www.opentip.org/installation.html). 85 | 86 | Usage 87 | ----- 88 | 89 | *Version 2.4.6* 90 | 91 | With HTML data attributes: 92 | 93 | ```html 94 |
Click me
95 | ``` 96 | 97 | or with the Javascript API: 98 | 99 | ```js 100 | $('elementId').opentip('Content', { showOn: "click", ...options... }); 101 | ``` 102 | 103 | For the complete documentation please visit [www.opentip.org][opentip]. 104 | 105 | 106 | Future plans 107 | ------------ 108 | 109 | - ~~Become library independant. I'm currently working on 110 | extracting all prototype functionality, so I can switch library easily. The 111 | next library I'll support will be jquery, and then mootools.~~ 112 | 113 | - Add more skins. 114 | 115 | - ~~Add cooler loading animation.~~ 116 | 117 | - ~~Implement unit tests.~~ 118 | 119 | 120 | If you have ideas, please contact me! 121 | 122 | 123 | Contribute 124 | ---------- 125 | 126 | Please refer to the [CONTRIBUTING](https://github.com/enyo/opentip/blob/develop/CONTRIBUTING.md) readme. 127 | 128 | 129 | 130 | Migrating from Opentip 1.x to 2.x 131 | --------------------------------- 132 | 133 | Those are the major changes you should look out for when migrating from 1.x to 2.x: 134 | 135 | - There's no `Tip` or `Tips` object anymore. Everything is done through 136 | `Opentip` 137 | 138 | - The recommend way to create opentips now is to call 139 | `new Opentip(element, content, title, options)`, or with the framework of 140 | your choice (eg, [ender]: `$("#my-div").opentip(content, title options)`) 141 | 142 | - The instantiation of new tips inside an event (eg: `onclick`, `onmouseover`) 143 | is no longer supported! This would create new opentips everytime the event 144 | is fired. 145 | 146 | - `Opentip.debugging = true;` does no longer exist. Use `Opentip.debug = true;` 147 | 148 | - Positions are no longer of the weird form `[ "left", "top" ]` but simply 149 | strings like `"top left"` or `"right"` 150 | 151 | - `stem.size` has been dropped in favor of `stem.length` and `stem.base` 152 | 153 | - Most of the design is now done in JS since the whole thing is a canvas now. 154 | 155 | - The way close buttons are defined has completely changed. Please refer to the 156 | docs for more information. 157 | 158 | Tagging 159 | ------- 160 | 161 | Tagging in this project is done with my [tag script](http://github.com/enyo/tag). 162 | 163 | 164 | Authors 165 | ------- 166 | 167 | Opentip is written by Matias Meno.
168 | Original graphics by Tjandra Mayerhold. 169 | 170 | ### Contributors 171 | 172 | Thanks to the following people for providing bug reports, feature requests and fixes: 173 | 174 | - Torsten Saam 175 | - Aaron Peckham 176 | - Oguri 177 | - MaxKirillov 178 | - Nick Daugherty 179 | 180 | If I forgot somebody, please just tell me. 181 | 182 | ### Related projects 183 | 184 | You might also be interested in my [formwatcher](http://www.formwatcher.org/) or 185 | [dropzone](http://www.dropzonejs.com/). 186 | 187 | License 188 | ------- 189 | (The MIT License) 190 | 191 | Copyright (c) 2012 Matias Meno <m@tias.me>
192 | 193 | Permission is hereby granted, free of charge, to any person obtaining a copy of 194 | this software and associated documentation files (the "Software"), to deal in 195 | the Software without restriction, including without limitation the rights to 196 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 197 | of the Software, and to permit persons to whom the Software is furnished to do 198 | so, subject to the following conditions: 199 | 200 | The above copyright notice and this permission notice shall be included in all 201 | copies or substantial portions of the Software. 202 | 203 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 204 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 205 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 206 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 207 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 208 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 209 | SOFTWARE. 210 | 211 | [opentip]: http://www.opentip.org/ 212 | [prototype]: http://www.prototypejs.org/ 213 | [jquery]: http://jquery.com/ 214 | [ajax]: http://en.wikipedia.org/wiki/Ajax_(programming) 215 | [excanvas]: https://github.com/enyo/excanvas 216 | [ender]: http://ender.no.de 217 | [component]: https://github.com/component 218 | [bower]: https://github.com/twitter/bower#readme 219 | -------------------------------------------------------------------------------- /test/src/050-opentip-draw.coffee: -------------------------------------------------------------------------------- 1 | 2 | $ = jQuery 3 | 4 | describe "Opentip - Drawing", -> 5 | adapter = Opentip.adapter 6 | opentip = null 7 | 8 | 9 | afterEach -> 10 | opentip[prop]?.restore?() for own prop of opentip 11 | opentip?.deactivate?() 12 | $(".opentip-container").remove() 13 | 14 | describe "_draw()", -> 15 | beforeEach -> 16 | opentip = new Opentip adapter.create("
"), "Test", delay: 0 17 | sinon.stub opentip, "_triggerElementExists", -> yes 18 | 19 | it "should abort if @redraw not set", -> 20 | sinon.stub opentip, "debug" 21 | opentip.backgroundCanvas = document.createElement "canvas" 22 | opentip.redraw = off 23 | opentip._draw() 24 | expect(opentip.debug.callCount).to.be 0 25 | 26 | it "should abort if no canvas not set", -> 27 | sinon.stub opentip, "debug" 28 | opentip.redraw = on 29 | opentip._draw() 30 | expect(opentip.debug.callCount).to.be 0 31 | 32 | it "should draw if canvas and @redraw", -> 33 | sinon.stub opentip, "debug" 34 | opentip._setup() 35 | opentip.backgroundCanvas = document.createElement "canvas" 36 | opentip.redraw = on 37 | opentip._draw() 38 | expect(opentip.debug.callCount).to.be.above 0 39 | expect(opentip.debug.args[1][0]).to.be "Drawing background." 40 | 41 | it "should add the stem classes", -> 42 | sinon.stub opentip, "debug" 43 | opentip._setup() 44 | opentip.backgroundCanvas = document.createElement "canvas" 45 | 46 | opentip.currentStem = new Opentip.Joint "bottom left" 47 | opentip.redraw = on 48 | opentip._draw() 49 | 50 | unwrappedContainer = Opentip.adapter.unwrap(opentip.container) 51 | expect(unwrappedContainer.classList.contains("stem-bottom")).to.be.ok() 52 | expect(unwrappedContainer.classList.contains("stem-left")).to.be.ok() 53 | 54 | opentip.currentStem = new Opentip.Joint "right middle" 55 | opentip.redraw = on 56 | opentip._draw() 57 | 58 | expect(unwrappedContainer.classList.contains("stem-bottom")).not.to.be.ok() 59 | expect(unwrappedContainer.classList.contains("stem-left")).not.to.be.ok() 60 | expect(unwrappedContainer.classList.contains("stem-middle")).to.be.ok() 61 | expect(unwrappedContainer.classList.contains("stem-right")).to.be.ok() 62 | 63 | it "should set the correct width of the canvas" 64 | it "should set the correct offset of the canvas" 65 | 66 | describe "with close button", -> 67 | options = { } 68 | element = null 69 | beforeEach -> 70 | element = $("
") 71 | $(document.body).append(element) 72 | sinon.stub Opentip.adapter, "dimensions", -> { width: 199, height: 100 } # -1 because of the firefox bug 73 | options = 74 | delay: 0 75 | stem: no 76 | hideTrigger: "closeButton" 77 | closeButtonRadius: 20 78 | closeButtonOffset: [ 0, 10 ] 79 | closeButtonCrossSize: 10 80 | closeButtonLinkOverscan: 5 81 | borderWidth: 0 82 | containInViewport: no 83 | 84 | afterEach -> 85 | element.remove() 86 | Opentip.adapter.dimensions.restore() 87 | 88 | createAndShowTooltip = -> 89 | opentip = new Opentip element.get(0), "Test", options 90 | # opentip._storeAndLockDimensions = -> @dimensions = { width: 200, height: 100 } 91 | # opentip._ensureViewportContainment = (e, position) -> 92 | # { 93 | # position: position 94 | # stem: @options.stem 95 | # } 96 | sinon.stub opentip, "_triggerElementExists", -> yes 97 | opentip.show() 98 | expect(opentip._dimensionsEqual opentip.dimensions, { width: 200, height: 100 }).to.be.ok() 99 | opentip 100 | 101 | 102 | it "should position the close link when no border", -> 103 | options.borderWidth = 0 104 | options.closeButtonOffset = [ 0, 10 ] 105 | 106 | createAndShowTooltip() 107 | 108 | el = adapter.unwrap opentip.closeButtonElement 109 | 110 | expect(el.style.left).to.be "190px" 111 | expect(el.style.top).to.be "0px" 112 | expect(el.style.width).to.be "20px" # cross size + overscan*2 113 | expect(el.style.height).to.be "20px" 114 | 115 | it "should position the close link when border and different overscan", -> 116 | options.borderWidth = 1 117 | options.closeButtonLinkOverscan = 10 118 | 119 | createAndShowTooltip() 120 | 121 | el = adapter.unwrap opentip.closeButtonElement 122 | 123 | expect(el.style.left).to.be "185px" 124 | expect(el.style.top).to.be "-5px" 125 | expect(el.style.width).to.be "30px" # cross size + overscan*2 126 | expect(el.style.height).to.be "30px" 127 | 128 | it "should position the close link with different offsets and overscans", -> 129 | options.closeButtonOffset = [ 10, 5 ] 130 | options.closeButtonCrossSize = 10 131 | options.closeButtonLinkOverscan = 0 132 | 133 | createAndShowTooltip() 134 | 135 | el = adapter.unwrap opentip.closeButtonElement 136 | 137 | expect(el.style.left).to.be "185px" 138 | expect(el.style.top).to.be "0px" 139 | expect(el.style.width).to.be "10px" # cross size + overscan*2 140 | expect(el.style.height).to.be "10px" 141 | 142 | it "should correctly position the close link on the left when stem on top right", -> 143 | options.closeButtonOffset = [ 20, 17 ] 144 | options.closeButtonCrossSize = 12 145 | options.closeButtonLinkOverscan = 5 146 | options.stem = "top right" 147 | 148 | opentip = createAndShowTooltip() 149 | 150 | el = adapter.unwrap opentip.closeButtonElement 151 | 152 | expect(opentip.options.stem.toString()).to.be "top right" 153 | 154 | expect(el.style.left).to.be "9px" 155 | expect(el.style.top).to.be "6px" 156 | expect(el.style.width).to.be "22px" # cross size + overscan*2 157 | expect(el.style.height).to.be "22px" 158 | 159 | 160 | describe "_getPathStemMeasures()", -> 161 | it "should just return the same measures if borderWidth is 0", -> 162 | {stemBase, stemLength} = opentip._getPathStemMeasures 6, 10, 0 163 | expect(stemBase).to.be 6 164 | expect(stemLength).to.be 10 165 | it "should properly calculate the pathStem information if borderWidth > 0", -> 166 | {stemBase, stemLength} = opentip._getPathStemMeasures 6, 10, 3 167 | expect(stemBase).to.be 3.767908047326835 168 | expect(stemLength).to.be 6.2798467455447256 169 | it "should throw an exception if the measures aren't right", -> 170 | expect(-> opentip._getPathStemMeasures 6, 10, 40).to.throwError() 171 | 172 | describe "_getColor()", -> 173 | dimensions = width: 200, height: 100 174 | 175 | cavans = document.createElement "canvas" 176 | ctx = cavans.getContext "2d" 177 | gradient = ctx.createLinearGradient 0, 0, 1, 1 178 | 179 | ctx = sinon.stub ctx 180 | 181 | gradient = sinon.stub gradient 182 | ctx.createLinearGradient.returns gradient 183 | 184 | it "should just return the hex color", -> 185 | expect(Opentip::_getColor ctx, dimensions, "#f00").to.be "#f00" 186 | 187 | it "should just return rgba color", -> 188 | expect(Opentip::_getColor ctx, dimensions, "rgba(0, 0, 0, 0.3)").to.be "rgba(0, 0, 0, 0.3)" 189 | 190 | it "should just return named color", -> 191 | expect(Opentip::_getColor ctx, dimensions, "red").to.be "red" 192 | 193 | it "should create and return gradient", -> 194 | color = Opentip::_getColor ctx, dimensions, [ [0, "black"], [1, "white"] ] 195 | expect(gradient.addColorStop.callCount).to.be 2 196 | expect(color).to.be gradient 197 | 198 | 199 | -------------------------------------------------------------------------------- /docs/docco.css: -------------------------------------------------------------------------------- 1 | /*--------------------- Layout and Typography ----------------------------*/ 2 | body { 3 | font-family: 'Palatino Linotype', 'Book Antiqua', Palatino, FreeSerif, serif; 4 | font-size: 15px; 5 | line-height: 22px; 6 | color: #252519; 7 | margin: 0; padding: 0; 8 | } 9 | a { 10 | color: #261a3b; 11 | } 12 | a:visited { 13 | color: #261a3b; 14 | } 15 | p { 16 | margin: 0 0 15px 0; 17 | } 18 | h1, h2, h3, h4, h5, h6 { 19 | margin: 0px 0 15px 0; 20 | } 21 | h1 { 22 | margin-top: 40px; 23 | } 24 | #container { 25 | position: relative; 26 | } 27 | #background { 28 | position: fixed; 29 | top: 0; left: 525px; right: 0; bottom: 0; 30 | background: #f5f5ff; 31 | border-left: 1px solid #e5e5ee; 32 | z-index: -1; 33 | } 34 | #jump_to, #jump_page { 35 | background: white; 36 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; 37 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; 38 | font: 10px Arial; 39 | text-transform: uppercase; 40 | cursor: pointer; 41 | text-align: right; 42 | } 43 | #jump_to, #jump_wrapper { 44 | position: fixed; 45 | right: 0; top: 0; 46 | padding: 5px 10px; 47 | } 48 | #jump_wrapper { 49 | padding: 0; 50 | display: none; 51 | } 52 | #jump_to:hover #jump_wrapper { 53 | display: block; 54 | } 55 | #jump_page { 56 | padding: 5px 0 3px; 57 | margin: 0 0 25px 25px; 58 | } 59 | #jump_page .source { 60 | display: block; 61 | padding: 5px 10px; 62 | text-decoration: none; 63 | border-top: 1px solid #eee; 64 | } 65 | #jump_page .source:hover { 66 | background: #f5f5ff; 67 | } 68 | #jump_page .source:first-child { 69 | } 70 | table td { 71 | border: 0; 72 | outline: 0; 73 | } 74 | td.docs, th.docs { 75 | max-width: 450px; 76 | min-width: 450px; 77 | min-height: 5px; 78 | padding: 10px 25px 1px 50px; 79 | overflow-x: hidden; 80 | vertical-align: top; 81 | text-align: left; 82 | } 83 | .docs pre { 84 | margin: 15px 0 15px; 85 | padding-left: 15px; 86 | } 87 | .docs p tt, .docs p code { 88 | background: #f8f8ff; 89 | border: 1px solid #dedede; 90 | font-size: 12px; 91 | padding: 0 0.2em; 92 | } 93 | .pilwrap { 94 | position: relative; 95 | } 96 | .pilcrow { 97 | font: 12px Arial; 98 | text-decoration: none; 99 | color: #454545; 100 | position: absolute; 101 | top: 3px; left: -20px; 102 | padding: 1px 2px; 103 | opacity: 0; 104 | -webkit-transition: opacity 0.2s linear; 105 | } 106 | td.docs:hover .pilcrow { 107 | opacity: 1; 108 | } 109 | td.code, th.code { 110 | padding: 14px 15px 16px 25px; 111 | width: 100%; 112 | vertical-align: top; 113 | background: #f5f5ff; 114 | border-left: 1px solid #e5e5ee; 115 | } 116 | pre, tt, code { 117 | font-size: 12px; line-height: 18px; 118 | font-family: Monaco, Consolas, "Lucida Console", monospace; 119 | margin: 0; padding: 0; 120 | } 121 | 122 | 123 | /*---------------------- Syntax Highlighting -----------------------------*/ 124 | td.linenos { background-color: #f0f0f0; padding-right: 10px; } 125 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } 126 | body .hll { background-color: #ffffcc } 127 | body .c { color: #408080; font-style: italic } /* Comment */ 128 | body .err { border: 1px solid #FF0000 } /* Error */ 129 | body .k { color: #954121 } /* Keyword */ 130 | body .o { color: #666666 } /* Operator */ 131 | body .cm { color: #408080; font-style: italic } /* Comment.Multiline */ 132 | body .cp { color: #BC7A00 } /* Comment.Preproc */ 133 | body .c1 { color: #408080; font-style: italic } /* Comment.Single */ 134 | body .cs { color: #408080; font-style: italic } /* Comment.Special */ 135 | body .gd { color: #A00000 } /* Generic.Deleted */ 136 | body .ge { font-style: italic } /* Generic.Emph */ 137 | body .gr { color: #FF0000 } /* Generic.Error */ 138 | body .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 139 | body .gi { color: #00A000 } /* Generic.Inserted */ 140 | body .go { color: #808080 } /* Generic.Output */ 141 | body .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 142 | body .gs { font-weight: bold } /* Generic.Strong */ 143 | body .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 144 | body .gt { color: #0040D0 } /* Generic.Traceback */ 145 | body .kc { color: #954121 } /* Keyword.Constant */ 146 | body .kd { color: #954121; font-weight: bold } /* Keyword.Declaration */ 147 | body .kn { color: #954121; font-weight: bold } /* Keyword.Namespace */ 148 | body .kp { color: #954121 } /* Keyword.Pseudo */ 149 | body .kr { color: #954121; font-weight: bold } /* Keyword.Reserved */ 150 | body .kt { color: #B00040 } /* Keyword.Type */ 151 | body .m { color: #666666 } /* Literal.Number */ 152 | body .s { color: #219161 } /* Literal.String */ 153 | body .na { color: #7D9029 } /* Name.Attribute */ 154 | body .nb { color: #954121 } /* Name.Builtin */ 155 | body .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 156 | body .no { color: #880000 } /* Name.Constant */ 157 | body .nd { color: #AA22FF } /* Name.Decorator */ 158 | body .ni { color: #999999; font-weight: bold } /* Name.Entity */ 159 | body .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ 160 | body .nf { color: #0000FF } /* Name.Function */ 161 | body .nl { color: #A0A000 } /* Name.Label */ 162 | body .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 163 | body .nt { color: #954121; font-weight: bold } /* Name.Tag */ 164 | body .nv { color: #19469D } /* Name.Variable */ 165 | body .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 166 | body .w { color: #bbbbbb } /* Text.Whitespace */ 167 | body .mf { color: #666666 } /* Literal.Number.Float */ 168 | body .mh { color: #666666 } /* Literal.Number.Hex */ 169 | body .mi { color: #666666 } /* Literal.Number.Integer */ 170 | body .mo { color: #666666 } /* Literal.Number.Oct */ 171 | body .sb { color: #219161 } /* Literal.String.Backtick */ 172 | body .sc { color: #219161 } /* Literal.String.Char */ 173 | body .sd { color: #219161; font-style: italic } /* Literal.String.Doc */ 174 | body .s2 { color: #219161 } /* Literal.String.Double */ 175 | body .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ 176 | body .sh { color: #219161 } /* Literal.String.Heredoc */ 177 | body .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ 178 | body .sx { color: #954121 } /* Literal.String.Other */ 179 | body .sr { color: #BB6688 } /* Literal.String.Regex */ 180 | body .s1 { color: #219161 } /* Literal.String.Single */ 181 | body .ss { color: #19469D } /* Literal.String.Symbol */ 182 | body .bp { color: #954121 } /* Name.Builtin.Pseudo */ 183 | body .vc { color: #19469D } /* Name.Variable.Class */ 184 | body .vg { color: #19469D } /* Name.Variable.Global */ 185 | body .vi { color: #19469D } /* Name.Variable.Instance */ 186 | body .il { color: #666666 } /* Literal.Number.Integer.Long */ -------------------------------------------------------------------------------- /src/adapter-native.coffee: -------------------------------------------------------------------------------- 1 | 2 | # Native Opentip Adapter 3 | # ====================== 4 | # 5 | # Use this adapter if you don't use a framework like jQuery and you don't 6 | # really care about oldschool browser compatibility. 7 | class Adapter 8 | 9 | name: "native" 10 | 11 | # Invoke callback as soon as dom is ready 12 | # Source: https://github.com/dperini/ContentLoaded/blob/master/src/contentloaded.js 13 | domReady: (callback) -> 14 | done = no 15 | top = true 16 | win = window 17 | doc = document 18 | 19 | return callback() if doc.readyState in [ "complete", "loaded" ] 20 | 21 | root = doc.documentElement 22 | add = (if doc.addEventListener then "addEventListener" else "attachEvent") 23 | rem = (if doc.addEventListener then "removeEventListener" else "detachEvent") 24 | pre = (if doc.addEventListener then "" else "on") 25 | 26 | init = (e) -> 27 | return if e.type is "readystatechange" and doc.readyState isnt "complete" 28 | (if e.type is "load" then win else doc)[rem] pre + e.type, init, false 29 | unless done 30 | done = yes 31 | callback() 32 | 33 | poll = -> 34 | try 35 | root.doScroll "left" 36 | catch e 37 | setTimeout poll, 50 38 | return 39 | init "poll" 40 | 41 | unless doc.readyState is "complete" 42 | if doc.createEventObject and root.doScroll 43 | try 44 | top = not win.frameElement 45 | poll() if top 46 | doc[add] pre + "DOMContentLoaded", init, false 47 | doc[add] pre + "readystatechange", init, false 48 | win[add] pre + "load", init, false 49 | 50 | 51 | # DOM 52 | # === 53 | 54 | 55 | # Create the HTML passed as string 56 | create: (htmlString) -> 57 | div = document.createElement "div" 58 | div.innerHTML = htmlString 59 | @wrap div.childNodes 60 | 61 | 62 | 63 | # Element handling 64 | # ---------------- 65 | 66 | # Wrap the element in the framework 67 | wrap: (element) -> 68 | if !element 69 | element = [ ] 70 | else if typeof element == "string" 71 | element = @find document.body, element 72 | element = if element then [ element ] else [ ] 73 | else if element instanceof NodeList 74 | element = (el for el in element) 75 | else if element not instanceof Array 76 | element = [ element ] 77 | element 78 | 79 | # Returns the unwrapped element 80 | unwrap: (element) -> @wrap(element)[0] 81 | 82 | # Returns the tag name of the element 83 | tagName: (element) -> @unwrap(element).tagName 84 | 85 | # Returns or sets the given attribute of element 86 | attr: (element, attr, value) -> 87 | if arguments.length == 3 88 | @unwrap(element).setAttribute attr, value 89 | else 90 | @unwrap(element).getAttribute attr 91 | 92 | 93 | lastDataId = 0 94 | dataValues = { } 95 | # Returns or sets the given data of element 96 | data: (element, name, value) -> 97 | dataId = @attr element, "data-id" 98 | unless dataId 99 | dataId = ++lastDataId 100 | @attr element, "data-id", dataId 101 | dataValues[dataId] = { } 102 | 103 | if arguments.length == 3 104 | # Setter 105 | dataValues[dataId][name] = value 106 | else 107 | value = dataValues[dataId][name] 108 | return value if value? 109 | 110 | value = @attr element, "data-#{Opentip::dasherize name}" 111 | if value 112 | dataValues[dataId][name] = value 113 | return value 114 | 115 | 116 | 117 | # Finds elements by selector 118 | find: (element, selector) -> @unwrap(element).querySelector selector 119 | 120 | # Finds all elements by selector 121 | findAll: (element, selector) -> @unwrap(element).querySelectorAll selector 122 | 123 | # Updates the content of the element 124 | update: (element, content, escape) -> 125 | element = @unwrap element 126 | if escape 127 | element.innerHTML = "" # Clearing the content 128 | element.appendChild document.createTextNode content 129 | else 130 | element.innerHTML = content 131 | 132 | # Appends given child to element 133 | append: (element, child) -> 134 | unwrappedChild = @unwrap child 135 | unwrappedElement = @unwrap element 136 | unwrappedElement.appendChild unwrappedChild 137 | 138 | # Removes element 139 | remove: (element) -> 140 | element = @unwrap element 141 | parentNode = element.parentNode 142 | parentNode.removeChild element if parentNode? 143 | 144 | # Add a class 145 | addClass: (element, className) -> @unwrap(element).classList.add className 146 | 147 | # Remove a class 148 | removeClass: (element, className) -> @unwrap(element).classList.remove className 149 | 150 | # Set given css properties 151 | css: (element, properties) -> 152 | element = @unwrap @wrap element 153 | for own key, value of properties 154 | element.style[key] = value 155 | 156 | # Returns an object with given dimensions 157 | dimensions: (element) -> 158 | element = @unwrap @wrap element 159 | dimensions = 160 | width: element.offsetWidth 161 | height: element.offsetHeight 162 | 163 | unless dimensions.width and dimensions.height 164 | # The element is probably invisible. So make it visible 165 | revert = 166 | position: element.style.position || '' 167 | visibility: element.style.visibility || '' 168 | display: element.style.display || '' 169 | 170 | @css element, 171 | position: "absolute" 172 | visibility: "hidden" 173 | display: "block" 174 | 175 | dimensions = 176 | width: element.offsetWidth 177 | height: element.offsetHeight 178 | 179 | @css element, revert 180 | 181 | dimensions 182 | 183 | # Returns the scroll offsets of current document 184 | scrollOffset: -> 185 | [ 186 | window.pageXOffset or document.documentElement.scrollLeft or document.body.scrollLeft 187 | window.pageYOffset or document.documentElement.scrollTop or document.body.scrollTop 188 | ] 189 | 190 | # Returns the dimensions of the viewport (currently visible browser area) 191 | viewportDimensions: -> 192 | { 193 | width: document.documentElement.clientWidth 194 | height: document.documentElement.clientHeight 195 | } 196 | 197 | # Returns an object with x and y 198 | mousePosition: (e) -> 199 | pos = x: 0, y: 0 200 | 201 | e ?= window.event 202 | 203 | return unless e? 204 | 205 | try 206 | if e.pageX or e.pageY 207 | pos.x = e.pageX 208 | pos.y = e.pageY 209 | else if e.clientX or e.clientY 210 | pos.x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft 211 | pos.y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop 212 | catch e 213 | pos 214 | 215 | # Returns the offset of the element 216 | offset: (element) -> 217 | element = @unwrap element 218 | 219 | offset = { 220 | top: element.offsetTop 221 | left: element.offsetLeft 222 | } 223 | 224 | while element = element.offsetParent 225 | offset.top += element.offsetTop 226 | offset.left += element.offsetLeft 227 | 228 | if element != document.body 229 | offset.top -= element.scrollTop 230 | offset.left -= element.scrollLeft 231 | 232 | offset 233 | 234 | # Observe given eventName 235 | observe: (element, eventName, observer) -> 236 | # Firefox <= 3.6 needs the last optional parameter `useCapture` 237 | @unwrap(element).addEventListener eventName, observer, false 238 | 239 | # Stop observing event 240 | stopObserving: (element, eventName, observer) -> 241 | # Firefox <= 3.6 needs the last optional parameter `useCapture` 242 | @unwrap(element).removeEventListener eventName, observer, false 243 | 244 | 245 | # Perform an AJAX request and call the appropriate callbacks. 246 | ajax: (options) -> 247 | throw new Error "No url provided" unless options.url? 248 | 249 | if window.XMLHttpRequest 250 | # Mozilla, Safari, ... 251 | request = new XMLHttpRequest 252 | else if window.ActiveXObject 253 | # IE 254 | try 255 | request = new ActiveXObject "Msxml2.XMLHTTP" 256 | catch e 257 | try 258 | request = new ActiveXObject "Microsoft.XMLHTTP" 259 | catch e 260 | 261 | throw new Error "Can't create XMLHttpRequest" unless request 262 | 263 | request.onreadystatechange = -> 264 | if request.readyState == 4 265 | try 266 | if request.status == 200 267 | options.onSuccess? request.responseText 268 | else 269 | options.onError? "Server responded with status #{request.status}" 270 | catch e 271 | options.onError? e.message 272 | 273 | options.onComplete?() 274 | 275 | 276 | request.open options.method?.toUpperCase() ? "GET", options.url 277 | request.send() 278 | 279 | # Utility functions 280 | # ================= 281 | 282 | # Creates a shallow copy of the object 283 | clone: (object) -> 284 | newObject = { } 285 | for own key, val of object 286 | newObject[key] = val 287 | newObject 288 | 289 | # Copies all properties from sources to target 290 | extend: (target, sources...) -> 291 | for source in sources 292 | for own key, val of source 293 | target[key] = val 294 | target 295 | 296 | 297 | 298 | 299 | 300 | 301 | # Add the adapter to the list 302 | Opentip.addAdapter new Adapter 303 | -------------------------------------------------------------------------------- /src/adapter-browserify.coffee: -------------------------------------------------------------------------------- 1 | Opentip = require('./opentip'); 2 | 3 | # Native Opentip Adapter 4 | # ====================== 5 | # 6 | # Use this adapter if you don't use a framework like jQuery and you don't 7 | # really care about oldschool browser compatibility. 8 | class Adapter 9 | 10 | name: "browserify" 11 | 12 | # Invoke callback as soon as dom is ready 13 | # Source: https://github.com/dperini/ContentLoaded/blob/master/src/contentloaded.js 14 | domReady: (callback) -> 15 | done = no 16 | top = true 17 | win = window 18 | doc = document 19 | 20 | return callback() if doc.readyState in [ "complete", "loaded" ] 21 | 22 | root = doc.documentElement 23 | add = (if doc.addEventListener then "addEventListener" else "attachEvent") 24 | rem = (if doc.addEventListener then "removeEventListener" else "detachEvent") 25 | pre = (if doc.addEventListener then "" else "on") 26 | 27 | init = (e) -> 28 | return if e.type is "readystatechange" and doc.readyState isnt "complete" 29 | (if e.type is "load" then win else doc)[rem] pre + e.type, init, false 30 | unless done 31 | done = yes 32 | callback() 33 | 34 | poll = -> 35 | try 36 | root.doScroll "left" 37 | catch e 38 | setTimeout poll, 50 39 | return 40 | init "poll" 41 | 42 | unless doc.readyState is "complete" 43 | if doc.createEventObject and root.doScroll 44 | try 45 | top = not win.frameElement 46 | poll() if top 47 | doc[add] pre + "DOMContentLoaded", init, false 48 | doc[add] pre + "readystatechange", init, false 49 | win[add] pre + "load", init, false 50 | 51 | 52 | # DOM 53 | # === 54 | 55 | 56 | # Create the HTML passed as string 57 | create: (htmlString) -> 58 | div = document.createElement "div" 59 | div.innerHTML = htmlString 60 | @wrap div.childNodes 61 | 62 | 63 | 64 | # Element handling 65 | # ---------------- 66 | 67 | # Wrap the element in the framework 68 | wrap: (element) -> 69 | if !element 70 | element = [ ] 71 | else if typeof element == "string" 72 | element = @find document.body, element 73 | element = if element then [ element ] else [ ] 74 | else if element instanceof NodeList 75 | element = (el for el in element) 76 | else if element not instanceof Array 77 | element = [ element ] 78 | element 79 | 80 | # Returns the unwrapped element 81 | unwrap: (element) -> @wrap(element)[0] 82 | 83 | # Returns the tag name of the element 84 | tagName: (element) -> @unwrap(element).tagName 85 | 86 | # Returns or sets the given attribute of element 87 | attr: (element, attr, value) -> 88 | if arguments.length == 3 89 | @unwrap(element).setAttribute attr, value 90 | else 91 | @unwrap(element).getAttribute attr 92 | 93 | 94 | lastDataId = 0 95 | dataValues = { } 96 | # Returns or sets the given data of element 97 | data: (element, name, value) -> 98 | dataId = @attr element, "data-id" 99 | unless dataId 100 | dataId = ++lastDataId 101 | @attr element, "data-id", dataId 102 | dataValues[dataId] = { } 103 | 104 | if arguments.length == 3 105 | # Setter 106 | dataValues[dataId][name] = value 107 | else 108 | value = dataValues[dataId][name] 109 | return value if value? 110 | 111 | value = @attr element, "data-#{Opentip::dasherize name}" 112 | if value 113 | dataValues[dataId][name] = value 114 | return value 115 | 116 | 117 | 118 | # Finds elements by selector 119 | find: (element, selector) -> @unwrap(element).querySelector selector 120 | 121 | # Finds all elements by selector 122 | findAll: (element, selector) -> @unwrap(element).querySelectorAll selector 123 | 124 | # Updates the content of the element 125 | update: (element, content, escape) -> 126 | element = @unwrap element 127 | if escape 128 | element.innerHTML = "" # Clearing the content 129 | element.appendChild document.createTextNode content 130 | else 131 | element.innerHTML = content 132 | 133 | # Appends given child to element 134 | append: (element, child) -> 135 | unwrappedChild = @unwrap child 136 | unwrappedElement = @unwrap element 137 | unwrappedElement.appendChild unwrappedChild 138 | 139 | # Removes element 140 | remove: (element) -> 141 | element = @unwrap element 142 | parentNode = element.parentNode 143 | parentNode.removeChild element if parentNode? 144 | 145 | # Add a class 146 | addClass: (element, className) -> @unwrap(element).classList.add className 147 | 148 | # Remove a class 149 | removeClass: (element, className) -> @unwrap(element).classList.remove className 150 | 151 | # Set given css properties 152 | css: (element, properties) -> 153 | element = @unwrap @wrap element 154 | for own key, value of properties 155 | element.style[key] = value 156 | 157 | # Returns an object with given dimensions 158 | dimensions: (element) -> 159 | element = @unwrap @wrap element 160 | dimensions = 161 | width: element.offsetWidth 162 | height: element.offsetHeight 163 | 164 | unless dimensions.width and dimensions.height 165 | # The element is probably invisible. So make it visible 166 | revert = 167 | position: element.style.position || '' 168 | visibility: element.style.visibility || '' 169 | display: element.style.display || '' 170 | 171 | @css element, 172 | position: "absolute" 173 | visibility: "hidden" 174 | display: "block" 175 | 176 | dimensions = 177 | width: element.offsetWidth 178 | height: element.offsetHeight 179 | 180 | @css element, revert 181 | 182 | dimensions 183 | 184 | # Returns the scroll offsets of current document 185 | scrollOffset: -> 186 | [ 187 | window.pageXOffset or document.documentElement.scrollLeft or document.body.scrollLeft 188 | window.pageYOffset or document.documentElement.scrollTop or document.body.scrollTop 189 | ] 190 | 191 | # Returns the dimensions of the viewport (currently visible browser area) 192 | viewportDimensions: -> 193 | { 194 | width: document.documentElement.clientWidth 195 | height: document.documentElement.clientHeight 196 | } 197 | 198 | # Returns an object with x and y 199 | mousePosition: (e) -> 200 | pos = x: 0, y: 0 201 | 202 | e ?= window.event 203 | 204 | return unless e? 205 | 206 | try 207 | if e.pageX or e.pageY 208 | pos.x = e.pageX 209 | pos.y = e.pageY 210 | else if e.clientX or e.clientY 211 | pos.x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft 212 | pos.y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop 213 | catch e 214 | pos 215 | 216 | # Returns the offset of the element 217 | offset: (element) -> 218 | element = @unwrap element 219 | 220 | offset = { 221 | top: element.offsetTop 222 | left: element.offsetLeft 223 | } 224 | 225 | while element = element.offsetParent 226 | offset.top += element.offsetTop 227 | offset.left += element.offsetLeft 228 | 229 | if element != document.body 230 | offset.top -= element.scrollTop 231 | offset.left -= element.scrollLeft 232 | 233 | offset 234 | 235 | # Observe given eventName 236 | observe: (element, eventName, observer) -> 237 | # Firefox <= 3.6 needs the last optional parameter `useCapture` 238 | @unwrap(element).addEventListener eventName, observer, false 239 | 240 | # Stop observing event 241 | stopObserving: (element, eventName, observer) -> 242 | # Firefox <= 3.6 needs the last optional parameter `useCapture` 243 | @unwrap(element).removeEventListener eventName, observer, false 244 | 245 | 246 | # Perform an AJAX request and call the appropriate callbacks. 247 | ajax: (options) -> 248 | throw new Error "No url provided" unless options.url? 249 | 250 | if window.XMLHttpRequest 251 | # Mozilla, Safari, ... 252 | request = new XMLHttpRequest 253 | else if window.ActiveXObject 254 | # IE 255 | try 256 | request = new ActiveXObject "Msxml2.XMLHTTP" 257 | catch e 258 | try 259 | request = new ActiveXObject "Microsoft.XMLHTTP" 260 | catch e 261 | 262 | throw new Error "Can't create XMLHttpRequest" unless request 263 | 264 | request.onreadystatechange = -> 265 | if request.readyState == 4 266 | try 267 | if request.status == 200 268 | options.onSuccess? request.responseText 269 | else 270 | options.onError? "Server responded with status #{request.status}" 271 | catch e 272 | options.onError? e.message 273 | 274 | options.onComplete?() 275 | 276 | 277 | request.open options.method?.toUpperCase() ? "GET", options.url 278 | request.send() 279 | 280 | # Utility functions 281 | # ================= 282 | 283 | # Creates a shallow copy of the object 284 | clone: (object) -> 285 | newObject = { } 286 | for own key, val of object 287 | newObject[key] = val 288 | newObject 289 | 290 | # Copies all properties from sources to target 291 | extend: (target, sources...) -> 292 | for source in sources 293 | for own key, val of source 294 | target[key] = val 295 | target 296 | 297 | 298 | 299 | 300 | 301 | 302 | # Add the adapter to the list 303 | Opentip.addAdapter new Adapter 304 | 305 | module.exports = Opentip 306 | -------------------------------------------------------------------------------- /css/opentip.css: -------------------------------------------------------------------------------- 1 | .opentip-container, 2 | .opentip-container * { 3 | -webkit-box-sizing: border-box; 4 | -moz-box-sizing: border-box; 5 | box-sizing: border-box; 6 | } 7 | .opentip-container { 8 | position: absolute; 9 | max-width: 300px; 10 | z-index: 100; 11 | -webkit-transition: -webkit-transform 1s ease-in-out; 12 | -moz-transition: -moz-transform 1s ease-in-out; 13 | -o-transition: -o-transform 1s ease-in-out; 14 | -ms-transition: -ms-transform 1s ease-in-out; 15 | transition: transform 1s ease-in-out; 16 | pointer-events: none; 17 | -webkit-transform: translateX(0) translateY(0); 18 | -moz-transform: translateX(0) translateY(0); 19 | -o-transform: translateX(0) translateY(0); 20 | -ms-transform: translateX(0) translateY(0); 21 | transform: translateX(0) translateY(0); 22 | } 23 | .opentip-container.ot-fixed.ot-hidden.stem-top.stem-center, 24 | .opentip-container.ot-fixed.ot-going-to-show.stem-top.stem-center, 25 | .opentip-container.ot-fixed.ot-hiding.stem-top.stem-center { 26 | -webkit-transform: translateY(-5px); 27 | -moz-transform: translateY(-5px); 28 | -o-transform: translateY(-5px); 29 | -ms-transform: translateY(-5px); 30 | transform: translateY(-5px); 31 | } 32 | .opentip-container.ot-fixed.ot-hidden.stem-top.stem-right, 33 | .opentip-container.ot-fixed.ot-going-to-show.stem-top.stem-right, 34 | .opentip-container.ot-fixed.ot-hiding.stem-top.stem-right { 35 | -webkit-transform: translateY(-5px) translateX(5px); 36 | -moz-transform: translateY(-5px) translateX(5px); 37 | -o-transform: translateY(-5px) translateX(5px); 38 | -ms-transform: translateY(-5px) translateX(5px); 39 | transform: translateY(-5px) translateX(5px); 40 | } 41 | .opentip-container.ot-fixed.ot-hidden.stem-middle.stem-right, 42 | .opentip-container.ot-fixed.ot-going-to-show.stem-middle.stem-right, 43 | .opentip-container.ot-fixed.ot-hiding.stem-middle.stem-right { 44 | -webkit-transform: translateX(5px); 45 | -moz-transform: translateX(5px); 46 | -o-transform: translateX(5px); 47 | -ms-transform: translateX(5px); 48 | transform: translateX(5px); 49 | } 50 | .opentip-container.ot-fixed.ot-hidden.stem-bottom.stem-right, 51 | .opentip-container.ot-fixed.ot-going-to-show.stem-bottom.stem-right, 52 | .opentip-container.ot-fixed.ot-hiding.stem-bottom.stem-right { 53 | -webkit-transform: translateY(5px) translateX(5px); 54 | -moz-transform: translateY(5px) translateX(5px); 55 | -o-transform: translateY(5px) translateX(5px); 56 | -ms-transform: translateY(5px) translateX(5px); 57 | transform: translateY(5px) translateX(5px); 58 | } 59 | .opentip-container.ot-fixed.ot-hidden.stem-bottom.stem-center, 60 | .opentip-container.ot-fixed.ot-going-to-show.stem-bottom.stem-center, 61 | .opentip-container.ot-fixed.ot-hiding.stem-bottom.stem-center { 62 | -webkit-transform: translateY(5px); 63 | -moz-transform: translateY(5px); 64 | -o-transform: translateY(5px); 65 | -ms-transform: translateY(5px); 66 | transform: translateY(5px); 67 | } 68 | .opentip-container.ot-fixed.ot-hidden.stem-bottom.stem-left, 69 | .opentip-container.ot-fixed.ot-going-to-show.stem-bottom.stem-left, 70 | .opentip-container.ot-fixed.ot-hiding.stem-bottom.stem-left { 71 | -webkit-transform: translateY(5px) translateX(-5px); 72 | -moz-transform: translateY(5px) translateX(-5px); 73 | -o-transform: translateY(5px) translateX(-5px); 74 | -ms-transform: translateY(5px) translateX(-5px); 75 | transform: translateY(5px) translateX(-5px); 76 | } 77 | .opentip-container.ot-fixed.ot-hidden.stem-middle.stem-left, 78 | .opentip-container.ot-fixed.ot-going-to-show.stem-middle.stem-left, 79 | .opentip-container.ot-fixed.ot-hiding.stem-middle.stem-left { 80 | -webkit-transform: translateX(-5px); 81 | -moz-transform: translateX(-5px); 82 | -o-transform: translateX(-5px); 83 | -ms-transform: translateX(-5px); 84 | transform: translateX(-5px); 85 | } 86 | .opentip-container.ot-fixed.ot-hidden.stem-top.stem-left, 87 | .opentip-container.ot-fixed.ot-going-to-show.stem-top.stem-left, 88 | .opentip-container.ot-fixed.ot-hiding.stem-top.stem-left { 89 | -webkit-transform: translateY(-5px) translateX(-5px); 90 | -moz-transform: translateY(-5px) translateX(-5px); 91 | -o-transform: translateY(-5px) translateX(-5px); 92 | -ms-transform: translateY(-5px) translateX(-5px); 93 | transform: translateY(-5px) translateX(-5px); 94 | } 95 | .opentip-container.ot-fixed .opentip { 96 | pointer-events: auto; 97 | } 98 | .opentip-container.ot-hidden { 99 | display: none; 100 | } 101 | .opentip-container .opentip { 102 | position: relative; 103 | font-size: 13px; 104 | line-height: 120%; 105 | padding: 9px 14px; 106 | color: #4f4b47; 107 | text-shadow: -1px -1px 0px rgba(255,255,255,0.2); 108 | } 109 | .opentip-container .opentip .header { 110 | margin: 0; 111 | padding: 0; 112 | } 113 | .opentip-container .opentip .ot-close { 114 | pointer-events: auto; 115 | display: block; 116 | position: absolute; 117 | top: -12px; 118 | left: 60px; 119 | color: rgba(0,0,0,0.5); 120 | background: rgba(0,0,0,0); 121 | text-decoration: none; 122 | } 123 | .opentip-container .opentip .ot-close span { 124 | display: none; 125 | } 126 | .opentip-container .opentip .ot-loading-indicator { 127 | display: none; 128 | } 129 | .opentip-container.ot-loading .ot-loading-indicator { 130 | width: 30px; 131 | height: 30px; 132 | font-size: 30px; 133 | line-height: 30px; 134 | font-weight: bold; 135 | display: block; 136 | } 137 | .opentip-container.ot-loading .ot-loading-indicator span { 138 | display: block; 139 | -webkit-animation: otloading 2s linear infinite; 140 | -moz-animation: otloading 2s linear infinite; 141 | -o-animation: otloading 2s linear infinite; 142 | -ms-animation: otloading 2s linear infinite; 143 | animation: otloading 2s linear infinite; 144 | text-align: center; 145 | } 146 | .opentip-container.style-dark .opentip, 147 | .opentip-container.style-alert .opentip { 148 | color: #f8f8f8; 149 | text-shadow: 1px 1px 0px rgba(0,0,0,0.2); 150 | } 151 | .opentip-container.style-glass .opentip { 152 | padding: 15px 25px; 153 | color: #317cc5; 154 | text-shadow: 1px 1px 8px rgba(0,94,153,0.3); 155 | } 156 | .opentip-container.ot-hide-effect-fade { 157 | -webkit-transition: -webkit-transform 0.5s ease-in-out, opacity 1s ease-in-out; 158 | -moz-transition: -moz-transform 0.5s ease-in-out, opacity 1s ease-in-out; 159 | -o-transition: -o-transform 0.5s ease-in-out, opacity 1s ease-in-out; 160 | -ms-transition: -ms-transform 0.5s ease-in-out, opacity 1s ease-in-out; 161 | transition: transform 0.5s ease-in-out, opacity 1s ease-in-out; 162 | opacity: 1; 163 | -ms-filter: none; 164 | filter: none; 165 | } 166 | .opentip-container.ot-hide-effect-fade.ot-hiding { 167 | opacity: 0; 168 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; 169 | filter: alpha(opacity=0); 170 | } 171 | .opentip-container.ot-show-effect-appear.ot-going-to-show, 172 | .opentip-container.ot-show-effect-appear.ot-showing { 173 | -webkit-transition: -webkit-transform 0.5s ease-in-out, opacity 1s ease-in-out; 174 | -moz-transition: -moz-transform 0.5s ease-in-out, opacity 1s ease-in-out; 175 | -o-transition: -o-transform 0.5s ease-in-out, opacity 1s ease-in-out; 176 | -ms-transition: -ms-transform 0.5s ease-in-out, opacity 1s ease-in-out; 177 | transition: transform 0.5s ease-in-out, opacity 1s ease-in-out; 178 | } 179 | .opentip-container.ot-show-effect-appear.ot-going-to-show { 180 | opacity: 0; 181 | -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; 182 | filter: alpha(opacity=0); 183 | } 184 | .opentip-container.ot-show-effect-appear.ot-showing { 185 | opacity: 1; 186 | -ms-filter: none; 187 | filter: none; 188 | } 189 | .opentip-container.ot-show-effect-appear.ot-visible { 190 | opacity: 1; 191 | -ms-filter: none; 192 | filter: none; 193 | } 194 | @-moz-keyframes otloading { 195 | from { 196 | -webkit-transform: rotate(0deg); 197 | -moz-transform: rotate(0deg); 198 | -o-transform: rotate(0deg); 199 | -ms-transform: rotate(0deg); 200 | transform: rotate(0deg); 201 | } 202 | to { 203 | -webkit-transform: rotate(360deg); 204 | -moz-transform: rotate(360deg); 205 | -o-transform: rotate(360deg); 206 | -ms-transform: rotate(360deg); 207 | transform: rotate(360deg); 208 | } 209 | } 210 | @-webkit-keyframes otloading { 211 | from { 212 | -webkit-transform: rotate(0deg); 213 | -moz-transform: rotate(0deg); 214 | -o-transform: rotate(0deg); 215 | -ms-transform: rotate(0deg); 216 | transform: rotate(0deg); 217 | } 218 | to { 219 | -webkit-transform: rotate(360deg); 220 | -moz-transform: rotate(360deg); 221 | -o-transform: rotate(360deg); 222 | -ms-transform: rotate(360deg); 223 | transform: rotate(360deg); 224 | } 225 | } 226 | @-o-keyframes otloading { 227 | from { 228 | -webkit-transform: rotate(0deg); 229 | -moz-transform: rotate(0deg); 230 | -o-transform: rotate(0deg); 231 | -ms-transform: rotate(0deg); 232 | transform: rotate(0deg); 233 | } 234 | to { 235 | -webkit-transform: rotate(360deg); 236 | -moz-transform: rotate(360deg); 237 | -o-transform: rotate(360deg); 238 | -ms-transform: rotate(360deg); 239 | transform: rotate(360deg); 240 | } 241 | } 242 | @keyframes otloading { 243 | from { 244 | -webkit-transform: rotate(0deg); 245 | -moz-transform: rotate(0deg); 246 | -o-transform: rotate(0deg); 247 | -ms-transform: rotate(0deg); 248 | transform: rotate(0deg); 249 | } 250 | to { 251 | -webkit-transform: rotate(360deg); 252 | -moz-transform: rotate(360deg); 253 | -o-transform: rotate(360deg); 254 | -ms-transform: rotate(360deg); 255 | transform: rotate(360deg); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /lib/adapter-native.js: -------------------------------------------------------------------------------- 1 | var Adapter, 2 | __hasProp = {}.hasOwnProperty, 3 | __slice = [].slice; 4 | 5 | Adapter = (function() { 6 | var dataValues, lastDataId; 7 | 8 | function Adapter() {} 9 | 10 | Adapter.prototype.name = "native"; 11 | 12 | Adapter.prototype.domReady = function(callback) { 13 | var add, doc, done, init, poll, pre, rem, root, top, win, _ref; 14 | done = false; 15 | top = true; 16 | win = window; 17 | doc = document; 18 | if ((_ref = doc.readyState) === "complete" || _ref === "loaded") { 19 | return callback(); 20 | } 21 | root = doc.documentElement; 22 | add = (doc.addEventListener ? "addEventListener" : "attachEvent"); 23 | rem = (doc.addEventListener ? "removeEventListener" : "detachEvent"); 24 | pre = (doc.addEventListener ? "" : "on"); 25 | init = function(e) { 26 | if (e.type === "readystatechange" && doc.readyState !== "complete") { 27 | return; 28 | } 29 | (e.type === "load" ? win : doc)[rem](pre + e.type, init, false); 30 | if (!done) { 31 | done = true; 32 | return callback(); 33 | } 34 | }; 35 | poll = function() { 36 | var e; 37 | try { 38 | root.doScroll("left"); 39 | } catch (_error) { 40 | e = _error; 41 | setTimeout(poll, 50); 42 | return; 43 | } 44 | return init("poll"); 45 | }; 46 | if (doc.readyState !== "complete") { 47 | if (doc.createEventObject && root.doScroll) { 48 | try { 49 | top = !win.frameElement; 50 | } catch (_error) {} 51 | if (top) { 52 | poll(); 53 | } 54 | } 55 | doc[add](pre + "DOMContentLoaded", init, false); 56 | doc[add](pre + "readystatechange", init, false); 57 | return win[add](pre + "load", init, false); 58 | } 59 | }; 60 | 61 | Adapter.prototype.create = function(htmlString) { 62 | var div; 63 | div = document.createElement("div"); 64 | div.innerHTML = htmlString; 65 | return this.wrap(div.childNodes); 66 | }; 67 | 68 | Adapter.prototype.wrap = function(element) { 69 | var el; 70 | if (!element) { 71 | element = []; 72 | } else if (typeof element === "string") { 73 | element = this.find(document.body, element); 74 | element = element ? [element] : []; 75 | } else if (element instanceof NodeList) { 76 | element = (function() { 77 | var _i, _len, _results; 78 | _results = []; 79 | for (_i = 0, _len = element.length; _i < _len; _i++) { 80 | el = element[_i]; 81 | _results.push(el); 82 | } 83 | return _results; 84 | })(); 85 | } else if (!(element instanceof Array)) { 86 | element = [element]; 87 | } 88 | return element; 89 | }; 90 | 91 | Adapter.prototype.unwrap = function(element) { 92 | return this.wrap(element)[0]; 93 | }; 94 | 95 | Adapter.prototype.tagName = function(element) { 96 | return this.unwrap(element).tagName; 97 | }; 98 | 99 | Adapter.prototype.attr = function(element, attr, value) { 100 | if (arguments.length === 3) { 101 | return this.unwrap(element).setAttribute(attr, value); 102 | } else { 103 | return this.unwrap(element).getAttribute(attr); 104 | } 105 | }; 106 | 107 | lastDataId = 0; 108 | 109 | dataValues = {}; 110 | 111 | Adapter.prototype.data = function(element, name, value) { 112 | var dataId; 113 | dataId = this.attr(element, "data-id"); 114 | if (!dataId) { 115 | dataId = ++lastDataId; 116 | this.attr(element, "data-id", dataId); 117 | dataValues[dataId] = {}; 118 | } 119 | if (arguments.length === 3) { 120 | return dataValues[dataId][name] = value; 121 | } else { 122 | value = dataValues[dataId][name]; 123 | if (value != null) { 124 | return value; 125 | } 126 | value = this.attr(element, "data-" + (Opentip.prototype.dasherize(name))); 127 | if (value) { 128 | dataValues[dataId][name] = value; 129 | } 130 | return value; 131 | } 132 | }; 133 | 134 | Adapter.prototype.find = function(element, selector) { 135 | return this.unwrap(element).querySelector(selector); 136 | }; 137 | 138 | Adapter.prototype.findAll = function(element, selector) { 139 | return this.unwrap(element).querySelectorAll(selector); 140 | }; 141 | 142 | Adapter.prototype.update = function(element, content, escape) { 143 | element = this.unwrap(element); 144 | if (escape) { 145 | element.innerHTML = ""; 146 | return element.appendChild(document.createTextNode(content)); 147 | } else { 148 | return element.innerHTML = content; 149 | } 150 | }; 151 | 152 | Adapter.prototype.append = function(element, child) { 153 | var unwrappedChild, unwrappedElement; 154 | unwrappedChild = this.unwrap(child); 155 | unwrappedElement = this.unwrap(element); 156 | return unwrappedElement.appendChild(unwrappedChild); 157 | }; 158 | 159 | Adapter.prototype.remove = function(element) { 160 | var parentNode; 161 | element = this.unwrap(element); 162 | parentNode = element.parentNode; 163 | if (parentNode != null) { 164 | return parentNode.removeChild(element); 165 | } 166 | }; 167 | 168 | Adapter.prototype.addClass = function(element, className) { 169 | return this.unwrap(element).classList.add(className); 170 | }; 171 | 172 | Adapter.prototype.removeClass = function(element, className) { 173 | return this.unwrap(element).classList.remove(className); 174 | }; 175 | 176 | Adapter.prototype.css = function(element, properties) { 177 | var key, value, _results; 178 | element = this.unwrap(this.wrap(element)); 179 | _results = []; 180 | for (key in properties) { 181 | if (!__hasProp.call(properties, key)) continue; 182 | value = properties[key]; 183 | _results.push(element.style[key] = value); 184 | } 185 | return _results; 186 | }; 187 | 188 | Adapter.prototype.dimensions = function(element) { 189 | var dimensions, revert; 190 | element = this.unwrap(this.wrap(element)); 191 | dimensions = { 192 | width: element.offsetWidth, 193 | height: element.offsetHeight 194 | }; 195 | if (!(dimensions.width && dimensions.height)) { 196 | revert = { 197 | position: element.style.position || '', 198 | visibility: element.style.visibility || '', 199 | display: element.style.display || '' 200 | }; 201 | this.css(element, { 202 | position: "absolute", 203 | visibility: "hidden", 204 | display: "block" 205 | }); 206 | dimensions = { 207 | width: element.offsetWidth, 208 | height: element.offsetHeight 209 | }; 210 | this.css(element, revert); 211 | } 212 | return dimensions; 213 | }; 214 | 215 | Adapter.prototype.scrollOffset = function() { 216 | return [window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop]; 217 | }; 218 | 219 | Adapter.prototype.viewportDimensions = function() { 220 | return { 221 | width: document.documentElement.clientWidth, 222 | height: document.documentElement.clientHeight 223 | }; 224 | }; 225 | 226 | Adapter.prototype.mousePosition = function(e) { 227 | var pos; 228 | pos = { 229 | x: 0, 230 | y: 0 231 | }; 232 | if (e == null) { 233 | e = window.event; 234 | } 235 | if (e == null) { 236 | return; 237 | } 238 | try { 239 | if (e.pageX || e.pageY) { 240 | pos.x = e.pageX; 241 | pos.y = e.pageY; 242 | } else if (e.clientX || e.clientY) { 243 | pos.x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; 244 | pos.y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; 245 | } 246 | } catch (_error) { 247 | e = _error; 248 | } 249 | return pos; 250 | }; 251 | 252 | Adapter.prototype.offset = function(element) { 253 | var offset; 254 | element = this.unwrap(element); 255 | offset = { 256 | top: element.offsetTop, 257 | left: element.offsetLeft 258 | }; 259 | while (element = element.offsetParent) { 260 | offset.top += element.offsetTop; 261 | offset.left += element.offsetLeft; 262 | if (element !== document.body) { 263 | offset.top -= element.scrollTop; 264 | offset.left -= element.scrollLeft; 265 | } 266 | } 267 | return offset; 268 | }; 269 | 270 | Adapter.prototype.observe = function(element, eventName, observer) { 271 | return this.unwrap(element).addEventListener(eventName, observer, false); 272 | }; 273 | 274 | Adapter.prototype.stopObserving = function(element, eventName, observer) { 275 | return this.unwrap(element).removeEventListener(eventName, observer, false); 276 | }; 277 | 278 | Adapter.prototype.ajax = function(options) { 279 | var e, request, _ref, _ref1; 280 | if (options.url == null) { 281 | throw new Error("No url provided"); 282 | } 283 | if (window.XMLHttpRequest) { 284 | request = new XMLHttpRequest; 285 | } else if (window.ActiveXObject) { 286 | try { 287 | request = new ActiveXObject("Msxml2.XMLHTTP"); 288 | } catch (_error) { 289 | e = _error; 290 | try { 291 | request = new ActiveXObject("Microsoft.XMLHTTP"); 292 | } catch (_error) { 293 | e = _error; 294 | } 295 | } 296 | } 297 | if (!request) { 298 | throw new Error("Can't create XMLHttpRequest"); 299 | } 300 | request.onreadystatechange = function() { 301 | if (request.readyState === 4) { 302 | try { 303 | if (request.status === 200) { 304 | if (typeof options.onSuccess === "function") { 305 | options.onSuccess(request.responseText); 306 | } 307 | } else { 308 | if (typeof options.onError === "function") { 309 | options.onError("Server responded with status " + request.status); 310 | } 311 | } 312 | } catch (_error) { 313 | e = _error; 314 | if (typeof options.onError === "function") { 315 | options.onError(e.message); 316 | } 317 | } 318 | return typeof options.onComplete === "function" ? options.onComplete() : void 0; 319 | } 320 | }; 321 | request.open((_ref = (_ref1 = options.method) != null ? _ref1.toUpperCase() : void 0) != null ? _ref : "GET", options.url); 322 | return request.send(); 323 | }; 324 | 325 | Adapter.prototype.clone = function(object) { 326 | var key, newObject, val; 327 | newObject = {}; 328 | for (key in object) { 329 | if (!__hasProp.call(object, key)) continue; 330 | val = object[key]; 331 | newObject[key] = val; 332 | } 333 | return newObject; 334 | }; 335 | 336 | Adapter.prototype.extend = function() { 337 | var key, source, sources, target, val, _i, _len; 338 | target = arguments[0], sources = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 339 | for (_i = 0, _len = sources.length; _i < _len; _i++) { 340 | source = sources[_i]; 341 | for (key in source) { 342 | if (!__hasProp.call(source, key)) continue; 343 | val = source[key]; 344 | target[key] = val; 345 | } 346 | } 347 | return target; 348 | }; 349 | 350 | return Adapter; 351 | 352 | })(); 353 | 354 | Opentip.addAdapter(new Adapter); 355 | -------------------------------------------------------------------------------- /lib/adapter-browserify.js: -------------------------------------------------------------------------------- 1 | var Adapter, Opentip, 2 | __hasProp = {}.hasOwnProperty, 3 | __slice = [].slice; 4 | 5 | Opentip = require('./opentip'); 6 | 7 | Adapter = (function() { 8 | var dataValues, lastDataId; 9 | 10 | function Adapter() {} 11 | 12 | Adapter.prototype.name = "browserify"; 13 | 14 | Adapter.prototype.domReady = function(callback) { 15 | var add, doc, done, init, poll, pre, rem, root, top, win, _ref; 16 | done = false; 17 | top = true; 18 | win = window; 19 | doc = document; 20 | if ((_ref = doc.readyState) === "complete" || _ref === "loaded") { 21 | return callback(); 22 | } 23 | root = doc.documentElement; 24 | add = (doc.addEventListener ? "addEventListener" : "attachEvent"); 25 | rem = (doc.addEventListener ? "removeEventListener" : "detachEvent"); 26 | pre = (doc.addEventListener ? "" : "on"); 27 | init = function(e) { 28 | if (e.type === "readystatechange" && doc.readyState !== "complete") { 29 | return; 30 | } 31 | (e.type === "load" ? win : doc)[rem](pre + e.type, init, false); 32 | if (!done) { 33 | done = true; 34 | return callback(); 35 | } 36 | }; 37 | poll = function() { 38 | var e; 39 | try { 40 | root.doScroll("left"); 41 | } catch (_error) { 42 | e = _error; 43 | setTimeout(poll, 50); 44 | return; 45 | } 46 | return init("poll"); 47 | }; 48 | if (doc.readyState !== "complete") { 49 | if (doc.createEventObject && root.doScroll) { 50 | try { 51 | top = !win.frameElement; 52 | } catch (_error) {} 53 | if (top) { 54 | poll(); 55 | } 56 | } 57 | doc[add](pre + "DOMContentLoaded", init, false); 58 | doc[add](pre + "readystatechange", init, false); 59 | return win[add](pre + "load", init, false); 60 | } 61 | }; 62 | 63 | Adapter.prototype.create = function(htmlString) { 64 | var div; 65 | div = document.createElement("div"); 66 | div.innerHTML = htmlString; 67 | return this.wrap(div.childNodes); 68 | }; 69 | 70 | Adapter.prototype.wrap = function(element) { 71 | var el; 72 | if (!element) { 73 | element = []; 74 | } else if (typeof element === "string") { 75 | element = this.find(document.body, element); 76 | element = element ? [element] : []; 77 | } else if (element instanceof NodeList) { 78 | element = (function() { 79 | var _i, _len, _results; 80 | _results = []; 81 | for (_i = 0, _len = element.length; _i < _len; _i++) { 82 | el = element[_i]; 83 | _results.push(el); 84 | } 85 | return _results; 86 | })(); 87 | } else if (!(element instanceof Array)) { 88 | element = [element]; 89 | } 90 | return element; 91 | }; 92 | 93 | Adapter.prototype.unwrap = function(element) { 94 | return this.wrap(element)[0]; 95 | }; 96 | 97 | Adapter.prototype.tagName = function(element) { 98 | return this.unwrap(element).tagName; 99 | }; 100 | 101 | Adapter.prototype.attr = function(element, attr, value) { 102 | if (arguments.length === 3) { 103 | return this.unwrap(element).setAttribute(attr, value); 104 | } else { 105 | return this.unwrap(element).getAttribute(attr); 106 | } 107 | }; 108 | 109 | lastDataId = 0; 110 | 111 | dataValues = {}; 112 | 113 | Adapter.prototype.data = function(element, name, value) { 114 | var dataId; 115 | dataId = this.attr(element, "data-id"); 116 | if (!dataId) { 117 | dataId = ++lastDataId; 118 | this.attr(element, "data-id", dataId); 119 | dataValues[dataId] = {}; 120 | } 121 | if (arguments.length === 3) { 122 | return dataValues[dataId][name] = value; 123 | } else { 124 | value = dataValues[dataId][name]; 125 | if (value != null) { 126 | return value; 127 | } 128 | value = this.attr(element, "data-" + (Opentip.prototype.dasherize(name))); 129 | if (value) { 130 | dataValues[dataId][name] = value; 131 | } 132 | return value; 133 | } 134 | }; 135 | 136 | Adapter.prototype.find = function(element, selector) { 137 | return this.unwrap(element).querySelector(selector); 138 | }; 139 | 140 | Adapter.prototype.findAll = function(element, selector) { 141 | return this.unwrap(element).querySelectorAll(selector); 142 | }; 143 | 144 | Adapter.prototype.update = function(element, content, escape) { 145 | element = this.unwrap(element); 146 | if (escape) { 147 | element.innerHTML = ""; 148 | return element.appendChild(document.createTextNode(content)); 149 | } else { 150 | return element.innerHTML = content; 151 | } 152 | }; 153 | 154 | Adapter.prototype.append = function(element, child) { 155 | var unwrappedChild, unwrappedElement; 156 | unwrappedChild = this.unwrap(child); 157 | unwrappedElement = this.unwrap(element); 158 | return unwrappedElement.appendChild(unwrappedChild); 159 | }; 160 | 161 | Adapter.prototype.remove = function(element) { 162 | var parentNode; 163 | element = this.unwrap(element); 164 | parentNode = element.parentNode; 165 | if (parentNode != null) { 166 | return parentNode.removeChild(element); 167 | } 168 | }; 169 | 170 | Adapter.prototype.addClass = function(element, className) { 171 | return this.unwrap(element).classList.add(className); 172 | }; 173 | 174 | Adapter.prototype.removeClass = function(element, className) { 175 | return this.unwrap(element).classList.remove(className); 176 | }; 177 | 178 | Adapter.prototype.css = function(element, properties) { 179 | var key, value, _results; 180 | element = this.unwrap(this.wrap(element)); 181 | _results = []; 182 | for (key in properties) { 183 | if (!__hasProp.call(properties, key)) continue; 184 | value = properties[key]; 185 | _results.push(element.style[key] = value); 186 | } 187 | return _results; 188 | }; 189 | 190 | Adapter.prototype.dimensions = function(element) { 191 | var dimensions, revert; 192 | element = this.unwrap(this.wrap(element)); 193 | dimensions = { 194 | width: element.offsetWidth, 195 | height: element.offsetHeight 196 | }; 197 | if (!(dimensions.width && dimensions.height)) { 198 | revert = { 199 | position: element.style.position || '', 200 | visibility: element.style.visibility || '', 201 | display: element.style.display || '' 202 | }; 203 | this.css(element, { 204 | position: "absolute", 205 | visibility: "hidden", 206 | display: "block" 207 | }); 208 | dimensions = { 209 | width: element.offsetWidth, 210 | height: element.offsetHeight 211 | }; 212 | this.css(element, revert); 213 | } 214 | return dimensions; 215 | }; 216 | 217 | Adapter.prototype.scrollOffset = function() { 218 | return [window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop]; 219 | }; 220 | 221 | Adapter.prototype.viewportDimensions = function() { 222 | return { 223 | width: document.documentElement.clientWidth, 224 | height: document.documentElement.clientHeight 225 | }; 226 | }; 227 | 228 | Adapter.prototype.mousePosition = function(e) { 229 | var pos; 230 | pos = { 231 | x: 0, 232 | y: 0 233 | }; 234 | if (e == null) { 235 | e = window.event; 236 | } 237 | if (e == null) { 238 | return; 239 | } 240 | try { 241 | if (e.pageX || e.pageY) { 242 | pos.x = e.pageX; 243 | pos.y = e.pageY; 244 | } else if (e.clientX || e.clientY) { 245 | pos.x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; 246 | pos.y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop; 247 | } 248 | } catch (_error) { 249 | e = _error; 250 | } 251 | return pos; 252 | }; 253 | 254 | Adapter.prototype.offset = function(element) { 255 | var offset; 256 | element = this.unwrap(element); 257 | offset = { 258 | top: element.offsetTop, 259 | left: element.offsetLeft 260 | }; 261 | while (element = element.offsetParent) { 262 | offset.top += element.offsetTop; 263 | offset.left += element.offsetLeft; 264 | if (element !== document.body) { 265 | offset.top -= element.scrollTop; 266 | offset.left -= element.scrollLeft; 267 | } 268 | } 269 | return offset; 270 | }; 271 | 272 | Adapter.prototype.observe = function(element, eventName, observer) { 273 | return this.unwrap(element).addEventListener(eventName, observer, false); 274 | }; 275 | 276 | Adapter.prototype.stopObserving = function(element, eventName, observer) { 277 | return this.unwrap(element).removeEventListener(eventName, observer, false); 278 | }; 279 | 280 | Adapter.prototype.ajax = function(options) { 281 | var e, request, _ref, _ref1; 282 | if (options.url == null) { 283 | throw new Error("No url provided"); 284 | } 285 | if (window.XMLHttpRequest) { 286 | request = new XMLHttpRequest; 287 | } else if (window.ActiveXObject) { 288 | try { 289 | request = new ActiveXObject("Msxml2.XMLHTTP"); 290 | } catch (_error) { 291 | e = _error; 292 | try { 293 | request = new ActiveXObject("Microsoft.XMLHTTP"); 294 | } catch (_error) { 295 | e = _error; 296 | } 297 | } 298 | } 299 | if (!request) { 300 | throw new Error("Can't create XMLHttpRequest"); 301 | } 302 | request.onreadystatechange = function() { 303 | if (request.readyState === 4) { 304 | try { 305 | if (request.status === 200) { 306 | if (typeof options.onSuccess === "function") { 307 | options.onSuccess(request.responseText); 308 | } 309 | } else { 310 | if (typeof options.onError === "function") { 311 | options.onError("Server responded with status " + request.status); 312 | } 313 | } 314 | } catch (_error) { 315 | e = _error; 316 | if (typeof options.onError === "function") { 317 | options.onError(e.message); 318 | } 319 | } 320 | return typeof options.onComplete === "function" ? options.onComplete() : void 0; 321 | } 322 | }; 323 | request.open((_ref = (_ref1 = options.method) != null ? _ref1.toUpperCase() : void 0) != null ? _ref : "GET", options.url); 324 | return request.send(); 325 | }; 326 | 327 | Adapter.prototype.clone = function(object) { 328 | var key, newObject, val; 329 | newObject = {}; 330 | for (key in object) { 331 | if (!__hasProp.call(object, key)) continue; 332 | val = object[key]; 333 | newObject[key] = val; 334 | } 335 | return newObject; 336 | }; 337 | 338 | Adapter.prototype.extend = function() { 339 | var key, source, sources, target, val, _i, _len; 340 | target = arguments[0], sources = 2 <= arguments.length ? __slice.call(arguments, 1) : []; 341 | for (_i = 0, _len = sources.length; _i < _len; _i++) { 342 | source = sources[_i]; 343 | for (key in source) { 344 | if (!__hasProp.call(source, key)) continue; 345 | val = source[key]; 346 | target[key] = val; 347 | } 348 | } 349 | return target; 350 | }; 351 | 352 | return Adapter; 353 | 354 | })(); 355 | 356 | Opentip.addAdapter(new Adapter); 357 | 358 | module.exports = Opentip; 359 | -------------------------------------------------------------------------------- /test/src/030-opentip-show.coffee: -------------------------------------------------------------------------------- 1 | 2 | $ = jQuery 3 | 4 | describe "Opentip - Appearing", -> 5 | adapter = Opentip.adapter 6 | opentip = null 7 | triggerElementExists = yes 8 | 9 | beforeEach -> 10 | triggerElementExists = yes 11 | 12 | afterEach -> 13 | if opentip 14 | opentip[prop]?.restore?() for own prop of opentip 15 | opentip.deactivate() 16 | 17 | $(".opentip-container").remove() 18 | 19 | describe "prepareToShow()", -> 20 | beforeEach -> 21 | triggerElementExists = no 22 | opentip = new Opentip adapter.create("
"), "Test", delay: 0 23 | sinon.stub opentip, "_triggerElementExists", -> triggerElementExists 24 | 25 | it "should call _setup if no container yet", -> 26 | sinon.spy opentip, "_setup" 27 | opentip.prepareToShow() 28 | opentip.prepareToShow() 29 | expect(opentip._setup.calledOnce).to.be.ok() 30 | 31 | it "should always abort a hiding process", -> 32 | sinon.stub opentip, "_abortHiding" 33 | opentip.prepareToShow() 34 | expect(opentip._abortHiding.callCount).to.be 1 35 | 36 | it "even when aborting because it's already visible", -> 37 | sinon.stub opentip, "_abortHiding" 38 | opentip.visible = yes 39 | opentip.prepareToShow() 40 | expect(opentip._abortHiding.callCount).to.be 1 41 | 42 | it "should abort when already visible", -> 43 | expect(opentip.preparingToShow).to.not.be.ok() 44 | opentip.visible = yes 45 | opentip.prepareToShow() 46 | expect(opentip.preparingToShow).to.not.be.ok() 47 | opentip.visible = no 48 | opentip.prepareToShow() 49 | expect(opentip.preparingToShow).to.be.ok() 50 | 51 | it "should log that it's preparing to show", -> 52 | sinon.stub opentip, "debug" 53 | opentip.prepareToShow() 54 | expect(opentip.debug.callCount).to.be 2 55 | expect(opentip.debug.getCall(0).args[0]).to.be "Showing in 0s." 56 | expect(opentip.debug.getCall(1).args[0]).to.be "Setting up the tooltip." 57 | 58 | it "should setup observers for 'showing'", -> 59 | sinon.stub opentip, "_setupObservers" 60 | opentip.prepareToShow() 61 | expect(opentip._setupObservers.callCount).to.be 1 62 | expect(opentip._setupObservers.getCall(0).args[0]).to.be "-hidden" 63 | expect(opentip._setupObservers.getCall(0).args[1]).to.be "-hiding" 64 | expect(opentip._setupObservers.getCall(0).args[2]).to.be "showing" 65 | 66 | it "should start following mouseposition", -> 67 | sinon.stub opentip, "_followMousePosition" 68 | opentip.prepareToShow() 69 | expect(opentip._followMousePosition.callCount).to.be 1 70 | 71 | it "should reposition itself «On se redresse!»", -> 72 | sinon.stub opentip, "reposition" 73 | opentip.prepareToShow() 74 | expect(opentip.reposition.callCount).to.be 1 75 | 76 | it "should call show() after the specified delay (50ms)", (done) -> 77 | opentip.options.delay = 0.05 78 | sinon.stub opentip, "show", -> done() 79 | opentip.prepareToShow() 80 | 81 | describe "show()", -> 82 | beforeEach -> 83 | opentip = new Opentip adapter.create("
"), "Test", delay: 0 84 | sinon.stub opentip, "_triggerElementExists", -> triggerElementExists 85 | 86 | it "should call _setup if no container yet", -> 87 | sinon.spy opentip, "_setup" 88 | opentip.show() 89 | opentip.show() 90 | expect(opentip._setup.calledOnce).to.be.ok() 91 | 92 | it "should clear all timeouts", -> 93 | triggerElementExists = no 94 | sinon.stub opentip, "_clearTimeouts" 95 | opentip.show() 96 | expect(opentip._clearTimeouts.callCount).to.be.above 0 97 | it "should not clear all timeouts when already visible", -> 98 | triggerElementExists = no 99 | sinon.stub opentip, "_clearTimeouts" 100 | opentip.visible = yes 101 | opentip.show() 102 | expect(opentip._clearTimeouts.callCount).to.be 0 103 | 104 | it "should abort if already visible", -> 105 | triggerElementExists = no 106 | sinon.stub opentip, "debug" 107 | opentip.visible = yes 108 | opentip.show() 109 | expect(opentip.debug.callCount).to.be 0 110 | 111 | it "should log that it's showing", -> 112 | sinon.stub opentip, "debug" 113 | opentip.show() 114 | expect(opentip.debug.callCount).to.be.above 1 115 | expect(opentip.debug.args[0][0]).to.be "Showing now." 116 | 117 | it "should set visible to true and preparingToShow to false", -> 118 | opentip.preparingToShow = yes 119 | opentip.show() 120 | expect(opentip.visible).to.be.ok() 121 | expect(opentip.preparingToShow).to.not.be.ok() 122 | 123 | it "should use _ensureViewportContaintment if options.containInViewport", -> 124 | sinon.spy opentip, "_ensureViewportContainment" 125 | sinon.stub opentip, "getPosition", -> x: 0, y: 0 126 | opentip.show() 127 | expect(opentip._ensureViewportContainment.callCount).to.be.above 1 128 | 129 | it "should not use _ensureViewportContaintment if !options.containInViewport", -> 130 | opentip.options.containInViewport = no 131 | sinon.stub opentip, "_ensureViewportContainment" 132 | opentip.show() 133 | expect(opentip._ensureViewportContainment.callCount).to.be 0 134 | 135 | describe "grouped Opentips", -> 136 | it "should hide all other opentips", -> 137 | Opentip.tips = [ ] 138 | opentip = new Opentip adapter.create("
"), "Test", { delay: 0, group: "test" } 139 | opentip2 = new Opentip adapter.create("
"), "Test", { delay: 0, group: "test" } 140 | opentip3 = new Opentip adapter.create("
"), "Test", { delay: 0, group: "test" } 141 | sinon.stub opentip, "_triggerElementExists", -> triggerElementExists 142 | sinon.stub opentip2, "_triggerElementExists", -> triggerElementExists 143 | sinon.stub opentip3, "_triggerElementExists", -> triggerElementExists 144 | 145 | opentip.show() 146 | expect(opentip.visible).to.be.ok() 147 | expect(opentip2.visible).to.not.be.ok() 148 | expect(opentip3.visible).to.not.be.ok() 149 | opentip2.show() 150 | expect(opentip.visible).to.not.be.ok() 151 | expect(opentip2.visible).to.be.ok() 152 | expect(opentip3.visible).to.not.be.ok() 153 | opentip3.show() 154 | expect(opentip.visible).to.not.be.ok() 155 | expect(opentip2.visible).to.not.be.ok() 156 | expect(opentip3.visible).to.be.ok() 157 | 158 | opentip.deactivate() 159 | opentip2.deactivate() 160 | opentip3.deactivate() 161 | 162 | it "should abort showing other opentips", -> 163 | Opentip.tips = [ ] 164 | opentip = new Opentip adapter.create("
"), "Test", { delay: 1000, group: "test" } 165 | opentip2 = new Opentip adapter.create("
"), "Test", { delay: 1000, group: "test" } 166 | opentip3 = new Opentip adapter.create("
"), "Test", { delay: 1000, group: "test" } 167 | sinon.stub opentip, "_triggerElementExists", -> triggerElementExists 168 | sinon.stub opentip2, "_triggerElementExists", -> triggerElementExists 169 | sinon.stub opentip3, "_triggerElementExists", -> triggerElementExists 170 | 171 | opentip.prepareToShow() 172 | expect(opentip.visible).to.not.be.ok() 173 | expect(opentip.preparingToShow).to.be.ok() 174 | expect(opentip2.visible).to.not.be.ok() 175 | expect(opentip2.preparingToShow).to.not.be.ok() 176 | 177 | opentip2.prepareToShow() 178 | expect(opentip.visible).to.not.be.ok() 179 | expect(opentip.preparingToShow).to.not.be.ok() 180 | expect(opentip2.visible).to.not.be.ok() 181 | expect(opentip2.preparingToShow).to.be.ok() 182 | 183 | opentip3.show() 184 | expect(opentip.visible).to.not.be.ok() 185 | expect(opentip.preparingToShow).to.not.be.ok() 186 | expect(opentip2.visible).to.not.be.ok() 187 | expect(opentip2.preparingToShow).to.not.be.ok() 188 | 189 | opentip.deactivate() 190 | opentip2.deactivate() 191 | opentip3.deactivate() 192 | 193 | describe "events", -> 194 | 195 | element = "" 196 | 197 | beforeEach -> 198 | element = document.createElement "div" 199 | 200 | testEvent = (opentip, event, done) -> 201 | expect(opentip.visible).to.not.be.ok() 202 | 203 | Test.triggerEvent element, event 204 | 205 | expect(opentip.preparingToShow).to.be.ok() 206 | expect(opentip.visible).to.not.be.ok() 207 | setTimeout -> 208 | try 209 | expect(opentip.visible).to.be.ok() 210 | done() 211 | catch e 212 | done e 213 | , 2 214 | 215 | for event in [ "click", "mouseover", "focus" ] 216 | it "should show on #{event}", (done) -> 217 | opentip = new Opentip element, "test", delay: 0, showOn: event 218 | sinon.stub opentip, "_triggerElementExists", -> triggerElementExists 219 | testEvent opentip, event, done 220 | 221 | describe "hide", -> 222 | it "should remove HTML elements if removeElementsOnHide: true", (done) -> 223 | opentip = new Opentip adapter.create("
"), "Test", { delay: 0, removeElementsOnHide: yes, hideEffectDuration: 0, hideDelay: 0 } 224 | sinon.stub opentip, "_triggerElementExists", -> yes 225 | opentip.show() 226 | expect($("#opentip-#{opentip.id}").length).to.be 1 227 | opentip.hide() 228 | 229 | setTimeout (-> 230 | expect($("#opentip-#{opentip.id}").length).to.be 0 231 | opentip.show() 232 | expect($("#opentip-#{opentip.id}").length).to.be 1 233 | opentip = null 234 | done() 235 | ), 100 236 | 237 | describe "visible", -> 238 | $element = null 239 | element = null 240 | span = null 241 | beforeEach -> 242 | $element = $ "
" 243 | span = $element.find "span" 244 | element = $element[0] 245 | 246 | 247 | # it "should not hide when hovering child elements and hideOn == mouseout", (done) -> 248 | # opentip = new Opentip element, "test", 249 | # delay: 0 250 | # hideDelay: 0 251 | # showOn: "click" 252 | # hideOn: "mouseout" 253 | 254 | # sinon.stub opentip, "_triggerElementExists", -> triggerElementExists 255 | 256 | # expect(opentip.visible).to.not.be.ok() 257 | # enderElement.trigger "click" 258 | # expect(opentip.preparingToShow).to.be.ok() 259 | # expect(opentip.visible).to.not.be.ok() 260 | # setTimeout -> 261 | # try 262 | # expect(opentip.visible).to.be.ok() 263 | # enderElement.trigger "mouseout" 264 | # enderElement.trigger "mouseover" 265 | # setTimeout -> 266 | # try 267 | # expect(opentip.visible).to.be.ok() 268 | # catch e 269 | # done e 270 | # , 4 271 | # done() 272 | # catch e 273 | # done e 274 | # , 4 275 | 276 | it "should activate all hide buttons", (done) -> 277 | closeClass = Opentip::class.close 278 | opentip = new Opentip element, """close""", 279 | escape: no 280 | delay: 0 281 | hideDelay: 0 282 | hideTrigger: "closeButton" 283 | hideOn: "click" 284 | 285 | sinon.stub opentip, "_triggerElementExists", -> triggerElementExists 286 | sinon.stub opentip, "prepareToHide" 287 | 288 | opentip.prepareToShow() 289 | 290 | 291 | setTimeout -> 292 | try 293 | closeButtons = $(opentip.container).find(".#{closeClass}") 294 | expect(closeButtons.length).to.be 2 # The close button created by opentip and the one in content 295 | 296 | expect(opentip.prepareToHide.callCount).to.be 0 297 | catch e 298 | done e 299 | return 300 | 301 | Test.triggerEvent closeButtons[0], "click" 302 | 303 | setTimeout -> 304 | try 305 | expect(opentip.prepareToHide.callCount).to.be 1 306 | done() 307 | catch e 308 | done e 309 | , 4 310 | 4 311 | 312 | 313 | 314 | -------------------------------------------------------------------------------- /test/src/010-opentip.coffee: -------------------------------------------------------------------------------- 1 | 2 | $ = jQuery 3 | 4 | describe "Opentip", -> 5 | adapter = null 6 | beforeEach -> 7 | adapter = Opentip.adapter 8 | 9 | afterEach -> 10 | elements = $(".opentip-container") 11 | elements.remove() 12 | 13 | 14 | describe "constructor()", -> 15 | before -> 16 | sinon.stub Opentip::, "_setup" 17 | after -> 18 | Opentip::_setup.restore() 19 | it "arguments should be optional", -> 20 | element = adapter.create "
" 21 | opentip = new Opentip element, "content" 22 | expect(opentip.content).to.equal "content" 23 | expect(adapter.unwrap(opentip.triggerElement)).to.equal adapter.unwrap element 24 | 25 | opentip = new Opentip element, "content", "title", { hideOn: "click" } 26 | expect(opentip.content).to.equal "content" 27 | expect(adapter.unwrap opentip.triggerElement).to.equal adapter.unwrap element 28 | expect(opentip.options.hideOn).to.equal "click" 29 | expect(opentip.options.title).to.equal "title" 30 | 31 | opentip = new Opentip element, { hideOn: "click" } 32 | expect(adapter.unwrap opentip.triggerElement).to.equal adapter.unwrap element 33 | expect(opentip.options.hideOn).to.equal "click" 34 | expect(opentip.content).to.equal "" 35 | expect(opentip.options.title).to.equal undefined 36 | 37 | it "should always use the next tip id", -> 38 | element = document.createElement "div" 39 | Opentip.lastId = 0 40 | opentip = new Opentip element, "Test" 41 | opentip2 = new Opentip element, "Test" 42 | opentip3 = new Opentip element, "Test" 43 | expect(opentip.id).to.be 1 44 | expect(opentip2.id).to.be 2 45 | expect(opentip3.id).to.be 3 46 | 47 | it "should use the href attribute if AJAX and an A element", -> 48 | element = $("""link""")[0] 49 | opentip = new Opentip element, ajax: on 50 | expect(opentip.options.ajax).to.equal "http://testlink" 51 | 52 | it "should disable AJAX if neither URL or a link HREF is provided", -> 53 | element = $("""
text
""")[0] 54 | opentip = new Opentip element, ajax: on 55 | expect(opentip.options.ajax).to.be false 56 | 57 | it "should disable a link if the event is onClick", -> 58 | sinon.stub adapter, "observe" 59 | element = $("""link""")[0] 60 | sinon.stub Opentip::, "_setupObservers" 61 | opentip = new Opentip element, showOn: "click" 62 | 63 | 64 | expect(adapter.observe.calledOnce).to.be.ok() 65 | expect(adapter.observe.getCall(0).args[1]).to.equal "click" 66 | 67 | 68 | Opentip::_setupObservers.restore() 69 | adapter.observe.restore() 70 | 71 | it "should take all options from selected style", -> 72 | element = document.createElement "div" 73 | opentip = new Opentip element, style: "glass", showOn: "click" 74 | 75 | # Should have been set by the options 76 | expect(opentip.options.showOn).to.equal "click" 77 | # Should have been set by the glass theme 78 | expect(opentip.options.className).to.equal "glass" 79 | # Should have been set by the standard theme 80 | expect(opentip.options.stemLength).to.equal 5 81 | 82 | it "the property 'style' should be handled the same as 'extends'", -> 83 | element = document.createElement "div" 84 | opentip = new Opentip element, extends: "glass", showOn: "click" 85 | 86 | # Should have been set by the options 87 | expect(opentip.options.showOn).to.equal "click" 88 | # Should have been set by the glass theme 89 | expect(opentip.options.className).to.equal "glass" 90 | # Should have been set by the standard theme 91 | expect(opentip.options.stemLength).to.equal 5 92 | 93 | it "chaining incorrect styles should throw an exception", -> 94 | element = document.createElement "div" 95 | expect(-> new Opentip element, { extends: "invalidstyle" }).to.throwException /Invalid style\: invalidstyle/ 96 | 97 | it "chaining styles should work", -> 98 | element = document.createElement "div" 99 | 100 | Opentip.styles.test1 = stemLength: 40 101 | Opentip.styles.test2 = extends: "test1", title: "overwritten title" 102 | Opentip.styles.test3 = extends: "test2", className: "test5", title: "some title" 103 | 104 | opentip = new Opentip element, { extends: "test3", stemBase: 20 } 105 | 106 | expect(opentip.options.className).to.equal "test5" 107 | expect(opentip.options.title).to.equal "some title" 108 | expect(opentip.options.stemLength).to.equal 40 109 | expect(opentip.options.stemBase).to.equal 20 110 | 111 | it "should set the options to fixed if a target is provided", -> 112 | element = document.createElement "div" 113 | opentip = new Opentip element, target: yes, fixed: no 114 | expect(opentip.options.fixed).to.be.ok() 115 | 116 | it "should use provided stem", -> 117 | element = document.createElement "div" 118 | opentip = new Opentip element, stem: "bottom", tipJoin: "topLeft" 119 | expect(opentip.options.stem.toString()).to.eql "bottom" 120 | 121 | it "should take the tipJoint as stem if stem is just true", -> 122 | element = document.createElement "div" 123 | opentip = new Opentip element, stem: yes, tipJoint: "top left" 124 | expect(opentip.options.stem.toString()).to.eql "top left" 125 | 126 | it "should use provided target", -> 127 | element = adapter.create "
" 128 | element2 = adapter.create "
" 129 | opentip = new Opentip element, target: element2 130 | expect(adapter.unwrap opentip.options.target).to.equal adapter.unwrap element2 131 | 132 | it "should take the triggerElement as target if target is just true", -> 133 | element = adapter.create "
" 134 | opentip = new Opentip element, target: yes 135 | expect(adapter.unwrap opentip.options.target).to.equal adapter.unwrap element 136 | 137 | it "currentStemPosition should be set to inital stemPosition", -> 138 | element = adapter.create "
" 139 | opentip = new Opentip element, stem: "topLeft" 140 | expect(opentip.currentStem.toString()).to.eql "top left" 141 | 142 | it "delay should be automatically set if none provided", -> 143 | element = document.createElement "div" 144 | opentip = new Opentip element, delay: null, showOn: "click" 145 | expect(opentip.options.delay).to.equal 0 146 | opentip = new Opentip element, delay: null, showOn: "mouseover" 147 | expect(opentip.options.delay).to.equal 0.2 148 | 149 | it "the targetJoint should be the inverse of the tipJoint if none provided", -> 150 | element = document.createElement "div" 151 | opentip = new Opentip element, tipJoint: "left" 152 | expect(opentip.options.targetJoint.toString()).to.eql "right" 153 | opentip = new Opentip element, tipJoint: "top" 154 | expect(opentip.options.targetJoint.toString()).to.eql "bottom" 155 | opentip = new Opentip element, tipJoint: "bottom right" 156 | expect(opentip.options.targetJoint.toString()).to.eql "top left" 157 | 158 | 159 | it "should setup all trigger elements", -> 160 | element = adapter.create "
" 161 | opentip = new Opentip element, showOn: "click" 162 | expect(opentip.showTriggers[0].event).to.eql "click" 163 | expect(adapter.unwrap opentip.showTriggers[0].element).to.equal adapter.unwrap element 164 | expect(opentip.showTriggersWhenVisible).to.eql [ ] 165 | expect(opentip.hideTriggers).to.eql [ ] 166 | opentip = new Opentip element, showOn: "creation" 167 | expect(opentip.showTriggers).to.eql [ ] 168 | expect(opentip.showTriggersWhenVisible).to.eql [ ] 169 | expect(opentip.hideTriggers).to.eql [ ] 170 | 171 | it "should copy options.hideTrigger onto options.hideTriggers", -> 172 | element = adapter.create "
" 173 | opentip = new Opentip element, hideTrigger: "closeButton", hideTriggers: [ ] 174 | expect(opentip.options.hideTriggers).to.eql [ "closeButton"] 175 | 176 | it "should NOT copy options.hideTrigger onto options.hideTriggers when hideTriggers are set", -> 177 | element = adapter.create "
" 178 | opentip = new Opentip element, hideTrigger: "closeButton", hideTriggers: [ "tip", "trigger" ] 179 | expect(opentip.options.hideTriggers).to.eql [ "tip", "trigger" ] 180 | 181 | it "should attach itself to the elements `data-opentips` property", -> 182 | element = $("
")[0] 183 | expect(adapter.data element, "opentips").to.not.be.ok() 184 | opentip = new Opentip element 185 | expect(adapter.data element, "opentips").to.eql [ opentip ] 186 | opentip2 = new Opentip element 187 | opentip3 = new Opentip element 188 | expect(adapter.data element, "opentips").to.eql [ opentip, opentip2, opentip3 ] 189 | 190 | it "should add itself to the Opentip.tips list", -> 191 | element = $("
")[0] 192 | Opentip.tips = [ ] 193 | opentip1 = new Opentip element 194 | opentip2 = new Opentip element 195 | expect(Opentip.tips.length).to.equal 2 196 | expect(Opentip.tips[0]).to.equal opentip1 197 | expect(Opentip.tips[1]).to.equal opentip2 198 | 199 | it "should rename ajaxCache to cache for backwards compatibility", -> 200 | element = $("
")[0] 201 | opentip1 = new Opentip element, ajaxCache: off 202 | opentip2 = new Opentip element, ajaxCache: on 203 | 204 | expect(opentip1.options.ajaxCache == opentip2.options.ajaxCache == undefined).to.be.ok() 205 | expect(opentip1.options.cache).to.not.be.ok() 206 | expect(opentip2.options.cache).to.be.ok() 207 | 208 | describe "init()", -> 209 | describe "showOn == creation", -> 210 | element = document.createElement "div" 211 | beforeEach -> sinon.stub Opentip::, "prepareToShow" 212 | afterEach -> Opentip::prepareToShow.restore() 213 | it "should immediately call prepareToShow()", -> 214 | opentip = new Opentip element, showOn: "creation" 215 | expect(opentip.prepareToShow.callCount).to.equal 1 216 | 217 | describe "setContent()", -> 218 | it "should update the content if tooltip currently visible", -> 219 | element = document.createElement "div" 220 | opentip = new Opentip element, showOn: "click" 221 | sinon.stub opentip, "_updateElementContent" 222 | opentip.visible = no 223 | opentip.setContent "TEST" 224 | expect(opentip.content).to.equal "TEST" 225 | opentip.visible = yes 226 | opentip.setContent "TEST2" 227 | expect(opentip.content).to.equal "TEST2" 228 | expect(opentip._updateElementContent.callCount).to.equal 1 229 | opentip._updateElementContent.restore() 230 | 231 | it "should not set the content directly if function", -> 232 | element = document.createElement "div" 233 | opentip = new Opentip element, showOn: "click" 234 | sinon.stub opentip, "_updateElementContent" 235 | opentip.setContent -> "TEST" 236 | expect(opentip.content).to.equal "" 237 | 238 | 239 | describe "_updateElementContent()", -> 240 | 241 | it "should escape the content if @options.escapeContent", -> 242 | element = document.createElement "div" 243 | opentip = new Opentip element, "
", escapeContent: yes 244 | sinon.stub opentip, "_triggerElementExists", -> yes 245 | opentip.show() 246 | expect($(opentip.container).find(".ot-content").html()).to.be """<div><span></span></div>""" 247 | 248 | it "should not escape the content if not @options.escapeContent", -> 249 | element = document.createElement "div" 250 | opentip = new Opentip element, "
", escapeContent: no 251 | sinon.stub opentip, "_triggerElementExists", -> yes 252 | opentip.show() 253 | expect($(opentip.container).find(".ot-content > div > span").length).to.be 1 254 | 255 | it "should storeAndLock dimensions and reposition the element", -> 256 | element = document.createElement "div" 257 | opentip = new Opentip element, showOn: "click" 258 | sinon.stub opentip, "_storeAndLockDimensions" 259 | sinon.stub opentip, "reposition" 260 | opentip.visible = yes 261 | opentip._updateElementContent() 262 | expect(opentip._storeAndLockDimensions.callCount).to.equal 1 263 | expect(opentip.reposition.callCount).to.equal 1 264 | 265 | it "should execute the content function", -> 266 | element = document.createElement "div" 267 | opentip = new Opentip element, showOn: "click" 268 | sinon.stub opentip.adapter, "find", -> "element" 269 | opentip.visible = yes 270 | opentip.setContent -> "BLA TEST" 271 | expect(opentip.content).to.be "BLA TEST" 272 | opentip.adapter.find.restore() 273 | 274 | it "should only execute the content function once if cache:true", -> 275 | element = document.createElement "div" 276 | opentip = new Opentip element, showOn: "click", cache: yes 277 | sinon.stub opentip.adapter, "find", -> "element" 278 | opentip.visible = yes 279 | counter = 0 280 | opentip.setContent -> "count#{counter++}" 281 | expect(opentip.content).to.be "count0" 282 | opentip._updateElementContent() 283 | opentip._updateElementContent() 284 | expect(opentip.content).to.be "count0" 285 | opentip.adapter.find.restore() 286 | 287 | it "should execute the content function multiple times if cache:false", -> 288 | element = document.createElement "div" 289 | opentip = new Opentip element, showOn: "click", cache: no 290 | sinon.stub opentip.adapter, "find", -> "element" 291 | opentip.visible = yes 292 | counter = 0 293 | opentip.setContent -> "count#{counter++}" 294 | expect(opentip.content).to.be "count0" 295 | opentip._updateElementContent() 296 | opentip._updateElementContent() 297 | expect(opentip.content).to.be "count2" 298 | opentip.adapter.find.restore() 299 | 300 | it "should only update the HTML elements if the content has been changed", -> 301 | element = document.createElement "div" 302 | opentip = new Opentip element, showOn: "click" 303 | sinon.stub opentip.adapter, "find", -> "element" 304 | sinon.stub opentip.adapter, "update", -> 305 | opentip.visible = yes 306 | 307 | opentip.setContent "TEST" 308 | expect(opentip.adapter.update.callCount).to.be 1 309 | opentip._updateElementContent() 310 | opentip._updateElementContent() 311 | expect(opentip.adapter.update.callCount).to.be 1 312 | opentip.setContent "TEST2" 313 | expect(opentip.adapter.update.callCount).to.be 2 314 | opentip.adapter.find.restore() 315 | opentip.adapter.update.restore() 316 | 317 | 318 | describe "_buildContainer()", -> 319 | element = document.createElement "div" 320 | opentip = null 321 | beforeEach -> 322 | opentip = new Opentip element, 323 | style: "glass" 324 | showEffect: "appear" 325 | hideEffect: "fade" 326 | opentip._setup() 327 | 328 | it "should set the id", -> 329 | expect(adapter.attr opentip.container, "id").to.equal "opentip-" + opentip.id 330 | it "should set the classes", -> 331 | enderElement = $ adapter.unwrap opentip.container 332 | expect(enderElement.hasClass "opentip-container").to.be.ok() 333 | expect(enderElement.hasClass "ot-hidden").to.be.ok() 334 | expect(enderElement.hasClass "style-glass").to.be.ok() 335 | expect(enderElement.hasClass "ot-show-effect-appear").to.be.ok() 336 | expect(enderElement.hasClass "ot-hide-effect-fade").to.be.ok() 337 | 338 | describe "_buildElements()", -> 339 | element = opentip = null 340 | 341 | beforeEach -> 342 | element = document.createElement "div" 343 | opentip = new Opentip element, "the content", "the title", hideTrigger: "closeButton", stem: "top left", ajax: "bla" 344 | opentip._setup() 345 | opentip._buildElements() 346 | 347 | it "should add a h1 if title is provided", -> 348 | enderElement = $ adapter.unwrap opentip.container 349 | headerElement = enderElement.find "> .opentip > .ot-header > h1" 350 | expect(headerElement.length).to.be.ok() 351 | expect(headerElement.html()).to.be "the title" 352 | 353 | it "should add a loading indicator if ajax", -> 354 | enderElement = $ adapter.unwrap opentip.container 355 | loadingElement = enderElement.find "> .opentip > .ot-loading-indicator > span" 356 | expect(loadingElement.length).to.be.ok() 357 | expect(loadingElement.html()).to.be "↻" 358 | 359 | it "should add a close button if hideTrigger = close", -> 360 | enderElement = $ adapter.unwrap opentip.container 361 | closeButton = enderElement.find "> .opentip > .ot-header > a.ot-close > span" 362 | expect(closeButton.length).to.be.ok() 363 | expect(closeButton.html()).to.be "Close" 364 | 365 | describe "addAdapter()", -> 366 | it "should set the current adapter, and add the adapter to the list", -> 367 | expect(Opentip.adapters.testa).to.not.be.ok() 368 | testAdapter = { name: "testa" } 369 | Opentip.addAdapter testAdapter 370 | expect(Opentip.adapters.testa).to.equal testAdapter 371 | 372 | it "should use adapter.domReady to call findElements() with it" 373 | 374 | describe "_setupObservers()", -> 375 | it "should never setup the same observers twice" 376 | 377 | describe "_searchAndActivateCloseButtons()", -> 378 | it "should do what it says" 379 | 380 | describe "_activateFirstInput()", -> 381 | it "should do what it says", -> 382 | element = document.createElement "div" 383 | opentip = new Opentip element, "