├── .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 | [ ](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 |
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 |
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 |
137 |
138 |
Additional external dialog with a # in its href, just for testing purposes:
139 |
140 |
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 |
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 |
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 |
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 |
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 |
201 |
202 |
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 | The dialog element's CSS display property does not equal "none"
215 | The dialog element's CSS visibility property does not equal "hidden"
216 | The dialog close link's (if it is in the markup) CSS display property equals "none"
217 | 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 |
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 = $("");
70 |
71 | // get the scripts and styles to ship to the iframe
72 | // TODO we should support styles/scripts elsewhere in the page
73 | var headHTML = $( "head" ).html();
74 |
75 | // wait until the iframe loads to send the data
76 | $iframe.bind("load",function(){
77 |
78 | // wait for the iframe page to transmit the height of the page
79 | $(window).bind("message", function(event){
80 | var data = JSON.parse(event.data || event.originalEvent.data);
81 |
82 | if( data.iframeid !== $self.data("id." + pluginName) ){
83 | return;
84 | }
85 |
86 | $iframe.attr("height", data.iframeheight);
87 | });
88 |
89 | // send a message to the iframe with the snippet to load and any
90 | // assets that are required to make it look right
91 | $iframe[0].contentWindow.postMessage({
92 | html: snippetHTML,
93 | head: headHTML,
94 | id: $self.data("id." + pluginName),
95 | selector: selector
96 | }, "*");
97 | });
98 |
99 | // style the iframe properly
100 | $iframe.addClass("xray-iframe");
101 |
102 | // replace the snippet which is rendered in the page with the iframe
103 | $(this).find(".snippet").html("").append($iframe);
104 | }
105 | },
106 | _createButton: function() {
107 | var btn = document.createElement( "a" ),
108 | txt = document.createTextNode( o.text.open ),
109 | el = $( this );
110 |
111 | btn.setAttribute( "class", o.classes.button );
112 | btn.href = "#";
113 | btn.appendChild( txt );
114 |
115 | $( btn )
116 | .bind( "click", function( e ) {
117 | var isOpen = el.attr( "class" ).indexOf( o.classes.open ) > -1;
118 |
119 | el[ isOpen ? "removeClass" : "addClass" ]( o.classes.open );
120 | btn.innerHTML = ( isOpen ? o.text.open : o.text.close );
121 |
122 | e.preventDefault();
123 |
124 | })
125 | .insertBefore( el );
126 | },
127 | _createSource: function() {
128 | var el = this;
129 | var getPrefixText = function () {
130 | if( el.className.match( new RegExp( "\\b" + o.classes.antipattern + "\\b", "gi" ) ) ) {
131 | return o.text.antipattern;
132 | }
133 | return o.text.titlePrefix;
134 | };
135 | var title = el.getElementsByClassName( o.classes.title );
136 | var deprecatedTitle;
137 | var preel = document.createElement( "pre" );
138 | var codeel = document.createElement( "code" );
139 | var wrap = document.createElement( "div" );
140 | var sourcepanel = document.createElement( "div" );
141 | var code;
142 | var leadingWhiteSpace;
143 | var source;
144 |
145 | if( title.length ) {
146 | title = title[ 0 ];
147 | title.parentNode.removeChild( title );
148 | title.innerHTML = getPrefixText() + ": " + title.innerHTML;
149 | } else {
150 | deprecatedTitle = el.getAttribute( "data-title" );
151 | title = document.createElement( "div" );
152 | title.className = o.classes.title;
153 | title.innerHTML = getPrefixText() + ( deprecatedTitle ? ": " + deprecatedTitle : "" );
154 | }
155 |
156 | // remove empty value attributes
157 | code = el.innerHTML.replace( /\=\"\"/g, '' );
158 | leadingWhiteSpace = code.match( /(^[\s]+)/ );
159 |
160 | if( leadingWhiteSpace ) {
161 | code = code.replace( new RegExp( leadingWhiteSpace[ 1 ], "gmi" ), "\n" );
162 | }
163 |
164 | source = document.createTextNode( code );
165 |
166 | wrap.setAttribute( "class", "snippet" );
167 |
168 | $( el ).wrapInner( wrap );
169 |
170 | codeel.appendChild( source );
171 | preel.appendChild( codeel );
172 |
173 | sourcepanel.setAttribute( "class", o.classes.sourcepanel );
174 | sourcepanel.appendChild( preel );
175 |
176 | this.appendChild( sourcepanel );
177 | this.insertBefore( title, this.firstChild );
178 | }
179 | };
180 |
181 | // Collection method.
182 | $.fn[ pluginName ] = function( arrg, a, b, c ) {
183 | return this.each(function() {
184 |
185 | // if it's a method
186 | if( arrg && typeof( arrg ) === "string" ){
187 | return $.fn[ pluginName ].prototype[ arrg ].call( this, a, b, c );
188 | }
189 |
190 | // don't re-init
191 | if( $( this ).data( pluginName + "data" ) ){
192 | return $( this );
193 | }
194 |
195 | // otherwise, init
196 | $( this ).data( pluginName + "active", true );
197 | $.fn[ pluginName ].prototype._create.call( this );
198 | });
199 | };
200 |
201 | // add methods
202 | $.extend( $.fn[ pluginName ].prototype, methods );
203 |
204 | // auto-init
205 | var initted;
206 | function init(){
207 | if( !initted ){
208 | $( o.initSelector )[ pluginName ]();
209 | initted = true;
210 | }
211 | }
212 | // init either on beforeenhance event or domready, whichever comes first.
213 | $( document ).bind("beforeenhance", init );
214 | $( init );
215 |
216 |
217 | }( jQuery ));
218 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fg-dialog",
3 | "description": "simple dialog",
4 | "version": "1.6.4",
5 | "homepage": "https://github.com/filamentgroup/dialog",
6 | "author": {
7 | "name": "Filament Group",
8 | "email": "thegroup@filamentgroup.com",
9 | "url": "http://filamentgroup.com"
10 | },
11 | "license": "MIT",
12 | "engines": {
13 | "node": "0.10"
14 | },
15 | "scripts": {
16 | "test": "./node_modules/.bin/grunt test --verbose"
17 | },
18 | "devDependencies": {
19 | "grunt": "^0.4.5",
20 | "grunt-cli": "^0.1.13",
21 | "grunt-contrib-concat": "^0.5.1",
22 | "grunt-contrib-jshint": "^0.11.2",
23 | "grunt-contrib-qunit": "^2.0.0",
24 | "grunt-contrib-uglify": "^0.9.1"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/dialog-init.js:
--------------------------------------------------------------------------------
1 | (function( w, $ ){
2 | var Dialog = w.componentNamespace.Dialog,
3 | doc = w.document,
4 | pluginName = "dialog";
5 |
6 | $.fn[ pluginName ] = function(){
7 | return this.each(function(){
8 | var $el = $( this );
9 |
10 | // prevent double init
11 | if( $el.data( "dialog" ) ){
12 | return;
13 | }
14 |
15 | var dialog = new Dialog( this );
16 | var onOpen, onClose, onClick, onBackgroundClick;
17 |
18 | $el.addClass( Dialog.classes.content )
19 |
20 | .bind( Dialog.events.open, onOpen = function(){
21 | dialog.open();
22 | })
23 | .bind( Dialog.events.close, onClose = function(){
24 | dialog.close();
25 | })
26 | .bind( "click", onClick = function( e ){
27 | if( $(e.target).closest(Dialog.selectors.close).length ){
28 | e.preventDefault();
29 | dialog.close();
30 | }
31 | });
32 |
33 | dialog.$background.bind( "click", onBackgroundClick = function() {
34 | dialog.close();
35 | });
36 |
37 | var onHashchange;
38 |
39 | // on load and hashchange, open the dialog if its hash matches the last part of the hash, and close if it doesn't
40 | if( Dialog.useHash ){
41 | $( w ).bind( "hashchange", onHashchange = function(){
42 | var hash = w.location.hash.split( "#" ).pop();
43 |
44 | // if the hash matches this dialog's, open!
45 | if( hash === dialog.hash ){
46 | if( !dialog.nohistory ){
47 | dialog.open();
48 | }
49 | }
50 | // if it doesn't match...
51 | else {
52 | dialog.close();
53 | }
54 | });
55 | }
56 |
57 | var onDocClick, onKeyup, onResize;
58 |
59 | // open on matching a[href=#id] click
60 | $( doc ).bind( "click", onDocClick = function( e ){
61 | var $matchingDialog, $a;
62 |
63 | $a = $( e.target ).closest( "a" );
64 |
65 |
66 | if( !dialog.isOpen && $a.length && $a.attr( "href" ) ){
67 | var id = $a.attr( "href" ).replace( /^#/, "" );
68 |
69 | // catch invalid selector exceptions
70 | try {
71 | // Attempt to find the matching dialog at the same id or at the
72 | // encoded id. This allows matching even when href url ids are being
73 | // changed back and forth between encoded and decoded forms.
74 | $matchingDialog =
75 | $( "[id='" + id + "'], [id='" + encodeURIComponent(id) + "']" );
76 | } catch ( error ) {
77 | // TODO should check the type of exception, it's not clear how well
78 | // the error name "SynatxError" is supported
79 | return;
80 | }
81 |
82 | if( $matchingDialog.length && $matchingDialog.is( $el ) ){
83 | e.preventDefault();
84 | $matchingDialog.trigger( Dialog.events.open );
85 | }
86 | }
87 | });
88 |
89 | // close on escape key
90 | $( doc ).bind( "keyup", onKeyup = function( e ){
91 | if( e.which === 27 ){
92 | dialog.close();
93 | }
94 | });
95 |
96 | dialog._checkInteractivity();
97 | var resizepoll;
98 | $( window ).bind( "resize", onResize = function(){
99 | if( resizepoll ){
100 | clearTimeout( resizepoll );
101 | }
102 | resizepoll = setTimeout( function(){
103 | dialog._checkInteractivity.call( dialog );
104 | }, 150 );
105 | });
106 |
107 | $el.bind("destroy", function(){
108 | $(w).unbind("hashchange", onHashchange);
109 |
110 | $el
111 | .unbind( Dialog.events.open, onOpen )
112 | .unbind( Dialog.events.close, onClose )
113 | .unbind( "click", onClick );
114 |
115 | dialog.$background.unbind( "click", onBackgroundClick);
116 |
117 | $( doc ).unbind( "click", onDocClick );
118 | $( doc ).unbind( "keyup", onKeyup );
119 | $( window ).unbind( "resize", onResize );
120 | });
121 |
122 | onHashchange();
123 |
124 | window.focusRegistry.register(dialog);
125 | });
126 | };
127 |
128 | // auto-init on enhance
129 | $( w.document ).bind( "enhance", function( e ){
130 | var target = e.target === w.document ? "" : e.target;
131 | $( "." + pluginName, e.target ).add( target ).filter( "." + pluginName )[ pluginName ]();
132 | });
133 |
134 | function FocusRegistry(){
135 | var self = this;
136 |
137 | this.registry = [];
138 |
139 | $(window.document).bind("focusin.focus-registry", function(event){
140 | self.check(event);
141 | });
142 | }
143 |
144 | FocusRegistry.prototype.register = function(obj){
145 | if( !obj.checkFocus ){
146 | throw new Error( "Obj must implement `checkFocus`" );
147 | }
148 |
149 | if( !obj.stealFocus ){
150 | throw new Error( "Obj must implement `stealFocus`" );
151 | }
152 |
153 | this.registry.push(obj);
154 | };
155 |
156 | FocusRegistry.prototype.unregister = function(obj){
157 | var newRegistry = [];
158 |
159 | for(var i = 0; i < this.registry.length; i++ ){
160 | if(this.registry[i] !== obj){
161 | newRegistry.push(this.registry[i]);
162 | }
163 | }
164 |
165 | this.registry = newRegistry;
166 | };
167 |
168 | FocusRegistry.prototype.check = function(event){
169 | var stealing = [];
170 |
171 | // for all the registered components
172 | for(var i = 0; i < this.registry.length; i++){
173 |
174 | // if a given component wants to steal the focus, record that
175 | if( this.registry[i].checkFocus(event) ){
176 | stealing.push(this.registry[i]);
177 | }
178 | }
179 |
180 | // if more than one component wants to steal focus throw an exception
181 | if( stealing.length > 1 ){
182 | throw new Error("Two components are attempting to steal focus.");
183 | }
184 |
185 | // otherwise allow the first component to steal focus
186 | if(stealing[0]) {
187 | event.preventDefault();
188 |
189 | // let this event stack unwind and then steal the focus
190 | // which will again trigger the check above
191 | setTimeout(function(){
192 | stealing[0].stealFocus(event);
193 | });
194 | }
195 | };
196 |
197 | // constructor in namespace
198 | window.componentNamespace.FocusRegistry = FocusRegistry;
199 |
200 | // singleton
201 | window.focusRegistry = new FocusRegistry();
202 | }( this, window.jQuery ));
203 |
--------------------------------------------------------------------------------
/src/dialog-linker.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Simple jQuery Dialog Linker
3 | * https://github.com/filamentgroup/dialog
4 | *
5 | * Copyright (c) 2013 Filament Group, Inc.
6 | * Author: @scottjehl
7 | * Licensed under the MIT, GPL licenses.
8 | */
9 |
10 | (function( w, $ ){
11 |
12 | $( w.document )
13 | // open on matching a[href=#id] click
14 | .bind( "click", function( e ){
15 |
16 | var $a = $( e.target ).closest( "a" );
17 | var link = $a.is( "[data-dialog-link]" );
18 | var iframe = $a.is( "[data-dialog-iframe]" );
19 |
20 | function createDialog(content){
21 | var linkHref = $a.attr( "href" );
22 | var dialogClasses = $a.attr( "data-dialog-addclass" ) || "";
23 | var dialogLabelledBy = $a.attr( "data-dialog-labeledby" ) || "";
24 | var dialogLabel = $a.attr( "data-dialog-label" ) || "";
25 |
26 | var dialogNoHistory =
27 | $a.attr( "data-dialog-history" ) === "false" ||
28 | !w.componentNamespace.Dialog.history;
29 |
30 | var id;
31 |
32 | if( linkHref ) {
33 | id = encodeURIComponent(linkHref);
34 | }
35 |
36 | // if there are two links in the page that point to the same url
37 | // then the same dialog will be reused and the content updated
38 | var $existing = $("[id='" + id + "']");
39 | if( $existing.length ){
40 | $existing
41 | .html("")
42 | .append(content)
43 | .dialog()
44 | .trigger("enhance")
45 | .trigger("dialog-update");
46 | return;
47 | }
48 |
49 | $a
50 | .attr("href", "#" + id )
51 | .removeAttr( "data-dialog-link" );
52 |
53 | var $dialog = $( "
" )
54 | .append( content )
55 | .appendTo( "body" )
56 | .dialog();
57 |
58 | function open(){
59 | $dialog.trigger( "dialog-open" );
60 | }
61 |
62 | // make sure the opener link is set as the focued item if one is not defined already
63 | var instance = $dialog.data( "dialog" );
64 | if( instance && !instance.focused ){
65 | instance.focused = $a[ 0 ];
66 | }
67 |
68 | if( iframe ){
69 | $dialog.find( "iframe" ).one( "load", open );
70 | }
71 | else {
72 | open();
73 | }
74 |
75 | $dialog.trigger( "enhance" );
76 | }
77 |
78 | if( link ){
79 | var url = $a.attr( "href" );
80 |
81 | // get content either from an iframe or not
82 | if( $a.is( "[data-dialog-iframe]" ) ){
83 | createDialog( "" );
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 |
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 |
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 |
--------------------------------------------------------------------------------