├── .gitignore ├── .jshintrc ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── _redirects ├── bower.json ├── demos ├── docs.css ├── example-external.html └── index.html ├── dist ├── dialog.build.js ├── dialog.css ├── dialog.linker.build.js ├── dialog.linker.min.js └── dialog.min.js ├── lib ├── shoestring-dev.js ├── xrayhtml.css └── xrayhtml.js ├── package-lock.json ├── package.json ├── src ├── dialog-init.js ├── dialog-linker.js ├── dialog.css └── dialog.js └── test ├── dialog.js ├── history.html ├── history.js ├── index.html ├── lib ├── qunit-1.12.0.css └── qunit-1.12.0.js ├── nohistory.html ├── nohistory.js ├── register.html └── register.js /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | node_modules 3 | bower_components 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "boss": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "eqnull": true, 6 | "expr": true, 7 | "immed": true, 8 | "noarg": true, 9 | "smarttabs": true, 10 | "trailing": true, 11 | "undef": true, 12 | "unused": true, 13 | "node": true, 14 | "loopfunc": true, 15 | "predef": [ "window", "document", "define", "shoestring", "XMLHttpRequest", "ActiveXObject", "Window", "localStorage" ] 16 | } 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to dialog 2 | 3 | Contributions are appreciated. In order for us to consider including a contribution, it does have to meet a few criteria: 4 | 5 | * Code is specific to one issue (eg. feature, extension or bug) 6 | * Code is formatted according to JavaScript Style Guide. 7 | * Code has full test coverage and all tests pass. 8 | 9 | ## Code to an Issue 10 | 11 | Use a separate git branch for each contribution. Give the branch a meaningful name. 12 | When you are contributing a new extensions use the name of this extension, like `dom-toggleclass`. 13 | Otherwise give it a descriptive name like `doc-generator` or reference a specific issue like `issues-12`. 14 | When the issue is resolved create a pull request to allow us to review and accept your contribution. 15 | 16 | ## JavaScript Style Guide 17 | 18 | Code should be formatted according to the [jQuery JavaScript Style Guide](http://contribute.jquery.org/style-guide/). 19 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | "use strict"; 3 | 4 | // Project configuration. 5 | grunt.initConfig({ 6 | qunit: { 7 | files: ['test/**/*.html'] 8 | }, 9 | 10 | jshint: { 11 | all: { 12 | options: { 13 | jshintrc: ".jshintrc" 14 | }, 15 | 16 | src: ['Gruntfile.js', 'src/**/*.js'] 17 | } 18 | }, 19 | 20 | uglify: { 21 | options: { 22 | report: true 23 | }, 24 | min: { 25 | files: { 26 | 'dist/dialog.min.js': ['dist/dialog.build.js'], 27 | 'dist/dialog.linker.min.js': ['dist/dialog.linker.build.js'] 28 | } 29 | } 30 | }, 31 | 32 | concat: { 33 | dist: { 34 | src: ['src/dialog.js', 'src/dialog-init.js'], 35 | dest: 'dist/dialog.build.js' 36 | }, 37 | distLinker: { 38 | src: ['src/dialog.js', 'src/dialog-linker.js', 'src/dialog-init.js'], 39 | dest: 'dist/dialog.linker.build.js' 40 | }, 41 | css: { 42 | src: ['src/dialog.css'], 43 | dest: 'dist/dialog.css' 44 | } 45 | } 46 | }); 47 | 48 | grunt.loadNpmTasks( 'grunt-contrib-concat' ); 49 | grunt.loadNpmTasks( 'grunt-contrib-jshint' ); 50 | grunt.loadNpmTasks( 'grunt-contrib-qunit' ); 51 | grunt.loadNpmTasks( 'grunt-contrib-uglify' ); 52 | 53 | // Default task. 54 | grunt.registerTask('test', ['jshint', 'qunit']); 55 | grunt.registerTask('default', ['test', 'concat', 'uglify']); 56 | grunt.registerTask('travis', ['test']); 57 | grunt.registerTask('stage', ['default']); 58 | }; 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Filament Group 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | :warning: This project is archived and the repository is no longer maintained. We recommend using the [HTML dialog tag](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog) now that it's [well supported](https://caniuse.com/dialog). 3 | 4 | # dialog 5 | 6 | [![Filament Group](http://filamentgroup.com/images/fg-logo-positive-sm-crop.png) ](http://www.filamentgroup.com/) 7 | 8 | Just a simple, minimal, Responsive jQuery/Shoestring dialog with typical interactivity 9 | -------------------------------------------------------------------------------- /_redirects: -------------------------------------------------------------------------------- 1 | / /demos -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "filament-dialog", 3 | "main": ["dist/dialog.build.js", "dist/dialog.css"], 4 | "ignore": [ 5 | "**/.*" 6 | ], 7 | "dependencies": { 8 | "jquery": ">=1.6.0 <2.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /demos/docs.css: -------------------------------------------------------------------------------- 1 | /* Logo */ 2 | .header { 3 | background: #247201 url(http://filamentgroup.com/images/headerbg-new.jpg) no-repeat bottom left; 4 | } 5 | #fg-logo { 6 | text-indent: -9999px; 7 | margin: 0 auto; 8 | width: 287px; 9 | height: 52px; 10 | background-image: url(http://filamentgroup.com/images/fg-logo-icon.png); 11 | } 12 | @media (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5){ 13 | #fg-logo { 14 | background-size: 287px 52px; 15 | background-image: url(http://filamentgroup.com/images/fg-logo-icon-lrg.png); 16 | } 17 | } 18 | /* Demo styles */ 19 | body { 20 | font-family: sans-serif; 21 | font-size: 100%; 22 | } 23 | .docs-main { 24 | margin: 1em 20px; 25 | max-width: 46em; 26 | } 27 | @media (min-width: 46em) { 28 | .docs-main { 29 | margin: 1em auto; 30 | } 31 | } 32 | label { 33 | display: block; 34 | margin: 1em 0; 35 | } 36 | input[type=text], 37 | textarea { 38 | display: block; 39 | width: 100%; 40 | -webkit-box-sizing: border-box; 41 | -moz-box-sizing: border-box; 42 | box-sizing: border-box; 43 | 44 | margin-top: .4em; 45 | padding: .6em; 46 | font-size: 100%; 47 | } 48 | 49 | .menu { 50 | background-color: white; 51 | box-sizing: border-box; 52 | border: 1px solid black; 53 | width: 10em; 54 | } 55 | 56 | .menu ul, .menu ol { 57 | list-style: none; 58 | padding: 5px; 59 | margin: 0; 60 | } 61 | 62 | .menu-selected { 63 | color: white; 64 | background-color: #aaa; 65 | } 66 | 67 | input[type=text] { 68 | box-sizing: border-box; 69 | width: 10em; 70 | } 71 | select { 72 | margin: 1em 0; 73 | } 74 | h1.docs, 75 | h2.docs, 76 | h3.docs, 77 | h4.docs, 78 | h5.docs { 79 | font-weight: 500; 80 | margin: 1em 0; 81 | text-transform: none; 82 | color: #000; 83 | clear: both; 84 | } 85 | 86 | h1.docs { font-size: 2.5em; margin-top: .8em; font-weight: bold; } 87 | h2.docs { font-size: 2em; margin-top: 1.5em; border-top:1px solid #ddd; padding-top: .6em; float:none; } 88 | h3.docs { font-size: 1.6em; margin-top: 1.5em; margin-bottom: .5em; } 89 | h4.docs { font-size: 1.4em; margin-top: 1.5em; } 90 | 91 | p.docs, 92 | p.docs-intro, 93 | ol.docs, 94 | ul.docs, 95 | p.docs-note, 96 | dl.docs { 97 | margin: 1em 0; 98 | font-size: 1em; 99 | } 100 | 101 | ul.docs, 102 | ol.docs { 103 | padding-bottom: .5em; 104 | } 105 | ol.docs li, 106 | ul.docs li { 107 | margin-bottom: 8px; 108 | } 109 | ul.docs ul, 110 | ol.docs ul { 111 | padding-top: 8px; 112 | } 113 | .docs code { 114 | font-size: 1.1em; 115 | } 116 | 117 | p.docs strong { 118 | font-weight: bold; 119 | } 120 | 121 | .docs-note { 122 | background-color: #FFFAA4; 123 | } 124 | .docs-note p, 125 | .docs-note pre, 126 | p.docs-note { 127 | padding: .5em; 128 | margin: 0; 129 | } 130 | 131 | 132 | /** 133 | * prism.js default theme for JavaScript, CSS and HTML 134 | * Based on dabblet (http://dabblet.com) 135 | * @author Lea Verou 136 | */ 137 | 138 | code[class*="language-"], 139 | pre[class*="language-"] { 140 | color: black; 141 | text-shadow: 0 1px white; 142 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 143 | direction: ltr; 144 | text-align: left; 145 | white-space: pre; 146 | word-spacing: normal; 147 | font-size: 0.8em; 148 | 149 | -moz-tab-size: 4; 150 | -o-tab-size: 4; 151 | tab-size: 4; 152 | 153 | -webkit-hyphens: none; 154 | -moz-hyphens: none; 155 | -ms-hyphens: none; 156 | hyphens: none; 157 | } 158 | 159 | @media print { 160 | code[class*="language-"], 161 | pre[class*="language-"] { 162 | text-shadow: none; 163 | } 164 | } 165 | 166 | /* Code blocks */ 167 | pre[class*="language-"] { 168 | padding: 1em; 169 | margin: .5em 0; 170 | overflow: auto; 171 | } 172 | 173 | :not(pre) > code[class*="language-"], 174 | pre[class*="language-"] { 175 | background: #f5f2f0; 176 | } 177 | 178 | /* Inline code */ 179 | :not(pre) > code[class*="language-"] { 180 | padding: .1em; 181 | border-radius: .3em; 182 | } 183 | 184 | pre[class*="language-"] { 185 | padding: 1em; 186 | margin: 0; 187 | margin-bottom: 1em; 188 | } 189 | 190 | pre { 191 | padding: 16px; 192 | background: #f7f7f7; 193 | tab-size: 2; 194 | } 195 | code { 196 | font-size: 1.2em; 197 | line-height: 1.4; 198 | font-weight: 300; 199 | } 200 | .docs-btn { 201 | padding: .5em; 202 | text-decoration: none; 203 | display: inline-block; 204 | background: #fff; 205 | border-radius: 5px; 206 | border: 1px solid #bbb; 207 | box-shadow: 0 0 1px #ccc; 208 | } 209 | .docs-btn:hover { 210 | box-shadow: 0 0 5px #ccc; 211 | } 212 | -------------------------------------------------------------------------------- /demos/example-external.html: -------------------------------------------------------------------------------- 1 | 2 |

This is an ajax dialog

3 | Close 4 | -------------------------------------------------------------------------------- /demos/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dialog Example 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | 25 |
26 |
27 | 28 |
29 |
30 |

Demo of Dialog 31 | Just a simple, minimal jQuery dialog with typical interactivity 32 |

33 | 37 |
38 |
39 | 40 |
41 | 42 | 43 |

Dialog Examples

44 | 45 |

The dialog itself is an element with a class of dialog. It'll auto-initialize on dom-ready.

46 | 47 |

Typical dialog example

48 | 49 |
50 |
51 |

This is a dialog

52 | Close 53 |
54 | Open Dialog 55 |
56 | 57 |

Markup broken down

58 | 59 |

The dialog itself is an element with a class of dialog:

60 | 61 |
<div class="dialog">Content here...</div>
 62 | 		
63 | 64 |

To open it via a link, give the dialog an id and link to that ID somewhere on the page:

65 | 66 |
<div class="dialog" id="mydialog">Content here....</div>
 67 | 
 68 | <a href="#mydialog">Open Dialog</a>
 69 | 		
70 | 71 |

Once open, the dialog can be closed via back button, escape key, clicking or tapping the overlay screen (if styled to be visible).

72 | 73 |

You can also add a close link by adding a link anywhere in the dialog element with an attribute data-dialog-close.

74 | 75 |
<div class="dialog" id="mydialog">
 76 | 			<p>This is a dialog</p>
 77 | 			<a href="#" data-dialog-close>Close</a>
 78 | 		</div>
 79 | 		
80 | 81 |

Rather than using a null # href for the close link, we find it's nice to link back to the link that opened the dialog, just in case the user scrolled away from that place while the dialog was open. You can do this by giving the opener link an id attribute and linking to that ID from the close link:

82 | 83 |
<div class="dialog" id="mydialog">
 84 | 			<p>This is a dialog</p>
 85 | 			<a href="#mydialog-btn" data-dialog-close>Close</a>
 86 | 		</div>
 87 | 
 88 | 		<a href="#mydialog" id="mydialog-btn">Open Dialog</a>
 89 | 			
90 | 91 |

Lastly, it's helpful to add a title for the dialog and identify it as such for assistive technology. You can either do that by setting the aria-labelledby attribute on the dialog element to reference the ID of another element that acts as its title, or by adding an aria-label attribute with a text value that acts as a title for the dialog. The demos on this page use aria-label.

92 | 93 |
<div class="dialog" id="mydialog" aria-label="dialog example">Content here...</div>
 94 | 			
95 | 96 | 97 |

Modal and non-modal

98 | 99 |

Much of the presentation of the dialog is configured in CSS. 100 | The default modal appearance is applied to .dialog-background element that the script generates by default. 101 | If you want to style this element differently, just style that selector however you'd like: 102 |

103 | 104 |
.dialog-background {
105 | 			background: red;
106 | 		}
107 | 		
108 | 109 |

You can also configure the background to be transparent via the dialog markup, by using the data-transbg attribute:

110 | 111 |
<div class="dialog" data-transbg>Content here...</div>
112 | 		
113 | 114 |

If you'd prefer to have no background element at all, you can use the data-dialog-nobg attribute:

115 | 116 |
<div class="dialog" data-dialog-nobg>Content here...</div>
117 | 		
118 | 119 |

Opening and closing programatically

120 | 121 |

You can open and close the dialog via JavaScript by triggering an open or close event:

122 | 123 |
// open:
124 | 		$( "#mydialog" ).trigger( "dialog-open" );
125 | 
126 | 		// close:
127 | 		$( "#mydialog" ).trigger( "dialog-close" );
128 | 		
129 | 130 |

Ajax linking

131 | 132 |

You can pull another page into a dialog by adding a data-dialog-link attribute to a link.

133 | 134 |
135 | Open in dialog 136 |
137 | 138 |

Additional external dialog with a # in its href, just for testing purposes:

139 | 140 |
141 | Open in dialog 142 |
143 | 144 |

Adding a class via ajax linking

145 | 146 |

To set classes on the ajax dialog container, you can specify classes in a data-dialog-addclass attribute on the anchor:

147 | 148 |
149 | Open in dialog 150 |
151 | 152 | 153 |

Adding a label via ajax linking

154 | 155 |

To set the aria-label or aria-labelledby attributes on the ajax dialog container, you can specify a value in a data-dialog-label or data-dialog-labelledby attribute on the anchor:

156 | 157 |
158 | Open in dialog 159 |
160 | 161 |

Iframe linking

162 | 163 |

You can pull another page into a dialog in an iframe by adding a data-dialog-iframe attribute to a data-dialog-link link.

164 | 165 |
166 | Open in dialog 167 |
168 | 169 | 170 |

"Nested" Dialogs

171 |

Dialogs can open without closing already-open dialogs, and they'll close in the order they were opened.

172 | 173 |
174 |
175 |

This is a dialog

176 |

Open the first Dialog on the page

177 |

Close

178 |
179 | Open Dialog 180 |
181 | 182 | 183 |

Non-history Dialogs

184 |

185 | By default, dialogs are re-openable via the back or forward button, and 186 | can be deep-linked via the hash. To opt out of this behavior, use 187 | the data-dialog-history="false" attribute and value on the 188 | dialog or a dialog link. History tracking can be turned of globally by 189 | setting a property on the dialog component 190 | constructor, window.Dialog.history = false 191 |

192 | 193 |
194 |
195 |

This is a no-history dialog

196 |

Open the first Dialog on the page (that one does have history!)

197 |

Close

198 |
199 | Open Dialog 200 |
201 | 202 |
203 | Open Non-history Ajax Dialog 204 |
205 | 206 | 207 |

Dialog styled to be visible at some breakpoints

208 |

If a dialog is styled to be visible and non-closeable at any breakpoint, the JavaScript will detect that and remove the dialog's role and tabindex attributes so that its role in the page is communicated accurately to assistive technology. 209 | This happens both upon initialization and whenever the viewport is resized.

210 | 211 | 212 |

To take advantage of this accessibility feature, the following criteria must be met in your dialog's CSS:

213 |
    214 |
  1. The dialog element's CSS display property does not equal "none" 215 |
  2. The dialog element's CSS visibility property does not equal "hidden" 216 |
  3. The dialog close link's (if it is in the markup) CSS display property equals "none" 217 |
  4. The dialog background screen's CSS display property equals "none" 218 |
219 | 220 |

The following dialog uses CSS to display it as static content at wider breakpoints, as well as hiding the elements associated with deeming the dialog non-closeable.

221 | 222 |
223 | 236 | 237 |
238 |

This is a dialog

239 |
240 | 241 |
242 | Close 243 |
244 | Open Dialog 245 |
246 | 247 | 248 |

Dialog is taller than the document

249 | 250 | 260 |
261 |
262 |

This is a dialog

263 | Close 264 |
265 |

< 600px: 5000px height

266 |

> 600px: 10000px height

