├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── demo
├── demo.css
├── demo.html
└── sprite.png
├── karma.conf.js
├── package-lock.json
├── package.json
├── site
├── back_pattern.png
├── fork_me.png
├── index.html
├── prism.css
├── prism.js
├── sprite.png
├── src
│ ├── jquery.collapse.js
│ ├── jquery.collapse_cookie_storage.js
│ └── jquery.collapse_storage.js
└── style.css
├── src
├── jquery.collapse.js
├── jquery.collapse_cookie_storage.js
└── jquery.collapse_storage.js
├── test
├── jquery.collapse.cookie_storage_test.coffee
├── jquery.collapse_storage_test.coffee
└── jquery.collapse_test.coffee
└── vendor
├── jquery-1.9.1.js
└── json2.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 | *.diff
3 | *.patch
4 | .DS_Store
5 | .settings
6 | spec/*_spec.js
7 | TODO
8 | node_modules
9 | coverage
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "stable"
4 | addons:
5 | chrome: stable
6 | script:
7 | - npm install
8 | - npm run test
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013 Daniel Stocks
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 | # jQuery Collapse
2 |
3 | A lightweight and flexible jQuery plugin that allows you to collapse content. A feature also known as 'progressive disclosure'.
4 |
5 | jQuery Collapse is tested against the latest version of jQuery but requires at least jQuery 1.7.0.
6 |
7 | [](https://travis-ci.org/danielstocks/jQuery-Collapse)
8 | [](https://codeclimate.com/github/danielstocks/jQuery-Collapse)
9 | [](https://coveralls.io/github/danielstocks/jQuery-Collapse?branch=master)
10 |
11 | ## Features
12 |
13 | - [WAI ARIA](http://dev.opera.com/articles/view/introduction-to-wai-aria/) compliant
14 | - Lightweight (~1.2kb minified and gzipped)
15 | - Cross Browser compliant (Tested in >= IE6, Firefox, Safari, Chrome, Opera)
16 | - **Accordion** behaviour can be enabled.
17 | - **Persistence** to remember open sections on page reload!
18 |
19 |
20 | ### Demo
21 |
22 | A demo showcasing all the features of the plugin can be found at 'demo/demo.html' in this repository.
23 |
24 | ## Usage
25 |
26 | Load jQuery and the jQuery Collapse plugin into your document:
27 |
28 | ```html
29 |
30 |
31 | ```
32 |
33 | Here's some sample HTML markup:
34 |
35 | ```html
36 |
37 |
Fruits
38 |
39 |
Apple
40 |
Pear
41 |
Orange
42 |
43 |
Info
44 |
45 |
You can use any container you like (in this case a div element)
46 |
47 |
48 | ```
49 |
50 | That's it! The *data-collapse* attribute will automatically trigger the script.
51 |
52 | ### Open/Collapse section by default
53 |
54 | The standard behaviour is to collapse all the sections on page load.
55 | If you want to show a section to the user on page load you can
56 | achieve this by adding an 'open' class to the section heading
57 |
58 | ```html
59 |
60 |
I'm open by default
61 |
Yay
62 |
63 | ```
64 |
65 | ### Open all sections
66 |
67 | You can open or close sections by utilizing events. Assume you have the following markup:
68 |
69 | ```html
70 |
71 |
Section 1
72 |
I'm first
73 |
Section 2
74 |
I'm second/p>
75 |
76 | ```
77 | You can now trigger events on the elements you want to affect. For instance:
78 |
79 | ```js
80 | $("#test").trigger("open") // Open all sections
81 | $("#test").trigger("close") // Close all sections
82 | $("#test h2 a").first().trigger("open") // Open first section
83 | ```
84 |
85 | For further information, please refer to the [events](#events) documentation.
86 |
87 | ## JavaScript usage
88 |
89 | If you'd rather omit the 'data-collapse' attribute in the HTML and load the plugin via jQuery, you can:
90 |
91 | ```js
92 | $("#demo").collapse({
93 | // options...
94 | });
95 | ```
96 |
97 | If you don't want to use the jQuery ($) wrapper, you can also access the
98 | plugin with *vanilla* JavaScript:
99 |
100 | ```js
101 | new jQueryCollapse($("#demo"), {
102 | // options...
103 | });
104 | ```
105 |
106 | ### Using custom markup
107 |
108 | By default the plugin will look for groups of two elements.
109 | In real life™ your markup may vary and you'll need to customize how the
110 | plugin interprets it. For example
111 |
112 | ```html
113 |
114 |
115 |
Summary
116 |
details...
117 |
118 |
119 |
Summary
120 |
details...
121 |
122 |
123 | ```
124 |
125 | In order for the plugin to understand the above markup, we can pass a 'query'
126 | option specifying where to find the header/summary element of a section:
127 |
128 | ```js
129 | new jQueryCollapse($("#demo"), {
130 | query: 'div h2'
131 | });
132 | ```
133 |
134 | #### External markup example
135 |
136 | You can also just use an arbitrary link on a page to collapse\expand a section:
137 |
138 | ```html
139 | Toggle section
140 |
141 |
Summary
142 |
details...
143 |
144 | ```
145 |
146 | Then attach an event handler to your link and make use of jQuery Collapse events to toggle the section:
147 |
148 | ```js
149 | $("#toggle").click(function() {
150 | $(this.hash).trigger("toggle");
151 | });
152 | ```
153 |
154 | #### Custom click query
155 |
156 | Sometimes you want to customize what element inside the collapse summary that should trigger the open/close action. Consider the following markup:
157 |
158 | ```html
159 |
173 | ```
174 |
175 | Now use the clickQuery option to trigger the action only when the span is clicked
176 |
177 | ```js
178 | $("#custom-click-query").collapse({
179 | clickQuery: "span.toggle"
180 | });
181 | ```
182 |
183 |
184 | ## Accordion
185 |
186 | To activate the accordion behaviour set 'accordion' as the value of the 'data-collapse' attribute:
187 |
188 | ```html
189 |
190 | ...
191 |
192 | ```
193 |
194 |
195 | ## Persistence
196 |
197 | By default, if the user reloads the page all the sections will be closed.
198 | If you want previously collapsed sections to stay open you can add 'persist' to the data-collapse attribute:
199 |
200 | ```html
201 |
202 | ...
203 |
204 | ```
205 | And include the storage module in your document *after* the other
206 | scripts.
207 |
208 | ```html
209 |
210 | ```
211 |
212 | As in the example above, the target element (#demo) **will require an ID** in order for the
213 | persistence to work.
214 |
215 | You can combine the accordion and persistence options by adding
216 | both values to the data-collapse attribute:
217 |
218 | ```html
219 |
220 | ...
221 |
222 | ```
223 |
224 | jQuery Collapse uses HTML5 localStorage if available, otherwise it
225 | will attempt to use cookies (read about IE support below). If that also fails, it will degrade
226 | to work but without any persistence.
227 |
228 | ### Internet Explorer =< 7 Support
229 |
230 | For IE 6-7 you'll need to include the cookie storage and JSON2 libraries
231 | for the cookie storage support to work properly:
232 |
233 | ```html
234 |
238 | ```
239 |
240 | ## API Documentation
241 |
242 | Here are the exposed options and events that you can play around with
243 | using JavaScript. Enjoy.
244 |
245 | ### Options
246 |
247 | You can pass the following options when initializing
248 | the plugin with JavaScript.
249 |
250 | * **open** (function) : Custom function for opening section (default: function(){ this.show() })
251 | * **close** (function) : Custom function for collapsing section (default: function(){ this.hide() })
252 | * **accordion** (bool) : Enable accordion behaviour by setting this option to 'true'
253 | * **persist** (bool) : Enable persistence between page loads by setting this option to 'true'
254 | * **query** (string) : Please refer to to [using custom markup](#using-custom-markup)
255 | * **clickQuery** (string): Please refer to [custom click query](#custom-click-query)
256 |
257 | Example usage of options:
258 |
259 | ```js
260 | // Initializing collapse plugin
261 | // with custom open/close methods,
262 | // persistence plugin and accordion behaviour
263 | $("#demo").collapse({
264 | open: function() {
265 | // The context of 'this' is applied to
266 | // the collapsed details in a jQuery wrapper
267 | this.slideDown(100);
268 | },
269 | close: function() {
270 | this.slideUp(100);
271 | },
272 | accordion: true,
273 | persist: true
274 | });
275 | ```
276 |
277 | ### Events
278 |
279 | #### Binding events
280 |
281 | You can listen for the **opened** and **closed** events on a collapsed collection.
282 |
283 | ```js
284 |
285 | $("#demo").bind("opened", function(e, section) {
286 | console.log(section, " was opened");
287 | });
288 |
289 | $("#demo").bind("closed", function(e, section) {
290 | console.log(section, " was closed");
291 | });
292 | ```
293 |
294 | #### Triggering events
295 |
296 | You can manually trigger an **open**, **close** or **toggle** event to change the state of a section:
297 |
298 | ```js
299 | $("#demo").trigger("open") // open all sections
300 | $("#demo").trigger("close") // close all sections
301 | $("#demo h2 a").last().trigger("toggle") // toggle last section
302 | ```
303 |
304 | When a section changes state, it will trigger either an "opened" or "closed" event in return, depending on it's new state.
305 |
306 | ### API methods
307 |
308 | If you're using vanilla JavaScript to instantiate the plugin, you'll get
309 | direct access to the **open**, **close** and **toggle** methods.
310 |
311 | ```js
312 | var demo = new jQueryCollapse($("#demo")); // Initializing plugin
313 | demo.open(); // Open all sections
314 | demo.close(); // Close all sections
315 | demo.open(0); // Open first section
316 | demo.open(1); // Open second section
317 | demo.close(0); // Close first section
318 | demo.toggle(1); // Toggle second section
319 | ```
320 |
321 | ## Contributing
322 |
323 | Did you find a bug? Do you want to introduce a feature? Here's what to do (in the following order)
324 |
325 | * Clone this repository, and run `npm install`
326 | * Write a test case
327 | * Watch it fail (red light)
328 | * Fix bug / introduce feature
329 | * Watch it pass (green light)
330 | * Refactor / Perfectionize!
331 | * Submit a pull request on Github and wait patiently...
332 | * Rejoice!
333 |
334 | ### A note about testing
335 |
336 | To run the tests simply type "npm test". The [Karma](http://karma-runner.github.io/) test runner will open Chrome and Firefox and run the tests.
337 |
338 | The tests are written in a BDD fashion using CoffeeScript and can be found in the test directory.
339 |
340 | The test suite uses [Mocha](http://mochajs.org/) (test framework), [Chai](http://chaijs.com/) (exceptions) and [Sinon](http://sinonjs.org/) (stubs and mocks).
341 |
--------------------------------------------------------------------------------
/demo/demo.css:
--------------------------------------------------------------------------------
1 | body {
2 | background: #ddd;
3 | margin: 0 auto;
4 | max-width: 1024px;
5 | font: normal 14px/1.2 'Helvetica Neue', 'Arial'
6 | }
7 |
8 | .fork {
9 | border:0;
10 | position:absolute;
11 | top:0;
12 | right:0;
13 | }
14 |
15 | .col {
16 | width: 320px;
17 | float: left;
18 | margin-right:32px;
19 | margin-bottom: 16px;
20 | }
21 | .c3 {
22 | margin-right:0;
23 | }
24 |
25 | h1 {
26 | border-bottom: 1px solid #333;
27 | font-size: 32px;
28 | color: #fff;
29 | padding-bottom: 12px;
30 | text-shadow: 0px 0px 2px rgba(0,0,0,0.6);
31 | }
32 |
33 | h2 {
34 | margin: 10px 0;
35 | color: #000;
36 | font-size: 18px;
37 | text-shadow: 1px 1px 2px #fff;
38 | }
39 |
40 | h3 {
41 | margin: 0;
42 | background-color: rgb(228,10,85);
43 | background-image: linear-gradient(bottom, rgb(228,10,85) 14%, rgb(255,36,111) 57%);
44 | background-image: -o-linear-gradient(bottom, rgb(228,10,85) 14%, rgb(255,36,111) 57%);
45 | background-image: -moz-linear-gradient(bottom, rgb(228,10,85) 14%, rgb(255,36,111) 57%);
46 | background-image: -webkit-linear-gradient(bottom, rgb(228,10,85) 14%, rgb(255,36,111) 57%);
47 | background-image: -ms-linear-gradient(bottom, rgb(228,10,85) 14%, rgb(255,36,111) 57%);
48 | }
49 |
50 | h3 a {
51 | background: url("sprite.png") 15px 13px no-repeat;
52 | display: block;
53 | padding: 10px;
54 | padding-left: 32px;
55 | margin: 0;
56 | color: #fff;
57 | text-decoration: none;
58 | font-weight: normal;
59 | border-bottom: 1px solid rgba(128, 10, 85, 0.5);
60 | text-shadow: 1px 1px 1px rgb(128,10,85);
61 | }
62 | h3:hover { background: rgb(228,10,85); }
63 | h3.open { background: rgb(255,70,120); }
64 | h3.open a { background-position: 13px -25px; }
65 | h3 + div { padding: 10px; }
66 | h2 + div,
67 | .example {
68 | background: #fff;
69 | overflow: hidden;
70 | border-radius: 3px;
71 | -moz-border-radius: 3px;
72 | -webkit-border-radius: 3px;
73 | margin-bottom: 20px;
74 | }
75 |
76 | /* Pre hide sections with JavaScript on
77 | --- */
78 | h3+div {
79 | display: none;
80 | }
81 |
82 | /* CSS3 Animation example
83 | --- */
84 | #css3-animated-example h3 + div {
85 | height: 0px;
86 | padding: 0px;
87 | overflow: hidden;
88 | background: #000;
89 | display: block!important;
90 | -webkit-transform: translateZ(0);
91 | -webkit-transition: all 0.3s ease;
92 | moz-transition: all 0.3s ease;
93 | -o-transition: all 0.3s ease;
94 | -ms-transition:all 0.3s ease;
95 | transition: all 0.3s ease;
96 | }
97 | #css3-animated-example .content {
98 | padding: 10px;
99 | }
100 |
101 | #css3-animated-example h3.open + div {
102 | height: auto;
103 | background: #aaffff;
104 | }
105 |
106 | /* Event example
107 | --- */
108 | pre#event-log {
109 | background: #fafacc;
110 | padding: 10px;
111 | display: block;
112 | }
113 |
114 | /* Responsive design
115 | --- */
116 | @media screen and (max-width: 1056px) {
117 | body {
118 | width: 672px;
119 | }
120 | .c2 {
121 | margin-right: 0;
122 | margin-bottom: 0;
123 | }
124 | }
125 |
126 | .test { background: #ccc; padding: 10px; border-bottom: 1px solid #aaa;}
127 | .test + div { background: #fff; padding: 10px; }
128 |
129 | @media
130 | only screen and (max-width: 704px),
131 | only screen and (-webkit-min-device-pixel-ratio : 1.5),
132 | only screen and (min-device-pixel-ratio : 1.5) {
133 | body {
134 | width: 320px;
135 | }
136 | .c1 {
137 | margin-right: 0;
138 | margin-bottom: 0;
139 | }
140 | h1 {
141 | font-size: 28px;
142 | text-shadow: 0px 0px 1px rgba(0,0,0,0.7);
143 | }
144 | }
145 |
146 | @media
147 | only screen and (-webkit-min-device-pixel-ratio : 1.5),
148 | only screen and (min-device-pixel-ratio : 1.5) {
149 | body {
150 | padding: 0 12px;
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/demo/demo.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | jQuery Collapse Demo
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
Remembers open sections on page reload using either HTML5 localStorage or cookies!
91 |
92 |
Accordion
93 |
94 |
jQuery Collapse is packed with the classic 'accordion' behaviour that can be easily toggled on and off.
95 |
96 |
WAI-ARIA Compliance
97 |
98 |
The plugin has been designed with WAI-ARIA in mind which defines a way to make Web content and Web applications more accessible to people with disabilities.
99 |
100 |
Lightweight
101 |
102 |
jQuery-Collapse is designed to be lightweight with minimal bloat. It's only ~1kb when compiled and gzipped!
103 |
104 |
Cross Browser
105 |
106 |
Fully tested in IE6+, Firefox3+, Chrome5+, Safari4+, Opera 10+. Degrades gracefully in unsupported browsers
107 |
108 |
109 |
110 |
111 |
112 |
— A few examples —
113 |
To better understand how these examples work I recommend you to view the source.
114 |
115 |
116 |
117 |
").append(@summary, @details)
141 |
142 | @parent =
143 | sections : []
144 | states: [0,1,0]
145 | options: {}
146 | $el: @rootEl
147 |
148 | @section = new jQueryCollapseSection(@summary, @parent)
149 |
150 | describe 'defaults', ->
151 |
152 | it 'is not open', ->
153 | expect(@section.isOpen).to.eql false
154 |
155 | it 'sets a summary', ->
156 | expect(@section.$summary.is(@summary)).to.eql true
157 |
158 | it 'sets a data-collapse summary attribute on summry', ->
159 | expect(@section.$summary.data()).to.eql { collapseSummary: '' }
160 |
161 | it 'injects a link inside el', ->
162 | expect(@section.$summary.find("a").length).to.eql 1
163 |
164 | it 'finds and sets details', ->
165 | expect(@section.$details.get(0)).to.eql @details.get(0)
166 |
167 | it 'applies a parent', ->
168 | expect(@section.parent).to.eql @parent
169 |
170 | it 'applies parent options', ->
171 | expect(@section.options).to.eql @parent.options
172 |
173 | it 'pushes to parent sections', ->
174 | expect(@parent.sections[0]).to.eql @section
175 |
176 | describe 'state is 1', ->
177 |
178 | before ->
179 | sinon.stub(jQueryCollapseSection.prototype, '_index').returns(1)
180 | @open = sinon.stub(jQueryCollapseSection.prototype, 'open')
181 | @section = new jQueryCollapseSection(@summary, @parent)
182 |
183 | after ->
184 | jQueryCollapseSection.prototype._index.restore()
185 | jQueryCollapseSection.prototype.open.restore()
186 |
187 | it 'opens with a bypass', ->
188 | expect(@open.calledWith(true)).to.be.ok
189 |
190 | describe 'state is 0', ->
191 |
192 | before ->
193 | sinon.stub(jQueryCollapseSection.prototype, '_index').returns(0)
194 | @close = sinon.stub(jQueryCollapseSection.prototype, 'close')
195 | @section = new jQueryCollapseSection(@summary, @parent)
196 |
197 | after ->
198 | jQueryCollapseSection.prototype._index.restore()
199 | jQueryCollapseSection.prototype.close.restore()
200 |
201 | it 'closes with a bypass', ->
202 | expect(@close.calledWith(true)).to.be.ok
203 |
204 | describe 'section summary has .open class', ->
205 |
206 | before ->
207 | sinon.stub($.fn, 'is').withArgs('.open').returns(true)
208 | @open = sinon.stub(jQueryCollapseSection.prototype, 'open')
209 | new jQueryCollapseSection(@summary, @parent)
210 |
211 | after ->
212 | $.fn.is.restore()
213 | jQueryCollapseSection.prototype.open.restore()
214 |
215 | it 'opens with a bypass', ->
216 | expect(@open.calledWith(true)).to.be.ok
217 |
--------------------------------------------------------------------------------
/vendor/json2.js:
--------------------------------------------------------------------------------
1 | /*
2 | json2.js
3 | 2011-10-19
4 |
5 | Public Domain.
6 |
7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
8 |
9 | See http://www.JSON.org/js.html
10 |
11 |
12 | This code should be minified before deployment.
13 | See http://javascript.crockford.com/jsmin.html
14 |
15 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
16 | NOT CONTROL.
17 |
18 |
19 | This file creates a global JSON object containing two methods: stringify
20 | and parse.
21 |
22 | JSON.stringify(value, replacer, space)
23 | value any JavaScript value, usually an object or array.
24 |
25 | replacer an optional parameter that determines how object
26 | values are stringified for objects. It can be a
27 | function or an array of strings.
28 |
29 | space an optional parameter that specifies the indentation
30 | of nested structures. If it is omitted, the text will
31 | be packed without extra whitespace. If it is a number,
32 | it will specify the number of spaces to indent at each
33 | level. If it is a string (such as '\t' or ' '),
34 | it contains the characters used to indent at each level.
35 |
36 | This method produces a JSON text from a JavaScript value.
37 |
38 | When an object value is found, if the object contains a toJSON
39 | method, its toJSON method will be called and the result will be
40 | stringified. A toJSON method does not serialize: it returns the
41 | value represented by the name/value pair that should be serialized,
42 | or undefined if nothing should be serialized. The toJSON method
43 | will be passed the key associated with the value, and this will be
44 | bound to the value
45 |
46 | For example, this would serialize Dates as ISO strings.
47 |
48 | Date.prototype.toJSON = function (key) {
49 | function f(n) {
50 | // Format integers to have at least two digits.
51 | return n < 10 ? '0' + n : n;
52 | }
53 |
54 | return this.getUTCFullYear() + '-' +
55 | f(this.getUTCMonth() + 1) + '-' +
56 | f(this.getUTCDate()) + 'T' +
57 | f(this.getUTCHours()) + ':' +
58 | f(this.getUTCMinutes()) + ':' +
59 | f(this.getUTCSeconds()) + 'Z';
60 | };
61 |
62 | You can provide an optional replacer method. It will be passed the
63 | key and value of each member, with this bound to the containing
64 | object. The value that is returned from your method will be
65 | serialized. If your method returns undefined, then the member will
66 | be excluded from the serialization.
67 |
68 | If the replacer parameter is an array of strings, then it will be
69 | used to select the members to be serialized. It filters the results
70 | such that only members with keys listed in the replacer array are
71 | stringified.
72 |
73 | Values that do not have JSON representations, such as undefined or
74 | functions, will not be serialized. Such values in objects will be
75 | dropped; in arrays they will be replaced with null. You can use
76 | a replacer function to replace those with JSON values.
77 | JSON.stringify(undefined) returns undefined.
78 |
79 | The optional space parameter produces a stringification of the
80 | value that is filled with line breaks and indentation to make it
81 | easier to read.
82 |
83 | If the space parameter is a non-empty string, then that string will
84 | be used for indentation. If the space parameter is a number, then
85 | the indentation will be that many spaces.
86 |
87 | Example:
88 |
89 | text = JSON.stringify(['e', {pluribus: 'unum'}]);
90 | // text is '["e",{"pluribus":"unum"}]'
91 |
92 |
93 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
94 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
95 |
96 | text = JSON.stringify([new Date()], function (key, value) {
97 | return this[key] instanceof Date ?
98 | 'Date(' + this[key] + ')' : value;
99 | });
100 | // text is '["Date(---current time---)"]'
101 |
102 |
103 | JSON.parse(text, reviver)
104 | This method parses a JSON text to produce an object or array.
105 | It can throw a SyntaxError exception.
106 |
107 | The optional reviver parameter is a function that can filter and
108 | transform the results. It receives each of the keys and values,
109 | and its return value is used instead of the original value.
110 | If it returns what it received, then the structure is not modified.
111 | If it returns undefined then the member is deleted.
112 |
113 | Example:
114 |
115 | // Parse the text. Values that look like ISO date strings will
116 | // be converted to Date objects.
117 |
118 | myData = JSON.parse(text, function (key, value) {
119 | var a;
120 | if (typeof value === 'string') {
121 | a =
122 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
123 | if (a) {
124 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
125 | +a[5], +a[6]));
126 | }
127 | }
128 | return value;
129 | });
130 |
131 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
132 | var d;
133 | if (typeof value === 'string' &&
134 | value.slice(0, 5) === 'Date(' &&
135 | value.slice(-1) === ')') {
136 | d = new Date(value.slice(5, -1));
137 | if (d) {
138 | return d;
139 | }
140 | }
141 | return value;
142 | });
143 |
144 |
145 | This is a reference implementation. You are free to copy, modify, or
146 | redistribute.
147 | */
148 |
149 | /*jslint evil: true, regexp: true */
150 |
151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
154 | lastIndex, length, parse, prototype, push, replace, slice, stringify,
155 | test, toJSON, toString, valueOf
156 | */
157 |
158 |
159 | // Create a JSON object only if one does not already exist. We create the
160 | // methods in a closure to avoid creating global variables.
161 |
162 | var JSON;
163 | if (!JSON) {
164 | JSON = {};
165 | }
166 |
167 | (function () {
168 | 'use strict';
169 |
170 | function f(n) {
171 | // Format integers to have at least two digits.
172 | return n < 10 ? '0' + n : n;
173 | }
174 |
175 | if (typeof Date.prototype.toJSON !== 'function') {
176 |
177 | Date.prototype.toJSON = function (key) {
178 |
179 | return isFinite(this.valueOf())
180 | ? this.getUTCFullYear() + '-' +
181 | f(this.getUTCMonth() + 1) + '-' +
182 | f(this.getUTCDate()) + 'T' +
183 | f(this.getUTCHours()) + ':' +
184 | f(this.getUTCMinutes()) + ':' +
185 | f(this.getUTCSeconds()) + 'Z'
186 | : null;
187 | };
188 |
189 | String.prototype.toJSON =
190 | Number.prototype.toJSON =
191 | Boolean.prototype.toJSON = function (key) {
192 | return this.valueOf();
193 | };
194 | }
195 |
196 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
197 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
198 | gap,
199 | indent,
200 | meta = { // table of character substitutions
201 | '\b': '\\b',
202 | '\t': '\\t',
203 | '\n': '\\n',
204 | '\f': '\\f',
205 | '\r': '\\r',
206 | '"' : '\\"',
207 | '\\': '\\\\'
208 | },
209 | rep;
210 |
211 |
212 | function quote(string) {
213 |
214 | // If the string contains no control characters, no quote characters, and no
215 | // backslash characters, then we can safely slap some quotes around it.
216 | // Otherwise we must also replace the offending characters with safe escape
217 | // sequences.
218 |
219 | escapable.lastIndex = 0;
220 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
221 | var c = meta[a];
222 | return typeof c === 'string'
223 | ? c
224 | : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
225 | }) + '"' : '"' + string + '"';
226 | }
227 |
228 |
229 | function str(key, holder) {
230 |
231 | // Produce a string from holder[key].
232 |
233 | var i, // The loop counter.
234 | k, // The member key.
235 | v, // The member value.
236 | length,
237 | mind = gap,
238 | partial,
239 | value = holder[key];
240 |
241 | // If the value has a toJSON method, call it to obtain a replacement value.
242 |
243 | if (value && typeof value === 'object' &&
244 | typeof value.toJSON === 'function') {
245 | value = value.toJSON(key);
246 | }
247 |
248 | // If we were called with a replacer function, then call the replacer to
249 | // obtain a replacement value.
250 |
251 | if (typeof rep === 'function') {
252 | value = rep.call(holder, key, value);
253 | }
254 |
255 | // What happens next depends on the value's type.
256 |
257 | switch (typeof value) {
258 | case 'string':
259 | return quote(value);
260 |
261 | case 'number':
262 |
263 | // JSON numbers must be finite. Encode non-finite numbers as null.
264 |
265 | return isFinite(value) ? String(value) : 'null';
266 |
267 | case 'boolean':
268 | case 'null':
269 |
270 | // If the value is a boolean or null, convert it to a string. Note:
271 | // typeof null does not produce 'null'. The case is included here in
272 | // the remote chance that this gets fixed someday.
273 |
274 | return String(value);
275 |
276 | // If the type is 'object', we might be dealing with an object or an array or
277 | // null.
278 |
279 | case 'object':
280 |
281 | // Due to a specification blunder in ECMAScript, typeof null is 'object',
282 | // so watch out for that case.
283 |
284 | if (!value) {
285 | return 'null';
286 | }
287 |
288 | // Make an array to hold the partial results of stringifying this object value.
289 |
290 | gap += indent;
291 | partial = [];
292 |
293 | // Is the value an array?
294 |
295 | if (Object.prototype.toString.apply(value) === '[object Array]') {
296 |
297 | // The value is an array. Stringify every element. Use null as a placeholder
298 | // for non-JSON values.
299 |
300 | length = value.length;
301 | for (i = 0; i < length; i += 1) {
302 | partial[i] = str(i, value) || 'null';
303 | }
304 |
305 | // Join all of the elements together, separated with commas, and wrap them in
306 | // brackets.
307 |
308 | v = partial.length === 0
309 | ? '[]'
310 | : gap
311 | ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']'
312 | : '[' + partial.join(',') + ']';
313 | gap = mind;
314 | return v;
315 | }
316 |
317 | // If the replacer is an array, use it to select the members to be stringified.
318 |
319 | if (rep && typeof rep === 'object') {
320 | length = rep.length;
321 | for (i = 0; i < length; i += 1) {
322 | if (typeof rep[i] === 'string') {
323 | k = rep[i];
324 | v = str(k, value);
325 | if (v) {
326 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
327 | }
328 | }
329 | }
330 | } else {
331 |
332 | // Otherwise, iterate through all of the keys in the object.
333 |
334 | for (k in value) {
335 | if (Object.prototype.hasOwnProperty.call(value, k)) {
336 | v = str(k, value);
337 | if (v) {
338 | partial.push(quote(k) + (gap ? ': ' : ':') + v);
339 | }
340 | }
341 | }
342 | }
343 |
344 | // Join all of the member texts together, separated with commas,
345 | // and wrap them in braces.
346 |
347 | v = partial.length === 0
348 | ? '{}'
349 | : gap
350 | ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}'
351 | : '{' + partial.join(',') + '}';
352 | gap = mind;
353 | return v;
354 | }
355 | }
356 |
357 | // If the JSON object does not yet have a stringify method, give it one.
358 |
359 | if (typeof JSON.stringify !== 'function') {
360 | JSON.stringify = function (value, replacer, space) {
361 |
362 | // The stringify method takes a value and an optional replacer, and an optional
363 | // space parameter, and returns a JSON text. The replacer can be a function
364 | // that can replace values, or an array of strings that will select the keys.
365 | // A default replacer method can be provided. Use of the space parameter can
366 | // produce text that is more easily readable.
367 |
368 | var i;
369 | gap = '';
370 | indent = '';
371 |
372 | // If the space parameter is a number, make an indent string containing that
373 | // many spaces.
374 |
375 | if (typeof space === 'number') {
376 | for (i = 0; i < space; i += 1) {
377 | indent += ' ';
378 | }
379 |
380 | // If the space parameter is a string, it will be used as the indent string.
381 |
382 | } else if (typeof space === 'string') {
383 | indent = space;
384 | }
385 |
386 | // If there is a replacer, it must be a function or an array.
387 | // Otherwise, throw an error.
388 |
389 | rep = replacer;
390 | if (replacer && typeof replacer !== 'function' &&
391 | (typeof replacer !== 'object' ||
392 | typeof replacer.length !== 'number')) {
393 | throw new Error('JSON.stringify');
394 | }
395 |
396 | // Make a fake root object containing our value under the key of ''.
397 | // Return the result of stringifying the value.
398 |
399 | return str('', {'': value});
400 | };
401 | }
402 |
403 |
404 | // If the JSON object does not yet have a parse method, give it one.
405 |
406 | if (typeof JSON.parse !== 'function') {
407 | JSON.parse = function (text, reviver) {
408 |
409 | // The parse method takes a text and an optional reviver function, and returns
410 | // a JavaScript value if the text is a valid JSON text.
411 |
412 | var j;
413 |
414 | function walk(holder, key) {
415 |
416 | // The walk method is used to recursively walk the resulting structure so
417 | // that modifications can be made.
418 |
419 | var k, v, value = holder[key];
420 | if (value && typeof value === 'object') {
421 | for (k in value) {
422 | if (Object.prototype.hasOwnProperty.call(value, k)) {
423 | v = walk(value, k);
424 | if (v !== undefined) {
425 | value[k] = v;
426 | } else {
427 | delete value[k];
428 | }
429 | }
430 | }
431 | }
432 | return reviver.call(holder, key, value);
433 | }
434 |
435 |
436 | // Parsing happens in four stages. In the first stage, we replace certain
437 | // Unicode characters with escape sequences. JavaScript handles many characters
438 | // incorrectly, either silently deleting them, or treating them as line endings.
439 |
440 | text = String(text);
441 | cx.lastIndex = 0;
442 | if (cx.test(text)) {
443 | text = text.replace(cx, function (a) {
444 | return '\\u' +
445 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
446 | });
447 | }
448 |
449 | // In the second stage, we run the text against regular expressions that look
450 | // for non-JSON patterns. We are especially concerned with '()' and 'new'
451 | // because they can cause invocation, and '=' because it can cause mutation.
452 | // But just to be safe, we want to reject all unexpected forms.
453 |
454 | // We split the second stage into 4 regexp operations in order to work around
455 | // crippling inefficiencies in IE's and Safari's regexp engines. First we
456 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
457 | // replace all simple value tokens with ']' characters. Third, we delete all
458 | // open brackets that follow a colon or comma or that begin the text. Finally,
459 | // we look to see that the remaining characters are only whitespace or ']' or
460 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
461 |
462 | if (/^[\],:{}\s]*$/
463 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
464 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
465 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
466 |
467 | // In the third stage we use the eval function to compile the text into a
468 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
469 | // in JavaScript: it can begin a block or an object literal. We wrap the text
470 | // in parens to eliminate the ambiguity.
471 |
472 | j = eval('(' + text + ')');
473 |
474 | // In the optional fourth stage, we recursively walk the new structure, passing
475 | // each name/value pair to a reviver function for possible transformation.
476 |
477 | return typeof reviver === 'function'
478 | ? walk({'': j}, '')
479 | : j;
480 | }
481 |
482 | // If the text is not JSON parseable, then a SyntaxError is thrown.
483 |
484 | throw new SyntaxError('JSON.parse');
485 | };
486 | }
487 | }());
488 |
--------------------------------------------------------------------------------