267 |
268 |
269 | Open Dialog 270 |
271 | 272 | 273 | 274 |
275 | 276 | 277 | -------------------------------------------------------------------------------- /dist/dialog.build.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Simple jQuery Dialog 3 | * https://github.com/filamentgroup/dialog 4 | * 5 | * Copyright (c) 2013 Filament Group, Inc. 6 | * Author: @scottjehl 7 | * Contributors: @johnbender, @zachleat 8 | * Licensed under the MIT, GPL licenses. 9 | */ 10 | 11 | window.jQuery = window.jQuery || window.shoestring; 12 | 13 | (function( w, $ ){ 14 | w.componentNamespace = w.componentNamespace || w; 15 | 16 | var pluginName = "dialog", cl, ev, 17 | doc = w.document, 18 | docElem = doc.documentElement, 19 | body = doc.body, 20 | $html = $( docElem ); 21 | 22 | var Dialog = w.componentNamespace.Dialog = function( element ){ 23 | this.$el = $( element ); 24 | 25 | // prevent double init 26 | if( this.$el.data( pluginName ) ){ 27 | return this.$el.data( pluginName ); 28 | } 29 | 30 | // record init 31 | this.$el.data( pluginName, this ); 32 | 33 | // keeping data-nobg here for compat. Deprecated. 34 | this.$background = !this.$el.is( '[data-' + pluginName + '-nobg]' ) ? 35 | $( doc.createElement('div') ).addClass( cl.bkgd ).attr( "tabindex", "-1" ).appendTo( "body") : 36 | $( [] ); 37 | 38 | // when dialog first inits, save a reference to the initial hash so we can know whether 39 | // there's room in the history stack to go back or not when closing 40 | this.initialLocationHash = w.location.hash; 41 | 42 | // the dialog's url hash is different from the dialog's actual ID attribute 43 | // this is because pairing the ID directly makes the browser jump to the top 44 | // of the dialog, rather than allowing us to space it off the top of the 45 | // viewport. also, if the dialog has a data-history attr, this property will 46 | // prevent its findability for onload and hashchanges 47 | this.nohistory = 48 | this.$el.attr( 'data-dialog-history' ) === "false" || !Dialog.history; 49 | 50 | var id = this.$el.attr( "id" ); 51 | // use the identifier and an extra tag for hash management 52 | this.hash = id + "-dialog"; 53 | 54 | // if won't pop up the dialog on initial load (`nohistory`) the user MAY 55 | // refresh a url with the dialog id as the hash then a change of the hash 56 | // won't be recognized by the browser when the dialog comes up and the back 57 | // button will return to the referring page. So, when nohistory is defined, 58 | // we append a "unique" identifier to the hash. 59 | this.hash += this.nohistory ? "-" + new Date().getTime().toString() : "" ; 60 | 61 | this.isOpen = false; 62 | this.isTransparentBackground = this.$el.is( '[data-transbg]' ); 63 | 64 | if( id ) { 65 | this.resizeEventName = "resize.dialog-" + id; 66 | } 67 | 68 | this._addA11yAttrs(); 69 | }; 70 | 71 | // default to tracking history with the dialog 72 | Dialog.history = true; 73 | 74 | // This property is global across dialogs - it determines whether the hash is get/set at all 75 | Dialog.useHash = true; 76 | 77 | Dialog.events = ev = { 78 | open: pluginName + "-open", 79 | opened: pluginName + "-opened", 80 | close: pluginName + "-close", 81 | closed: pluginName + "-closed", 82 | resize: pluginName + "-resize" 83 | }; 84 | 85 | Dialog.classes = cl = { 86 | open: pluginName + "-open", 87 | opened: pluginName + "-opened", 88 | content: pluginName + "-content", 89 | close: pluginName + "-close", 90 | closed: pluginName + "-closed", 91 | bkgd: pluginName + "-background", 92 | bkgdOpen: pluginName + "-background-open", 93 | bkgdTrans: pluginName + "-background-trans" 94 | }; 95 | 96 | Dialog.selectors = { 97 | close: "." + Dialog.classes.close + ", [data-close], [data-dialog-close]" 98 | }; 99 | 100 | 101 | Dialog.prototype.destroy = function() { 102 | // unregister the focus stealing 103 | window.focusRegistry.unregister(this); 104 | 105 | this.$el.trigger("destroy"); 106 | 107 | // clear init for this dom element 108 | this.$el.data()[pluginName] = undefined; 109 | 110 | // remove the backdrop for the dialog 111 | this.$background.remove(); 112 | }; 113 | 114 | Dialog.prototype.checkFocus = function(event){ 115 | var $target = $( event.target ); 116 | var shouldSteal; 117 | 118 | shouldSteal = 119 | this.isOpen && 120 | !$target.closest( this.$el[0]).length && 121 | this.isLastDialog() && 122 | !this._isNonInteractive(); 123 | 124 | return shouldSteal; 125 | }; 126 | 127 | Dialog.prototype.stealFocus = function(){ 128 | this.$el[0].focus(); 129 | }; 130 | 131 | 132 | 133 | Dialog.prototype._addA11yAttrs = function(){ 134 | this.$el 135 | .attr( "role", "dialog" ) 136 | .attr( "tabindex", "-1" ) 137 | .find( Dialog.selectors.close ).attr( "role", "button" ); 138 | 139 | }; 140 | 141 | Dialog.prototype._removeA11yAttrs = function(){ 142 | this.$el.removeAttr( "role" ); 143 | this.$el.removeAttr( "tabindex" ); 144 | }; 145 | 146 | Dialog.prototype._isNonInteractive = function(){ 147 | var computedDialog = window.getComputedStyle( this.$el[ 0 ], null ); 148 | var closeLink = this.$el.find( Dialog.selectors.close )[0]; 149 | var computedCloseLink; 150 | if( closeLink ){ 151 | computedCloseLink = window.getComputedStyle( closeLink, null ); 152 | } 153 | var computedBackground = window.getComputedStyle( this.$background[ 0 ], null ); 154 | return computedDialog.getPropertyValue( "display" ) !== "none" && 155 | computedDialog.getPropertyValue( "visibility" ) !== "hidden" && 156 | ( !computedCloseLink || computedCloseLink.getPropertyValue( "display" ) === "none" ) && 157 | computedBackground.getPropertyValue( "display" ) === "none"; 158 | }; 159 | 160 | Dialog.prototype._checkInteractivity = function(){ 161 | if( this._isNonInteractive() ){ 162 | this._removeA11yAttrs(); 163 | this._ariaShowUnrelatedElems(); 164 | } 165 | else{ 166 | this._addA11yAttrs(); 167 | 168 | } 169 | }; 170 | 171 | 172 | Dialog.prototype._ariaHideUnrelatedElems = function(){ 173 | this._ariaShowUnrelatedElems(); 174 | var ignoredElems = "script, style"; 175 | var hideList = this.$el.siblings().not( ignoredElems ); 176 | this.$el.parents().not( "body, html" ).each(function(){ 177 | hideList = hideList.add( $( this ).siblings().not( ignoredElems ) ); 178 | }); 179 | hideList.each(function(){ 180 | var priorHidden = $( this ).attr( "aria-hidden" ) || ""; 181 | $( this ) 182 | .attr( "data-dialog-aria-hidden", priorHidden ) 183 | .attr( "aria-hidden", "true" ); 184 | }); 185 | }; 186 | 187 | 188 | Dialog.prototype._ariaShowUnrelatedElems = function(){ 189 | $( "[data-dialog-aria-hidden]" ).each(function(){ 190 | if( $( this ).attr( "data-dialog-aria-hidden" ).match( "true|false" ) ){ 191 | $( this ).attr( "aria-hidden", $( this ).attr( "data-dialog-aria-hidden" ) ); 192 | } 193 | else { 194 | $( this ).removeAttr( "aria-hidden" ); 195 | } 196 | }).removeAttr( "data-dialog-aria-hidden" ); 197 | }; 198 | 199 | Dialog.prototype.resizeBackground = function() { 200 | if( this.$background.length ) { 201 | var bg = this.$background[ 0 ]; 202 | // don’t let the background size interfere with our height measurements 203 | bg.style.display = "none"; 204 | 205 | var scrollPlusHeight = (this.scroll || 0) + this.$el[0].clientHeight; 206 | var height = Math.max( scrollPlusHeight, docElem.scrollHeight, docElem.clientHeight ); 207 | bg.style.height = height + "px"; 208 | bg.style.display = ""; 209 | } 210 | }; 211 | 212 | Dialog.prototype.open = function() { 213 | if( this.isOpen ){ 214 | return; 215 | } 216 | 217 | var self = this; 218 | 219 | this.$el.addClass( cl.open ); 220 | 221 | this.$background.addClass( cl.bkgdOpen ); 222 | this.$background.attr( "id", this.$el.attr( "id" ) + "-background" ); 223 | this._setBackgroundTransparency(); 224 | 225 | this.scroll = "pageYOffset" in w ? w.pageYOffset : ( docElem.scrollY || docElem.scrollTop || ( body && body.scrollY ) || 0 ); 226 | this.$el[ 0 ].style.top = this.scroll + "px"; 227 | this.resizeBackground(); 228 | 229 | $html.addClass( cl.open ); 230 | this.isOpen = true; 231 | 232 | var cleanHash = w.location.hash.replace( /^#/, "" ); 233 | 234 | if( w.Dialog.useHash ){ 235 | if( cleanHash.indexOf( "-dialog" ) > -1 && !this.isLastDialog() ){ 236 | w.location.hash += "#" + this.hash; 237 | } else if( !this.isLastDialog() ){ 238 | w.location.hash = this.hash; 239 | } 240 | } 241 | 242 | if( doc.activeElement ){ 243 | this.focused = doc.activeElement; 244 | } 245 | 246 | this.$el[ 0 ].focus(); 247 | 248 | setTimeout(function(){ 249 | self._ariaHideUnrelatedElems(); 250 | }); 251 | 252 | this.$el.on( ev.resize, function() { 253 | self.resizeBackground(); 254 | }); 255 | 256 | if( this.resizeEventName ) { 257 | var timeout; 258 | $(w).on(this.resizeEventName, function() { 259 | w.clearTimeout(timeout); 260 | timeout = setTimeout(function() { 261 | self.resizeBackground(); 262 | }, 50); 263 | }); 264 | } 265 | 266 | this.$el.trigger( ev.opened ); 267 | }; 268 | 269 | Dialog.prototype.lastHash = function(){ 270 | return w.location.hash.split( "#" ).pop(); 271 | }; 272 | 273 | // is this the newest/last dialog that was opened based on the hash 274 | Dialog.prototype.isLastDialog = function(){ 275 | return this.lastHash() === this.hash; 276 | }; 277 | 278 | Dialog.prototype._setBackgroundTransparency = function() { 279 | if( this.isTransparentBackground ){ 280 | this.$background.addClass( cl.bkgdTrans ); 281 | } 282 | }; 283 | 284 | Dialog.prototype.close = function(){ 285 | if( !this.isOpen ){ 286 | return; 287 | } 288 | 289 | this._ariaShowUnrelatedElems(); 290 | 291 | // if close() is called directly and the hash for this dialog is at the end 292 | // of the url, then we need to change the hash to remove it, either by going 293 | // back if we can, or by adding a history state that doesn't have it at the 294 | // end 295 | if( window.location.hash.split( "#" ).pop() === this.hash ){ 296 | // check if we're back at the original hash, if we are then we can't 297 | // go back again otherwise we'll move away from the page 298 | var hashKeys = window.location.hash.split( "#" ); 299 | var initialHashKeys = this.initialLocationHash.split( "#" ); 300 | 301 | // if we are not at the original hash then use history 302 | // otherwise, if it's the same starting hash as it was at init time, we 303 | // can't trigger back to close the dialog, as it might take us elsewhere. 304 | // so we have to go forward and create a new hash that does not have this 305 | // dialog's hash at the end 306 | if( window.Dialog.useHash ){ 307 | if( hashKeys.join("") !== initialHashKeys.join("") ){ 308 | window.history.back(); 309 | } else { 310 | var escapedRegexpHash = this 311 | .hash 312 | .replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); 313 | 314 | window.location.hash = window 315 | .location 316 | .hash 317 | .replace( new RegExp( "#" + escapedRegexpHash + "$" ), "" ); 318 | } 319 | } 320 | 321 | return; 322 | } 323 | 324 | this.$el.removeClass( cl.open ); 325 | 326 | this.$background.removeClass( cl.bkgdOpen ); 327 | 328 | this.isOpen = false; 329 | 330 | // we only want to throw focus on close if we aren't 331 | // opening a nested dialog or some other UI state 332 | if( this.focused && !this.isLastDialog()){ 333 | this.focused.focus(); 334 | } 335 | if( $( "." + pluginName + "." + cl.open ).length === 0 ){ 336 | $html.removeClass( cl.open ); 337 | w.scrollTo( 0, this.scroll ); 338 | } 339 | 340 | this.$el.off( ev.resize ); 341 | 342 | if( this.resizeEventName ) { 343 | $(w).off(this.resizeEventName); 344 | } 345 | 346 | this.$el.trigger( ev.closed ); 347 | }; 348 | }( this, window.jQuery )); 349 | 350 | (function( w, $ ){ 351 | var Dialog = w.componentNamespace.Dialog, 352 | doc = w.document, 353 | pluginName = "dialog"; 354 | 355 | $.fn[ pluginName ] = function(){ 356 | return this.each(function(){ 357 | var $el = $( this ); 358 | 359 | // prevent double init 360 | if( $el.data( "dialog" ) ){ 361 | return; 362 | } 363 | 364 | var dialog = new Dialog( this ); 365 | var onOpen, onClose, onClick, onBackgroundClick; 366 | 367 | $el.addClass( Dialog.classes.content ) 368 | 369 | .bind( Dialog.events.open, onOpen = function(){ 370 | dialog.open(); 371 | }) 372 | .bind( Dialog.events.close, onClose = function(){ 373 | dialog.close(); 374 | }) 375 | .bind( "click", onClick = function( e ){ 376 | if( $(e.target).closest(Dialog.selectors.close).length ){ 377 | e.preventDefault(); 378 | dialog.close(); 379 | } 380 | }); 381 | 382 | dialog.$background.bind( "click", onBackgroundClick = function() { 383 | dialog.close(); 384 | }); 385 | 386 | var onHashchange; 387 | 388 | // on load and hashchange, open the dialog if its hash matches the last part of the hash, and close if it doesn't 389 | if( Dialog.useHash ){ 390 | $( w ).bind( "hashchange", onHashchange = function(){ 391 | var hash = w.location.hash.split( "#" ).pop(); 392 | 393 | // if the hash matches this dialog's, open! 394 | if( hash === dialog.hash ){ 395 | if( !dialog.nohistory ){ 396 | dialog.open(); 397 | } 398 | } 399 | // if it doesn't match... 400 | else { 401 | dialog.close(); 402 | } 403 | }); 404 | } 405 | 406 | var onDocClick, onKeyup, onResize; 407 | 408 | // open on matching a[href=#id] click 409 | $( doc ).bind( "click", onDocClick = function( e ){ 410 | var $matchingDialog, $a; 411 | 412 | $a = $( e.target ).closest( "a" ); 413 | 414 | 415 | if( !dialog.isOpen && $a.length && $a.attr( "href" ) ){ 416 | var id = $a.attr( "href" ).replace( /^#/, "" ); 417 | 418 | // catch invalid selector exceptions 419 | try { 420 | // Attempt to find the matching dialog at the same id or at the 421 | // encoded id. This allows matching even when href url ids are being 422 | // changed back and forth between encoded and decoded forms. 423 | $matchingDialog = 424 | $( "[id='" + id + "'], [id='" + encodeURIComponent(id) + "']" ); 425 | } catch ( error ) { 426 | // TODO should check the type of exception, it's not clear how well 427 | // the error name "SynatxError" is supported 428 | return; 429 | } 430 | 431 | if( $matchingDialog.length && $matchingDialog.is( $el ) ){ 432 | e.preventDefault(); 433 | $matchingDialog.trigger( Dialog.events.open ); 434 | } 435 | } 436 | }); 437 | 438 | // close on escape key 439 | $( doc ).bind( "keyup", onKeyup = function( e ){ 440 | if( e.which === 27 ){ 441 | dialog.close(); 442 | } 443 | }); 444 | 445 | dialog._checkInteractivity(); 446 | var resizepoll; 447 | $( window ).bind( "resize", onResize = function(){ 448 | if( resizepoll ){ 449 | clearTimeout( resizepoll ); 450 | } 451 | resizepoll = setTimeout( function(){ 452 | dialog._checkInteractivity.call( dialog ); 453 | }, 150 ); 454 | }); 455 | 456 | $el.bind("destroy", function(){ 457 | $(w).unbind("hashchange", onHashchange); 458 | 459 | $el 460 | .unbind( Dialog.events.open, onOpen ) 461 | .unbind( Dialog.events.close, onClose ) 462 | .unbind( "click", onClick ); 463 | 464 | dialog.$background.unbind( "click", onBackgroundClick); 465 | 466 | $( doc ).unbind( "click", onDocClick ); 467 | $( doc ).unbind( "keyup", onKeyup ); 468 | $( window ).unbind( "resize", onResize ); 469 | }); 470 | 471 | onHashchange(); 472 | 473 | window.focusRegistry.register(dialog); 474 | }); 475 | }; 476 | 477 | // auto-init on enhance 478 | $( w.document ).bind( "enhance", function( e ){ 479 | var target = e.target === w.document ? "" : e.target; 480 | $( "." + pluginName, e.target ).add( target ).filter( "." + pluginName )[ pluginName ](); 481 | }); 482 | 483 | function FocusRegistry(){ 484 | var self = this; 485 | 486 | this.registry = []; 487 | 488 | $(window.document).bind("focusin.focus-registry", function(event){ 489 | self.check(event); 490 | }); 491 | } 492 | 493 | FocusRegistry.prototype.register = function(obj){ 494 | if( !obj.checkFocus ){ 495 | throw new Error( "Obj must implement `checkFocus`" ); 496 | } 497 | 498 | if( !obj.stealFocus ){ 499 | throw new Error( "Obj must implement `stealFocus`" ); 500 | } 501 | 502 | this.registry.push(obj); 503 | }; 504 | 505 | FocusRegistry.prototype.unregister = function(obj){ 506 | var newRegistry = []; 507 | 508 | for(var i = 0; i < this.registry.length; i++ ){ 509 | if(this.registry[i] !== obj){ 510 | newRegistry.push(this.registry[i]); 511 | } 512 | } 513 | 514 | this.registry = newRegistry; 515 | }; 516 | 517 | FocusRegistry.prototype.check = function(event){ 518 | var stealing = []; 519 | 520 | // for all the registered components 521 | for(var i = 0; i < this.registry.length; i++){ 522 | 523 | // if a given component wants to steal the focus, record that 524 | if( this.registry[i].checkFocus(event) ){ 525 | stealing.push(this.registry[i]); 526 | } 527 | } 528 | 529 | // if more than one component wants to steal focus throw an exception 530 | if( stealing.length > 1 ){ 531 | throw new Error("Two components are attempting to steal focus."); 532 | } 533 | 534 | // otherwise allow the first component to steal focus 535 | if(stealing[0]) { 536 | event.preventDefault(); 537 | 538 | // let this event stack unwind and then steal the focus 539 | // which will again trigger the check above 540 | setTimeout(function(){ 541 | stealing[0].stealFocus(event); 542 | }); 543 | } 544 | }; 545 | 546 | // constructor in namespace 547 | window.componentNamespace.FocusRegistry = FocusRegistry; 548 | 549 | // singleton 550 | window.focusRegistry = new FocusRegistry(); 551 | }( this, window.jQuery )); 552 | -------------------------------------------------------------------------------- /dist/dialog.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Simple jQuery Dialog 3 | * https://github.com/filamentgroup/dialog 4 | * 5 | * Copyright (c) 2013 Filament Group, Inc. 6 | * Licensed under the MIT, GPL licenses. 7 | */ 8 | .dialog-content, 9 | .dialog-background { 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | right: 0; 14 | display: none; 15 | } 16 | .dialog-background { 17 | background: #aaa; 18 | filter: alpha(opacity=40); 19 | background-color: rgba(0,0,0,.4); 20 | z-index: 99999; 21 | height: 100%; 22 | bottom: 0; 23 | } 24 | .dialog-content { 25 | margin: 1em; 26 | background: #fff; 27 | padding: 1em 2em; 28 | max-width: 30em; 29 | box-shadow: 0 1px 2px #777; 30 | z-index: 100000; 31 | } 32 | .dialog-iframe { 33 | margin: 0; 34 | padding: 0; 35 | width: 100%; 36 | height: 100%; 37 | border: 0; 38 | } 39 | /* 40 | IE8+ issue with centering dialog 41 | https://github.com/filamentgroup/dialog/issues/6 42 | requires Respond.JS for IE8 43 | */ 44 | @media (min-width: 30em) { 45 | .dialog-content { 46 | width: 30em; 47 | } 48 | } 49 | .dialog-open:focus { 50 | outline: none; 51 | } 52 | .dialog-open, 53 | .dialog-background-open { 54 | display: block; 55 | } 56 | .dialog-background-trans { 57 | background: transparent; 58 | } 59 | 60 | @media (min-width: 32em){ 61 | .dialog-content { 62 | margin: 4em auto 1em; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /dist/dialog.linker.build.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Simple jQuery Dialog 3 | * https://github.com/filamentgroup/dialog 4 | * 5 | * Copyright (c) 2013 Filament Group, Inc. 6 | * Author: @scottjehl 7 | * Contributors: @johnbender, @zachleat 8 | * Licensed under the MIT, GPL licenses. 9 | */ 10 | 11 | window.jQuery = window.jQuery || window.shoestring; 12 | 13 | (function( w, $ ){ 14 | w.componentNamespace = w.componentNamespace || w; 15 | 16 | var pluginName = "dialog", cl, ev, 17 | doc = w.document, 18 | docElem = doc.documentElement, 19 | body = doc.body, 20 | $html = $( docElem ); 21 | 22 | var Dialog = w.componentNamespace.Dialog = function( element ){ 23 | this.$el = $( element ); 24 | 25 | // prevent double init 26 | if( this.$el.data( pluginName ) ){ 27 | return this.$el.data( pluginName ); 28 | } 29 | 30 | // record init 31 | this.$el.data( pluginName, this ); 32 | 33 | // keeping data-nobg here for compat. Deprecated. 34 | this.$background = !this.$el.is( '[data-' + pluginName + '-nobg]' ) ? 35 | $( doc.createElement('div') ).addClass( cl.bkgd ).attr( "tabindex", "-1" ).appendTo( "body") : 36 | $( [] ); 37 | 38 | // when dialog first inits, save a reference to the initial hash so we can know whether 39 | // there's room in the history stack to go back or not when closing 40 | this.initialLocationHash = w.location.hash; 41 | 42 | // the dialog's url hash is different from the dialog's actual ID attribute 43 | // this is because pairing the ID directly makes the browser jump to the top 44 | // of the dialog, rather than allowing us to space it off the top of the 45 | // viewport. also, if the dialog has a data-history attr, this property will 46 | // prevent its findability for onload and hashchanges 47 | this.nohistory = 48 | this.$el.attr( 'data-dialog-history' ) === "false" || !Dialog.history; 49 | 50 | var id = this.$el.attr( "id" ); 51 | // use the identifier and an extra tag for hash management 52 | this.hash = id + "-dialog"; 53 | 54 | // if won't pop up the dialog on initial load (`nohistory`) the user MAY 55 | // refresh a url with the dialog id as the hash then a change of the hash 56 | // won't be recognized by the browser when the dialog comes up and the back 57 | // button will return to the referring page. So, when nohistory is defined, 58 | // we append a "unique" identifier to the hash. 59 | this.hash += this.nohistory ? "-" + new Date().getTime().toString() : "" ; 60 | 61 | this.isOpen = false; 62 | this.isTransparentBackground = this.$el.is( '[data-transbg]' ); 63 | 64 | if( id ) { 65 | this.resizeEventName = "resize.dialog-" + id; 66 | } 67 | 68 | this._addA11yAttrs(); 69 | }; 70 | 71 | // default to tracking history with the dialog 72 | Dialog.history = true; 73 | 74 | // This property is global across dialogs - it determines whether the hash is get/set at all 75 | Dialog.useHash = true; 76 | 77 | Dialog.events = ev = { 78 | open: pluginName + "-open", 79 | opened: pluginName + "-opened", 80 | close: pluginName + "-close", 81 | closed: pluginName + "-closed", 82 | resize: pluginName + "-resize" 83 | }; 84 | 85 | Dialog.classes = cl = { 86 | open: pluginName + "-open", 87 | opened: pluginName + "-opened", 88 | content: pluginName + "-content", 89 | close: pluginName + "-close", 90 | closed: pluginName + "-closed", 91 | bkgd: pluginName + "-background", 92 | bkgdOpen: pluginName + "-background-open", 93 | bkgdTrans: pluginName + "-background-trans" 94 | }; 95 | 96 | Dialog.selectors = { 97 | close: "." + Dialog.classes.close + ", [data-close], [data-dialog-close]" 98 | }; 99 | 100 | 101 | Dialog.prototype.destroy = function() { 102 | // unregister the focus stealing 103 | window.focusRegistry.unregister(this); 104 | 105 | this.$el.trigger("destroy"); 106 | 107 | // clear init for this dom element 108 | this.$el.data()[pluginName] = undefined; 109 | 110 | // remove the backdrop for the dialog 111 | this.$background.remove(); 112 | }; 113 | 114 | Dialog.prototype.checkFocus = function(event){ 115 | var $target = $( event.target ); 116 | var shouldSteal; 117 | 118 | shouldSteal = 119 | this.isOpen && 120 | !$target.closest( this.$el[0]).length && 121 | this.isLastDialog() && 122 | !this._isNonInteractive(); 123 | 124 | return shouldSteal; 125 | }; 126 | 127 | Dialog.prototype.stealFocus = function(){ 128 | this.$el[0].focus(); 129 | }; 130 | 131 | 132 | 133 | Dialog.prototype._addA11yAttrs = function(){ 134 | this.$el 135 | .attr( "role", "dialog" ) 136 | .attr( "tabindex", "-1" ) 137 | .find( Dialog.selectors.close ).attr( "role", "button" ); 138 | 139 | }; 140 | 141 | Dialog.prototype._removeA11yAttrs = function(){ 142 | this.$el.removeAttr( "role" ); 143 | this.$el.removeAttr( "tabindex" ); 144 | }; 145 | 146 | Dialog.prototype._isNonInteractive = function(){ 147 | var computedDialog = window.getComputedStyle( this.$el[ 0 ], null ); 148 | var closeLink = this.$el.find( Dialog.selectors.close )[0]; 149 | var computedCloseLink; 150 | if( closeLink ){ 151 | computedCloseLink = window.getComputedStyle( closeLink, null ); 152 | } 153 | var computedBackground = window.getComputedStyle( this.$background[ 0 ], null ); 154 | return computedDialog.getPropertyValue( "display" ) !== "none" && 155 | computedDialog.getPropertyValue( "visibility" ) !== "hidden" && 156 | ( !computedCloseLink || computedCloseLink.getPropertyValue( "display" ) === "none" ) && 157 | computedBackground.getPropertyValue( "display" ) === "none"; 158 | }; 159 | 160 | Dialog.prototype._checkInteractivity = function(){ 161 | if( this._isNonInteractive() ){ 162 | this._removeA11yAttrs(); 163 | this._ariaShowUnrelatedElems(); 164 | } 165 | else{ 166 | this._addA11yAttrs(); 167 | 168 | } 169 | }; 170 | 171 | 172 | Dialog.prototype._ariaHideUnrelatedElems = function(){ 173 | this._ariaShowUnrelatedElems(); 174 | var ignoredElems = "script, style"; 175 | var hideList = this.$el.siblings().not( ignoredElems ); 176 | this.$el.parents().not( "body, html" ).each(function(){ 177 | hideList = hideList.add( $( this ).siblings().not( ignoredElems ) ); 178 | }); 179 | hideList.each(function(){ 180 | var priorHidden = $( this ).attr( "aria-hidden" ) || ""; 181 | $( this ) 182 | .attr( "data-dialog-aria-hidden", priorHidden ) 183 | .attr( "aria-hidden", "true" ); 184 | }); 185 | }; 186 | 187 | 188 | Dialog.prototype._ariaShowUnrelatedElems = function(){ 189 | $( "[data-dialog-aria-hidden]" ).each(function(){ 190 | if( $( this ).attr( "data-dialog-aria-hidden" ).match( "true|false" ) ){ 191 | $( this ).attr( "aria-hidden", $( this ).attr( "data-dialog-aria-hidden" ) ); 192 | } 193 | else { 194 | $( this ).removeAttr( "aria-hidden" ); 195 | } 196 | }).removeAttr( "data-dialog-aria-hidden" ); 197 | }; 198 | 199 | Dialog.prototype.resizeBackground = function() { 200 | if( this.$background.length ) { 201 | var bg = this.$background[ 0 ]; 202 | // don’t let the background size interfere with our height measurements 203 | bg.style.display = "none"; 204 | 205 | var scrollPlusHeight = (this.scroll || 0) + this.$el[0].clientHeight; 206 | var height = Math.max( scrollPlusHeight, docElem.scrollHeight, docElem.clientHeight ); 207 | bg.style.height = height + "px"; 208 | bg.style.display = ""; 209 | } 210 | }; 211 | 212 | Dialog.prototype.open = function() { 213 | if( this.isOpen ){ 214 | return; 215 | } 216 | 217 | var self = this; 218 | 219 | this.$el.addClass( cl.open ); 220 | 221 | this.$background.addClass( cl.bkgdOpen ); 222 | this.$background.attr( "id", this.$el.attr( "id" ) + "-background" ); 223 | this._setBackgroundTransparency(); 224 | 225 | this.scroll = "pageYOffset" in w ? w.pageYOffset : ( docElem.scrollY || docElem.scrollTop || ( body && body.scrollY ) || 0 ); 226 | this.$el[ 0 ].style.top = this.scroll + "px"; 227 | this.resizeBackground(); 228 | 229 | $html.addClass( cl.open ); 230 | this.isOpen = true; 231 | 232 | var cleanHash = w.location.hash.replace( /^#/, "" ); 233 | 234 | if( w.Dialog.useHash ){ 235 | if( cleanHash.indexOf( "-dialog" ) > -1 && !this.isLastDialog() ){ 236 | w.location.hash += "#" + this.hash; 237 | } else if( !this.isLastDialog() ){ 238 | w.location.hash = this.hash; 239 | } 240 | } 241 | 242 | if( doc.activeElement ){ 243 | this.focused = doc.activeElement; 244 | } 245 | 246 | this.$el[ 0 ].focus(); 247 | 248 | setTimeout(function(){ 249 | self._ariaHideUnrelatedElems(); 250 | }); 251 | 252 | this.$el.on( ev.resize, function() { 253 | self.resizeBackground(); 254 | }); 255 | 256 | if( this.resizeEventName ) { 257 | var timeout; 258 | $(w).on(this.resizeEventName, function() { 259 | w.clearTimeout(timeout); 260 | timeout = setTimeout(function() { 261 | self.resizeBackground(); 262 | }, 50); 263 | }); 264 | } 265 | 266 | this.$el.trigger( ev.opened ); 267 | }; 268 | 269 | Dialog.prototype.lastHash = function(){ 270 | return w.location.hash.split( "#" ).pop(); 271 | }; 272 | 273 | // is this the newest/last dialog that was opened based on the hash 274 | Dialog.prototype.isLastDialog = function(){ 275 | return this.lastHash() === this.hash; 276 | }; 277 | 278 | Dialog.prototype._setBackgroundTransparency = function() { 279 | if( this.isTransparentBackground ){ 280 | this.$background.addClass( cl.bkgdTrans ); 281 | } 282 | }; 283 | 284 | Dialog.prototype.close = function(){ 285 | if( !this.isOpen ){ 286 | return; 287 | } 288 | 289 | this._ariaShowUnrelatedElems(); 290 | 291 | // if close() is called directly and the hash for this dialog is at the end 292 | // of the url, then we need to change the hash to remove it, either by going 293 | // back if we can, or by adding a history state that doesn't have it at the 294 | // end 295 | if( window.location.hash.split( "#" ).pop() === this.hash ){ 296 | // check if we're back at the original hash, if we are then we can't 297 | // go back again otherwise we'll move away from the page 298 | var hashKeys = window.location.hash.split( "#" ); 299 | var initialHashKeys = this.initialLocationHash.split( "#" ); 300 | 301 | // if we are not at the original hash then use history 302 | // otherwise, if it's the same starting hash as it was at init time, we 303 | // can't trigger back to close the dialog, as it might take us elsewhere. 304 | // so we have to go forward and create a new hash that does not have this 305 | // dialog's hash at the end 306 | if( window.Dialog.useHash ){ 307 | if( hashKeys.join("") !== initialHashKeys.join("") ){ 308 | window.history.back(); 309 | } else { 310 | var escapedRegexpHash = this 311 | .hash 312 | .replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); 313 | 314 | window.location.hash = window 315 | .location 316 | .hash 317 | .replace( new RegExp( "#" + escapedRegexpHash + "$" ), "" ); 318 | } 319 | } 320 | 321 | return; 322 | } 323 | 324 | this.$el.removeClass( cl.open ); 325 | 326 | this.$background.removeClass( cl.bkgdOpen ); 327 | 328 | this.isOpen = false; 329 | 330 | // we only want to throw focus on close if we aren't 331 | // opening a nested dialog or some other UI state 332 | if( this.focused && !this.isLastDialog()){ 333 | this.focused.focus(); 334 | } 335 | if( $( "." + pluginName + "." + cl.open ).length === 0 ){ 336 | $html.removeClass( cl.open ); 337 | w.scrollTo( 0, this.scroll ); 338 | } 339 | 340 | this.$el.off( ev.resize ); 341 | 342 | if( this.resizeEventName ) { 343 | $(w).off(this.resizeEventName); 344 | } 345 | 346 | this.$el.trigger( ev.closed ); 347 | }; 348 | }( this, window.jQuery )); 349 | 350 | /* 351 | * Simple jQuery Dialog Linker 352 | * https://github.com/filamentgroup/dialog 353 | * 354 | * Copyright (c) 2013 Filament Group, Inc. 355 | * Author: @scottjehl 356 | * Licensed under the MIT, GPL licenses. 357 | */ 358 | 359 | (function( w, $ ){ 360 | 361 | $( w.document ) 362 | // open on matching a[href=#id] click 363 | .bind( "click", function( e ){ 364 | 365 | var $a = $( e.target ).closest( "a" ); 366 | var link = $a.is( "[data-dialog-link]" ); 367 | var iframe = $a.is( "[data-dialog-iframe]" ); 368 | 369 | function createDialog(content){ 370 | var linkHref = $a.attr( "href" ); 371 | var dialogClasses = $a.attr( "data-dialog-addclass" ) || ""; 372 | var dialogLabelledBy = $a.attr( "data-dialog-labeledby" ) || ""; 373 | var dialogLabel = $a.attr( "data-dialog-label" ) || ""; 374 | 375 | var dialogNoHistory = 376 | $a.attr( "data-dialog-history" ) === "false" || 377 | !w.componentNamespace.Dialog.history; 378 | 379 | var id; 380 | 381 | if( linkHref ) { 382 | id = encodeURIComponent(linkHref); 383 | } 384 | 385 | // if there are two links in the page that point to the same url 386 | // then the same dialog will be reused and the content updated 387 | var $existing = $("[id='" + id + "']"); 388 | if( $existing.length ){ 389 | $existing 390 | .html("") 391 | .append(content) 392 | .dialog() 393 | .trigger("enhance") 394 | .trigger("dialog-update"); 395 | return; 396 | } 397 | 398 | $a 399 | .attr("href", "#" + id ) 400 | .removeAttr( "data-dialog-link" ); 401 | 402 | var $dialog = $( "
" ) 403 | .append( content ) 404 | .appendTo( "body" ) 405 | .dialog(); 406 | 407 | function open(){ 408 | $dialog.trigger( "dialog-open" ); 409 | } 410 | 411 | // make sure the opener link is set as the focued item if one is not defined already 412 | var instance = $dialog.data( "dialog" ); 413 | if( instance && !instance.focused ){ 414 | instance.focused = $a[ 0 ]; 415 | } 416 | 417 | if( iframe ){ 418 | $dialog.find( "iframe" ).one( "load", open ); 419 | } 420 | else { 421 | open(); 422 | } 423 | 424 | $dialog.trigger( "enhance" ); 425 | } 426 | 427 | if( link ){ 428 | var url = $a.attr( "href" ); 429 | 430 | // get content either from an iframe or not 431 | if( $a.is( "[data-dialog-iframe]" ) ){ 432 | createDialog( "" ); 433 | } 434 | else { 435 | $.get( url, createDialog ); 436 | } 437 | 438 | e.preventDefault(); 439 | } 440 | }); 441 | 442 | // if the hash matches an ajaxlink's url, open it by triggering a click on the ajaxlink 443 | $( w ).bind( "hashchange load", function(){ 444 | var hash = w.location.hash.split( "#" ).pop(); 445 | var id = hash.replace( /-dialog$/, "" ); 446 | var $ajaxLink = $( 'a[href="' + decodeURIComponent(id) +'"][data-dialog-link], a[href="' + id +'"][data-dialog-link]' ); 447 | // if the link specified nohistory, don't click it 448 | var nohistory = 449 | $ajaxLink.attr( "data-dialog-history" ) === "false" || 450 | !w.componentNamespace.Dialog.history; 451 | 452 | var $dialogInPage = $( '.dialog[id="' + id + '"]' ); 453 | if( $ajaxLink.length && !nohistory && !$dialogInPage.length ){ 454 | $ajaxLink.eq( 0 ).trigger( "click" ); 455 | } 456 | }); 457 | 458 | }( this, window.jQuery )); 459 | 460 | (function( w, $ ){ 461 | var Dialog = w.componentNamespace.Dialog, 462 | doc = w.document, 463 | pluginName = "dialog"; 464 | 465 | $.fn[ pluginName ] = function(){ 466 | return this.each(function(){ 467 | var $el = $( this ); 468 | 469 | // prevent double init 470 | if( $el.data( "dialog" ) ){ 471 | return; 472 | } 473 | 474 | var dialog = new Dialog( this ); 475 | var onOpen, onClose, onClick, onBackgroundClick; 476 | 477 | $el.addClass( Dialog.classes.content ) 478 | 479 | .bind( Dialog.events.open, onOpen = function(){ 480 | dialog.open(); 481 | }) 482 | .bind( Dialog.events.close, onClose = function(){ 483 | dialog.close(); 484 | }) 485 | .bind( "click", onClick = function( e ){ 486 | if( $(e.target).closest(Dialog.selectors.close).length ){ 487 | e.preventDefault(); 488 | dialog.close(); 489 | } 490 | }); 491 | 492 | dialog.$background.bind( "click", onBackgroundClick = function() { 493 | dialog.close(); 494 | }); 495 | 496 | var onHashchange; 497 | 498 | // on load and hashchange, open the dialog if its hash matches the last part of the hash, and close if it doesn't 499 | if( Dialog.useHash ){ 500 | $( w ).bind( "hashchange", onHashchange = function(){ 501 | var hash = w.location.hash.split( "#" ).pop(); 502 | 503 | // if the hash matches this dialog's, open! 504 | if( hash === dialog.hash ){ 505 | if( !dialog.nohistory ){ 506 | dialog.open(); 507 | } 508 | } 509 | // if it doesn't match... 510 | else { 511 | dialog.close(); 512 | } 513 | }); 514 | } 515 | 516 | var onDocClick, onKeyup, onResize; 517 | 518 | // open on matching a[href=#id] click 519 | $( doc ).bind( "click", onDocClick = function( e ){ 520 | var $matchingDialog, $a; 521 | 522 | $a = $( e.target ).closest( "a" ); 523 | 524 | 525 | if( !dialog.isOpen && $a.length && $a.attr( "href" ) ){ 526 | var id = $a.attr( "href" ).replace( /^#/, "" ); 527 | 528 | // catch invalid selector exceptions 529 | try { 530 | // Attempt to find the matching dialog at the same id or at the 531 | // encoded id. This allows matching even when href url ids are being 532 | // changed back and forth between encoded and decoded forms. 533 | $matchingDialog = 534 | $( "[id='" + id + "'], [id='" + encodeURIComponent(id) + "']" ); 535 | } catch ( error ) { 536 | // TODO should check the type of exception, it's not clear how well 537 | // the error name "SynatxError" is supported 538 | return; 539 | } 540 | 541 | if( $matchingDialog.length && $matchingDialog.is( $el ) ){ 542 | e.preventDefault(); 543 | $matchingDialog.trigger( Dialog.events.open ); 544 | } 545 | } 546 | }); 547 | 548 | // close on escape key 549 | $( doc ).bind( "keyup", onKeyup = function( e ){ 550 | if( e.which === 27 ){ 551 | dialog.close(); 552 | } 553 | }); 554 | 555 | dialog._checkInteractivity(); 556 | var resizepoll; 557 | $( window ).bind( "resize", onResize = function(){ 558 | if( resizepoll ){ 559 | clearTimeout( resizepoll ); 560 | } 561 | resizepoll = setTimeout( function(){ 562 | dialog._checkInteractivity.call( dialog ); 563 | }, 150 ); 564 | }); 565 | 566 | $el.bind("destroy", function(){ 567 | $(w).unbind("hashchange", onHashchange); 568 | 569 | $el 570 | .unbind( Dialog.events.open, onOpen ) 571 | .unbind( Dialog.events.close, onClose ) 572 | .unbind( "click", onClick ); 573 | 574 | dialog.$background.unbind( "click", onBackgroundClick); 575 | 576 | $( doc ).unbind( "click", onDocClick ); 577 | $( doc ).unbind( "keyup", onKeyup ); 578 | $( window ).unbind( "resize", onResize ); 579 | }); 580 | 581 | onHashchange(); 582 | 583 | window.focusRegistry.register(dialog); 584 | }); 585 | }; 586 | 587 | // auto-init on enhance 588 | $( w.document ).bind( "enhance", function( e ){ 589 | var target = e.target === w.document ? "" : e.target; 590 | $( "." + pluginName, e.target ).add( target ).filter( "." + pluginName )[ pluginName ](); 591 | }); 592 | 593 | function FocusRegistry(){ 594 | var self = this; 595 | 596 | this.registry = []; 597 | 598 | $(window.document).bind("focusin.focus-registry", function(event){ 599 | self.check(event); 600 | }); 601 | } 602 | 603 | FocusRegistry.prototype.register = function(obj){ 604 | if( !obj.checkFocus ){ 605 | throw new Error( "Obj must implement `checkFocus`" ); 606 | } 607 | 608 | if( !obj.stealFocus ){ 609 | throw new Error( "Obj must implement `stealFocus`" ); 610 | } 611 | 612 | this.registry.push(obj); 613 | }; 614 | 615 | FocusRegistry.prototype.unregister = function(obj){ 616 | var newRegistry = []; 617 | 618 | for(var i = 0; i < this.registry.length; i++ ){ 619 | if(this.registry[i] !== obj){ 620 | newRegistry.push(this.registry[i]); 621 | } 622 | } 623 | 624 | this.registry = newRegistry; 625 | }; 626 | 627 | FocusRegistry.prototype.check = function(event){ 628 | var stealing = []; 629 | 630 | // for all the registered components 631 | for(var i = 0; i < this.registry.length; i++){ 632 | 633 | // if a given component wants to steal the focus, record that 634 | if( this.registry[i].checkFocus(event) ){ 635 | stealing.push(this.registry[i]); 636 | } 637 | } 638 | 639 | // if more than one component wants to steal focus throw an exception 640 | if( stealing.length > 1 ){ 641 | throw new Error("Two components are attempting to steal focus."); 642 | } 643 | 644 | // otherwise allow the first component to steal focus 645 | if(stealing[0]) { 646 | event.preventDefault(); 647 | 648 | // let this event stack unwind and then steal the focus 649 | // which will again trigger the check above 650 | setTimeout(function(){ 651 | stealing[0].stealFocus(event); 652 | }); 653 | } 654 | }; 655 | 656 | // constructor in namespace 657 | window.componentNamespace.FocusRegistry = FocusRegistry; 658 | 659 | // singleton 660 | window.focusRegistry = new FocusRegistry(); 661 | }( this, window.jQuery )); 662 | -------------------------------------------------------------------------------- /dist/dialog.linker.min.js: -------------------------------------------------------------------------------- 1 | window.jQuery=window.jQuery||window.shoestring,function(a,b){a.componentNamespace=a.componentNamespace||a;var c,d,e="dialog",f=a.document,g=f.documentElement,h=f.body,i=b(g),j=a.componentNamespace.Dialog=function(d){if(this.$el=b(d),this.$el.data(e))return this.$el.data(e);this.$el.data(e,this),this.$background=this.$el.is("[data-"+e+"-nobg]")?b([]):b(f.createElement("div")).addClass(c.bkgd).attr("tabindex","-1").appendTo("body"),this.initialLocationHash=a.location.hash,this.nohistory="false"===this.$el.attr("data-dialog-history")||!j.history;var g=this.$el.attr("id");this.hash=g+"-dialog",this.hash+=this.nohistory?"-"+(new Date).getTime().toString():"",this.isOpen=!1,this.isTransparentBackground=this.$el.is("[data-transbg]"),g&&(this.resizeEventName="resize.dialog-"+g),this._addA11yAttrs()};j.history=!0,j.useHash=!0,j.events=d={open:e+"-open",opened:e+"-opened",close:e+"-close",closed:e+"-closed",resize:e+"-resize"},j.classes=c={open:e+"-open",opened:e+"-opened",content:e+"-content",close:e+"-close",closed:e+"-closed",bkgd:e+"-background",bkgdOpen:e+"-background-open",bkgdTrans:e+"-background-trans"},j.selectors={close:"."+j.classes.close+", [data-close], [data-dialog-close]"},j.prototype.destroy=function(){window.focusRegistry.unregister(this),this.$el.trigger("destroy"),this.$el.data()[e]=void 0,this.$background.remove()},j.prototype.checkFocus=function(a){var c,d=b(a.target);return c=this.isOpen&&!d.closest(this.$el[0]).length&&this.isLastDialog()&&!this._isNonInteractive()},j.prototype.stealFocus=function(){this.$el[0].focus()},j.prototype._addA11yAttrs=function(){this.$el.attr("role","dialog").attr("tabindex","-1").find(j.selectors.close).attr("role","button")},j.prototype._removeA11yAttrs=function(){this.$el.removeAttr("role"),this.$el.removeAttr("tabindex")},j.prototype._isNonInteractive=function(){var a,b=window.getComputedStyle(this.$el[0],null),c=this.$el.find(j.selectors.close)[0];c&&(a=window.getComputedStyle(c,null));var d=window.getComputedStyle(this.$background[0],null);return"none"!==b.getPropertyValue("display")&&"hidden"!==b.getPropertyValue("visibility")&&(!a||"none"===a.getPropertyValue("display"))&&"none"===d.getPropertyValue("display")},j.prototype._checkInteractivity=function(){this._isNonInteractive()?(this._removeA11yAttrs(),this._ariaShowUnrelatedElems()):this._addA11yAttrs()},j.prototype._ariaHideUnrelatedElems=function(){this._ariaShowUnrelatedElems();var a="script, style",c=this.$el.siblings().not(a);this.$el.parents().not("body, html").each(function(){c=c.add(b(this).siblings().not(a))}),c.each(function(){var a=b(this).attr("aria-hidden")||"";b(this).attr("data-dialog-aria-hidden",a).attr("aria-hidden","true")})},j.prototype._ariaShowUnrelatedElems=function(){b("[data-dialog-aria-hidden]").each(function(){b(this).attr("data-dialog-aria-hidden").match("true|false")?b(this).attr("aria-hidden",b(this).attr("data-dialog-aria-hidden")):b(this).removeAttr("aria-hidden")}).removeAttr("data-dialog-aria-hidden")},j.prototype.resizeBackground=function(){if(this.$background.length){var a=this.$background[0];a.style.display="none";var b=(this.scroll||0)+this.$el[0].clientHeight,c=Math.max(b,g.scrollHeight,g.clientHeight);a.style.height=c+"px",a.style.display=""}},j.prototype.open=function(){if(!this.isOpen){var e=this;this.$el.addClass(c.open),this.$background.addClass(c.bkgdOpen),this.$background.attr("id",this.$el.attr("id")+"-background"),this._setBackgroundTransparency(),this.scroll="pageYOffset"in a?a.pageYOffset:g.scrollY||g.scrollTop||h&&h.scrollY||0,this.$el[0].style.top=this.scroll+"px",this.resizeBackground(),i.addClass(c.open),this.isOpen=!0;var j=a.location.hash.replace(/^#/,"");if(a.Dialog.useHash&&(j.indexOf("-dialog")>-1&&!this.isLastDialog()?a.location.hash+="#"+this.hash:this.isLastDialog()||(a.location.hash=this.hash)),f.activeElement&&(this.focused=f.activeElement),this.$el[0].focus(),setTimeout(function(){e._ariaHideUnrelatedElems()}),this.$el.on(d.resize,function(){e.resizeBackground()}),this.resizeEventName){var k;b(a).on(this.resizeEventName,function(){a.clearTimeout(k),k=setTimeout(function(){e.resizeBackground()},50)})}this.$el.trigger(d.opened)}},j.prototype.lastHash=function(){return a.location.hash.split("#").pop()},j.prototype.isLastDialog=function(){return this.lastHash()===this.hash},j.prototype._setBackgroundTransparency=function(){this.isTransparentBackground&&this.$background.addClass(c.bkgdTrans)},j.prototype.close=function(){if(this.isOpen)if(this._ariaShowUnrelatedElems(),window.location.hash.split("#").pop()!==this.hash)this.$el.removeClass(c.open),this.$background.removeClass(c.bkgdOpen),this.isOpen=!1,this.focused&&!this.isLastDialog()&&this.focused.focus(),0===b("."+e+"."+c.open).length&&(i.removeClass(c.open),a.scrollTo(0,this.scroll)),this.$el.off(d.resize),this.resizeEventName&&b(a).off(this.resizeEventName),this.$el.trigger(d.closed);else{var f=window.location.hash.split("#"),g=this.initialLocationHash.split("#");if(window.Dialog.useHash)if(f.join("")!==g.join(""))window.history.back();else{var h=this.hash.replace(/([.*+?^=!:${}()|\[\]\/\\])/g,"\\$1");window.location.hash=window.location.hash.replace(new RegExp("#"+h+"$"),"")}}}}(this,window.jQuery),function(a,b){b(a.document).bind("click",function(c){function d(c){function d(){n.trigger("dialog-open")}var f,h=e.attr("href"),i=e.attr("data-dialog-addclass")||"",j=e.attr("data-dialog-labeledby")||"",k=e.attr("data-dialog-label")||"",l="false"===e.attr("data-dialog-history")||!a.componentNamespace.Dialog.history;h&&(f=encodeURIComponent(h));var m=b("[id='"+f+"']");if(m.length)return void m.html("").append(c).dialog().trigger("enhance").trigger("dialog-update");e.attr("href","#"+f).removeAttr("data-dialog-link");var n=b("
").append(c).appendTo("body").dialog(),o=n.data("dialog");o&&!o.focused&&(o.focused=e[0]),g?n.find("iframe").one("load",d):d(),n.trigger("enhance")}var e=b(c.target).closest("a"),f=e.is("[data-dialog-link]"),g=e.is("[data-dialog-iframe]");if(f){var h=e.attr("href");e.is("[data-dialog-iframe]")?d(""):b.get(h,d),c.preventDefault()}}),b(a).bind("hashchange load",function(){var c=a.location.hash.split("#").pop(),d=c.replace(/-dialog$/,""),e=b('a[href="'+decodeURIComponent(d)+'"][data-dialog-link], a[href="'+d+'"][data-dialog-link]'),f="false"===e.attr("data-dialog-history")||!a.componentNamespace.Dialog.history,g=b('.dialog[id="'+d+'"]');!e.length||f||g.length||e.eq(0).trigger("click")})}(this,window.jQuery),function(a,b){function c(){var a=this;this.registry=[],b(window.document).bind("focusin.focus-registry",function(b){a.check(b)})}var d=a.componentNamespace.Dialog,e=a.document,f="dialog";b.fn[f]=function(){return this.each(function(){var c=b(this);if(!c.data("dialog")){var f,g,h,i,j=new d(this);c.addClass(d.classes.content).bind(d.events.open,f=function(){j.open()}).bind(d.events.close,g=function(){j.close()}).bind("click",h=function(a){b(a.target).closest(d.selectors.close).length&&(a.preventDefault(),j.close())}),j.$background.bind("click",i=function(){j.close()});var k;d.useHash&&b(a).bind("hashchange",k=function(){var b=a.location.hash.split("#").pop();b===j.hash?j.nohistory||j.open():j.close()});var l,m,n;b(e).bind("click",l=function(a){var e,f;if(f=b(a.target).closest("a"),!j.isOpen&&f.length&&f.attr("href")){var g=f.attr("href").replace(/^#/,"");try{e=b("[id='"+g+"'],\t[id='"+encodeURIComponent(g)+"']")}catch(h){return}e.length&&e.is(c)&&(a.preventDefault(),e.trigger(d.events.open))}}),b(e).bind("keyup",m=function(a){27===a.which&&j.close()}),j._checkInteractivity();var o;b(window).bind("resize",n=function(){o&&clearTimeout(o),o=setTimeout(function(){j._checkInteractivity.call(j)},150)}),c.bind("destroy",function(){b(a).unbind("hashchange",k),c.unbind(d.events.open,f).unbind(d.events.close,g).unbind("click",h),j.$background.unbind("click",i),b(e).unbind("click",l),b(e).unbind("keyup",m),b(window).unbind("resize",n)}),k(),window.focusRegistry.register(j)}})},b(a.document).bind("enhance",function(c){var d=c.target===a.document?"":c.target;b("."+f,c.target).add(d).filter("."+f)[f]()}),c.prototype.register=function(a){if(!a.checkFocus)throw new Error("Obj must implement `checkFocus`");if(!a.stealFocus)throw new Error("Obj must implement `stealFocus`");this.registry.push(a)},c.prototype.unregister=function(a){for(var b=[],c=0;c1)throw new Error("Two components are attempting to steal focus.");b[0]&&(a.preventDefault(),setTimeout(function(){b[0].stealFocus(a)}))},window.componentNamespace.FocusRegistry=c,window.focusRegistry=new c}(this,window.jQuery); -------------------------------------------------------------------------------- /dist/dialog.min.js: -------------------------------------------------------------------------------- 1 | window.jQuery=window.jQuery||window.shoestring,function(a,b){a.componentNamespace=a.componentNamespace||a;var c,d,e="dialog",f=a.document,g=f.documentElement,h=f.body,i=b(g),j=a.componentNamespace.Dialog=function(d){if(this.$el=b(d),this.$el.data(e))return this.$el.data(e);this.$el.data(e,this),this.$background=this.$el.is("[data-"+e+"-nobg]")?b([]):b(f.createElement("div")).addClass(c.bkgd).attr("tabindex","-1").appendTo("body"),this.initialLocationHash=a.location.hash,this.nohistory="false"===this.$el.attr("data-dialog-history")||!j.history;var g=this.$el.attr("id");this.hash=g+"-dialog",this.hash+=this.nohistory?"-"+(new Date).getTime().toString():"",this.isOpen=!1,this.isTransparentBackground=this.$el.is("[data-transbg]"),g&&(this.resizeEventName="resize.dialog-"+g),this._addA11yAttrs()};j.history=!0,j.useHash=!0,j.events=d={open:e+"-open",opened:e+"-opened",close:e+"-close",closed:e+"-closed",resize:e+"-resize"},j.classes=c={open:e+"-open",opened:e+"-opened",content:e+"-content",close:e+"-close",closed:e+"-closed",bkgd:e+"-background",bkgdOpen:e+"-background-open",bkgdTrans:e+"-background-trans"},j.selectors={close:"."+j.classes.close+", [data-close], [data-dialog-close]"},j.prototype.destroy=function(){window.focusRegistry.unregister(this),this.$el.trigger("destroy"),this.$el.data()[e]=void 0,this.$background.remove()},j.prototype.checkFocus=function(a){var c,d=b(a.target);return c=this.isOpen&&!d.closest(this.$el[0]).length&&this.isLastDialog()&&!this._isNonInteractive()},j.prototype.stealFocus=function(){this.$el[0].focus()},j.prototype._addA11yAttrs=function(){this.$el.attr("role","dialog").attr("tabindex","-1").find(j.selectors.close).attr("role","button")},j.prototype._removeA11yAttrs=function(){this.$el.removeAttr("role"),this.$el.removeAttr("tabindex")},j.prototype._isNonInteractive=function(){var a,b=window.getComputedStyle(this.$el[0],null),c=this.$el.find(j.selectors.close)[0];c&&(a=window.getComputedStyle(c,null));var d=window.getComputedStyle(this.$background[0],null);return"none"!==b.getPropertyValue("display")&&"hidden"!==b.getPropertyValue("visibility")&&(!a||"none"===a.getPropertyValue("display"))&&"none"===d.getPropertyValue("display")},j.prototype._checkInteractivity=function(){this._isNonInteractive()?(this._removeA11yAttrs(),this._ariaShowUnrelatedElems()):this._addA11yAttrs()},j.prototype._ariaHideUnrelatedElems=function(){this._ariaShowUnrelatedElems();var a="script, style",c=this.$el.siblings().not(a);this.$el.parents().not("body, html").each(function(){c=c.add(b(this).siblings().not(a))}),c.each(function(){var a=b(this).attr("aria-hidden")||"";b(this).attr("data-dialog-aria-hidden",a).attr("aria-hidden","true")})},j.prototype._ariaShowUnrelatedElems=function(){b("[data-dialog-aria-hidden]").each(function(){b(this).attr("data-dialog-aria-hidden").match("true|false")?b(this).attr("aria-hidden",b(this).attr("data-dialog-aria-hidden")):b(this).removeAttr("aria-hidden")}).removeAttr("data-dialog-aria-hidden")},j.prototype.resizeBackground=function(){if(this.$background.length){var a=this.$background[0];a.style.display="none";var b=(this.scroll||0)+this.$el[0].clientHeight,c=Math.max(b,g.scrollHeight,g.clientHeight);a.style.height=c+"px",a.style.display=""}},j.prototype.open=function(){if(!this.isOpen){var e=this;this.$el.addClass(c.open),this.$background.addClass(c.bkgdOpen),this.$background.attr("id",this.$el.attr("id")+"-background"),this._setBackgroundTransparency(),this.scroll="pageYOffset"in a?a.pageYOffset:g.scrollY||g.scrollTop||h&&h.scrollY||0,this.$el[0].style.top=this.scroll+"px",this.resizeBackground(),i.addClass(c.open),this.isOpen=!0;var j=a.location.hash.replace(/^#/,"");if(a.Dialog.useHash&&(j.indexOf("-dialog")>-1&&!this.isLastDialog()?a.location.hash+="#"+this.hash:this.isLastDialog()||(a.location.hash=this.hash)),f.activeElement&&(this.focused=f.activeElement),this.$el[0].focus(),setTimeout(function(){e._ariaHideUnrelatedElems()}),this.$el.on(d.resize,function(){e.resizeBackground()}),this.resizeEventName){var k;b(a).on(this.resizeEventName,function(){a.clearTimeout(k),k=setTimeout(function(){e.resizeBackground()},50)})}this.$el.trigger(d.opened)}},j.prototype.lastHash=function(){return a.location.hash.split("#").pop()},j.prototype.isLastDialog=function(){return this.lastHash()===this.hash},j.prototype._setBackgroundTransparency=function(){this.isTransparentBackground&&this.$background.addClass(c.bkgdTrans)},j.prototype.close=function(){if(this.isOpen)if(this._ariaShowUnrelatedElems(),window.location.hash.split("#").pop()!==this.hash)this.$el.removeClass(c.open),this.$background.removeClass(c.bkgdOpen),this.isOpen=!1,this.focused&&!this.isLastDialog()&&this.focused.focus(),0===b("."+e+"."+c.open).length&&(i.removeClass(c.open),a.scrollTo(0,this.scroll)),this.$el.off(d.resize),this.resizeEventName&&b(a).off(this.resizeEventName),this.$el.trigger(d.closed);else{var f=window.location.hash.split("#"),g=this.initialLocationHash.split("#");if(window.Dialog.useHash)if(f.join("")!==g.join(""))window.history.back();else{var h=this.hash.replace(/([.*+?^=!:${}()|\[\]\/\\])/g,"\\$1");window.location.hash=window.location.hash.replace(new RegExp("#"+h+"$"),"")}}}}(this,window.jQuery),function(a,b){function c(){var a=this;this.registry=[],b(window.document).bind("focusin.focus-registry",function(b){a.check(b)})}var d=a.componentNamespace.Dialog,e=a.document,f="dialog";b.fn[f]=function(){return this.each(function(){var c=b(this);if(!c.data("dialog")){var f,g,h,i,j=new d(this);c.addClass(d.classes.content).bind(d.events.open,f=function(){j.open()}).bind(d.events.close,g=function(){j.close()}).bind("click",h=function(a){b(a.target).closest(d.selectors.close).length&&(a.preventDefault(),j.close())}),j.$background.bind("click",i=function(){j.close()});var k;d.useHash&&b(a).bind("hashchange",k=function(){var b=a.location.hash.split("#").pop();b===j.hash?j.nohistory||j.open():j.close()});var l,m,n;b(e).bind("click",l=function(a){var e,f;if(f=b(a.target).closest("a"),!j.isOpen&&f.length&&f.attr("href")){var g=f.attr("href").replace(/^#/,"");try{e=b("[id='"+g+"'],\t[id='"+encodeURIComponent(g)+"']")}catch(h){return}e.length&&e.is(c)&&(a.preventDefault(),e.trigger(d.events.open))}}),b(e).bind("keyup",m=function(a){27===a.which&&j.close()}),j._checkInteractivity();var o;b(window).bind("resize",n=function(){o&&clearTimeout(o),o=setTimeout(function(){j._checkInteractivity.call(j)},150)}),c.bind("destroy",function(){b(a).unbind("hashchange",k),c.unbind(d.events.open,f).unbind(d.events.close,g).unbind("click",h),j.$background.unbind("click",i),b(e).unbind("click",l),b(e).unbind("keyup",m),b(window).unbind("resize",n)}),k(),window.focusRegistry.register(j)}})},b(a.document).bind("enhance",function(c){var d=c.target===a.document?"":c.target;b("."+f,c.target).add(d).filter("."+f)[f]()}),c.prototype.register=function(a){if(!a.checkFocus)throw new Error("Obj must implement `checkFocus`");if(!a.stealFocus)throw new Error("Obj must implement `stealFocus`");this.registry.push(a)},c.prototype.unregister=function(a){for(var b=[],c=0;c1)throw new Error("Two components are attempting to steal focus.");b[0]&&(a.preventDefault(),setTimeout(function(){b[0].stealFocus(a)}))},window.componentNamespace.FocusRegistry=c,window.focusRegistry=new c}(this,window.jQuery); -------------------------------------------------------------------------------- /lib/shoestring-dev.js: -------------------------------------------------------------------------------- 1 | /*! Shoestring - v2.0.1 - 2017-09-25 2 | * http://github.com/filamentgroup/shoestring/ 3 | * Copyright (c) 2017 Scott Jehl, Filament Group, Inc; Licensed MIT & GPLv2 */ 4 | (function( factory ) { 5 | if( typeof define === 'function' && define.amd ) { 6 | // AMD. Register as an anonymous module. 7 | define( [ 'shoestring' ], factory ); 8 | } else if (typeof module === 'object' && module.exports) { 9 | // Node/CommonJS 10 | module.exports = factory(); 11 | } else { 12 | // Browser globals 13 | factory(); 14 | } 15 | }(function () { 16 | var win = typeof window !== "undefined" ? window : this; 17 | var doc = win.document; 18 | 19 | 20 | /** 21 | * The shoestring object constructor. 22 | * 23 | * @param {string,object} prim The selector to find or element to wrap. 24 | * @param {object} sec The context in which to match the `prim` selector. 25 | * @returns shoestring 26 | * @this window 27 | */ 28 | function shoestring( prim, sec ){ 29 | var pType = typeof( prim ), 30 | ret = [], 31 | sel; 32 | 33 | // return an empty shoestring object 34 | if( !prim ){ 35 | return new Shoestring( ret ); 36 | } 37 | 38 | // ready calls 39 | if( prim.call ){ 40 | return shoestring.ready( prim ); 41 | } 42 | 43 | // handle re-wrapping shoestring objects 44 | if( prim.constructor === Shoestring && !sec ){ 45 | return prim; 46 | } 47 | 48 | // if string starting with <, make html 49 | if( pType === "string" && prim.indexOf( "<" ) === 0 ){ 50 | var dfrag = doc.createElement( "div" ); 51 | 52 | dfrag.innerHTML = prim; 53 | 54 | // TODO depends on children (circular) 55 | return shoestring( dfrag ).children().each(function(){ 56 | dfrag.removeChild( this ); 57 | }); 58 | } 59 | 60 | // if string, it's a selector, use qsa 61 | if( pType === "string" ){ 62 | if( sec ){ 63 | return shoestring( sec ).find( prim ); 64 | } 65 | 66 | try { 67 | sel = doc.querySelectorAll( prim ); 68 | } catch( e ) { 69 | shoestring.error( 'queryselector', prim ); 70 | } 71 | 72 | return new Shoestring( sel, prim ); 73 | } 74 | 75 | // array like objects or node lists 76 | if( Object.prototype.toString.call( pType ) === '[object Array]' || 77 | (win.NodeList && prim instanceof win.NodeList) ){ 78 | 79 | return new Shoestring( prim, prim ); 80 | } 81 | 82 | // if it's an array, use all the elements 83 | if( prim.constructor === Array ){ 84 | return new Shoestring( prim, prim ); 85 | } 86 | 87 | // otherwise assume it's an object the we want at an index 88 | return new Shoestring( [prim], prim ); 89 | } 90 | 91 | var Shoestring = function( ret, prim ) { 92 | this.length = 0; 93 | this.selector = prim; 94 | shoestring.merge(this, ret); 95 | }; 96 | 97 | // TODO only required for tests 98 | Shoestring.prototype.reverse = [].reverse; 99 | 100 | // For adding element set methods 101 | shoestring.fn = Shoestring.prototype; 102 | 103 | shoestring.Shoestring = Shoestring; 104 | 105 | // For extending objects 106 | // TODO move to separate module when we use prototypes 107 | shoestring.extend = function( first, second ){ 108 | for( var i in second ){ 109 | if( second.hasOwnProperty( i ) ){ 110 | first[ i ] = second[ i ]; 111 | } 112 | } 113 | 114 | return first; 115 | }; 116 | 117 | // taken directly from jQuery 118 | shoestring.merge = function( first, second ) { 119 | var len, j, i; 120 | 121 | len = +second.length, 122 | j = 0, 123 | i = first.length; 124 | 125 | for ( ; j < len; j++ ) { 126 | first[ i++ ] = second[ j ]; 127 | } 128 | 129 | first.length = i; 130 | 131 | return first; 132 | }; 133 | 134 | // expose 135 | win.shoestring = shoestring; 136 | 137 | 138 | 139 | shoestring.enUS = { 140 | errors: { 141 | "prefix": "Shoestring does not support", 142 | 143 | "ajax-url-query": "data with urls that have existing query params", 144 | "children-selector" : "passing selectors into .child, try .children().filter( selector )", 145 | "click": "the click method. Try using .on( 'click', function(){}) or .trigger( 'click' ) instead.", 146 | "css-get" : "getting computed attributes from the DOM.", 147 | "data-attr-alias": "the data method aliased to `data-` DOM attributes.", 148 | "each-length": "objects without a length passed into each", 149 | "has-class" : "the hasClass method. Try using .is( '.klassname' ) instead.", 150 | "html-function" : "passing a function into .html. Try generating the html you're passing in an outside function", 151 | "index-shoestring-object": "an index call with a shoestring object argument. Use .get(0) on the argument instead.", 152 | "live-delegate" : "the .live or .delegate methods. Use .bind or .on instead.", 153 | "map": "the map method. Try using .each to make a new object.", 154 | "next-selector" : "passing selectors into .next, try .next().filter( selector )", 155 | "off-delegate" : ".off( events, selector, handler ) or .off( events, selector ). Use .off( eventName, callback ) instead.", 156 | "next-until" : "the .nextUntil method. Use .next in a loop until you reach the selector, don't include the selector", 157 | "on-delegate" : "the .on method with three or more arguments. Using .on( eventName, callback ) instead.", 158 | "outer-width": "the outerWidth method. Try combining .width() with .css for padding-left, padding-right, and the border of the left and right side.", 159 | "prev-selector" : "passing selectors into .prev, try .prev().filter( selector )", 160 | "prevall-selector" : "passing selectors into .prevAll, try .prevAll().filter( selector )", 161 | "queryselector": "all CSS selectors on querySelector (varies per browser support). Specifically, this failed: ", 162 | "siblings-selector": "passing selector into siblings not supported, try .siblings().find( ... )", 163 | "show-hide": "the show or hide methods. Use display: block (or whatever you'd like it to be) or none instead", 164 | "text-setter": "setting text via the .text method.", 165 | "toggle-class" : "the toggleClass method. Try using addClass or removeClass instead.", 166 | "trim": "the trim method. Use String.prototype.trim." 167 | } 168 | }; 169 | 170 | shoestring.error = function( id, str ) { 171 | var errors = shoestring.enUS.errors; 172 | throw new Error( errors.prefix + " " + errors[id] + ( str ? " " + str : "" ) ); 173 | }; 174 | 175 | 176 | 177 | /** 178 | * Make an HTTP request to a url. 179 | * 180 | * **NOTE** the following options are supported: 181 | * 182 | * - *method* - The HTTP method used with the request. Default: `GET`. 183 | * - *data* - Raw object with keys and values to pass with request as query params. Default `null`. 184 | * - *headers* - Set of request headers to add. Default `{}`. 185 | * - *async* - Whether the opened request is asynchronouse. Default `true`. 186 | * - *success* - Callback for successful request and response. Passed the response data. 187 | * - *error* - Callback for failed request and response. 188 | * - *cancel* - Callback for cancelled request and response. 189 | * 190 | * @param {string} url The url to request. 191 | * @param {object} options The options object, see Notes. 192 | * @return shoestring 193 | * @this shoestring 194 | */ 195 | 196 | shoestring.ajax = function( url, options ) { 197 | var params = "", req = new XMLHttpRequest(), settings, key; 198 | 199 | settings = shoestring.extend( {}, shoestring.ajax.settings ); 200 | 201 | if( options ){ 202 | shoestring.extend( settings, options ); 203 | } 204 | 205 | if( !url ){ 206 | url = settings.url; 207 | } 208 | 209 | if( !req || !url ){ 210 | return; 211 | } 212 | 213 | // create parameter string from data object 214 | if( settings.data ){ 215 | for( key in settings.data ){ 216 | if( settings.data.hasOwnProperty( key ) ){ 217 | if( params !== "" ){ 218 | params += "&"; 219 | } 220 | params += encodeURIComponent( key ) + "=" + 221 | encodeURIComponent( settings.data[key] ); 222 | } 223 | } 224 | } 225 | 226 | // append params to url for GET requests 227 | if( settings.method === "GET" && params ){ 228 | if( url.indexOf("?") >= 0 ){ 229 | shoestring.error( 'ajax-url-query' ); 230 | } 231 | 232 | url += "?" + params; 233 | } 234 | 235 | req.open( settings.method, url, settings.async ); 236 | 237 | if( req.setRequestHeader ){ 238 | req.setRequestHeader( "X-Requested-With", "XMLHttpRequest" ); 239 | 240 | // Set 'Content-type' header for POST requests 241 | if( settings.method === "POST" && params ){ 242 | req.setRequestHeader( "Content-type", "application/x-www-form-urlencoded" ); 243 | } 244 | 245 | for( key in settings.headers ){ 246 | if( settings.headers.hasOwnProperty( key ) ){ 247 | req.setRequestHeader(key, settings.headers[ key ]); 248 | } 249 | } 250 | } 251 | 252 | req.onreadystatechange = function () { 253 | if( req.readyState === 4 ){ 254 | // Trim the whitespace so shoestring('
') works 255 | var res = (req.responseText || '').replace(/^\s+|\s+$/g, ''); 256 | if( req.status.toString().indexOf( "0" ) === 0 ){ 257 | return settings.cancel( res, req.status, req ); 258 | } 259 | else if ( req.status.toString().match( /^(4|5)/ ) && RegExp.$1 ){ 260 | return settings.error( res, req.status, req ); 261 | } 262 | else if (settings.success) { 263 | return settings.success( res, req.status, req ); 264 | } 265 | } 266 | }; 267 | 268 | if( req.readyState === 4 ){ 269 | return req; 270 | } 271 | 272 | // Send request 273 | if( settings.method === "POST" && params ){ 274 | req.send( params ); 275 | } else { 276 | req.send(); 277 | } 278 | 279 | return req; 280 | }; 281 | 282 | shoestring.ajax.settings = { 283 | success: function(){}, 284 | error: function(){}, 285 | cancel: function(){}, 286 | method: "GET", 287 | async: true, 288 | data: null, 289 | headers: {} 290 | }; 291 | 292 | 293 | 294 | /** 295 | * Helper function wrapping a call to [ajax](ajax.js.html) using the `GET` method. 296 | * 297 | * @param {string} url The url to GET from. 298 | * @param {function} callback Callback to invoke on success. 299 | * @return shoestring 300 | * @this shoestring 301 | */ 302 | shoestring.get = function( url, callback ){ 303 | return shoestring.ajax( url, { success: callback } ); 304 | }; 305 | 306 | 307 | 308 | /** 309 | * Load the HTML response from `url` into the current set of elements. 310 | * 311 | * @param {string} url The url to GET from. 312 | * @param {function} callback Callback to invoke after HTML is inserted. 313 | * @return shoestring 314 | * @this shoestring 315 | */ 316 | shoestring.fn.load = function( url, callback ){ 317 | var self = this, 318 | args = arguments, 319 | intCB = function( data ){ 320 | self.each(function(){ 321 | shoestring( this ).html( data ); 322 | }); 323 | 324 | if( callback ){ 325 | callback.apply( self, args ); 326 | } 327 | }; 328 | 329 | shoestring.ajax( url, { success: intCB } ); 330 | return this; 331 | }; 332 | 333 | 334 | 335 | /** 336 | * Helper function wrapping a call to [ajax](ajax.js.html) using the `POST` method. 337 | * 338 | * @param {string} url The url to POST to. 339 | * @param {object} data The data to send. 340 | * @param {function} callback Callback to invoke on success. 341 | * @return shoestring 342 | * @this shoestring 343 | */ 344 | shoestring.post = function( url, data, callback ){ 345 | return shoestring.ajax( url, { data: data, method: "POST", success: callback } ); 346 | }; 347 | 348 | 349 | 350 | /** 351 | * Iterates over `shoestring` collections. 352 | * 353 | * @param {function} callback The callback to be invoked on each element and index 354 | * @return shoestring 355 | * @this shoestring 356 | */ 357 | shoestring.fn.each = function( callback ){ 358 | return shoestring.each( this, callback ); 359 | }; 360 | 361 | shoestring.each = function( collection, callback ) { 362 | var val; 363 | if( !( "length" in collection ) ) { 364 | shoestring.error( 'each-length' ); 365 | } 366 | for( var i = 0, il = collection.length; i < il; i++ ){ 367 | val = callback.call( collection[i], i, collection[i] ); 368 | if( val === false ){ 369 | break; 370 | } 371 | } 372 | 373 | return collection; 374 | }; 375 | 376 | 377 | 378 | /** 379 | * Check for array membership. 380 | * 381 | * @param {object} needle The thing to find. 382 | * @param {object} haystack The thing to find the needle in. 383 | * @return {boolean} 384 | * @this window 385 | */ 386 | shoestring.inArray = function( needle, haystack ){ 387 | var isin = -1; 388 | for( var i = 0, il = haystack.length; i < il; i++ ){ 389 | if( haystack.hasOwnProperty( i ) && haystack[ i ] === needle ){ 390 | isin = i; 391 | } 392 | } 393 | return isin; 394 | }; 395 | 396 | 397 | 398 | /** 399 | * Bind callbacks to be run when the DOM is "ready". 400 | * 401 | * @param {function} fn The callback to be run 402 | * @return shoestring 403 | * @this shoestring 404 | */ 405 | shoestring.ready = function( fn ){ 406 | if( ready && fn ){ 407 | fn.call( doc ); 408 | } 409 | else if( fn ){ 410 | readyQueue.push( fn ); 411 | } 412 | else { 413 | runReady(); 414 | } 415 | 416 | return [doc]; 417 | }; 418 | 419 | // TODO necessary? 420 | shoestring.fn.ready = function( fn ){ 421 | shoestring.ready( fn ); 422 | return this; 423 | }; 424 | 425 | // Empty and exec the ready queue 426 | var ready = false, 427 | readyQueue = [], 428 | runReady = function(){ 429 | if( !ready ){ 430 | while( readyQueue.length ){ 431 | readyQueue.shift().call( doc ); 432 | } 433 | ready = true; 434 | } 435 | }; 436 | 437 | // If DOM is already ready at exec time, depends on the browser. 438 | // From: https://github.com/mobify/mobifyjs/blob/526841be5509e28fc949038021799e4223479f8d/src/capture.js#L128 439 | if (doc.attachEvent ? doc.readyState === "complete" : doc.readyState !== "loading") { 440 | runReady(); 441 | } else { 442 | doc.addEventListener( "DOMContentLoaded", runReady, false ); 443 | doc.addEventListener( "readystatechange", runReady, false ); 444 | win.addEventListener( "load", runReady, false ); 445 | } 446 | 447 | 448 | 449 | /** 450 | * Checks the current set of elements against the selector, if one matches return `true`. 451 | * 452 | * @param {string} selector The selector to check. 453 | * @return {boolean} 454 | * @this {shoestring} 455 | */ 456 | shoestring.fn.is = function( selector ){ 457 | var ret = false, self = this, parents, check; 458 | 459 | // assume a dom element 460 | if( typeof selector !== "string" ){ 461 | // array-like, ie shoestring objects or element arrays 462 | if( selector.length && selector[0] ){ 463 | check = selector; 464 | } else { 465 | check = [selector]; 466 | } 467 | 468 | return _checkElements(this, check); 469 | } 470 | 471 | parents = this.parent(); 472 | 473 | if( !parents.length ){ 474 | parents = shoestring( doc ); 475 | } 476 | 477 | parents.each(function( i, e ) { 478 | var children; 479 | 480 | try { 481 | children = e.querySelectorAll( selector ); 482 | } catch( e ) { 483 | shoestring.error( 'queryselector', selector ); 484 | } 485 | 486 | ret = _checkElements( self, children ); 487 | }); 488 | 489 | return ret; 490 | }; 491 | 492 | function _checkElements(needles, haystack){ 493 | var ret = false; 494 | 495 | needles.each(function() { 496 | var j = 0; 497 | 498 | while( j < haystack.length ){ 499 | if( this === haystack[j] ){ 500 | ret = true; 501 | } 502 | 503 | j++; 504 | } 505 | }); 506 | 507 | return ret; 508 | } 509 | 510 | 511 | 512 | /** 513 | * Get data attached to the first element or set data values on all elements in the current set. 514 | * 515 | * @param {string} name The data attribute name. 516 | * @param {any} value The value assigned to the data attribute. 517 | * @return {any|shoestring} 518 | * @this shoestring 519 | */ 520 | shoestring.fn.data = function( name, value ){ 521 | if( name !== undefined ){ 522 | if( value !== undefined ){ 523 | return this.each(function(){ 524 | if( !this.shoestringData ){ 525 | this.shoestringData = {}; 526 | } 527 | 528 | this.shoestringData[ name ] = value; 529 | }); 530 | } 531 | else { 532 | if( this[ 0 ] ) { 533 | if( this[ 0 ].shoestringData ) { 534 | return this[ 0 ].shoestringData[ name ]; 535 | } 536 | if( shoestring( this[ 0 ] ).is( "[data-" + name + "]" ) ){ 537 | shoestring.error( 'data-attr-alias' ); 538 | } 539 | } 540 | } 541 | } 542 | else { 543 | return this[ 0 ] ? this[ 0 ].shoestringData || {} : undefined; 544 | } 545 | }; 546 | 547 | 548 | /** 549 | * Remove data associated with `name` or all the data, for each element in the current set. 550 | * 551 | * @param {string} name The data attribute name. 552 | * @return shoestring 553 | * @this shoestring 554 | */ 555 | shoestring.fn.removeData = function( name ){ 556 | return this.each(function(){ 557 | if( name !== undefined && this.shoestringData ){ 558 | this.shoestringData[ name ] = undefined; 559 | delete this.shoestringData[ name ]; 560 | } else { 561 | this[ 0 ].shoestringData = {}; 562 | } 563 | }); 564 | }; 565 | 566 | 567 | 568 | /** 569 | * An alias for the `shoestring` constructor. 570 | */ 571 | win.$ = shoestring; 572 | 573 | 574 | 575 | /** 576 | * Add a class to each DOM element in the set of elements. 577 | * 578 | * @param {string} className The name of the class to be added. 579 | * @return shoestring 580 | * @this shoestring 581 | */ 582 | shoestring.fn.addClass = function( className ){ 583 | var classes = className.replace(/^\s+|\s+$/g, '').split( " " ); 584 | 585 | return this.each(function(){ 586 | for( var i = 0, il = classes.length; i < il; i++ ){ 587 | if( this.className !== undefined && 588 | (this.className === "" || 589 | !this.className.match( new RegExp( "(^|\\s)" + classes[ i ] + "($|\\s)"))) ){ 590 | this.className += " " + classes[ i ]; 591 | } 592 | } 593 | }); 594 | }; 595 | 596 | 597 | 598 | /** 599 | * Add elements matching the selector to the current set. 600 | * 601 | * @param {string} selector The selector for the elements to add from the DOM 602 | * @return shoestring 603 | * @this shoestring 604 | */ 605 | shoestring.fn.add = function( selector ){ 606 | var ret = []; 607 | this.each(function(){ 608 | ret.push( this ); 609 | }); 610 | 611 | shoestring( selector ).each(function(){ 612 | ret.push( this ); 613 | }); 614 | 615 | return shoestring( ret ); 616 | }; 617 | 618 | 619 | 620 | /** 621 | * Insert an element or HTML string after each element in the current set. 622 | * 623 | * @param {string|HTMLElement} fragment The HTML or HTMLElement to insert. 624 | * @return shoestring 625 | * @this shoestring 626 | */ 627 | shoestring.fn.after = function( fragment ){ 628 | if( typeof( fragment ) === "string" || fragment.nodeType !== undefined ){ 629 | fragment = shoestring( fragment ); 630 | } 631 | 632 | if( fragment.length > 1 ){ 633 | fragment = fragment.reverse(); 634 | } 635 | return this.each(function( i ){ 636 | for( var j = 0, jl = fragment.length; j < jl; j++ ){ 637 | var insertEl = i > 0 ? fragment[ j ].cloneNode( true ) : fragment[ j ]; 638 | this.parentNode.insertBefore( insertEl, this.nextSibling ); 639 | } 640 | }); 641 | }; 642 | 643 | 644 | 645 | /** 646 | * Insert an element or HTML string as the last child of each element in the set. 647 | * 648 | * @param {string|HTMLElement} fragment The HTML or HTMLElement to insert. 649 | * @return shoestring 650 | * @this shoestring 651 | */ 652 | shoestring.fn.append = function( fragment ){ 653 | if( typeof( fragment ) === "string" || fragment.nodeType !== undefined ){ 654 | fragment = shoestring( fragment ); 655 | } 656 | 657 | return this.each(function( i ){ 658 | for( var j = 0, jl = fragment.length; j < jl; j++ ){ 659 | this.appendChild( i > 0 ? fragment[ j ].cloneNode( true ) : fragment[ j ] ); 660 | } 661 | }); 662 | }; 663 | 664 | 665 | 666 | /** 667 | * Insert the current set as the last child of the elements matching the selector. 668 | * 669 | * @param {string} selector The selector after which to append the current set. 670 | * @return shoestring 671 | * @this shoestring 672 | */ 673 | shoestring.fn.appendTo = function( selector ){ 674 | return this.each(function(){ 675 | shoestring( selector ).append( this ); 676 | }); 677 | }; 678 | 679 | 680 | 681 | /** 682 | * Get the value of the first element of the set or set the value of all the elements in the set. 683 | * 684 | * @param {string} name The attribute name. 685 | * @param {string} value The new value for the attribute. 686 | * @return {shoestring|string|undefined} 687 | * @this {shoestring} 688 | */ 689 | shoestring.fn.attr = function( name, value ){ 690 | var nameStr = typeof( name ) === "string"; 691 | 692 | if( value !== undefined || !nameStr ){ 693 | return this.each(function(){ 694 | if( nameStr ){ 695 | this.setAttribute( name, value ); 696 | } else { 697 | for( var i in name ){ 698 | if( name.hasOwnProperty( i ) ){ 699 | this.setAttribute( i, name[ i ] ); 700 | } 701 | } 702 | } 703 | }); 704 | } else { 705 | return this[ 0 ] ? this[ 0 ].getAttribute( name ) : undefined; 706 | } 707 | }; 708 | 709 | 710 | 711 | /** 712 | * Insert an element or HTML string before each element in the current set. 713 | * 714 | * @param {string|HTMLElement} fragment The HTML or HTMLElement to insert. 715 | * @return shoestring 716 | * @this shoestring 717 | */ 718 | shoestring.fn.before = function( fragment ){ 719 | if( typeof( fragment ) === "string" || fragment.nodeType !== undefined ){ 720 | fragment = shoestring( fragment ); 721 | } 722 | 723 | return this.each(function( i ){ 724 | for( var j = 0, jl = fragment.length; j < jl; j++ ){ 725 | this.parentNode.insertBefore( i > 0 ? fragment[ j ].cloneNode( true ) : fragment[ j ], this ); 726 | } 727 | }); 728 | }; 729 | 730 | 731 | 732 | /** 733 | * Get the children of the current collection. 734 | * @return shoestring 735 | * @this shoestring 736 | */ 737 | shoestring.fn.children = function(){ 738 | if( arguments.length > 0 ){ 739 | shoestring.error( 'children-selector' ); 740 | } 741 | var ret = [], 742 | childs, 743 | j; 744 | this.each(function(){ 745 | childs = this.children; 746 | j = -1; 747 | 748 | while( j++ < childs.length-1 ){ 749 | if( shoestring.inArray( childs[ j ], ret ) === -1 ){ 750 | ret.push( childs[ j ] ); 751 | } 752 | } 753 | }); 754 | return shoestring(ret); 755 | }; 756 | 757 | 758 | 759 | /** 760 | * Clone and return the current set of nodes into a new `shoestring` object. 761 | * 762 | * @return shoestring 763 | * @this shoestring 764 | */ 765 | shoestring.fn.clone = function() { 766 | var ret = []; 767 | 768 | this.each(function() { 769 | ret.push( this.cloneNode( true ) ); 770 | }); 771 | 772 | return shoestring( ret ); 773 | }; 774 | 775 | 776 | 777 | /** 778 | * Find an element matching the selector in the set of the current element and its parents. 779 | * 780 | * @param {string} selector The selector used to identify the target element. 781 | * @return shoestring 782 | * @this shoestring 783 | */ 784 | shoestring.fn.closest = function( selector ){ 785 | var ret = []; 786 | 787 | if( !selector ){ 788 | return shoestring( ret ); 789 | } 790 | 791 | this.each(function(){ 792 | var element, $self = shoestring( element = this ); 793 | 794 | if( $self.is(selector) ){ 795 | ret.push( this ); 796 | return; 797 | } 798 | 799 | while( element.parentElement ) { 800 | if( shoestring(element.parentElement).is(selector) ){ 801 | ret.push( element.parentElement ); 802 | break; 803 | } 804 | 805 | element = element.parentElement; 806 | } 807 | }); 808 | 809 | return shoestring( ret ); 810 | }; 811 | 812 | 813 | 814 | shoestring.cssExceptions = { 815 | 'float': [ 'cssFloat' ] 816 | }; 817 | 818 | 819 | 820 | (function() { 821 | var cssExceptions = shoestring.cssExceptions; 822 | 823 | // marginRight instead of margin-right 824 | function convertPropertyName( str ) { 825 | return str.replace( /\-([A-Za-z])/g, function ( match, character ) { 826 | return character.toUpperCase(); 827 | }); 828 | } 829 | 830 | function _getStyle( element, property ) { 831 | return win.getComputedStyle( element, null ).getPropertyValue( property ); 832 | } 833 | 834 | var vendorPrefixes = [ '', '-webkit-', '-ms-', '-moz-', '-o-', '-khtml-' ]; 835 | 836 | /** 837 | * Private function for getting the computed style of an element. 838 | * 839 | * **NOTE** Please use the [css](../css.js.html) method instead. 840 | * 841 | * @method _getStyle 842 | * @param {HTMLElement} element The element we want the style property for. 843 | * @param {string} property The css property we want the style for. 844 | */ 845 | shoestring._getStyle = function( element, property ) { 846 | var convert, value, j, k; 847 | 848 | if( cssExceptions[ property ] ) { 849 | for( j = 0, k = cssExceptions[ property ].length; j < k; j++ ) { 850 | value = _getStyle( element, cssExceptions[ property ][ j ] ); 851 | 852 | if( value ) { 853 | return value; 854 | } 855 | } 856 | } 857 | 858 | for( j = 0, k = vendorPrefixes.length; j < k; j++ ) { 859 | convert = convertPropertyName( vendorPrefixes[ j ] + property ); 860 | 861 | // VendorprefixKeyName || key-name 862 | value = _getStyle( element, convert ); 863 | 864 | if( convert !== property ) { 865 | value = value || _getStyle( element, property ); 866 | } 867 | 868 | if( vendorPrefixes[ j ] ) { 869 | // -vendorprefix-key-name 870 | value = value || _getStyle( element, vendorPrefixes[ j ] + property ); 871 | } 872 | 873 | if( value ) { 874 | return value; 875 | } 876 | } 877 | 878 | return undefined; 879 | }; 880 | })(); 881 | 882 | 883 | 884 | (function() { 885 | var cssExceptions = shoestring.cssExceptions; 886 | 887 | // marginRight instead of margin-right 888 | function convertPropertyName( str ) { 889 | return str.replace( /\-([A-Za-z])/g, function ( match, character ) { 890 | return character.toUpperCase(); 891 | }); 892 | } 893 | 894 | /** 895 | * Private function for setting the style of an element. 896 | * 897 | * **NOTE** Please use the [css](../css.js.html) method instead. 898 | * 899 | * @method _setStyle 900 | * @param {HTMLElement} element The element we want to style. 901 | * @param {string} property The property being used to style the element. 902 | * @param {string} value The css value for the style property. 903 | */ 904 | shoestring._setStyle = function( element, property, value ) { 905 | var convertedProperty = convertPropertyName(property); 906 | 907 | element.style[ property ] = value; 908 | 909 | if( convertedProperty !== property ) { 910 | element.style[ convertedProperty ] = value; 911 | } 912 | 913 | if( cssExceptions[ property ] ) { 914 | for( var j = 0, k = cssExceptions[ property ].length; j -1 ){ 1011 | ret.push( this ); 1012 | } 1013 | } 1014 | }); 1015 | 1016 | return shoestring( ret ); 1017 | }; 1018 | 1019 | 1020 | 1021 | /** 1022 | * Find descendant elements of the current collection. 1023 | * 1024 | * @param {string} selector The selector used to find the children 1025 | * @return shoestring 1026 | * @this shoestring 1027 | */ 1028 | shoestring.fn.find = function( selector ){ 1029 | var ret = [], 1030 | finds; 1031 | this.each(function(){ 1032 | try { 1033 | finds = this.querySelectorAll( selector ); 1034 | } catch( e ) { 1035 | shoestring.error( 'queryselector', selector ); 1036 | } 1037 | 1038 | for( var i = 0, il = finds.length; i < il; i++ ){ 1039 | ret = ret.concat( finds[i] ); 1040 | } 1041 | }); 1042 | return shoestring( ret ); 1043 | }; 1044 | 1045 | 1046 | 1047 | /** 1048 | * Returns the first element of the set wrapped in a new `shoestring` object. 1049 | * 1050 | * @return shoestring 1051 | * @this shoestring 1052 | */ 1053 | shoestring.fn.first = function(){ 1054 | return this.eq( 0 ); 1055 | }; 1056 | 1057 | 1058 | 1059 | /** 1060 | * Returns the raw DOM node at the passed index. 1061 | * 1062 | * @param {integer} index The index of the element to wrap and return. 1063 | * @return {HTMLElement|undefined|array} 1064 | * @this shoestring 1065 | */ 1066 | shoestring.fn.get = function( index ){ 1067 | 1068 | // return an array of elements if index is undefined 1069 | if( index === undefined ){ 1070 | var elements = []; 1071 | 1072 | for( var i = 0; i < this.length; i++ ){ 1073 | elements.push( this[ i ] ); 1074 | } 1075 | 1076 | return elements; 1077 | } else { 1078 | return this[ index ]; 1079 | } 1080 | }; 1081 | 1082 | 1083 | 1084 | /** 1085 | * Private function for setting/getting the offset property for height/width. 1086 | * 1087 | * **NOTE** Please use the [width](width.js.html) or [height](height.js.html) methods instead. 1088 | * 1089 | * @param {shoestring} set The set of elements. 1090 | * @param {string} name The string "height" or "width". 1091 | * @param {float|undefined} value The value to assign. 1092 | * @return shoestring 1093 | * @this window 1094 | */ 1095 | shoestring._dimension = function( set, name, value ){ 1096 | var offsetName; 1097 | 1098 | if( value === undefined ){ 1099 | offsetName = name.replace(/^[a-z]/, function( letter ) { 1100 | return letter.toUpperCase(); 1101 | }); 1102 | 1103 | return set[ 0 ][ "offset" + offsetName ]; 1104 | } else { 1105 | // support integer values as pixels 1106 | value = typeof value === "string" ? value : value + "px"; 1107 | 1108 | return set.each(function(){ 1109 | this.style[ name ] = value; 1110 | }); 1111 | } 1112 | }; 1113 | 1114 | 1115 | 1116 | /** 1117 | * Gets the height value of the first element or sets the height for the whole set. 1118 | * 1119 | * @param {float|undefined} value The value to assign. 1120 | * @return shoestring 1121 | * @this shoestring 1122 | */ 1123 | shoestring.fn.height = function( value ){ 1124 | return shoestring._dimension( this, "height", value ); 1125 | }; 1126 | 1127 | 1128 | 1129 | var set = function( html ){ 1130 | if( typeof html === "string" || typeof html === "number" ){ 1131 | return this.each(function(){ 1132 | this.innerHTML = "" + html; 1133 | }); 1134 | } else { 1135 | var h = ""; 1136 | if( typeof html.length !== "undefined" ){ 1137 | for( var i = 0, l = html.length; i < l; i++ ){ 1138 | h += html[i].outerHTML; 1139 | } 1140 | } else { 1141 | h = html.outerHTML; 1142 | } 1143 | return this.each(function(){ 1144 | this.innerHTML = h; 1145 | }); 1146 | } 1147 | }; 1148 | /** 1149 | * Gets or sets the `innerHTML` from all the elements in the set. 1150 | * 1151 | * @param {string|undefined} html The html to assign 1152 | * @return {string|shoestring} 1153 | * @this shoestring 1154 | */ 1155 | shoestring.fn.html = function( html ){ 1156 | if( !!html && typeof html === "function" ){ 1157 | shoestring.error( 'html-function' ); 1158 | } 1159 | if( typeof html !== "undefined" ){ 1160 | return set.call( this, html ); 1161 | } else { // get 1162 | var pile = ""; 1163 | 1164 | this.each(function(){ 1165 | pile += this.innerHTML; 1166 | }); 1167 | 1168 | return pile; 1169 | } 1170 | }; 1171 | 1172 | 1173 | 1174 | (function() { 1175 | function _getIndex( set, test ) { 1176 | var i, result, element; 1177 | 1178 | for( i = result = 0; i < set.length; i++ ) { 1179 | element = set.item ? set.item(i) : set[i]; 1180 | 1181 | if( test(element) ){ 1182 | return result; 1183 | } 1184 | 1185 | // ignore text nodes, etc 1186 | // NOTE may need to be more permissive 1187 | if( element.nodeType === 1 ){ 1188 | result++; 1189 | } 1190 | } 1191 | 1192 | return -1; 1193 | } 1194 | 1195 | /** 1196 | * Find the index in the current set for the passed selector. 1197 | * Without a selector it returns the index of the first node within the array of its siblings. 1198 | * 1199 | * @param {string|undefined} selector The selector used to search for the index. 1200 | * @return {integer} 1201 | * @this {shoestring} 1202 | */ 1203 | shoestring.fn.index = function( selector ){ 1204 | var self = this; 1205 | var children; 1206 | 1207 | // no arg? check the children, otherwise check each element that matches 1208 | if( selector === undefined ){ 1209 | children = this[0] && this[0].parentNode ? this[0].parentNode.childNodes : []; 1210 | 1211 | // check if the element matches the first of the set 1212 | return _getIndex(children, function( element ) { 1213 | return self[0] === element; 1214 | }); 1215 | } else { 1216 | if( selector.constructor === shoestring.Shoestring ) { 1217 | shoestring.error( "index-shoestring-object" ); 1218 | } 1219 | 1220 | // check if the element matches the first selected node from the parent 1221 | return _getIndex(self, function( element ) { 1222 | return element === shoestring( selector, element.parentNode )[0]; 1223 | }); 1224 | } 1225 | }; 1226 | })(); 1227 | 1228 | 1229 | 1230 | /** 1231 | * Insert the current set after the elements matching the selector. 1232 | * 1233 | * @param {string} selector The selector after which to insert the current set. 1234 | * @return shoestring 1235 | * @this shoestring 1236 | */ 1237 | shoestring.fn.insertAfter = function( selector ){ 1238 | return this.each(function(){ 1239 | shoestring( selector ).after( this ); 1240 | }); 1241 | }; 1242 | 1243 | 1244 | 1245 | /** 1246 | * Insert the current set before the elements matching the selector. 1247 | * 1248 | * @param {string} selector The selector before which to insert the current set. 1249 | * @return shoestring 1250 | * @this shoestring 1251 | */ 1252 | shoestring.fn.insertBefore = function( selector ){ 1253 | return this.each(function(){ 1254 | shoestring( selector ).before( this ); 1255 | }); 1256 | }; 1257 | 1258 | 1259 | 1260 | /** 1261 | * Returns the last element of the set wrapped in a new `shoestring` object. 1262 | * 1263 | * @return shoestring 1264 | * @this shoestring 1265 | */ 1266 | shoestring.fn.last = function(){ 1267 | return this.eq( this.length - 1 ); 1268 | }; 1269 | 1270 | 1271 | 1272 | /** 1273 | * Returns a `shoestring` object with the set of siblings of each element in the original set. 1274 | * 1275 | * @return shoestring 1276 | * @this shoestring 1277 | */ 1278 | shoestring.fn.next = function(){ 1279 | if( arguments.length > 0 ){ 1280 | shoestring.error( 'next-selector' ); 1281 | } 1282 | 1283 | var result = []; 1284 | 1285 | // TODO need to implement map 1286 | this.each(function() { 1287 | var children, item, found; 1288 | 1289 | // get the child nodes for this member of the set 1290 | children = shoestring( this.parentNode )[0].childNodes; 1291 | 1292 | for( var i = 0; i < children.length; i++ ){ 1293 | item = children.item( i ); 1294 | 1295 | // found the item we needed (found) which means current item value is 1296 | // the next node in the list, as long as it's viable grab it 1297 | // NOTE may need to be more permissive 1298 | if( found && item.nodeType === 1 ){ 1299 | result.push( item ); 1300 | break; 1301 | } 1302 | 1303 | // find the current item and mark it as found 1304 | if( item === this ){ 1305 | found = true; 1306 | } 1307 | } 1308 | }); 1309 | 1310 | return shoestring( result ); 1311 | }; 1312 | 1313 | 1314 | 1315 | /** 1316 | * Removes elements from the current set. 1317 | * 1318 | * @param {string} selector The selector to use when removing the elements. 1319 | * @return shoestring 1320 | * @this shoestring 1321 | */ 1322 | shoestring.fn.not = function( selector ){ 1323 | var ret = []; 1324 | 1325 | this.each(function(){ 1326 | var found = shoestring( selector, this.parentNode ); 1327 | 1328 | if( shoestring.inArray(this, found) === -1 ){ 1329 | ret.push( this ); 1330 | } 1331 | }); 1332 | 1333 | return shoestring( ret ); 1334 | }; 1335 | 1336 | 1337 | 1338 | /** 1339 | * Returns an object with the `top` and `left` properties corresponging to the first elements offsets. 1340 | * 1341 | * @return object 1342 | * @this shoestring 1343 | */ 1344 | shoestring.fn.offset = function(){ 1345 | return { 1346 | top: this[ 0 ].offsetTop, 1347 | left: this[ 0 ].offsetLeft 1348 | }; 1349 | }; 1350 | 1351 | 1352 | 1353 | /** 1354 | * Returns the set of first parents for each element in the current set. 1355 | * 1356 | * @return shoestring 1357 | * @this shoestring 1358 | */ 1359 | shoestring.fn.parent = function(){ 1360 | var ret = [], 1361 | parent; 1362 | 1363 | this.each(function(){ 1364 | // no parent node, assume top level 1365 | // jQuery parent: return the document object for or the parent node if it exists 1366 | parent = (this === doc.documentElement ? doc : this.parentNode); 1367 | 1368 | // if there is a parent and it's not a document fragment 1369 | if( parent && parent.nodeType !== 11 ){ 1370 | ret.push( parent ); 1371 | } 1372 | }); 1373 | 1374 | return shoestring(ret); 1375 | }; 1376 | 1377 | 1378 | 1379 | /** 1380 | * Returns the set of all parents matching the selector if provided for each element in the current set. 1381 | * 1382 | * @param {string} selector The selector to check the parents with. 1383 | * @return shoestring 1384 | * @this shoestring 1385 | */ 1386 | shoestring.fn.parents = function( selector ){ 1387 | var ret = []; 1388 | 1389 | this.each(function(){ 1390 | var curr = this, match; 1391 | 1392 | while( curr.parentElement && !match ){ 1393 | curr = curr.parentElement; 1394 | 1395 | if( selector ){ 1396 | if( curr === shoestring( selector )[0] ){ 1397 | match = true; 1398 | 1399 | if( shoestring.inArray( curr, ret ) === -1 ){ 1400 | ret.push( curr ); 1401 | } 1402 | } 1403 | } else { 1404 | if( shoestring.inArray( curr, ret ) === -1 ){ 1405 | ret.push( curr ); 1406 | } 1407 | } 1408 | } 1409 | }); 1410 | 1411 | return shoestring(ret); 1412 | }; 1413 | 1414 | 1415 | 1416 | /** 1417 | * Add an HTML string or element before the children of each element in the current set. 1418 | * 1419 | * @param {string|HTMLElement} fragment The HTML string or element to add. 1420 | * @return shoestring 1421 | * @this shoestring 1422 | */ 1423 | shoestring.fn.prepend = function( fragment ){ 1424 | if( typeof( fragment ) === "string" || fragment.nodeType !== undefined ){ 1425 | fragment = shoestring( fragment ); 1426 | } 1427 | 1428 | return this.each(function( i ){ 1429 | 1430 | for( var j = 0, jl = fragment.length; j < jl; j++ ){ 1431 | var insertEl = i > 0 ? fragment[ j ].cloneNode( true ) : fragment[ j ]; 1432 | if ( this.firstChild ){ 1433 | this.insertBefore( insertEl, this.firstChild ); 1434 | } else { 1435 | this.appendChild( insertEl ); 1436 | } 1437 | } 1438 | }); 1439 | }; 1440 | 1441 | 1442 | 1443 | /** 1444 | * Add each element of the current set before the children of the selected elements. 1445 | * 1446 | * @param {string} selector The selector for the elements to add the current set to.. 1447 | * @return shoestring 1448 | * @this shoestring 1449 | */ 1450 | shoestring.fn.prependTo = function( selector ){ 1451 | return this.each(function(){ 1452 | shoestring( selector ).prepend( this ); 1453 | }); 1454 | }; 1455 | 1456 | 1457 | 1458 | /** 1459 | * Returns a `shoestring` object with the set of *one* siblingx before each element in the original set. 1460 | * 1461 | * @return shoestring 1462 | * @this shoestring 1463 | */ 1464 | shoestring.fn.prev = function(){ 1465 | if( arguments.length > 0 ){ 1466 | shoestring.error( 'prev-selector' ); 1467 | } 1468 | 1469 | var result = []; 1470 | 1471 | // TODO need to implement map 1472 | this.each(function() { 1473 | var children, item, found; 1474 | 1475 | // get the child nodes for this member of the set 1476 | children = shoestring( this.parentNode )[0].childNodes; 1477 | 1478 | for( var i = children.length -1; i >= 0; i-- ){ 1479 | item = children.item( i ); 1480 | 1481 | // found the item we needed (found) which means current item value is 1482 | // the next node in the list, as long as it's viable grab it 1483 | // NOTE may need to be more permissive 1484 | if( found && item.nodeType === 1 ){ 1485 | result.push( item ); 1486 | break; 1487 | } 1488 | 1489 | // find the current item and mark it as found 1490 | if( item === this ){ 1491 | found = true; 1492 | } 1493 | } 1494 | }); 1495 | 1496 | return shoestring( result ); 1497 | }; 1498 | 1499 | 1500 | 1501 | /** 1502 | * Returns a `shoestring` object with the set of *all* siblings before each element in the original set. 1503 | * 1504 | * @return shoestring 1505 | * @this shoestring 1506 | */ 1507 | shoestring.fn.prevAll = function(){ 1508 | if( arguments.length > 0 ){ 1509 | shoestring.error( 'prevall-selector' ); 1510 | } 1511 | 1512 | var result = []; 1513 | 1514 | this.each(function() { 1515 | var $previous = shoestring( this ).prev(); 1516 | 1517 | while( $previous.length ){ 1518 | result.push( $previous[0] ); 1519 | $previous = $previous.prev(); 1520 | } 1521 | }); 1522 | 1523 | return shoestring( result ); 1524 | }; 1525 | 1526 | 1527 | 1528 | // Property normalization, a subset taken from jQuery src 1529 | shoestring.propFix = { 1530 | "class": "className", 1531 | contenteditable: "contentEditable", 1532 | "for": "htmlFor", 1533 | readonly: "readOnly", 1534 | tabindex: "tabIndex" 1535 | }; 1536 | 1537 | 1538 | 1539 | /** 1540 | * Gets the property value from the first element or sets the property value on all elements of the currrent set. 1541 | * 1542 | * @param {string} name The property name. 1543 | * @param {any} value The property value. 1544 | * @return {any|shoestring} 1545 | * @this shoestring 1546 | */ 1547 | shoestring.fn.prop = function( name, value ){ 1548 | if( !this[0] ){ 1549 | return; 1550 | } 1551 | 1552 | name = shoestring.propFix[ name ] || name; 1553 | 1554 | if( value !== undefined ){ 1555 | return this.each(function(){ 1556 | this[ name ] = value; 1557 | }); 1558 | } else { 1559 | return this[ 0 ][ name ]; 1560 | } 1561 | }; 1562 | 1563 | 1564 | 1565 | /** 1566 | * Remove an attribute from each element in the current set. 1567 | * 1568 | * @param {string} name The name of the attribute. 1569 | * @return shoestring 1570 | * @this shoestring 1571 | */ 1572 | shoestring.fn.removeAttr = function( name ){ 1573 | return this.each(function(){ 1574 | this.removeAttribute( name ); 1575 | }); 1576 | }; 1577 | 1578 | 1579 | 1580 | /** 1581 | * Remove a class from each DOM element in the set of elements. 1582 | * 1583 | * @param {string} className The name of the class to be removed. 1584 | * @return shoestring 1585 | * @this shoestring 1586 | */ 1587 | shoestring.fn.removeClass = function( cname ){ 1588 | var classes = cname.replace(/^\s+|\s+$/g, '').split( " " ); 1589 | 1590 | return this.each(function(){ 1591 | var newClassName, regex; 1592 | 1593 | for( var i = 0, il = classes.length; i < il; i++ ){ 1594 | if( this.className !== undefined ){ 1595 | regex = new RegExp( "(^|\\s)" + classes[ i ] + "($|\\s)", "gmi" ); 1596 | newClassName = this.className.replace( regex, " " ); 1597 | 1598 | this.className = newClassName.replace(/^\s+|\s+$/g, ''); 1599 | } 1600 | } 1601 | }); 1602 | }; 1603 | 1604 | 1605 | 1606 | /** 1607 | * Remove the current set of elements from the DOM. 1608 | * 1609 | * @return shoestring 1610 | * @this shoestring 1611 | */ 1612 | shoestring.fn.remove = function(){ 1613 | return this.each(function(){ 1614 | if( this.parentNode ) { 1615 | this.parentNode.removeChild( this ); 1616 | } 1617 | }); 1618 | }; 1619 | 1620 | 1621 | 1622 | /** 1623 | * Remove a proprety from each element in the current set. 1624 | * 1625 | * @param {string} name The name of the property. 1626 | * @return shoestring 1627 | * @this shoestring 1628 | */ 1629 | shoestring.fn.removeProp = function( property ){ 1630 | var name = shoestring.propFix[ property ] || property; 1631 | 1632 | return this.each(function(){ 1633 | this[ name ] = undefined; 1634 | delete this[ name ]; 1635 | }); 1636 | }; 1637 | 1638 | 1639 | 1640 | /** 1641 | * Replace each element in the current set with that argument HTML string or HTMLElement. 1642 | * 1643 | * @param {string|HTMLElement} fragment The value to assign. 1644 | * @return shoestring 1645 | * @this shoestring 1646 | */ 1647 | shoestring.fn.replaceWith = function( fragment ){ 1648 | if( typeof( fragment ) === "string" ){ 1649 | fragment = shoestring( fragment ); 1650 | } 1651 | 1652 | var ret = []; 1653 | 1654 | if( fragment.length > 1 ){ 1655 | fragment = fragment.reverse(); 1656 | } 1657 | this.each(function( i ){ 1658 | var clone = this.cloneNode( true ), 1659 | insertEl; 1660 | ret.push( clone ); 1661 | 1662 | // If there is no parentNode, this is pointless, drop it. 1663 | if( !this.parentNode ){ return; } 1664 | 1665 | if( fragment.length === 1 ){ 1666 | insertEl = i > 0 ? fragment[ 0 ].cloneNode( true ) : fragment[ 0 ]; 1667 | this.parentNode.replaceChild( insertEl, this ); 1668 | } else { 1669 | for( var j = 0, jl = fragment.length; j < jl; j++ ){ 1670 | insertEl = i > 0 ? fragment[ j ].cloneNode( true ) : fragment[ j ]; 1671 | this.parentNode.insertBefore( insertEl, this.nextSibling ); 1672 | } 1673 | this.parentNode.removeChild( this ); 1674 | } 1675 | }); 1676 | 1677 | return shoestring( ret ); 1678 | }; 1679 | 1680 | 1681 | 1682 | shoestring.inputTypes = [ 1683 | "text", 1684 | "hidden", 1685 | "password", 1686 | "color", 1687 | "date", 1688 | "datetime", 1689 | // "datetime\-local" matched by datetime 1690 | "email", 1691 | "month", 1692 | "number", 1693 | "range", 1694 | "search", 1695 | "tel", 1696 | "time", 1697 | "url", 1698 | "week" 1699 | ]; 1700 | 1701 | shoestring.inputTypeTest = new RegExp( shoestring.inputTypes.join( "|" ) ); 1702 | 1703 | 1704 | /** 1705 | * Serialize child input element values into an object. 1706 | * 1707 | * @return shoestring 1708 | * @this shoestring 1709 | */ 1710 | shoestring.fn.serialize = function(){ 1711 | var data = {}; 1712 | 1713 | shoestring( "input, select", this ).each(function(){ 1714 | var type = this.type, name = this.name, value = this.value; 1715 | 1716 | if( shoestring.inputTypeTest.test( type ) || 1717 | ( type === "checkbox" || type === "radio" ) && 1718 | this.checked ){ 1719 | 1720 | data[ name ] = value; 1721 | } else if( this.nodeName === "SELECT" ){ 1722 | data[ name ] = this.options[ this.selectedIndex ].nodeValue; 1723 | } 1724 | }); 1725 | 1726 | return data; 1727 | }; 1728 | 1729 | 1730 | 1731 | /** 1732 | * Get all of the sibling elements for each element in the current set. 1733 | * 1734 | * @return shoestring 1735 | * @this shoestring 1736 | */ 1737 | shoestring.fn.siblings = function(){ 1738 | if( arguments.length > 0 ) { 1739 | shoestring.error( 'siblings-selector' ); 1740 | } 1741 | 1742 | if( !this.length ) { 1743 | return shoestring( [] ); 1744 | } 1745 | 1746 | var sibs = [], el; 1747 | 1748 | el = (this[ 0 ].parentNode || {}).firstChild; 1749 | 1750 | while( el ) { 1751 | if( el.nodeType === 1 && el !== this[ 0 ] ) { 1752 | sibs.push( el ); 1753 | } 1754 | 1755 | el = el.nextSibling; 1756 | } 1757 | 1758 | return shoestring( sibs ); 1759 | }; 1760 | 1761 | 1762 | 1763 | var getText = function( elem ){ 1764 | var node, 1765 | ret = "", 1766 | i = 0, 1767 | nodeType = elem.nodeType; 1768 | 1769 | if ( !nodeType ) { 1770 | // If no nodeType, this is expected to be an array 1771 | while ( (node = elem[i++]) ) { 1772 | // Do not traverse comment nodes 1773 | ret += getText( node ); 1774 | } 1775 | } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { 1776 | // Use textContent for elements 1777 | // innerText usage removed for consistency of new lines (jQuery #11153) 1778 | if ( typeof elem.textContent === "string" ) { 1779 | return elem.textContent; 1780 | } else { 1781 | // Traverse its children 1782 | for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { 1783 | ret += getText( elem ); 1784 | } 1785 | } 1786 | } else if ( nodeType === 3 || nodeType === 4 ) { 1787 | return elem.nodeValue; 1788 | } 1789 | // Do not include comment or processing instruction nodes 1790 | 1791 | return ret; 1792 | }; 1793 | 1794 | /** 1795 | * Recursively retrieve the text content of the each element in the current set. 1796 | * 1797 | * @return shoestring 1798 | * @this shoestring 1799 | */ 1800 | shoestring.fn.text = function() { 1801 | if( arguments.length > 0 ){ 1802 | shoestring.error( 'text-setter' ); 1803 | } 1804 | 1805 | return getText( this ); 1806 | }; 1807 | 1808 | 1809 | 1810 | 1811 | /** 1812 | * Get the value of the first element or set the value of all elements in the current set. 1813 | * 1814 | * @param {string} value The value to set. 1815 | * @return shoestring 1816 | * @this shoestring 1817 | */ 1818 | shoestring.fn.val = function( value ){ 1819 | var el; 1820 | if( value !== undefined ){ 1821 | return this.each(function(){ 1822 | if( this.tagName === "SELECT" ){ 1823 | var optionSet, option, 1824 | options = this.options, 1825 | values = [], 1826 | i = options.length, 1827 | newIndex; 1828 | 1829 | values[0] = value; 1830 | while ( i-- ) { 1831 | option = options[ i ]; 1832 | if ( (option.selected = shoestring.inArray( option.value, values ) >= 0) ) { 1833 | optionSet = true; 1834 | newIndex = i; 1835 | } 1836 | } 1837 | // force browsers to behave consistently when non-matching value is set 1838 | if ( !optionSet ) { 1839 | this.selectedIndex = -1; 1840 | } else { 1841 | this.selectedIndex = newIndex; 1842 | } 1843 | } else { 1844 | this.value = value; 1845 | } 1846 | }); 1847 | } else if (this[0]) { 1848 | el = this[0]; 1849 | 1850 | if( el.tagName === "SELECT" ){ 1851 | if( el.selectedIndex < 0 ){ return ""; } 1852 | return el.options[ el.selectedIndex ].value; 1853 | } else { 1854 | return el.value; 1855 | } 1856 | } 1857 | }; 1858 | 1859 | 1860 | 1861 | /** 1862 | * Gets the width value of the first element or sets the width for the whole set. 1863 | * 1864 | * @param {float|undefined} value The value to assign. 1865 | * @return shoestring 1866 | * @this shoestring 1867 | */ 1868 | shoestring.fn.width = function( value ){ 1869 | return shoestring._dimension( this, "width", value ); 1870 | }; 1871 | 1872 | 1873 | 1874 | /** 1875 | * Wraps the child elements in the provided HTML. 1876 | * 1877 | * @param {string} html The wrapping HTML. 1878 | * @return shoestring 1879 | * @this shoestring 1880 | */ 1881 | shoestring.fn.wrapInner = function( html ){ 1882 | return this.each(function(){ 1883 | var inH = this.innerHTML; 1884 | 1885 | this.innerHTML = ""; 1886 | shoestring( this ).append( shoestring( html ).html( inH ) ); 1887 | }); 1888 | }; 1889 | 1890 | 1891 | 1892 | function initEventCache( el, evt ) { 1893 | if ( !el.shoestringData ) { 1894 | el.shoestringData = {}; 1895 | } 1896 | if ( !el.shoestringData.events ) { 1897 | el.shoestringData.events = {}; 1898 | } 1899 | if ( !el.shoestringData.loop ) { 1900 | el.shoestringData.loop = {}; 1901 | } 1902 | if ( !el.shoestringData.events[ evt ] ) { 1903 | el.shoestringData.events[ evt ] = []; 1904 | } 1905 | } 1906 | 1907 | function addToEventCache( el, evt, eventInfo ) { 1908 | var obj = {}; 1909 | obj.isCustomEvent = eventInfo.isCustomEvent; 1910 | obj.callback = eventInfo.callfunc; 1911 | obj.originalCallback = eventInfo.originalCallback; 1912 | obj.namespace = eventInfo.namespace; 1913 | 1914 | el.shoestringData.events[ evt ].push( obj ); 1915 | 1916 | if( eventInfo.customEventLoop ) { 1917 | el.shoestringData.loop[ evt ] = eventInfo.customEventLoop; 1918 | } 1919 | } 1920 | 1921 | /** 1922 | * Bind a callback to an event for the currrent set of elements. 1923 | * 1924 | * @param {string} evt The event(s) to watch for. 1925 | * @param {object,function} data Data to be included with each event or the callback. 1926 | * @param {function} originalCallback Callback to be invoked when data is define.d. 1927 | * @return shoestring 1928 | * @this shoestring 1929 | */ 1930 | shoestring.fn.bind = function( evt, data, originalCallback ){ 1931 | 1932 | if( arguments.length > 3 ){ 1933 | shoestring.error( 'on-delegate' ); 1934 | } 1935 | if( typeof data === "string" ){ 1936 | shoestring.error( 'on-delegate' ); 1937 | } 1938 | if( typeof data === "function" ){ 1939 | originalCallback = data; 1940 | data = null; 1941 | } 1942 | 1943 | var evts = evt.split( " " ); 1944 | 1945 | // NOTE the `triggeredElement` is purely for custom events from IE 1946 | function encasedCallback( e, namespace, triggeredElement ){ 1947 | var result; 1948 | 1949 | if( e._namespace && e._namespace !== namespace ) { 1950 | return; 1951 | } 1952 | 1953 | e.data = data; 1954 | e.namespace = e._namespace; 1955 | 1956 | var returnTrue = function(){ 1957 | return true; 1958 | }; 1959 | 1960 | e.isDefaultPrevented = function(){ 1961 | return false; 1962 | }; 1963 | 1964 | var originalPreventDefault = e.preventDefault; 1965 | var preventDefaultConstructor = function(){ 1966 | if( originalPreventDefault ) { 1967 | return function(){ 1968 | e.isDefaultPrevented = returnTrue; 1969 | originalPreventDefault.call(e); 1970 | }; 1971 | } else { 1972 | return function(){ 1973 | e.isDefaultPrevented = returnTrue; 1974 | e.returnValue = false; 1975 | }; 1976 | } 1977 | }; 1978 | 1979 | // thanks https://github.com/jonathantneal/EventListener 1980 | e.target = triggeredElement || e.target || e.srcElement; 1981 | e.preventDefault = preventDefaultConstructor(); 1982 | e.stopPropagation = e.stopPropagation || function () { 1983 | e.cancelBubble = true; 1984 | }; 1985 | 1986 | result = originalCallback.apply(this, [ e ].concat( e._args ) ); 1987 | 1988 | if( result === false ){ 1989 | e.preventDefault(); 1990 | e.stopPropagation(); 1991 | } 1992 | 1993 | return result; 1994 | } 1995 | 1996 | return this.each(function(){ 1997 | var domEventCallback, 1998 | customEventCallback, 1999 | customEventLoop, 2000 | oEl = this; 2001 | 2002 | for( var i = 0, il = evts.length; i < il; i++ ){ 2003 | var split = evts[ i ].split( "." ), 2004 | evt = split[ 0 ], 2005 | namespace = split.length > 0 ? split[ 1 ] : null; 2006 | 2007 | domEventCallback = function( originalEvent ) { 2008 | if( oEl.ssEventTrigger ) { 2009 | originalEvent._namespace = oEl.ssEventTrigger._namespace; 2010 | originalEvent._args = oEl.ssEventTrigger._args; 2011 | 2012 | oEl.ssEventTrigger = null; 2013 | } 2014 | return encasedCallback.call( oEl, originalEvent, namespace ); 2015 | }; 2016 | customEventCallback = null; 2017 | customEventLoop = null; 2018 | 2019 | initEventCache( this, evt ); 2020 | 2021 | this.addEventListener( evt, domEventCallback, false ); 2022 | 2023 | addToEventCache( this, evt, { 2024 | callfunc: customEventCallback || domEventCallback, 2025 | isCustomEvent: !!customEventCallback, 2026 | customEventLoop: customEventLoop, 2027 | originalCallback: originalCallback, 2028 | namespace: namespace 2029 | }); 2030 | } 2031 | }); 2032 | }; 2033 | 2034 | shoestring.fn.on = shoestring.fn.bind; 2035 | 2036 | shoestring.fn.live = function(){ 2037 | shoestring.error( 'live-delegate' ); 2038 | }; 2039 | 2040 | shoestring.fn.delegate = function(){ 2041 | shoestring.error( 'live-delegate' ); 2042 | }; 2043 | 2044 | 2045 | 2046 | /** 2047 | * Unbind a previous bound callback for an event. 2048 | * 2049 | * @param {string} event The event(s) the callback was bound to.. 2050 | * @param {function} callback Callback to unbind. 2051 | * @return shoestring 2052 | * @this shoestring 2053 | */ 2054 | shoestring.fn.unbind = function( event, callback ){ 2055 | 2056 | if( arguments.length >= 3 || typeof callback === "string" ){ 2057 | shoestring.error( 'off-delegate' ); 2058 | } 2059 | 2060 | var evts = event ? event.split( " " ) : []; 2061 | 2062 | return this.each(function(){ 2063 | if( !this.shoestringData || !this.shoestringData.events ) { 2064 | return; 2065 | } 2066 | 2067 | if( !evts.length ) { 2068 | unbindAll.call( this ); 2069 | } else { 2070 | var split, evt, namespace; 2071 | for( var i = 0, il = evts.length; i < il; i++ ){ 2072 | split = evts[ i ].split( "." ), 2073 | evt = split[ 0 ], 2074 | namespace = split.length > 0 ? split[ 1 ] : null; 2075 | 2076 | if( evt ) { 2077 | unbind.call( this, evt, namespace, callback ); 2078 | } else { 2079 | unbindAll.call( this, namespace, callback ); 2080 | } 2081 | } 2082 | } 2083 | }); 2084 | }; 2085 | 2086 | function unbind( evt, namespace, callback ) { 2087 | var bound = this.shoestringData.events[ evt ]; 2088 | if( !(bound && bound.length) ) { 2089 | return; 2090 | } 2091 | 2092 | var matched = [], j, jl; 2093 | for( j = 0, jl = bound.length; j < jl; j++ ) { 2094 | if( !namespace || namespace === bound[ j ].namespace ) { 2095 | if( callback === undefined || callback === bound[ j ].originalCallback ) { 2096 | this.removeEventListener( evt, bound[ j ].callback, false ); 2097 | matched.push( j ); 2098 | } 2099 | } 2100 | } 2101 | 2102 | for( j = 0, jl = matched.length; j < jl; j++ ) { 2103 | this.shoestringData.events[ evt ].splice( j, 1 ); 2104 | } 2105 | } 2106 | 2107 | function unbindAll( namespace, callback ) { 2108 | for( var evtKey in this.shoestringData.events ) { 2109 | unbind.call( this, evtKey, namespace, callback ); 2110 | } 2111 | } 2112 | 2113 | shoestring.fn.off = shoestring.fn.unbind; 2114 | 2115 | 2116 | /** 2117 | * Bind a callback to an event for the currrent set of elements, unbind after one occurence. 2118 | * 2119 | * @param {string} event The event(s) to watch for. 2120 | * @param {function} callback Callback to invoke on the event. 2121 | * @return shoestring 2122 | * @this shoestring 2123 | */ 2124 | shoestring.fn.one = function( event, callback ){ 2125 | var evts = event.split( " " ); 2126 | 2127 | return this.each(function(){ 2128 | var thisevt, cbs = {}, $t = shoestring( this ); 2129 | 2130 | for( var i = 0, il = evts.length; i < il; i++ ){ 2131 | thisevt = evts[ i ]; 2132 | 2133 | cbs[ thisevt ] = function( e ){ 2134 | var $t = shoestring( this ); 2135 | 2136 | for( var j in cbs ) { 2137 | $t.unbind( j, cbs[ j ] ); 2138 | } 2139 | 2140 | return callback.apply( this, [ e ].concat( e._args ) ); 2141 | }; 2142 | 2143 | $t.bind( thisevt, cbs[ thisevt ] ); 2144 | } 2145 | }); 2146 | }; 2147 | 2148 | 2149 | 2150 | /** 2151 | * Trigger an event on the first element in the set, no bubbling, no defaults. 2152 | * 2153 | * @param {string} event The event(s) to trigger. 2154 | * @param {object} args Arguments to append to callback invocations. 2155 | * @return shoestring 2156 | * @this shoestring 2157 | */ 2158 | shoestring.fn.triggerHandler = function( event, args ){ 2159 | var e = event.split( " " )[ 0 ], 2160 | el = this[ 0 ], 2161 | ret; 2162 | 2163 | // See this.fireEvent( 'on' + evts[ i ], document.createEventObject() ); instead of click() etc in trigger. 2164 | if( doc.createEvent && el.shoestringData && el.shoestringData.events && el.shoestringData.events[ e ] ){ 2165 | var bindings = el.shoestringData.events[ e ]; 2166 | for (var i in bindings ){ 2167 | if( bindings.hasOwnProperty( i ) ){ 2168 | event = doc.createEvent( "Event" ); 2169 | event.initEvent( e, true, true ); 2170 | event._args = args; 2171 | args.unshift( event ); 2172 | 2173 | ret = bindings[ i ].originalCallback.apply( event.target, args ); 2174 | } 2175 | } 2176 | } 2177 | 2178 | return ret; 2179 | }; 2180 | 2181 | 2182 | 2183 | /** 2184 | * Trigger an event on each of the DOM elements in the current set. 2185 | * 2186 | * @param {string} event The event(s) to trigger. 2187 | * @param {object} args Arguments to append to callback invocations. 2188 | * @return shoestring 2189 | * @this shoestring 2190 | */ 2191 | shoestring.fn.trigger = function( event, args ){ 2192 | var evts = event.split( " " ); 2193 | 2194 | return this.each(function(){ 2195 | var split, evt, namespace; 2196 | for( var i = 0, il = evts.length; i < il; i++ ){ 2197 | split = evts[ i ].split( "." ), 2198 | evt = split[ 0 ], 2199 | namespace = split.length > 0 ? split[ 1 ] : null; 2200 | 2201 | if( evt === "click" ){ 2202 | if( this.tagName === "INPUT" && this.type === "checkbox" && this.click ){ 2203 | this.click(); 2204 | return false; 2205 | } 2206 | } 2207 | 2208 | if( doc.createEvent ){ 2209 | var event = doc.createEvent( "Event" ); 2210 | event.initEvent( evt, true, true ); 2211 | event._args = args; 2212 | event._namespace = namespace; 2213 | 2214 | this.dispatchEvent( event ); 2215 | } 2216 | } 2217 | }); 2218 | }; 2219 | 2220 | 2221 | 2222 | 2223 | shoestring.fn.hasClass = function(){ 2224 | shoestring.error( 'has-class' ); 2225 | }; 2226 | 2227 | 2228 | 2229 | shoestring.fn.hide = function(){ 2230 | shoestring.error( 'show-hide' ); 2231 | }; 2232 | 2233 | 2234 | 2235 | shoestring.fn.outerWidth = function(){ 2236 | shoestring.error( 'outer-width' ); 2237 | }; 2238 | 2239 | 2240 | 2241 | shoestring.fn.show = function(){ 2242 | shoestring.error( 'show-hide' ); 2243 | }; 2244 | 2245 | 2246 | 2247 | shoestring.fn.click = function(){ 2248 | shoestring.error( 'click' ); 2249 | }; 2250 | 2251 | 2252 | 2253 | shoestring.map = function(){ 2254 | shoestring.error( 'map' ); 2255 | }; 2256 | 2257 | 2258 | 2259 | shoestring.fn.map = function(){ 2260 | shoestring.error( 'map' ); 2261 | }; 2262 | 2263 | 2264 | 2265 | shoestring.trim = function(){ 2266 | shoestring.error( 'trim' ); 2267 | }; 2268 | 2269 | 2270 | 2271 | (function() { 2272 | shoestring.trackedMethodsKey = "shoestringMethods"; 2273 | 2274 | // simple check for localStorage from Modernizr - https://github.com/Modernizr/Modernizr/blob/master/feature-detects/storage/localstorage.js 2275 | function supportsStorage() { 2276 | var mod = "modernizr"; 2277 | try { 2278 | localStorage.setItem(mod, mod); 2279 | localStorage.removeItem(mod); 2280 | return true; 2281 | } catch(e) { 2282 | return false; 2283 | } 2284 | } 2285 | 2286 | // return a new function closed over the old implementation 2287 | function recordProxy( old, name ) { 2288 | return function() { 2289 | var tracked; 2290 | try { 2291 | tracked = JSON.parse(win.localStorage.getItem( shoestring.trackedMethodsKey ) || "{}"); 2292 | } catch (e) { 2293 | if( e instanceof SyntaxError) { 2294 | tracked = {}; 2295 | } 2296 | } 2297 | 2298 | tracked[ name ] = true; 2299 | win.localStorage.setItem( shoestring.trackedMethodsKey, JSON.stringify(tracked) ); 2300 | 2301 | return old.apply(this, arguments); 2302 | }; 2303 | } 2304 | 2305 | // proxy each of the methods defined on fn 2306 | if( supportsStorage() ){ 2307 | for( var method in shoestring.fn ){ 2308 | if( shoestring.fn.hasOwnProperty(method) ) { 2309 | shoestring.fn[ method ] = recordProxy(shoestring.fn[ method ], method); 2310 | } 2311 | } 2312 | } 2313 | })(); 2314 | 2315 | 2316 | 2317 | return shoestring; 2318 | })); 2319 | -------------------------------------------------------------------------------- /lib/xrayhtml.css: -------------------------------------------------------------------------------- 1 | /*! X-rayHTML - v2.1.2 - 2016-05-18 2 | * https://github.com/filamentgroup/x-rayhtml 3 | * Copyright (c) 2016 Filament Group; Licensed MIT */ 4 | .xrayhtml { 5 | border: 1px solid rgba(0,0,0,.1); 6 | border-radius: .3em; 7 | margin: 1.5em 0 2.5em 0; 8 | padding: 1em 1em 2em; 9 | } 10 | .xrayhtml .xraytitle { 11 | text-transform: uppercase; 12 | letter-spacing: 1px; 13 | font: .75em sans-serif; 14 | color: rgba(0,0,0,.5); 15 | background-color: #fff; 16 | border-radius: 3px; 17 | display: inline-block; 18 | position: relative; 19 | top: -2.166666667em; /* 26px */ 20 | padding-left: .1em; 21 | padding-right: .1em; 22 | z-index: 3; 23 | margin: 0; 24 | } 25 | .xrayhtml.method-flip:before { 26 | background-color: rgba(255,255,255,.6); 27 | } 28 | .xrayhtml .source-panel { 29 | background: #f7f7f7; 30 | margin-top: 2em; 31 | tab-size: 2; 32 | } 33 | .xrayhtml .source-panel pre { 34 | margin: 0; 35 | } 36 | .xrayhtml .source-panel code { 37 | white-space: pre-wrap; 38 | } 39 | .xrayhtml.method-flip .source-panel { 40 | margin-top: 0; 41 | border-radius: 0.3em; 42 | } 43 | .xrayhtml.method-inline .source-panel { 44 | margin: 2em -1em -2em -1em !important; /* Prism style override. */ 45 | border-top: 1px solid rgba(0,0,0,.1); 46 | border-radius: 0 0 .3em .3em; 47 | } 48 | .xrayhtml pre { 49 | padding: 16px; 50 | margin: 0 !important; /* Prism style override. */ 51 | border-radius: 0 0 .3em .3em; 52 | } 53 | .xrayhtml code { 54 | white-space: pre-wrap !important; /* Prism style override. */ 55 | } 56 | 57 | .xrayhtml.antipattern { 58 | border-color: #C9282D; 59 | } 60 | .xrayhtml.antipattern .xraytitle { 61 | color: #d75e72; 62 | font-weight: 700; 63 | } 64 | 65 | /* Flip Animation */ 66 | 67 | .method-flip { 68 | -webkit-perspective: 2500px; 69 | -moz-perspective: 2500px; 70 | perspective: 2500px; 71 | } 72 | .method-flip .snippet { 73 | padding: 0; 74 | margin: 0; 75 | position: relative; 76 | top: 0; 77 | left: 0; 78 | z-index: 2; 79 | min-height: 100%; 80 | } 81 | .method-flip .source-panel { 82 | position: absolute; 83 | top: 0; 84 | left: 0; 85 | width: 100%; 86 | height: 100%; 87 | overflow-x: scroll; 88 | } 89 | 90 | .method-flip .snippet { 91 | -webkit-transform: rotateY(0deg); 92 | -webkit-transform-style: preserve-3d; 93 | -webkit-backface-visibility: hidden; 94 | 95 | -moz-transform: rotateY(0deg); 96 | -moz-transform-style: preserve-3d; 97 | -moz-backface-visibility: hidden; 98 | 99 | -webkit-transition: -webkit-transform .4s ease-in-out; 100 | -moz-transition: -moz-transform .4s ease-in-out; 101 | } 102 | .method-flip.view-source .snippet { 103 | z-index: 1; 104 | -webkit-transform: rotateY(180deg); 105 | -moz-transform: rotateY(180deg); 106 | } 107 | .method-flip .source-panel { 108 | -webkit-transform: rotateY(-180deg); 109 | -webkit-backface-visibility: hidden; 110 | 111 | -moz-transform: rotateY(-180deg); 112 | -moz-backface-visibility: hidden; 113 | 114 | -moz-transition: all .4s ease-in-out; 115 | -webkit-transition: all .4s ease-in-out; 116 | } 117 | .method-flip.view-source .source-panel { 118 | z-index: 2; 119 | -webkit-transform: rotateY(0deg); 120 | -moz-transform: rotateY(0deg); 121 | } 122 | 123 | .method-flip.view-source .xraytitle { 124 | background-color: transparent; 125 | background-image: linear-gradient( 126 | to bottom, 127 | transparent, 128 | transparent 40%, 129 | #ffffff 40%, 130 | transparent); 131 | } 132 | 133 | iframe.xray-iframe { 134 | border: 0; 135 | width: 100% 136 | } -------------------------------------------------------------------------------- /lib/xrayhtml.js: -------------------------------------------------------------------------------- 1 | /*! X-rayHTML - v2.1.2 - 2016-05-18 2 | * https://github.com/filamentgroup/x-rayhtml 3 | * Copyright (c) 2016 Filament Group; Licensed MIT */ 4 | window.jQuery = window.jQuery || window.shoestring; 5 | 6 | (function( $ ) { 7 | var xrayiframeid = 0; 8 | var pluginName = "xrayhtml", 9 | o = { 10 | text: { 11 | open: "View Source", 12 | close: "View Demo", 13 | titlePrefix: "Example", 14 | antipattern: "Do Not Use" 15 | }, 16 | classes: { 17 | button: "btn btn-small", 18 | open: "view-source", 19 | sourcepanel: "source-panel", 20 | title: "xraytitle", 21 | antipattern: "antipattern" 22 | }, 23 | initSelector: "[data-" + pluginName + "]", 24 | defaultReveal: "inline" 25 | }, 26 | methods = { 27 | _create: function() { 28 | return $( this ).each(function() { 29 | var init = $( this ).data( "init." + pluginName ); 30 | 31 | if( init ) { 32 | return false; 33 | } 34 | 35 | $( this ) 36 | .data( "init." + pluginName, true ) 37 | [ pluginName ]( "_init" ) 38 | .trigger( "create." + pluginName ); 39 | }); 40 | }, 41 | _init: function() { 42 | var $self = $(this); 43 | 44 | $self.data( "id." + pluginName, xrayiframeid++); 45 | 46 | var method = $( this ).attr( "data-" + pluginName ) || o.defaultReveal; 47 | 48 | if( method === "flip" ) { 49 | $( this )[ pluginName ]( "_createButton" ); 50 | } 51 | 52 | $( this ) 53 | .addClass( pluginName + " " + "method-" + method ) 54 | [ pluginName ]( "_createSource" ); 55 | 56 | // use an iframe to host the source 57 | if( $(this).is("[data-" + pluginName + "-iframe]") ){ 58 | 59 | // grab the snippet html to ship to the iframe 60 | var snippetHTML = $(this).find(".snippet").html(); 61 | 62 | // grab the url of the iframe to load 63 | var url = $(this).attr("data-" + pluginName + "-iframe"); 64 | 65 | // grab the selector for the element in the iframe to put the html in 66 | var selector = $(this).attr("data-" + pluginName + "-iframe-target"); 67 | 68 | // create the iframe element, so we can bind to the load event 69 | var $iframe = $("" ); 84 | } 85 | else { 86 | $.get( url, createDialog ); 87 | } 88 | 89 | e.preventDefault(); 90 | } 91 | }); 92 | 93 | // if the hash matches an ajaxlink's url, open it by triggering a click on the ajaxlink 94 | $( w ).bind( "hashchange load", function(){ 95 | var hash = w.location.hash.split( "#" ).pop(); 96 | var id = hash.replace( /-dialog$/, "" ); 97 | var $ajaxLink = $( 'a[href="' + decodeURIComponent(id) +'"][data-dialog-link], a[href="' + id +'"][data-dialog-link]' ); 98 | // if the link specified nohistory, don't click it 99 | var nohistory = 100 | $ajaxLink.attr( "data-dialog-history" ) === "false" || 101 | !w.componentNamespace.Dialog.history; 102 | 103 | var $dialogInPage = $( '.dialog[id="' + id + '"]' ); 104 | if( $ajaxLink.length && !nohistory && !$dialogInPage.length ){ 105 | $ajaxLink.eq( 0 ).trigger( "click" ); 106 | } 107 | }); 108 | 109 | }( this, window.jQuery )); 110 | -------------------------------------------------------------------------------- /src/dialog.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Simple jQuery Dialog 3 | * https://github.com/filamentgroup/dialog 4 | * 5 | * Copyright (c) 2013 Filament Group, Inc. 6 | * Licensed under the MIT, GPL licenses. 7 | */ 8 | .dialog-content, 9 | .dialog-background { 10 | position: absolute; 11 | top: 0; 12 | left: 0; 13 | right: 0; 14 | display: none; 15 | } 16 | .dialog-background { 17 | background: #aaa; 18 | filter: alpha(opacity=40); 19 | background-color: rgba(0,0,0,.4); 20 | z-index: 99999; 21 | height: 100%; 22 | bottom: 0; 23 | } 24 | .dialog-content { 25 | margin: 1em; 26 | background: #fff; 27 | padding: 1em 2em; 28 | max-width: 30em; 29 | box-shadow: 0 1px 2px #777; 30 | z-index: 100000; 31 | } 32 | .dialog-iframe { 33 | margin: 0; 34 | padding: 0; 35 | width: 100%; 36 | height: 100%; 37 | border: 0; 38 | } 39 | /* 40 | IE8+ issue with centering dialog 41 | https://github.com/filamentgroup/dialog/issues/6 42 | requires Respond.JS for IE8 43 | */ 44 | @media (min-width: 30em) { 45 | .dialog-content { 46 | width: 30em; 47 | } 48 | } 49 | .dialog-open:focus { 50 | outline: none; 51 | } 52 | .dialog-open, 53 | .dialog-background-open { 54 | display: block; 55 | } 56 | .dialog-background-trans { 57 | background: transparent; 58 | } 59 | 60 | @media (min-width: 32em){ 61 | .dialog-content { 62 | margin: 4em auto 1em; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/dialog.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Simple jQuery Dialog 3 | * https://github.com/filamentgroup/dialog 4 | * 5 | * Copyright (c) 2013 Filament Group, Inc. 6 | * Author: @scottjehl 7 | * Contributors: @johnbender, @zachleat 8 | * Licensed under the MIT, GPL licenses. 9 | */ 10 | 11 | window.jQuery = window.jQuery || window.shoestring; 12 | 13 | (function( w, $ ){ 14 | w.componentNamespace = w.componentNamespace || w; 15 | 16 | var pluginName = "dialog", cl, ev, 17 | doc = w.document, 18 | docElem = doc.documentElement, 19 | body = doc.body, 20 | $html = $( docElem ); 21 | 22 | var Dialog = w.componentNamespace.Dialog = function( element ){ 23 | this.$el = $( element ); 24 | 25 | // prevent double init 26 | if( this.$el.data( pluginName ) ){ 27 | return this.$el.data( pluginName ); 28 | } 29 | 30 | // record init 31 | this.$el.data( pluginName, this ); 32 | 33 | // keeping data-nobg here for compat. Deprecated. 34 | this.$background = !this.$el.is( '[data-' + pluginName + '-nobg]' ) ? 35 | $( doc.createElement('div') ).addClass( cl.bkgd ).attr( "tabindex", "-1" ).appendTo( "body") : 36 | $( [] ); 37 | 38 | // when dialog first inits, save a reference to the initial hash so we can know whether 39 | // there's room in the history stack to go back or not when closing 40 | this.initialLocationHash = w.location.hash; 41 | 42 | // the dialog's url hash is different from the dialog's actual ID attribute 43 | // this is because pairing the ID directly makes the browser jump to the top 44 | // of the dialog, rather than allowing us to space it off the top of the 45 | // viewport. also, if the dialog has a data-history attr, this property will 46 | // prevent its findability for onload and hashchanges 47 | this.nohistory = 48 | this.$el.attr( 'data-dialog-history' ) === "false" || !Dialog.history; 49 | 50 | var id = this.$el.attr( "id" ); 51 | // use the identifier and an extra tag for hash management 52 | this.hash = id + "-dialog"; 53 | 54 | // if won't pop up the dialog on initial load (`nohistory`) the user MAY 55 | // refresh a url with the dialog id as the hash then a change of the hash 56 | // won't be recognized by the browser when the dialog comes up and the back 57 | // button will return to the referring page. So, when nohistory is defined, 58 | // we append a "unique" identifier to the hash. 59 | this.hash += this.nohistory ? "-" + new Date().getTime().toString() : "" ; 60 | 61 | this.isOpen = false; 62 | this.isTransparentBackground = this.$el.is( '[data-transbg]' ); 63 | 64 | if( id ) { 65 | this.resizeEventName = "resize.dialog-" + id; 66 | } 67 | 68 | this._addA11yAttrs(); 69 | }; 70 | 71 | // default to tracking history with the dialog 72 | Dialog.history = true; 73 | 74 | // This property is global across dialogs - it determines whether the hash is get/set at all 75 | Dialog.useHash = true; 76 | 77 | Dialog.events = ev = { 78 | open: pluginName + "-open", 79 | opened: pluginName + "-opened", 80 | close: pluginName + "-close", 81 | closed: pluginName + "-closed", 82 | resize: pluginName + "-resize" 83 | }; 84 | 85 | Dialog.classes = cl = { 86 | open: pluginName + "-open", 87 | opened: pluginName + "-opened", 88 | content: pluginName + "-content", 89 | close: pluginName + "-close", 90 | closed: pluginName + "-closed", 91 | bkgd: pluginName + "-background", 92 | bkgdOpen: pluginName + "-background-open", 93 | bkgdTrans: pluginName + "-background-trans" 94 | }; 95 | 96 | Dialog.selectors = { 97 | close: "." + Dialog.classes.close + ", [data-close], [data-dialog-close]" 98 | }; 99 | 100 | 101 | Dialog.prototype.destroy = function() { 102 | // unregister the focus stealing 103 | window.focusRegistry.unregister(this); 104 | 105 | this.$el.trigger("destroy"); 106 | 107 | // clear init for this dom element 108 | this.$el.data()[pluginName] = undefined; 109 | 110 | // remove the backdrop for the dialog 111 | this.$background.remove(); 112 | }; 113 | 114 | Dialog.prototype.checkFocus = function(event){ 115 | var $target = $( event.target ); 116 | var shouldSteal; 117 | 118 | shouldSteal = 119 | this.isOpen && 120 | !$target.closest( this.$el[0]).length && 121 | this.isLastDialog() && 122 | !this._isNonInteractive(); 123 | 124 | return shouldSteal; 125 | }; 126 | 127 | Dialog.prototype.stealFocus = function(){ 128 | this.$el[0].focus(); 129 | }; 130 | 131 | 132 | 133 | Dialog.prototype._addA11yAttrs = function(){ 134 | this.$el 135 | .attr( "role", "dialog" ) 136 | .attr( "tabindex", "-1" ) 137 | .find( Dialog.selectors.close ).attr( "role", "button" ); 138 | 139 | }; 140 | 141 | Dialog.prototype._removeA11yAttrs = function(){ 142 | this.$el.removeAttr( "role" ); 143 | this.$el.removeAttr( "tabindex" ); 144 | }; 145 | 146 | Dialog.prototype._isNonInteractive = function(){ 147 | var computedDialog = window.getComputedStyle( this.$el[ 0 ], null ); 148 | var closeLink = this.$el.find( Dialog.selectors.close )[0]; 149 | var computedCloseLink; 150 | if( closeLink ){ 151 | computedCloseLink = window.getComputedStyle( closeLink, null ); 152 | } 153 | var computedBackground = window.getComputedStyle( this.$background[ 0 ], null ); 154 | return computedDialog.getPropertyValue( "display" ) !== "none" && 155 | computedDialog.getPropertyValue( "visibility" ) !== "hidden" && 156 | ( !computedCloseLink || computedCloseLink.getPropertyValue( "display" ) === "none" ) && 157 | computedBackground.getPropertyValue( "display" ) === "none"; 158 | }; 159 | 160 | Dialog.prototype._checkInteractivity = function(){ 161 | if( this._isNonInteractive() ){ 162 | this._removeA11yAttrs(); 163 | this._ariaShowUnrelatedElems(); 164 | } 165 | else{ 166 | this._addA11yAttrs(); 167 | 168 | } 169 | }; 170 | 171 | 172 | Dialog.prototype._ariaHideUnrelatedElems = function(){ 173 | this._ariaShowUnrelatedElems(); 174 | var ignoredElems = "script, style"; 175 | var hideList = this.$el.siblings().not( ignoredElems ); 176 | this.$el.parents().not( "body, html" ).each(function(){ 177 | hideList = hideList.add( $( this ).siblings().not( ignoredElems ) ); 178 | }); 179 | hideList.each(function(){ 180 | var priorHidden = $( this ).attr( "aria-hidden" ) || ""; 181 | $( this ) 182 | .attr( "data-dialog-aria-hidden", priorHidden ) 183 | .attr( "aria-hidden", "true" ); 184 | }); 185 | }; 186 | 187 | 188 | Dialog.prototype._ariaShowUnrelatedElems = function(){ 189 | $( "[data-dialog-aria-hidden]" ).each(function(){ 190 | if( $( this ).attr( "data-dialog-aria-hidden" ).match( "true|false" ) ){ 191 | $( this ).attr( "aria-hidden", $( this ).attr( "data-dialog-aria-hidden" ) ); 192 | } 193 | else { 194 | $( this ).removeAttr( "aria-hidden" ); 195 | } 196 | }).removeAttr( "data-dialog-aria-hidden" ); 197 | }; 198 | 199 | Dialog.prototype.resizeBackground = function() { 200 | if( this.$background.length ) { 201 | var bg = this.$background[ 0 ]; 202 | // don’t let the background size interfere with our height measurements 203 | bg.style.display = "none"; 204 | 205 | var scrollPlusHeight = (this.scroll || 0) + this.$el[0].clientHeight; 206 | var height = Math.max( scrollPlusHeight, docElem.scrollHeight, docElem.clientHeight ); 207 | bg.style.height = height + "px"; 208 | bg.style.display = ""; 209 | } 210 | }; 211 | 212 | Dialog.prototype.open = function() { 213 | if( this.isOpen ){ 214 | return; 215 | } 216 | 217 | var self = this; 218 | 219 | this.$el.addClass( cl.open ); 220 | 221 | this.$background.addClass( cl.bkgdOpen ); 222 | this.$background.attr( "id", this.$el.attr( "id" ) + "-background" ); 223 | this._setBackgroundTransparency(); 224 | 225 | this.scroll = "pageYOffset" in w ? w.pageYOffset : ( docElem.scrollY || docElem.scrollTop || ( body && body.scrollY ) || 0 ); 226 | this.$el[ 0 ].style.top = this.scroll + "px"; 227 | this.resizeBackground(); 228 | 229 | $html.addClass( cl.open ); 230 | this.isOpen = true; 231 | 232 | var cleanHash = w.location.hash.replace( /^#/, "" ); 233 | 234 | if( w.Dialog.useHash ){ 235 | if( cleanHash.indexOf( "-dialog" ) > -1 && !this.isLastDialog() ){ 236 | w.location.hash += "#" + this.hash; 237 | } else if( !this.isLastDialog() ){ 238 | w.location.hash = this.hash; 239 | } 240 | } 241 | 242 | if( doc.activeElement ){ 243 | this.focused = doc.activeElement; 244 | } 245 | 246 | this.$el[ 0 ].focus(); 247 | 248 | setTimeout(function(){ 249 | self._ariaHideUnrelatedElems(); 250 | }); 251 | 252 | this.$el.on( ev.resize, function() { 253 | self.resizeBackground(); 254 | }); 255 | 256 | if( this.resizeEventName ) { 257 | var timeout; 258 | $(w).on(this.resizeEventName, function() { 259 | w.clearTimeout(timeout); 260 | timeout = setTimeout(function() { 261 | self.resizeBackground(); 262 | }, 50); 263 | }); 264 | } 265 | 266 | this.$el.trigger( ev.opened ); 267 | }; 268 | 269 | Dialog.prototype.lastHash = function(){ 270 | return w.location.hash.split( "#" ).pop(); 271 | }; 272 | 273 | // is this the newest/last dialog that was opened based on the hash 274 | Dialog.prototype.isLastDialog = function(){ 275 | return this.lastHash() === this.hash; 276 | }; 277 | 278 | Dialog.prototype._setBackgroundTransparency = function() { 279 | if( this.isTransparentBackground ){ 280 | this.$background.addClass( cl.bkgdTrans ); 281 | } 282 | }; 283 | 284 | Dialog.prototype.close = function(){ 285 | if( !this.isOpen ){ 286 | return; 287 | } 288 | 289 | this._ariaShowUnrelatedElems(); 290 | 291 | // if close() is called directly and the hash for this dialog is at the end 292 | // of the url, then we need to change the hash to remove it, either by going 293 | // back if we can, or by adding a history state that doesn't have it at the 294 | // end 295 | if( window.location.hash.split( "#" ).pop() === this.hash ){ 296 | // check if we're back at the original hash, if we are then we can't 297 | // go back again otherwise we'll move away from the page 298 | var hashKeys = window.location.hash.split( "#" ); 299 | var initialHashKeys = this.initialLocationHash.split( "#" ); 300 | 301 | // if we are not at the original hash then use history 302 | // otherwise, if it's the same starting hash as it was at init time, we 303 | // can't trigger back to close the dialog, as it might take us elsewhere. 304 | // so we have to go forward and create a new hash that does not have this 305 | // dialog's hash at the end 306 | if( window.Dialog.useHash ){ 307 | if( hashKeys.join("") !== initialHashKeys.join("") ){ 308 | window.history.back(); 309 | } else { 310 | var escapedRegexpHash = this 311 | .hash 312 | .replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); 313 | 314 | window.location.hash = window 315 | .location 316 | .hash 317 | .replace( new RegExp( "#" + escapedRegexpHash + "$" ), "" ); 318 | } 319 | } 320 | 321 | return; 322 | } 323 | 324 | this.$el.removeClass( cl.open ); 325 | 326 | this.$background.removeClass( cl.bkgdOpen ); 327 | 328 | this.isOpen = false; 329 | 330 | // we only want to throw focus on close if we aren't 331 | // opening a nested dialog or some other UI state 332 | if( this.focused && !this.isLastDialog()){ 333 | this.focused.focus(); 334 | } 335 | if( $( "." + pluginName + "." + cl.open ).length === 0 ){ 336 | $html.removeClass( cl.open ); 337 | w.scrollTo( 0, this.scroll ); 338 | } 339 | 340 | this.$el.off( ev.resize ); 341 | 342 | if( this.resizeEventName ) { 343 | $(w).off(this.resizeEventName); 344 | } 345 | 346 | this.$el.trigger( ev.closed ); 347 | }; 348 | }( this, window.jQuery )); 349 | -------------------------------------------------------------------------------- /test/dialog.js: -------------------------------------------------------------------------------- 1 | (function( $, window ) { 2 | if(location.hash !== ""){ 3 | throw "Hash must be empty for tests to work properly"; 4 | } 5 | 6 | var $doc, $instance, commonSetup, commonTeardown; 7 | 8 | commonSetup = function() { 9 | $instance = $( "#dialog" ); 10 | 11 | if( $instance.data("dialog") ) { 12 | $instance.data( "dialog" ).destroy(); 13 | } 14 | 15 | $instance.dialog(); 16 | }; 17 | 18 | commonTeardown = function() { 19 | $instance.unbind( "dialog-closed" ); 20 | $instance.unbind( "dialog-opened" ); 21 | 22 | $instance.data( "dialog" ).destroy(); 23 | }; 24 | 25 | // we have to give the browser the time to trigger a hashchange 26 | // so there's no mixup 27 | function closeInstance(){ 28 | $instance.trigger( "dialog-close" ); 29 | setTimeout(function(){ 30 | start() 31 | }, 400); 32 | } 33 | 34 | module( "opening", { 35 | setup: commonSetup, 36 | teardown: commonTeardown 37 | }); 38 | 39 | var openTest = function( open ) { 40 | $instance.one( "dialog-opened", function(){ 41 | ok( $instance.is(".dialog-open") ); 42 | closeInstance(); 43 | }); 44 | 45 | ok( !$instance.is(".dialog-open") ); 46 | 47 | open(); 48 | }; 49 | 50 | asyncTest( "with the link", function() { 51 | var $link = $( $instance.find("a").attr( "href" ) ); 52 | 53 | openTest(function() { 54 | $link.trigger( "click" ); 55 | }); 56 | }); 57 | 58 | asyncTest( "with a trigger", function() { 59 | openTest(function() { 60 | $instance.trigger( "dialog-open" ); 61 | }); 62 | }); 63 | 64 | asyncTest( "with trigger sets the hash to #dialog", function() { 65 | if( !Dialog.useHash ){ 66 | ok("Hash use is disabled"); 67 | return start(); 68 | } 69 | $instance.one( "dialog-opened", function(){ 70 | equal( location.hash, "#dialog-dialog" ); 71 | closeInstance(); 72 | }); 73 | 74 | ok( !$instance.is(".dialog-open") ); 75 | $instance.trigger( "dialog-open" ); 76 | }); 77 | 78 | module( "background", { 79 | setup: commonSetup, 80 | teardown: commonTeardown 81 | }); 82 | 83 | test( "is added to the body", function() { 84 | equal($( "body" ).find( ".dialog-background" ).length ,1 ); 85 | }); 86 | 87 | module( "closing", { 88 | setup: commonSetup, 89 | teardown: commonTeardown 90 | }); 91 | 92 | var closeTest = function( close ) { 93 | expect( 3 ); 94 | 95 | $instance.one( "dialog-opened", function(){ 96 | ok( $instance.is(".dialog-open") ); 97 | $instance.trigger( "dialog-close" ); 98 | }); 99 | 100 | $instance.one( "dialog-closed", function(){ 101 | ok( !$instance.is(".dialog-open") ); 102 | start(); 103 | }); 104 | 105 | ok( !$instance.is(".dialog-open") ); 106 | $instance.trigger( "dialog-open" ); 107 | }; 108 | 109 | asyncTest( "using trigger makes the dialog invisible", function() { 110 | window.foo = true; 111 | closeTest(function() { 112 | $instance.trigger( "dialog-close" ); 113 | }); 114 | }); 115 | 116 | asyncTest( "using the back button makes the dialog invisible", function() { 117 | closeTest(function() { 118 | window.history.back(); 119 | }); 120 | }); 121 | 122 | asyncTest( "using the escape key makes the dialog invisible", function() { 123 | var keyupEvent = { 124 | type: "keyup", 125 | timestamp: (new Date()).getTime() 126 | }; 127 | 128 | keyupEvent.which = 27; 129 | 130 | closeTest(function() { 131 | $( document ).trigger( keyupEvent ); 132 | }); 133 | }); 134 | 135 | asyncTest( "closing an open dialog clears the hash", function() { 136 | if( !Dialog.useHash ){ 137 | ok("Hash use is disabled"); 138 | return start(); 139 | } 140 | $(window).one("hashchange", function(){ 141 | equal(location.hash, "#dialog-dialog"); 142 | 143 | $(window).one("hashchange", function(){ 144 | equal(location.hash, ""); 145 | start(); 146 | }); 147 | 148 | $instance.trigger("dialog-close"); 149 | }); 150 | 151 | $instance.trigger("dialog-open"); 152 | }); 153 | 154 | asyncTest( "closing an open dialog doesn't clear other hash", function() { 155 | if( !Dialog.useHash ){ 156 | ok("Hash use is disabled"); 157 | return start(); 158 | } 159 | $(window).one("hashchange", function(){ 160 | equal(location.hash, "#dialog-dialog"); 161 | 162 | $instance.one("dialog-closed", function(){ 163 | // the hash should not have changed ... 164 | equal(location.hash, "#foo"); 165 | 166 | // ... but the dialog should be closed 167 | ok( !$instance.is(".dialog-open") ); 168 | start(); 169 | }); 170 | 171 | location.hash = "foo" 172 | $instance.trigger("dialog-close"); 173 | }); 174 | 175 | $instance.trigger("dialog-open"); 176 | }); 177 | 178 | test( "dialog has aria role and tabindex depending on visibility", function(){ 179 | 180 | expect(4); 181 | ok( $( "#dialog" ).attr( "role" ) === "dialog" ); 182 | ok( $( "#dialog" ).attr( "tabindex" ) === "-1" ); 183 | ok( !$( "#mydialog-alwaysstatic" ).attr( "role" ) ); 184 | ok( !$( "#mydialog-alwaysstatic" ).attr( "tabindex" ) ); 185 | 186 | } ); 187 | 188 | })( window.jQuery || window.shoestring, this ); 189 | -------------------------------------------------------------------------------- /test/history.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dialog History Tests 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | Open Dialog 21 | 22 |
23 |

This is a dialog

24 | 25 | Open Nested Dialog 26 | 27 | Close 28 |
29 | 30 |
31 |
32 | 33 | 38 |
39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /test/history.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var $ = jQuery; 3 | if( location.hash !== "" ){ 4 | throw "hash must be clear to start the tests"; 5 | } 6 | 7 | function debugEqual(a, b){ 8 | debugger; 9 | equal(a,b); 10 | } 11 | 12 | var $doc, instance, $instance, $nested, $nohist, commonSetup, commonTeardown; 13 | 14 | commonSetup = function() { 15 | $instance = $( "#dialog" ); 16 | $nested = $( "#nested" ); 17 | $nohist = $( "#nohist" ); 18 | 19 | if( $instance.data("dialog") ) { 20 | $instance.data( "dialog" ).destroy(); 21 | } 22 | 23 | if( $nested.data("dialog") ) { 24 | $nested.data( "dialog" ).destroy(); 25 | } 26 | 27 | if( $nohist.data("dialog") ) { 28 | $nohist.data( "dialog" ).destroy(); 29 | } 30 | 31 | $instance.dialog(); 32 | $nested.dialog(); 33 | $nohist.dialog(); 34 | }; 35 | 36 | commonTeardown = function() { 37 | $instance.unbind( "dialog-closed" ); 38 | $instance.unbind( "dialog-opened" ); 39 | $nested.unbind( "dialog-closed" ); 40 | $nested.unbind( "dialog-opened" ); 41 | $nohist.unbind( "dialog-closed" ); 42 | $nohist.unbind( "dialog-opened" ); 43 | 44 | $instance.data( "dialog" ).destroy(); 45 | $nested.data( "dialog" ).destroy(); 46 | $nohist.data( "dialog" ).destroy(); 47 | }; 48 | 49 | // we have to give the browser the time to trigger a hashchange 50 | // so there's no mixup 51 | function closeInstance($dialog, teardown){ 52 | $("#dialog").trigger( "dialog-close" ); 53 | setTimeout(function(){ 54 | if(teardown){ commonTeardown() }; 55 | start(); 56 | }, 400); 57 | } 58 | 59 | // NOTE this must come after the first test so that the init of the dialog 60 | // comes from the page's enhance event 61 | module( "init", { 62 | setup: commonSetup, 63 | teardown: commonTeardown 64 | }); 65 | 66 | asyncTest("should go back to dialog after closing the dialog ", function(){ 67 | if( !Dialog.useHash ){ 68 | expect(1); 69 | ok("Hash use is disabled"); 70 | return start(); 71 | } 72 | expect(3); 73 | var isOpen = $instance.data("dialog").isOpen; 74 | 75 | function testSeq(){ 76 | equal(location.hash, "#dialog-dialog", "#dialog-dialog hash"); 77 | 78 | $(window).one( "hashchange", function(){ 79 | equal(location.hash, "", "no hash"); 80 | 81 | $(window).one("hashchange", function(){ 82 | equal(location.hash, "#dialog-dialog", "#dialog-dialog hash again"); 83 | start(); 84 | }); 85 | 86 | $instance.trigger( "dialog-open" ); 87 | }); 88 | 89 | $instance.trigger( "dialog-close" ); 90 | } 91 | 92 | if(!isOpen) { 93 | $(window).one("hashchange", testSeq); 94 | $instance.trigger( "dialog-open" ); 95 | } else { 96 | testSeq(); 97 | } 98 | }); 99 | 100 | // TODO move to `dialog.js` tests 101 | test("should prevent double init", function(){ 102 | if( !Dialog.useHash ){ 103 | expect(1); 104 | ok("Hash use is disabled"); 105 | return start(); 106 | } 107 | // the `isOpen` propery of the dialog object is set 108 | // to `false` at the end of init, we check that it never gets there 109 | $instance.data("dialog").hash = "foo"; 110 | $instance.dialog(); 111 | equal($instance.data("dialog").hash, "foo"); 112 | }); 113 | 114 | asyncTest("should append dialog name to hash for nested dialogs", function(){ 115 | if( !Dialog.useHash ){ 116 | expect(1); 117 | ok("Hash use is disabled"); 118 | return start(); 119 | } 120 | expect(3); 121 | 122 | $instance.trigger( "dialog-open" ); 123 | equal(location.hash, "#dialog-dialog"); 124 | $instance.find( "#nested-dialog-anchor" ).trigger( "click" ); 125 | equal(location.hash, "#dialog-dialog#nested-dialog"); 126 | 127 | $nested.one("dialog-closed", function(){ 128 | equal("#dialog-dialog", location.hash); 129 | closeInstance($instance); 130 | }); 131 | 132 | window.history.back(); 133 | }); 134 | 135 | asyncTest("nohist dialog should still work as normal after page load", function(){ 136 | if( !Dialog.useHash ){ 137 | expect(1); 138 | ok("Hash use is disabled"); 139 | return start(); 140 | } 141 | expect(2); 142 | 143 | var oldHash = location.hash; 144 | 145 | $nohist.trigger( "dialog-open" ); 146 | equal(location.hash.indexOf(oldHash + "#nohist-dialog"), 0); 147 | $nohist.one("dialog-closed", function(){ 148 | equal(oldHash, location.hash); 149 | closeInstance($nohist); 150 | }); 151 | 152 | window.history.back(); 153 | }); 154 | })(); 155 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dialog Tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | Open Dialog 16 | 17 |
18 |

This is a dialog

19 | Close 20 |
21 | 22 | 33 | 34 |
35 |

This is a dialog

36 | Close 37 |
38 | 39 | 40 |
41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /test/lib/qunit-1.12.0.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit v1.12.0 - A JavaScript Unit Testing Framework 3 | * 4 | * http://qunitjs.com 5 | * 6 | * Copyright 2012 jQuery Foundation and other contributors 7 | * Released under the MIT license. 8 | * http://jquery.org/license 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 5px 5px 0 0; 42 | -moz-border-radius: 5px 5px 0 0; 43 | -webkit-border-top-right-radius: 5px; 44 | -webkit-border-top-left-radius: 5px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-testrunner-toolbar label { 58 | display: inline-block; 59 | padding: 0 .5em 0 .1em; 60 | } 61 | 62 | #qunit-banner { 63 | height: 5px; 64 | } 65 | 66 | #qunit-testrunner-toolbar { 67 | padding: 0.5em 0 0.5em 2em; 68 | color: #5E740B; 69 | background-color: #eee; 70 | overflow: hidden; 71 | } 72 | 73 | #qunit-userAgent { 74 | padding: 0.5em 0 0.5em 2.5em; 75 | background-color: #2b81af; 76 | color: #fff; 77 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 78 | } 79 | 80 | #qunit-modulefilter-container { 81 | float: right; 82 | } 83 | 84 | /** Tests: Pass/Fail */ 85 | 86 | #qunit-tests { 87 | list-style-position: inside; 88 | } 89 | 90 | #qunit-tests li { 91 | padding: 0.4em 0.5em 0.4em 2.5em; 92 | border-bottom: 1px solid #fff; 93 | list-style-position: inside; 94 | } 95 | 96 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 97 | display: none; 98 | } 99 | 100 | #qunit-tests li strong { 101 | cursor: pointer; 102 | } 103 | 104 | #qunit-tests li a { 105 | padding: 0.5em; 106 | color: #c2ccd1; 107 | text-decoration: none; 108 | } 109 | #qunit-tests li a:hover, 110 | #qunit-tests li a:focus { 111 | color: #000; 112 | } 113 | 114 | #qunit-tests li .runtime { 115 | float: right; 116 | font-size: smaller; 117 | } 118 | 119 | .qunit-assert-list { 120 | margin-top: 0.5em; 121 | padding: 0.5em; 122 | 123 | background-color: #fff; 124 | 125 | border-radius: 5px; 126 | -moz-border-radius: 5px; 127 | -webkit-border-radius: 5px; 128 | } 129 | 130 | .qunit-collapsed { 131 | display: none; 132 | } 133 | 134 | #qunit-tests table { 135 | border-collapse: collapse; 136 | margin-top: .2em; 137 | } 138 | 139 | #qunit-tests th { 140 | text-align: right; 141 | vertical-align: top; 142 | padding: 0 .5em 0 0; 143 | } 144 | 145 | #qunit-tests td { 146 | vertical-align: top; 147 | } 148 | 149 | #qunit-tests pre { 150 | margin: 0; 151 | white-space: pre-wrap; 152 | word-wrap: break-word; 153 | } 154 | 155 | #qunit-tests del { 156 | background-color: #e0f2be; 157 | color: #374e0c; 158 | text-decoration: none; 159 | } 160 | 161 | #qunit-tests ins { 162 | background-color: #ffcaca; 163 | color: #500; 164 | text-decoration: none; 165 | } 166 | 167 | /*** Test Counts */ 168 | 169 | #qunit-tests b.counts { color: black; } 170 | #qunit-tests b.passed { color: #5E740B; } 171 | #qunit-tests b.failed { color: #710909; } 172 | 173 | #qunit-tests li li { 174 | padding: 5px; 175 | background-color: #fff; 176 | border-bottom: none; 177 | list-style-position: inside; 178 | } 179 | 180 | /*** Passing Styles */ 181 | 182 | #qunit-tests li li.pass { 183 | color: #3c510c; 184 | background-color: #fff; 185 | border-left: 10px solid #C6E746; 186 | } 187 | 188 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 189 | #qunit-tests .pass .test-name { color: #366097; } 190 | 191 | #qunit-tests .pass .test-actual, 192 | #qunit-tests .pass .test-expected { color: #999999; } 193 | 194 | #qunit-banner.qunit-pass { background-color: #C6E746; } 195 | 196 | /*** Failing Styles */ 197 | 198 | #qunit-tests li li.fail { 199 | color: #710909; 200 | background-color: #fff; 201 | border-left: 10px solid #EE5757; 202 | white-space: pre; 203 | } 204 | 205 | #qunit-tests > li:last-child { 206 | border-radius: 0 0 5px 5px; 207 | -moz-border-radius: 0 0 5px 5px; 208 | -webkit-border-bottom-right-radius: 5px; 209 | -webkit-border-bottom-left-radius: 5px; 210 | } 211 | 212 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 213 | #qunit-tests .fail .test-name, 214 | #qunit-tests .fail .module-name { color: #000000; } 215 | 216 | #qunit-tests .fail .test-actual { color: #EE5757; } 217 | #qunit-tests .fail .test-expected { color: green; } 218 | 219 | #qunit-banner.qunit-fail { background-color: #EE5757; } 220 | 221 | 222 | /** Result */ 223 | 224 | #qunit-testresult { 225 | padding: 0.5em 0.5em 0.5em 2.5em; 226 | 227 | color: #2b81af; 228 | background-color: #D2E0E6; 229 | 230 | border-bottom: 1px solid white; 231 | } 232 | #qunit-testresult .module-name { 233 | font-weight: bold; 234 | } 235 | 236 | /** Fixture */ 237 | 238 | #qunit-fixture { 239 | position: absolute; 240 | top: -10000px; 241 | left: -10000px; 242 | width: 1000px; 243 | height: 1000px; 244 | } 245 | -------------------------------------------------------------------------------- /test/nohistory.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dialog History Tests 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 |
22 |
23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/nohistory.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var $ = jQuery; 3 | if( location.hash !== "" ){ 4 | throw "hash must be clear to start the tests"; 5 | } 6 | 7 | // force the hash to test init 8 | location.hash = "#nohist-dialog"; 9 | 10 | var $doc, instance, $instance, $nested, $nohist, commonSetup, commonTeardown; 11 | 12 | commonSetup = function() { 13 | $nohist = $( "#nohist" ); 14 | 15 | if( $nohist.data("dialog") ) { 16 | $nohist.data( "dialog" ).destroy(); 17 | } 18 | 19 | 20 | }; 21 | 22 | commonTeardown = function() { 23 | $nohist.unbind( "dialog-closed" ); 24 | $nohist.unbind( "dialog-opened" ); 25 | 26 | $nohist.data( "dialog" ).destroy(); 27 | }; 28 | 29 | // we have to give the browser the time to trigger a hashchange 30 | // so there's no mixup 31 | function closeInstance($dialog){ 32 | $dialog.trigger( "dialog-close" ); 33 | setTimeout(function(){ 34 | start() 35 | }, 400); 36 | } 37 | 38 | var initOpened; 39 | 40 | $(window).one("dialog-opened", function(event){ 41 | $nohist = $(event.target).data( "dialog" ).$el; 42 | initOpened = event.target; 43 | }); 44 | 45 | asyncTest("nohist dialog should open on init if hash is present", function(){ 46 | ok(!initOpened); 47 | start(); 48 | }); 49 | })(); 50 | -------------------------------------------------------------------------------- /test/register.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dialog Tests 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/register.js: -------------------------------------------------------------------------------- 1 | (function( $, window ) { 2 | var instance; 3 | 4 | module("Focus", { 5 | setup: function(){ 6 | instance = new window.componentNamespace.FocusRegistry(); 7 | }, 8 | 9 | teardown: function(){ 10 | $(window.document).unbind(".focus-registry"); 11 | } 12 | }); 13 | 14 | test("should register many objects", function(){ 15 | expect(2); 16 | 17 | var obj = { 18 | checkFocus: function(){ 19 | ok( "focus check called" ); 20 | return false; 21 | }, 22 | stealFocus: function(){} 23 | }; 24 | 25 | instance.register(obj); 26 | instance.register(obj); 27 | 28 | instance.check({ preventDefault: function(){} }); 29 | }); 30 | 31 | test("should throw an exception on two matches", function(){ 32 | expect( 3 ); 33 | var obj = { 34 | checkFocus: function(){ 35 | ok( "focus check called" ); 36 | return true; 37 | }, 38 | stealFocus: function(){ 39 | ok( false ); 40 | } 41 | }; 42 | 43 | instance.register(obj); 44 | instance.register(obj); 45 | 46 | throws(function(){ 47 | instance.check({ preventDefault: function(){} }); 48 | }); 49 | }); 50 | 51 | test("should throw an execption if an object doesn't meet reqs", function(){ 52 | expect( 3 ); 53 | var obj = { 54 | checkFocus: function(){} 55 | 56 | // missing `stealFocus` 57 | }; 58 | 59 | throws(function(){ 60 | instance.register(obj); 61 | }); 62 | 63 | obj = { 64 | stealFocus: function(){} 65 | 66 | // missing `checkFocus` 67 | }; 68 | 69 | throws(function(){ 70 | instance.register(obj); 71 | }); 72 | 73 | obj = { 74 | // missing `checkFocus` 75 | // missing `stealFocus` 76 | }; 77 | 78 | throws(function(){ 79 | instance.register(obj); 80 | }); 81 | }); 82 | 83 | 84 | asyncTest("should call stealFocus on checkFocus", function(){ 85 | expect( 2 ); 86 | 87 | var obj = { 88 | checkFocus: function(){ 89 | ok( true, "focus check called" ); 90 | return true; 91 | }, 92 | 93 | stealFocus: function(){ 94 | ok( true, "focus steal called" ); 95 | start(); 96 | } 97 | }; 98 | 99 | instance.register(obj); 100 | 101 | instance.check({ preventDefault: function(){} }); 102 | }); 103 | 104 | asyncTest("should not call stealFocus without checkFocus", function(){ 105 | expect( 1 ); 106 | var obj = { 107 | checkFocus: function(){ 108 | ok( true, "focus check called" ); 109 | return false; 110 | }, 111 | stealFocus: function(){ 112 | ok( false, "focus steal called" ); 113 | } 114 | }; 115 | 116 | instance.register(obj); 117 | 118 | instance.check({ preventDefault: function(){} }); 119 | 120 | // give the focus event stack time to unwind and time 121 | // for the timeout in `check` to fire if it's going to 122 | setTimeout(function(){ 123 | start(); 124 | }, 1000); 125 | }); 126 | 127 | test("should remove object from registry", function(){ 128 | var obj1 = { 129 | checkFocus: function(){}, 130 | stealFocus: function(){} 131 | }; 132 | 133 | var obj2 = { 134 | checkFocus: function(){}, 135 | stealFocus: function(){}, 136 | foo: "foo" 137 | }; 138 | 139 | instance.register(obj1); 140 | instance.register(obj1); 141 | instance.register(obj2); 142 | 143 | equal(instance.registry.length, 3); 144 | equal(instance.registry[0], obj1); 145 | equal(instance.registry[1], obj1); 146 | equal(instance.registry[2], obj2); 147 | 148 | instance.unregister(obj1); 149 | 150 | equal(instance.registry.length, 1); 151 | equal(instance.registry[0], obj2); 152 | }); 153 | 154 | asyncTest("should call stealFocus on focusin if checkFocus", function(){ 155 | expect(1); 156 | var obj = { 157 | checkFocus: function(){ return true; }, 158 | stealFocus: function(){ 159 | ok( true, "steal focus called" ); 160 | start(); 161 | } 162 | }; 163 | 164 | instance.register(obj); 165 | 166 | $(window.document).trigger("focusin"); 167 | }); 168 | })(window.jQuery, window); 169 | --------------------------------------------------------------------------------