├── .gitignore ├── LICENSE ├── QualtricsExample.qsf ├── README.md ├── dependencies ├── mousehold.d.ts └── mousehold.js ├── distributionbuilder.css ├── distributionbuilder.min.js ├── distributionbuilder.zip ├── docs ├── demo │ └── js │ │ └── main.js ├── fonts │ ├── copse-regular-webfont.eot │ ├── copse-regular-webfont.svg │ ├── copse-regular-webfont.ttf │ ├── copse-regular-webfont.woff │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ ├── glyphicons-halflings-regular.woff2 │ ├── quattrocentosans-bold-webfont.eot │ ├── quattrocentosans-bold-webfont.svg │ ├── quattrocentosans-bold-webfont.ttf │ ├── quattrocentosans-bold-webfont.woff │ ├── quattrocentosans-bolditalic-webfont.eot │ ├── quattrocentosans-bolditalic-webfont.svg │ ├── quattrocentosans-bolditalic-webfont.ttf │ ├── quattrocentosans-bolditalic-webfont.woff │ ├── quattrocentosans-italic-webfont.eot │ ├── quattrocentosans-italic-webfont.svg │ ├── quattrocentosans-italic-webfont.ttf │ ├── quattrocentosans-italic-webfont.woff │ ├── quattrocentosans-regular-webfont.eot │ ├── quattrocentosans-regular-webfont.svg │ ├── quattrocentosans-regular-webfont.ttf │ └── quattrocentosans-regular-webfont.woff ├── images │ ├── background.png │ ├── body-background.png │ ├── bullet.png │ ├── hr.gif │ └── octocat-logo.png ├── index.html ├── javascripts │ └── main.js └── stylesheets │ ├── distributionbuilder.css │ ├── github-dark.css │ ├── normalize.css │ └── styles.css ├── lib ├── distributionbuilder.css ├── distributionbuilder.d.ts ├── distributionbuilder.js └── distributionbuilder.min.js ├── package.json ├── public └── fonts │ └── glyphicons-halflings-regular.svg ├── reference.bib ├── src ├── bootstrap.config.js ├── bootstrap.config.less ├── demo.js ├── distributionbuilder.css ├── distributionbuilder.js ├── distributionbuilder.ts ├── entry.js └── postcss.config.js ├── tsconfig.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ 3 | yarn.lock 4 | test.html 5 | yarn-error.log 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Quentin André. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /QualtricsExample.qsf: -------------------------------------------------------------------------------- 1 | {"SurveyEntry":{"SurveyID":"SV_06w5Ku7JpLrmPAx","SurveyName":"DistributionBuilder Preview","SurveyDescription":null,"SurveyOwnerID":"UR_8HNsUBv4Xq1C36B","SurveyBrandID":"insead","DivisionID":null,"SurveyLanguage":"EN","SurveyActiveResponseSet":"RS_57JoMFau2H81LSt","SurveyStatus":"Active","SurveyStartDate":"0000-00-00 00:00:00","SurveyExpirationDate":"0000-00-00 00:00:00","SurveyCreationDate":"2016-11-12 02:26:34","CreatorID":"UR_8HNsUBv4Xq1C36B","LastModified":"2016-11-12 04:11:40","LastAccessed":"0000-00-00 00:00:00","LastActivated":"2016-11-12 02:29:04","Deleted":null},"SurveyElements":[{"SurveyID":"SV_06w5Ku7JpLrmPAx","Element":"BL","PrimaryAttribute":"Survey Blocks","SecondaryAttribute":null,"TertiaryAttribute":null,"Payload":[{"Type":"Default","Description":"DistributionBuilder","ID":"BL_8A1qQupvnjNcAGV","BlockElements":[{"Type":"Question","QuestionID":"QID1"}]},{"Type":"Trash","Description":"Trash \/ Unused Questions","ID":"BL_b89VG1W4bPgdUfb"}]},{"SurveyID":"SV_06w5Ku7JpLrmPAx","Element":"FL","PrimaryAttribute":"Survey Flow","SecondaryAttribute":null,"TertiaryAttribute":null,"Payload":{"Type":"Root","FlowID":"FL_1","Flow":[{"Type":"EmbeddedData","FlowID":"FL_3","EmbeddedData":[{"Description":"DistributionResult","Type":"Recipient","Field":"DistributionResult","VariableType":"Nominal"}]},{"Type":"Block","ID":"BL_8A1qQupvnjNcAGV","FlowID":"FL_2"}],"Properties":{"Count":3}}},{"SurveyID":"SV_06w5Ku7JpLrmPAx","Element":"SO","PrimaryAttribute":"Survey Options","SecondaryAttribute":null,"TertiaryAttribute":null,"Payload":{"BackButton":"false","SaveAndContinue":"true","SurveyProtection":"PublicSurvey","BallotBoxStuffingPrevention":"false","NoIndex":"Yes","SecureResponseFiles":"true","SurveyExpiration":"None","SurveyTermination":"DefaultMessage","Header":" 13 | 14 | 15 | 17 | 18 | 19 |
20 |

distBuilder

21 |

A Javascript library adding distribution builders to your experiments.

22 |
23 | 24 | 44 |
45 | 48 |
49 |

1. Introduction

50 |

A. Why distBuilder?

51 |

In their paper 'Lay 52 | understanding of probability distributions' (2014), published in 2014, Daniel Goldstein and 53 | David Rothschild have highlighted the benefits of using graphical interfaces called distribution 54 | builders to study subjective probabilities, perceptions of frequency, and confidence 55 | judgements. This tool was first developed by Sharpe, Goldstein and Blythe (2000), and was 57 | featured later in Goldstein, Johnson and Sharpe (2008) and Delavande and Rhowedder (2008). 61 | However, the implementation of such distribution builders in online studies is not straightforward, as most 62 | survey platforms do not implement this type of question.

63 |

The distBuilder library was created to address this issue, and make the implementation of 64 | distribution builders easy and accessible to researchers. The library is user-friendly, and requires very 65 | little programming knowledge to be used.

66 |

B. Citing distBuilder

67 |

You can cite distBuilder using its Digital Object Identifier 68 | (DOI).

69 |

C. About the author

70 |

This library is developed by Quentin André, an assistant professor of Marketing at 71 | the Leeds School of Business, University of Colorado Boulder. If you have any comment, feedback 72 | or suggestion regarding this library or its documentation, 73 | please let me know at quentin.andre@colorado.edu or use GitHub issues.

74 | 75 |

2. Installation

76 |

To add the library to your projects and start using distBuilder, just follow the instructions 77 | below.

78 |

A. Adding to a website

79 |

If you are hosting your experiment on a separate website, here is how to add the script:

80 |
    81 |
  1. 82 | Download the Library using the button at the top of the page. 83 |
  2. 84 |
  3. Add the Javascript file distributionbuilder.min.js and the CSS file distributionbuilder.css 85 | to your html code:

    86 |
    <link rel="stylesheet" href="distributionbuilder.css">
     87 | <script src="distributionbuilder.min.js"></script>
    88 |
  4. 89 |
  5. 90 | That's it! You can now add your own distribution builders to your webpage. No need to include jQuery! 91 | distBuilder automatically injects it, and makes it accessible under the $j alias. 92 | Instead of typing $('#Hello'), type $j('#Hello'). 93 |
  6. 94 |
95 |

B. Adding to Qualtrics

96 |

If you prefer starting from a minimal working example and tweaking it, you can find 98 | a fully functional integration to Qualtrics here in the form of a .qsf file, that you can import in 99 | Qualtrics.

100 |

If you prefer detailed instructions, just follow these simple steps:

101 |
    102 |
  1. Navigate to the "Look and Feel" section of your survey, and click on the "Advanced" tab
  2. 103 |
  3. 104 | Edit the "Header" section, and add the following lines to load the library script and the library 105 | styles:
    106 |
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/QuentinAndre/DistributionBuilder@master/lib/distributionbuilder.css">
    107 | <script src="https://cdn.jsdelivr.net/gh/QuentinAndre/DistributionBuilder@master/lib/distributionbuilder.min.js"></script>
    108 |
  4. 109 |
  5. 110 | That's it! You can now add your own distribution builders in the "Custom Javascript" section of your 111 | Qualtrics questions. Make sure to wrap the code in the Qualtrics.SurveyEngine.addOnReady section. 112 |
    You also do not need to include jQuery! distBuilder automatically injects it, and makes 113 | it accessible under the $j alias. Instead of typing $('#Hello'), type $j('#Hello'). 114 |
  6. 115 |
116 | 117 |

3. Basic Setup

118 | 119 |

The library is extremely easy to use: it allows you to add fully functional distribution builders in a few 120 | lines of code.

121 | 131 | 132 |
133 |
134 |
135 |
136 |
var distbuilder = new DistributionBuilder();
137 | distbuilder.render("targetdiv");
138 | distbuilder.labelize();
139 |
140 |

In just three lines of code, you have created an interactive (try it!) Distribution Builder in the <div> 141 | section called “targetdiv”. It consists of three elements:

142 | 143 | 148 |

Each of these elements can be customized to your liking, both using the Javascript methods wrapped by the 149 | library and the CSS. Browse the sections below to discover how you can customize the appearance and 150 | functionalities of the distribution builder, access the results of your participants, and implement more 151 | complex logics in your studies.

152 | 153 |

4. Customization

154 |

The complete creation of a Distribution Builder object is done in three phases:

155 | 160 |

A. Initialization

161 |

The first step in creating a Distribution Builder is to initialize the object:

162 |

myDistBuilder = new DistributionBuilder({})

163 |

This function call initializes the internal state of the DistributionBuilder object, with a certain number of 164 | parameters that you can specify:

165 | 201 |
202 |
203 |
204 |
205 |
var distbuilder = new DistributionBuilder({
206 |     nRows: 20,
207 |     nBuckets: 20,
208 |     minVal: 0,
209 |     maxVal: 100,
210 |     nBalls: 20,
211 |     onTouch: function () {
212 |         console.log("Distbuilder was touched!")
213 |     },
214 |     onChange: function () {
215 |         console.log("Distbuilder was updated!")
216 |     },
217 |     toggleGridClick: true,
218 |     addTotals: true
219 |     });
220 | distbuilder.render("targetdiv");
221 | distbuilder.labelize();
222 |
223 |

B. Render

224 |

After the DistributionBuilder object is initialized, you must call another method to indicate where you want 225 | to see it displayed on the page:

226 |

DistributionBuilder.render()

227 |

This method requires the argument target, and you can optionally supply the order 228 | argument to further tweak the appearance of the Distribution Builder.

229 | 241 | Using this method to put the labels at the top of the Distribution Builder: 242 |
243 |
244 |
245 |
246 |
var distbuilder = new DistributionBuilder({
247 |     nRows: 10,
248 |     nBuckets: 20,
249 |     minVal: 0,
250 |     maxVal: 100,
251 |     nBalls: 10,
252 |     onTouch: function () {
253 |         console.log("Distbuilder was touched!")
254 |     },
255 |     onChange: function () {
256 |         console.log("Distbuilder was updated!")
257 |     }
258 |     });
259 | distbuilder.render("targetdiv", "labels-grid-buttons");
260 | distbuilder.labelize();
261 | 
262 |
263 |

C. Labelize

264 |

You generally want to add some labels to the buckets of your distribution builder:

265 |

DistributionBuilder.labelize()

266 |

By default, the Distribution Builder automatically creates evenly spaced labels, using the distance between 267 | the minimum and maximum value and the number of buckets using the following code:

268 |

269 |

step = (maxValue - minValue)/nBuckets;
270 | labels = [minValue + step/2 + step*0, minValue + step/2 + step*1, ..., minValue + step/2 +step*(nBuckets-1)]
271 |

Calling the method DistributionBuilder.labelize() without arguments will display those 272 | labels. However, you are free to customize the labels using the following arguments:

273 |
    274 |
  1. labels, an array of length nBucketscode>. Supplying this argument will 275 | override the default labels. 276 |
  2. 277 |
  3. prefix, a string (e.g. ‘$’) which you would like to see prepended to all the labels. 278 |
  4. 279 |
  5. suffix, a string (e.g. ‘€’) which you would like to see appended to all the labels. 280 |
  6. 281 |
282 |

The prefix and suffix arguments are always applied, whether or not you specify 283 | custom labels using the label argument.

284 |
285 |
286 |
287 |
288 |
var distbuilder = new DistributionBuilder({
289 |     nRows: 10,
290 |     nBuckets: 20,
291 |     minVal: 0,
292 |     maxVal: 100,
293 |     nBalls: 10,
294 |     onTouch: function () {
295 |         console.log("Distbuilder was touched!")
296 |     },
297 |     onChange: function () {
298 |         console.log("Distbuilder was updated!")
299 |     }
300 |     });
301 | distbuilder.render();
302 | distbuilder.labelize({
303 |     prefix: '~',
304 |     suffix: '€'
305 | });
306 | 
307 |
308 |

5. Changing the Distribution

309 |

By default, respondents will start from an empty distribution (i.e. zero balls in all buckets).

310 |

However, you might want to specify a different starting distribution, or change the current distribution 311 | to a specific value. To do so, you can use the method:

312 |

DistributionBuilder.setDistribution(dist)

313 |

Here is an example:

314 |
315 |
var distbuilder = new DistributionBuilder({
316 |     nRows: 10,
317 |     nBuckets: 10,
318 |     minVal: 0,
319 |     maxVal: 100,
320 |     nBalls: 60
321 |     });
322 | distbuilder.render();
323 | distbuilder.labelize(});
324 | var dist = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
325 | distbuilder.setDistribution(dist);
326 | 
327 |
328 |
329 |
330 |
331 | 332 | 333 |

6. Reading the Data

334 |

After setting up the Distribution Builder, its internal state can be conveniently accessed through three 335 | methods:

336 | 352 |

The two examples below demonstrate how those three methods can be used in your experiments and studies.

353 |

A. Number of balls allocated

354 |

This can be achieved by creating two <div>s called “BallsLeft” and “BallsAllocated”, and 355 | assigning a function updating those <div>s to the onChange argument of the 356 | Distribution Builder initialization function.

357 | 358 |
359 |
360 |
361 |
362 |
363 |
364 | 365 |
var n_balls = 10;
366 | $j('#BallsLeft').text("You have " + n_balls + " balls left.");
367 | $j('#BallsAllocated').text("You have allocated " + 0 + " balls.");
368 | var distbuilder = new DistributionBuilder({
369 |     minVal: 0,
370 |     maxVal: 100,
371 |     nRows: 10,
372 |     nBuckets: 20,
373 |     nBalls: n_balls,
374 |     onChange: function () {
375 |         var remainingballs = this.getRemainingBalls();
376 |         var ballsallocated = n_balls - this.getRemainingBalls();
377 |         $j('#BallsLeft').text("You have " + remainingballs + " balls left.");
378 |         $j('#BallsAllocated').text("You have allocated " + ballsallocated + " balls.");
379 |     }
380 | });
381 | distbuilder.render("targetdiv");
382 | distbuilder.labelize();
383 |
384 |

B. Validating the distribution

385 |

You probably want the participants to your studies to allocate all the balls before being able to validate 386 | their distribution. Fortunately, it is easy to add a button, and only enable it when all the balls have been 387 | allocated:

388 | 399 |

Together, those two steps are ensuring that the distribution is fully specified before the user can click the 400 | button.

401 |
402 |
403 | Submit Distribution 404 |
405 |
406 |
407 | 408 |
409 |
var distbuilder = new DistributionBuilder({
410 |     minVal: 0,
411 |     maxVal: 100,
412 |     nRows: 10,
413 |     nBuckets: 20,
414 |     nBalls: 10,
415 |     onChange: function () {
416 |         if (this.isComplete()) {
417 |             $j("#SubmitDistribution").attr("disabled", false)
418 |         } else {
419 |             $j("#SubmitDistribution").attr("disabled", true)
420 |         }
421 |     }
422 | });
423 | $j("#SubmitDistribution").click(function() {alert("Distribution Validated!")});
424 | distbuilder.render("targetdiv");
425 | distbuilder.labelize({});
426 |
427 |

C. Accessing the distribution

428 |

We would now like to see in which buckets the balls have been allocated when the user validates the 429 | distribution by clicking on the button.

430 |

Building upon the previous example, we can change the message "Distribution Validated!" to display the actual 431 | distribution that the user has specified. by using the method 432 | DistributionBuilder.getDistribution().

433 |
434 |
435 | View Distribution 436 |
437 |
438 |
439 | 440 |
441 |
var distbuilder = new DistributionBuilder({
442 |     minVal: 0,
443 |     maxVal: 100,
444 |     nRows: 10,
445 |     nBuckets: 20,
446 |     nBalls: 10,
447 |     onChange: function () {
448 |         if (this.isComplete()) {
449 |             $j("#SubmitDistribution").attr("disabled", false)
450 |         } else {
451 |             $j("#SubmitDistribution").attr("disabled", true)
452 |         }
453 |     }
454 | });
455 | $j("#SubmitDistribution").click(function() {
456 |     var message = "The distribution specified by the user is: " + distbuilder.getDistribution();
457 |     alert(message)
458 | });
459 | distbuilder.render("targetdiv");
460 | distbuilder.labelize({});
461 |
462 |

D. Storing the data in Qualtrics

463 |

You will often want the Distribution Builder to be part of larger survey on Qualtrics, and you will want to 464 | store the resulting distribution in a variable. This result can be achieved in the following way:

465 | 481 |

Adding the code to the previous example:

482 |
483 |
484 | Store Distribution in Qualtrics 485 |
486 |
487 |
488 | 489 |
490 |
var distbuilder = new DistributionBuilder({
491 |     minVal: 0,
492 |     maxVal: 100,
493 |     nRows: 10,
494 |     nBuckets: 20,
495 |     nBalls: 10,
496 |     onChange: function () {
497 |         if (this.isComplete()) {
498 |             $j("#SubmitDistribution").attr("disabled", false)
499 |         } else {
500 |             $j("#SubmitDistribution").attr("disabled", true)
501 |         }
502 |     }
503 | });
504 | distbuilder.render("targetdiv");
505 | distbuilder.labelize({});
506 | $j("#SubmitDistribution").click(function () {
507 |     var results = distbuilder.getDistribution().join()
508 |     Qualtrics.SurveyEngine.setEmbeddedData("MyDistributionResult", results);
509 |     var message = 'The function "Qualtrics.SurveyEngine.setEmbeddedData("MyDistributionResult", ';
510 |     message +=  results + ') was called. Your data would have been stored in Qualtrics!';
511 |     alert(message)
512 | });
513 |
514 | 515 |

7. CSS Specification

516 |

If you want to customize the appearance of the distribution builder, the full list of classes defined by the 517 | stylesheet can be found in the distributionbuilder.css file.

518 |

The following CSS tree will help you understand the mapping between the CSS classes and the way the 519 | Distribution Builder object is rendered on screen.

520 |
<div id="MyContainer" class="distbuilder">
521 |     <div class="grid">
522 |         <div class="distrow row2">
523 |             <div class="cell col0">
524 |                 <div class="ball col0"></div> // Present in all "cells"
525 |             </div>
526 |             <div class="cell col1"></div>
527 |             <div class="cell col2"></div>
528 |         </div>
529 |         <div class="distrow row1">
530 |             <div class="cell col0 filled"></div>
531 |             <div class="cell col1"></div>
532 |             <div class="cell col2"></div>
533 |         </div>
534 |         <div class="distrow row0">
535 |             <div class="cell col0 filled"></div>
536 |             <div class="cell col1"></div>
537 |             <div class="cell col2"></div>
538 |         </div>
539 |     </div>
540 |     <div class="buttons">
541 |         <div class="distrow">
542 |             <div class="buttongroup">
543 |                 <a class="btn btn-default distbutton glyphicon glyphicon-plus"></a>
544 |                 <a class="btn btn-default distbutton glyphicon glyphicon-minus"></a>
545 |             </div>
546 |             <div class="buttongroup">
547 |                 <a class="btn btn-default distbutton glyphicon glyphicon-plus"></a>
548 |                 <a class="btn btn-default distbutton glyphicon glyphicon-minus"></a>
549 |             </div>
550 |             <div class="buttongroup">
551 |                 <a class="btn btn-default distbutton glyphicon glyphicon-plus"></a>
552 |                 <a class="btn btn-default distbutton glyphicon glyphicon-minus"></a>
553 |             </div>
554 |         </div>
555 |     </div>
556 |     <div class="labels">
557 |         <div class="distrow">
558 |             <div class="label col0">~1€</div>
559 |             <div class="label col1">~2€</div>
560 |             <div class="label col2">~3€</div>
561 |         </div>
562 |     </div>
563 |     </div class="totals"> // Only present if argument `addTotals` is `true`.
564 |         <div class="distrow">
565 |             <div class="total col0">2</div>
566 |             <div class="total col1">0</div>
567 |             <div class="total col2">0</div>
568 |         </div>
569 |     </div>
570 | </div>
571 |

8. About

572 |

distBuilder is published under the MIT 574 | license. The code is written in Typescript, and is transpiled in legacy code 575 | for cross-browsers compatibility. It is developed upon the jQuery 576 | library, and uses some elements of Bootstrap 577 | for styling.

578 |

I am grateful for the financial support of ADL Partner and INSEAD, the comments of Nicholas Reinholtz and Bart de Langhe on previous 581 | versions of the Distribution Builder, and Dan Goldstein's 582 | references on the history of the distribution builder. I would also like to thank Ignazio Ziano for uncovering a bug 584 | when trying to use the library.

585 |

This page is hosted on GitHub Pages. Theme by mattgraham

586 |
587 |
588 | 590 | 591 | 608 | 609 | -------------------------------------------------------------------------------- /docs/javascripts/main.js: -------------------------------------------------------------------------------- 1 | var sectionHeight = function() { 2 | var total = $j(window).height(), 3 | $jsection = $j('section').css('height','auto'); 4 | 5 | if ($jsection.outerHeight(true) < total) { 6 | var margin = $jsection.outerHeight(true) - $jsection.height(); 7 | $jsection.height(total - margin - 20); 8 | } else { 9 | $jsection.css('height','auto'); 10 | } 11 | }; 12 | 13 | $j(window).resize(sectionHeight); 14 | 15 | $j(document).ready(function(){ 16 | $j("section h1, section h2").each(function(){ 17 | $j("nav ul").append("
  • " + $j(this).text() + "
  • "); 18 | $j(this).attr("id",$j(this).text().toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g,'')); 19 | $j("nav ul li:first-child a").parent().addClass("active"); 20 | }); 21 | 22 | $j("nav ul li").on("click", "a", function(event) { 23 | var position = $j($j(this).attr("href")).offset().top - 190; 24 | $j("html, body").animate({scrollTop: position}, 400); 25 | $j("nav ul li a").parent().removeClass("active"); 26 | $j(this).parent().addClass("active"); 27 | event.preventDefault(); 28 | }); 29 | 30 | sectionHeight(); 31 | 32 | $j('img').on('load', sectionHeight); 33 | }); 34 | 35 | fixScale = function(doc) { 36 | 37 | var addEvent = 'addEventListener', 38 | type = 'gesturestart', 39 | qsa = 'querySelectorAll', 40 | scales = [1, 1], 41 | meta = qsa in doc ? doc[qsa]('meta[name=viewport]') : []; 42 | 43 | function fix() { 44 | meta.content = 'width=device-width,minimum-scale=' + scales[0] + ',maximum-scale=' + scales[1]; 45 | doc.removeEventListener(type, fix, true); 46 | } 47 | 48 | if ((meta = meta[meta.length - 1]) && addEvent in doc) { 49 | fix(); 50 | scales = [.25, 1.6]; 51 | doc[addEvent](type, fix, true); 52 | } 53 | }; -------------------------------------------------------------------------------- /docs/stylesheets/github-dark.css: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 GitHub, Inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | 24 | */ 25 | 26 | .pl-c /* comment */ { 27 | color: #969896; 28 | } 29 | 30 | .pl-c1 /* constant, variable.other.constant, support, meta.property-name, support.constant, support.variable, meta.module-reference, markup.raw, meta.diff.header */, 31 | .pl-s .pl-v /* string variable */ { 32 | color: #0099cd; 33 | } 34 | 35 | .pl-e /* entity */, 36 | .pl-en /* entity.name */ { 37 | color: #9774cb; 38 | } 39 | 40 | .pl-smi /* variable.parameter.function, storage.modifier.package, storage.modifier.import, storage.type.java, variable.other */, 41 | .pl-s .pl-s1 /* string source */ { 42 | color: #ddd; 43 | } 44 | 45 | .pl-ent /* entity.name.tag */ { 46 | color: #7bcc72; 47 | } 48 | 49 | .pl-k /* keyword, storage, storage.type */ { 50 | color: #cc2372; 51 | } 52 | 53 | .pl-s /* string */, 54 | .pl-pds /* punctuation.definition.string, string.regexp.character-class */, 55 | .pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */, 56 | .pl-sr /* string.regexp */, 57 | .pl-sr .pl-cce /* string.regexp constant.character.escape */, 58 | .pl-sr .pl-sre /* string.regexp source.ruby.embedded */, 59 | .pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */ { 60 | color: #3c66e2; 61 | } 62 | 63 | .pl-v /* variable */ { 64 | color: #fb8764; 65 | } 66 | 67 | .pl-id /* invalid.deprecated */ { 68 | color: #e63525; 69 | } 70 | 71 | .pl-ii /* invalid.illegal */ { 72 | color: #f8f8f8; 73 | background-color: #e63525; 74 | } 75 | 76 | .pl-sr .pl-cce /* string.regexp constant.character.escape */ { 77 | font-weight: bold; 78 | color: #7bcc72; 79 | } 80 | 81 | .pl-ml /* markup.list */ { 82 | color: #c26b2b; 83 | } 84 | 85 | .pl-mh /* markup.heading */, 86 | .pl-mh .pl-en /* markup.heading entity.name */, 87 | .pl-ms /* meta.separator */ { 88 | font-weight: bold; 89 | color: #264ec5; 90 | } 91 | 92 | .pl-mq /* markup.quote */ { 93 | color: #00acac; 94 | } 95 | 96 | .pl-mi /* markup.italic */ { 97 | font-style: italic; 98 | color: #ddd; 99 | } 100 | 101 | .pl-mb /* markup.bold */ { 102 | font-weight: bold; 103 | color: #ddd; 104 | } 105 | 106 | .pl-md /* markup.deleted, meta.diff.header.from-file */ { 107 | color: #bd2c00; 108 | background-color: #ffecec; 109 | } 110 | 111 | .pl-mi1 /* markup.inserted, meta.diff.header.to-file */ { 112 | color: #55a532; 113 | background-color: #eaffea; 114 | } 115 | 116 | .pl-mdr /* meta.diff.range */ { 117 | font-weight: bold; 118 | color: #9774cb; 119 | } 120 | 121 | .pl-mo /* meta.output */ { 122 | color: #264ec5; 123 | } 124 | 125 | -------------------------------------------------------------------------------- /docs/stylesheets/normalize.css: -------------------------------------------------------------------------------- 1 | /* normalize.css 2012-02-07T12:37 UTC - https://github.com/necolas/normalize.css */ 2 | /* ============================================================================= 3 | HTML5 display definitions 4 | ========================================================================== */ 5 | /* 6 | * Corrects block display not defined in IE6/7/8/9 & FF3 7 | */ 8 | article, 9 | aside, 10 | details, 11 | figcaption, 12 | figure, 13 | footer, 14 | header, 15 | hgroup, 16 | nav, 17 | section, 18 | summary { 19 | display: block; 20 | } 21 | 22 | /* 23 | * Corrects inline-block display not defined in IE6/7/8/9 & FF3 24 | */ 25 | audio, 26 | canvas, 27 | video { 28 | display: inline-block; 29 | *display: inline; 30 | *zoom: 1; 31 | } 32 | 33 | /* 34 | * Prevents modern browsers from displaying 'audio' without controls 35 | */ 36 | audio:not([controls]) { 37 | display: none; 38 | } 39 | 40 | /* 41 | * Addresses styling for 'hidden' attribute not present in IE7/8/9, FF3, S4 42 | * Known issue: no IE6 support 43 | */ 44 | [hidden] { 45 | display: none; 46 | } 47 | 48 | /* ============================================================================= 49 | Base 50 | ========================================================================== */ 51 | /* 52 | * 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units 53 | * http://clagnut.com/blog/348/#c790 54 | * 2. Prevents iOS text size adjust after orientation change, without disabling user zoom 55 | * www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ 56 | */ 57 | html { 58 | font-size: 100%; 59 | /* 1 */ 60 | -webkit-text-size-adjust: 100%; 61 | /* 2 */ 62 | -ms-text-size-adjust: 100%; 63 | /* 2 */ 64 | } 65 | 66 | /* 67 | * Addresses font-family inconsistency between 'textarea' and other form elements. 68 | */ 69 | html, 70 | button, 71 | input, 72 | select, 73 | textarea { 74 | font-family: sans-serif; 75 | } 76 | 77 | /* 78 | * Addresses margins handled incorrectly in IE6/7 79 | */ 80 | body { 81 | margin: 0; 82 | } 83 | 84 | /* ============================================================================= 85 | Links 86 | ========================================================================== */ 87 | /* 88 | * Addresses outline displayed oddly in Chrome 89 | */ 90 | a:focus { 91 | outline: thin dotted; 92 | } 93 | 94 | /* 95 | * Improves readability when focused and also mouse hovered in all browsers 96 | * people.opera.com/patrickl/experiments/keyboard/test 97 | */ 98 | a:hover, 99 | a:active { 100 | outline: 0; 101 | } 102 | 103 | /* ============================================================================= 104 | Typography 105 | ========================================================================== */ 106 | /* 107 | * Addresses font sizes and margins set differently in IE6/7 108 | * Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5 109 | */ 110 | h1 { 111 | font-size: 2em; 112 | margin: 0.67em 0; 113 | } 114 | 115 | h2 { 116 | font-size: 1.5em; 117 | margin: 0.83em 0; 118 | } 119 | 120 | h3 { 121 | font-size: 1.17em; 122 | margin: 1em 0; 123 | } 124 | 125 | h4 { 126 | font-size: 1em; 127 | margin: 1.33em 0; 128 | } 129 | 130 | h5 { 131 | font-size: 0.83em; 132 | margin: 1.67em 0; 133 | } 134 | 135 | h6 { 136 | font-size: 0.75em; 137 | margin: 2.33em 0; 138 | } 139 | 140 | /* 141 | * Addresses styling not present in IE7/8/9, S5, Chrome 142 | */ 143 | abbr[title] { 144 | border-bottom: 1px dotted; 145 | } 146 | 147 | /* 148 | * Addresses style set to 'bolder' in FF3+, S4/5, Chrome 149 | */ 150 | b, 151 | strong { 152 | font-weight: bold; 153 | } 154 | 155 | blockquote { 156 | margin: 1em 40px; 157 | } 158 | 159 | /* 160 | * Addresses styling not present in S5, Chrome 161 | */ 162 | dfn { 163 | font-style: italic; 164 | } 165 | 166 | /* 167 | * Addresses styling not present in IE6/7/8/9 168 | */ 169 | mark { 170 | background: #ff0; 171 | color: #000; 172 | } 173 | 174 | /* 175 | * Addresses margins set differently in IE6/7 176 | */ 177 | p, 178 | pre { 179 | margin: 1em 0; 180 | } 181 | 182 | /* 183 | * Corrects font family set oddly in IE6, S4/5, Chrome 184 | * en.wikipedia.org/wiki/User:Davidgothberg/Test59 185 | */ 186 | pre, 187 | code, 188 | kbd, 189 | samp { 190 | font-family: monospace, serif; 191 | _font-family: 'courier new', monospace; 192 | font-size: 1em; 193 | } 194 | 195 | /* 196 | * 1. Addresses CSS quotes not supported in IE6/7 197 | * 2. Addresses quote property not supported in S4 198 | */ 199 | /* 1 */ 200 | q { 201 | quotes: none; 202 | } 203 | 204 | /* 2 */ 205 | q:before, 206 | q:after { 207 | content: ''; 208 | content: none; 209 | } 210 | 211 | small { 212 | font-size: 75%; 213 | } 214 | 215 | /* 216 | * Prevents sub and sup affecting line-height in all browsers 217 | * gist.github.com/413930 218 | */ 219 | sub, 220 | sup { 221 | font-size: 75%; 222 | line-height: 0; 223 | position: relative; 224 | vertical-align: baseline; 225 | } 226 | 227 | sup { 228 | top: -0.5em; 229 | } 230 | 231 | sub { 232 | bottom: -0.25em; 233 | } 234 | 235 | /* ============================================================================= 236 | Lists 237 | ========================================================================== */ 238 | /* 239 | * Addresses margins set differently in IE6/7 240 | */ 241 | dl, 242 | menu, 243 | ol, 244 | ul { 245 | margin: 1em 0; 246 | } 247 | 248 | dd { 249 | margin: 0 0 0 40px; 250 | } 251 | 252 | /* 253 | * Addresses paddings set differently in IE6/7 254 | */ 255 | menu, 256 | ol, 257 | ul { 258 | padding: 0 0 0 40px; 259 | } 260 | 261 | /* 262 | * Corrects list images handled incorrectly in IE7 263 | */ 264 | nav ul, 265 | nav ol { 266 | list-style: none; 267 | list-style-image: none; 268 | } 269 | 270 | /* ============================================================================= 271 | Embedded content 272 | ========================================================================== */ 273 | /* 274 | * 1. Removes border when inside 'a' element in IE6/7/8/9, FF3 275 | * 2. Improves image quality when scaled in IE7 276 | * code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ 277 | */ 278 | img { 279 | border: 0; 280 | /* 1 */ 281 | -ms-interpolation-mode: bicubic; 282 | /* 2 */ 283 | } 284 | 285 | /* 286 | * Corrects overflow displayed oddly in IE9 287 | */ 288 | svg:not(:root) { 289 | overflow: hidden; 290 | } 291 | 292 | /* ============================================================================= 293 | Figures 294 | ========================================================================== */ 295 | /* 296 | * Addresses margin not present in IE6/7/8/9, S5, O11 297 | */ 298 | figure { 299 | margin: 0; 300 | } 301 | 302 | /* ============================================================================= 303 | Forms 304 | ========================================================================== */ 305 | /* 306 | * Corrects margin displayed oddly in IE6/7 307 | */ 308 | form { 309 | margin: 0; 310 | } 311 | 312 | /* 313 | * Define consistent border, margin, and padding 314 | */ 315 | fieldset { 316 | border: 1px solid #c0c0c0; 317 | margin: 0 2px; 318 | padding: 0.35em 0.625em 0.75em; 319 | } 320 | 321 | /* 322 | * 1. Corrects color not being inherited in IE6/7/8/9 323 | * 2. Corrects text not wrapping in FF3 324 | * 3. Corrects alignment displayed oddly in IE6/7 325 | */ 326 | legend { 327 | border: 0; 328 | /* 1 */ 329 | padding: 0; 330 | white-space: normal; 331 | /* 2 */ 332 | *margin-left: -7px; 333 | /* 3 */ 334 | } 335 | 336 | /* 337 | * 1. Corrects font size not being inherited in all browsers 338 | * 2. Addresses margins set differently in IE6/7, FF3+, S5, Chrome 339 | * 3. Improves appearance and consistency in all browsers 340 | */ 341 | button, 342 | input, 343 | select, 344 | textarea { 345 | font-size: 100%; 346 | /* 1 */ 347 | margin: 0; 348 | /* 2 */ 349 | vertical-align: baseline; 350 | /* 3 */ 351 | *vertical-align: middle; 352 | /* 3 */ 353 | } 354 | 355 | /* 356 | * Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet 357 | */ 358 | button, 359 | input { 360 | line-height: normal; 361 | /* 1 */ 362 | } 363 | 364 | /* 365 | * 1. Improves usability and consistency of cursor style between image-type 'input' and others 366 | * 2. Corrects inability to style clickable 'input' types in iOS 367 | * 3. Removes inner spacing in IE7 without affecting normal text inputs 368 | * Known issue: inner spacing remains in IE6 369 | */ 370 | button, 371 | input[type="button"], 372 | input[type="reset"], 373 | input[type="submit"] { 374 | cursor: pointer; 375 | /* 1 */ 376 | -webkit-appearance: button; 377 | /* 2 */ 378 | *overflow: visible; 379 | /* 3 */ 380 | } 381 | 382 | /* 383 | * Re-set default cursor for disabled elements 384 | */ 385 | button[disabled], 386 | input[disabled] { 387 | cursor: default; 388 | } 389 | 390 | /* 391 | * 1. Addresses box sizing set to content-box in IE8/9 392 | * 2. Removes excess padding in IE8/9 393 | * 3. Removes excess padding in IE7 394 | Known issue: excess padding remains in IE6 395 | */ 396 | input[type="checkbox"], 397 | input[type="radio"] { 398 | box-sizing: border-box; 399 | /* 1 */ 400 | padding: 0; 401 | /* 2 */ 402 | *height: 13px; 403 | /* 3 */ 404 | *width: 13px; 405 | /* 3 */ 406 | } 407 | 408 | /* 409 | * 1. Addresses appearance set to searchfield in S5, Chrome 410 | * 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof) 411 | */ 412 | input[type="search"] { 413 | -webkit-appearance: textfield; 414 | /* 1 */ 415 | -moz-box-sizing: content-box; 416 | -webkit-box-sizing: content-box; 417 | /* 2 */ 418 | box-sizing: content-box; 419 | } 420 | 421 | /* 422 | * Removes inner padding and search cancel button in S5, Chrome on OS X 423 | */ 424 | input[type="search"]::-webkit-search-decoration, 425 | input[type="search"]::-webkit-search-cancel-button { 426 | -webkit-appearance: none; 427 | } 428 | 429 | /* 430 | * Removes inner padding and border in FF3+ 431 | * www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/ 432 | */ 433 | button::-moz-focus-inner, 434 | input::-moz-focus-inner { 435 | border: 0; 436 | padding: 0; 437 | } 438 | 439 | /* 440 | * 1. Removes default vertical scrollbar in IE6/7/8/9 441 | * 2. Improves readability and alignment in all browsers 442 | */ 443 | textarea { 444 | overflow: auto; 445 | /* 1 */ 446 | vertical-align: top; 447 | /* 2 */ 448 | } 449 | 450 | /* ============================================================================= 451 | Tables 452 | ========================================================================== */ 453 | /* 454 | * Remove most spacing between table cells 455 | */ 456 | table { 457 | border-collapse: collapse; 458 | border-spacing: 0; 459 | } 460 | -------------------------------------------------------------------------------- /docs/stylesheets/styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | Leap Day for GitHub Pages 3 | by Matt Graham 4 | */ 5 | @font-face { 6 | font-family: 'Quattrocento Sans'; 7 | src: url("../fonts/quattrocentosans-bold-webfont.eot"); 8 | src: url("../fonts/quattrocentosans-bold-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/quattrocentosans-bold-webfont.woff") format("woff"), url("../fonts/quattrocentosans-bold-webfont.ttf") format("truetype"), url("../fonts/quattrocentosans-bold-webfont.svg#QuattrocentoSansBold") format("svg"); 9 | font-weight: bold; 10 | font-style: normal; 11 | } 12 | 13 | @font-face { 14 | font-family: 'Quattrocento Sans'; 15 | src: url("../fonts/quattrocentosans-bolditalic-webfont.eot"); 16 | src: url("../fonts/quattrocentosans-bolditalic-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/quattrocentosans-bolditalic-webfont.woff") format("woff"), url("../fonts/quattrocentosans-bolditalic-webfont.ttf") format("truetype"), url("../fonts/quattrocentosans-bolditalic-webfont.svg#QuattrocentoSansBoldItalic") format("svg"); 17 | font-weight: bold; 18 | font-style: italic; 19 | } 20 | 21 | @font-face { 22 | font-family: 'Quattrocento Sans'; 23 | src: url("../fonts/quattrocentosans-italic-webfont.eot"); 24 | src: url("../fonts/quattrocentosans-italic-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/quattrocentosans-italic-webfont.woff") format("woff"), url("../fonts/quattrocentosans-italic-webfont.ttf") format("truetype"), url("../fonts/quattrocentosans-italic-webfont.svg#QuattrocentoSansItalic") format("svg"); 25 | font-weight: normal; 26 | font-style: italic; 27 | } 28 | 29 | @font-face { 30 | font-family: 'Quattrocento Sans'; 31 | src: url("../fonts/quattrocentosans-regular-webfont.eot"); 32 | src: url("../fonts/quattrocentosans-regular-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/quattrocentosans-regular-webfont.woff") format("woff"), url("../fonts/quattrocentosans-regular-webfont.ttf") format("truetype"), url("../fonts/quattrocentosans-regular-webfont.svg#QuattrocentoSansRegular") format("svg"); 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | @font-face { 38 | font-family: 'Copse'; 39 | src: url("../fonts/copse-regular-webfont.eot"); 40 | src: url("../fonts/copse-regular-webfont.eot?#iefix") format("embedded-opentype"), url("../fonts/copse-regular-webfont.woff") format("woff"), url("../fonts/copse-regular-webfont.ttf") format("truetype"), url("../fonts/copse-regular-webfont.svg#CopseRegular") format("svg"); 41 | font-weight: normal; 42 | font-style: normal; 43 | } 44 | 45 | /* normalize.css 2012-02-07T12:37 UTC - https://github.com/necolas/normalize.css */ 46 | /* ============================================================================= 47 | HTML5 display definitions 48 | ========================================================================== */ 49 | /* 50 | * Corrects block display not defined in IE6/7/8/9 & FF3 51 | */ 52 | article, 53 | aside, 54 | details, 55 | figcaption, 56 | figure, 57 | footer, 58 | header, 59 | hgroup, 60 | nav, 61 | section, 62 | summary { 63 | display: block; 64 | } 65 | 66 | /* 67 | * Corrects inline-block display not defined in IE6/7/8/9 & FF3 68 | */ 69 | audio, 70 | canvas, 71 | video { 72 | display: inline-block; 73 | *display: inline; 74 | *zoom: 1; 75 | } 76 | 77 | /* 78 | * Prevents modern browsers from displaying 'audio' without controls 79 | */ 80 | audio:not([controls]) { 81 | display: none; 82 | } 83 | 84 | /* 85 | * Addresses styling for 'hidden' attribute not present in IE7/8/9, FF3, S4 86 | * Known issue: no IE6 support 87 | */ 88 | [hidden] { 89 | display: none; 90 | } 91 | 92 | /* ============================================================================= 93 | Base 94 | ========================================================================== */ 95 | /* 96 | * 1. Corrects text resizing oddly in IE6/7 when body font-size is set using em units 97 | * http://clagnut.com/blog/348/#c790 98 | * 2. Prevents iOS text size adjust after orientation change, without disabling user zoom 99 | * www.456bereastreet.com/archive/201012/controlling_text_size_in_safari_for_ios_without_disabling_user_zoom/ 100 | */ 101 | html { 102 | font-size: 100%; 103 | /* 1 */ 104 | -webkit-text-size-adjust: 100%; 105 | /* 2 */ 106 | -ms-text-size-adjust: 100%; 107 | /* 2 */ 108 | } 109 | 110 | /* 111 | * Addresses font-family inconsistency between 'textarea' and other form elements. 112 | */ 113 | html, 114 | button, 115 | input, 116 | select, 117 | textarea { 118 | font-family: sans-serif; 119 | } 120 | 121 | /* 122 | * Addresses margins handled incorrectly in IE6/7 123 | */ 124 | body { 125 | margin: 0; 126 | } 127 | 128 | /* ============================================================================= 129 | Links 130 | ========================================================================== */ 131 | /* 132 | * Addresses outline displayed oddly in Chrome 133 | */ 134 | a:focus { 135 | outline: thin dotted; 136 | } 137 | 138 | /* 139 | * Improves readability when focused and also mouse hovered in all browsers 140 | * people.opera.com/patrickl/experiments/keyboard/test 141 | */ 142 | a:hover, 143 | a:active { 144 | outline: 0; 145 | } 146 | 147 | /* ============================================================================= 148 | Typography 149 | ========================================================================== */ 150 | /* 151 | * Addresses font sizes and margins set differently in IE6/7 152 | * Addresses font sizes within 'section' and 'article' in FF4+, Chrome, S5 153 | */ 154 | h1 { 155 | font-size: 2em; 156 | margin: 0.67em 0; 157 | } 158 | 159 | h2 { 160 | font-size: 1.5em; 161 | margin: 0.83em 0; 162 | } 163 | 164 | h3 { 165 | font-size: 1.17em; 166 | margin: 1em 0; 167 | } 168 | 169 | h4 { 170 | font-size: 1em; 171 | margin: 1.33em 0; 172 | } 173 | 174 | h5 { 175 | font-size: 0.83em; 176 | margin: 1.67em 0; 177 | } 178 | 179 | h6 { 180 | font-size: 0.75em; 181 | margin: 2.33em 0; 182 | } 183 | 184 | /* 185 | * Addresses styling not present in IE7/8/9, S5, Chrome 186 | */ 187 | abbr[title] { 188 | border-bottom: 1px dotted; 189 | } 190 | 191 | /* 192 | * Addresses style set to 'bolder' in FF3+, S4/5, Chrome 193 | */ 194 | b, 195 | strong { 196 | font-weight: bold; 197 | } 198 | 199 | blockquote { 200 | margin: 1em 40px; 201 | } 202 | 203 | /* 204 | * Addresses styling not present in S5, Chrome 205 | */ 206 | dfn { 207 | font-style: italic; 208 | } 209 | 210 | /* 211 | * Addresses styling not present in IE6/7/8/9 212 | */ 213 | mark { 214 | background: #ff0; 215 | color: #000; 216 | } 217 | 218 | /* 219 | * Addresses margins set differently in IE6/7 220 | */ 221 | p, 222 | pre { 223 | margin: 1em 0; 224 | } 225 | 226 | /* 227 | * Corrects font family set oddly in IE6, S4/5, Chrome 228 | * en.wikipedia.org/wiki/User:Davidgothberg/Test59 229 | */ 230 | pre, 231 | code, 232 | kbd, 233 | samp { 234 | font-family: monospace, serif; 235 | _font-family: 'courier new', monospace; 236 | font-size: 1em; 237 | } 238 | 239 | /* 240 | * 1. Addresses CSS quotes not supported in IE6/7 241 | * 2. Addresses quote property not supported in S4 242 | */ 243 | /* 1 */ 244 | q { 245 | quotes: none; 246 | } 247 | 248 | /* 2 */ 249 | q:before, 250 | q:after { 251 | content: ''; 252 | content: none; 253 | } 254 | 255 | small { 256 | font-size: 75%; 257 | } 258 | 259 | /* 260 | * Prevents sub and sup affecting line-height in all browsers 261 | * gist.github.com/413930 262 | */ 263 | sub, 264 | sup { 265 | font-size: 75%; 266 | line-height: 0; 267 | position: relative; 268 | vertical-align: baseline; 269 | } 270 | 271 | sup { 272 | top: -0.5em; 273 | } 274 | 275 | sub { 276 | bottom: -0.25em; 277 | } 278 | 279 | /* ============================================================================= 280 | Lists 281 | ========================================================================== */ 282 | /* 283 | * Addresses margins set differently in IE6/7 284 | */ 285 | dl, 286 | menu, 287 | ol, 288 | ul { 289 | margin: 1em 0; 290 | } 291 | 292 | dd { 293 | margin: 0 0 0 40px; 294 | } 295 | 296 | /* 297 | * Addresses paddings set differently in IE6/7 298 | */ 299 | menu, 300 | ol, 301 | ul { 302 | padding: 0 0 0 40px; 303 | } 304 | 305 | /* 306 | * Corrects list images handled incorrectly in IE7 307 | */ 308 | nav ul, 309 | nav ol { 310 | list-style: none; 311 | list-style-image: none; 312 | } 313 | 314 | /* ============================================================================= 315 | Embedded content 316 | ========================================================================== */ 317 | /* 318 | * 1. Removes border when inside 'a' element in IE6/7/8/9, FF3 319 | * 2. Improves image quality when scaled in IE7 320 | * code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ 321 | */ 322 | img { 323 | border: 0; 324 | /* 1 */ 325 | -ms-interpolation-mode: bicubic; 326 | /* 2 */ 327 | } 328 | 329 | /* 330 | * Corrects overflow displayed oddly in IE9 331 | */ 332 | svg:not(:root) { 333 | overflow: hidden; 334 | } 335 | 336 | /* ============================================================================= 337 | Figures 338 | ========================================================================== */ 339 | /* 340 | * Addresses margin not present in IE6/7/8/9, S5, O11 341 | */ 342 | figure { 343 | margin: 0; 344 | } 345 | 346 | /* ============================================================================= 347 | Forms 348 | ========================================================================== */ 349 | /* 350 | * Corrects margin displayed oddly in IE6/7 351 | */ 352 | form { 353 | margin: 0; 354 | } 355 | 356 | /* 357 | * Define consistent border, margin, and padding 358 | */ 359 | fieldset { 360 | border: 1px solid #c0c0c0; 361 | margin: 0 2px; 362 | padding: 0.35em 0.625em 0.75em; 363 | } 364 | 365 | /* 366 | * 1. Corrects color not being inherited in IE6/7/8/9 367 | * 2. Corrects text not wrapping in FF3 368 | * 3. Corrects alignment displayed oddly in IE6/7 369 | */ 370 | legend { 371 | border: 0; 372 | /* 1 */ 373 | padding: 0; 374 | white-space: normal; 375 | /* 2 */ 376 | *margin-left: -7px; 377 | /* 3 */ 378 | } 379 | 380 | /* 381 | * 1. Corrects font size not being inherited in all browsers 382 | * 2. Addresses margins set differently in IE6/7, FF3+, S5, Chrome 383 | * 3. Improves appearance and consistency in all browsers 384 | */ 385 | button, 386 | input, 387 | select, 388 | textarea { 389 | font-size: 100%; 390 | /* 1 */ 391 | margin: 0; 392 | /* 2 */ 393 | vertical-align: baseline; 394 | /* 3 */ 395 | *vertical-align: middle; 396 | /* 3 */ 397 | } 398 | 399 | /* 400 | * Addresses FF3/4 setting line-height on 'input' using !important in the UA stylesheet 401 | */ 402 | button, 403 | input { 404 | line-height: normal; 405 | /* 1 */ 406 | } 407 | 408 | /* 409 | * 1. Improves usability and consistency of cursor style between image-type 'input' and others 410 | * 2. Corrects inability to style clickable 'input' types in iOS 411 | * 3. Removes inner spacing in IE7 without affecting normal text inputs 412 | * Known issue: inner spacing remains in IE6 413 | */ 414 | button, 415 | input[type="button"], 416 | input[type="reset"], 417 | input[type="submit"] { 418 | cursor: pointer; 419 | /* 1 */ 420 | -webkit-appearance: button; 421 | /* 2 */ 422 | *overflow: visible; 423 | /* 3 */ 424 | } 425 | 426 | /* 427 | * Re-set default cursor for disabled elements 428 | */ 429 | button[disabled], 430 | input[disabled] { 431 | cursor: default; 432 | } 433 | 434 | /* 435 | * 1. Addresses box sizing set to content-box in IE8/9 436 | * 2. Removes excess padding in IE8/9 437 | * 3. Removes excess padding in IE7 438 | Known issue: excess padding remains in IE6 439 | */ 440 | input[type="checkbox"], 441 | input[type="radio"] { 442 | box-sizing: border-box; 443 | /* 1 */ 444 | padding: 0; 445 | /* 2 */ 446 | *height: 13px; 447 | /* 3 */ 448 | *width: 13px; 449 | /* 3 */ 450 | } 451 | 452 | /* 453 | * 1. Addresses appearance set to searchfield in S5, Chrome 454 | * 2. Addresses box-sizing set to border-box in S5, Chrome (include -moz to future-proof) 455 | */ 456 | input[type="search"] { 457 | -webkit-appearance: textfield; 458 | /* 1 */ 459 | -moz-box-sizing: content-box; 460 | -webkit-box-sizing: content-box; 461 | /* 2 */ 462 | box-sizing: content-box; 463 | } 464 | 465 | /* 466 | * Removes inner padding and search cancel button in S5, Chrome on OS X 467 | */ 468 | input[type="search"]::-webkit-search-decoration, 469 | input[type="search"]::-webkit-search-cancel-button { 470 | -webkit-appearance: none; 471 | } 472 | 473 | /* 474 | * Removes inner padding and border in FF3+ 475 | * www.sitepen.com/blog/2008/05/14/the-devils-in-the-details-fixing-dojos-toolbar-buttons/ 476 | */ 477 | button::-moz-focus-inner, 478 | input::-moz-focus-inner { 479 | border: 0; 480 | padding: 0; 481 | } 482 | 483 | /* 484 | * 1. Removes default vertical scrollbar in IE6/7/8/9 485 | * 2. Improves readability and alignment in all browsers 486 | */ 487 | textarea { 488 | overflow: auto; 489 | /* 1 */ 490 | vertical-align: top; 491 | /* 2 */ 492 | } 493 | 494 | /* ============================================================================= 495 | Tables 496 | ========================================================================== */ 497 | /* 498 | * Remove most spacing between table cells 499 | */ 500 | table { 501 | border-collapse: collapse; 502 | border-spacing: 0; 503 | } 504 | 505 | body { 506 | font: 14px/22px "Quattrocento Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 507 | color: #666; 508 | font-weight: 300; 509 | margin: 0px; 510 | padding: 0px 0 20px 0px; 511 | background: url(../images/body-background.png) #eae6d1; 512 | } 513 | 514 | h1, h2, h3, h4, h5, h6 { 515 | color: #333; 516 | margin: 0 0 10px; 517 | } 518 | 519 | p, ul, ol, table, pre, dl { 520 | margin: 0 0 20px; 521 | } 522 | 523 | h1, h2, h3 { 524 | line-height: 1.1; 525 | } 526 | 527 | h1 { 528 | font-size: 28px; 529 | } 530 | 531 | h2 { 532 | font-size: 24px; 533 | color: #393939; 534 | } 535 | 536 | h3, h4, h5, h6 { 537 | color: #666666; 538 | } 539 | 540 | h3 { 541 | font-size: 18px; 542 | line-height: 24px; 543 | } 544 | 545 | a { 546 | color: #3399cc; 547 | font-weight: 400; 548 | text-decoration: none; 549 | } 550 | 551 | a small { 552 | font-size: 11px; 553 | color: #666; 554 | margin-top: -0.6em; 555 | display: block; 556 | } 557 | 558 | ul { 559 | list-style-image: url("../images/bullet.png"); 560 | } 561 | 562 | strong { 563 | font-weight: bold; 564 | color: #333; 565 | } 566 | 567 | .wrapper { 568 | width: 750px; 569 | margin: 0 auto; 570 | position: relative; 571 | } 572 | 573 | section img { 574 | max-width: 100%; 575 | } 576 | 577 | blockquote { 578 | border-left: 1px solid #ffcc00; 579 | margin: 0; 580 | padding: 0 0 0 20px; 581 | font-style: italic; 582 | } 583 | 584 | code { 585 | font-family: "Lucida Sans", Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; 586 | font-size: 13px; 587 | color: #efefef; 588 | margin: 0 4px; 589 | padding: 2px 6px; 590 | background: palevioletred; 591 | -moz-border-radius: 2px; 592 | -webkit-border-radius: 2px; 593 | -o-border-radius: 2px; 594 | -ms-border-radius: 2px; 595 | -khtml-border-radius: 2px; 596 | border-radius: 2px; 597 | } 598 | 599 | pre { 600 | padding: 8px 15px; 601 | background-color: #f0f0f0; 602 | overflow: auto; 603 | overflow-y: hidden; 604 | } 605 | 606 | pre code { 607 | margin: 0px; 608 | padding: 0px; 609 | } 610 | 611 | table { 612 | width: 100%; 613 | border-collapse: collapse; 614 | } 615 | 616 | th { 617 | text-align: left; 618 | padding: 5px 10px; 619 | border-bottom: 1px solid #e5e5e5; 620 | color: #444; 621 | } 622 | 623 | td { 624 | text-align: left; 625 | padding: 5px 10px; 626 | border-bottom: 1px solid #e5e5e5; 627 | border-right: 1px solid #ffcc00; 628 | } 629 | 630 | td:first-child { 631 | border-left: 1px solid #ffcc00; 632 | } 633 | 634 | hr { 635 | border: 0; 636 | outline: none; 637 | height: 11px; 638 | background: transparent url("../images/hr.gif") center center repeat-x; 639 | margin: 0 0 20px; 640 | } 641 | 642 | dt { 643 | color: #444; 644 | font-weight: 700; 645 | } 646 | 647 | header { 648 | padding: 15px 10px 20px 10px; 649 | margin: 0; 650 | position: fixed; 651 | top: 0; 652 | left: 0; 653 | right: 0; 654 | width: 100%; 655 | text-align: center; 656 | background: url(../images/background.png) #4276b6; 657 | -moz-box-shadow: 1px 0px 2px rgba(0, 0, 0, 0.75); 658 | -webkit-box-shadow: 1px 0px 2px rgba(0, 0, 0, 0.75); 659 | -o-box-shadow: 1px 0px 2px rgba(0, 0, 0, 0.75); 660 | box-shadow: 1px 0px 2px rgba(0, 0, 0, 0.75); 661 | z-index: 99; 662 | -webkit-font-smoothing: antialiased; 663 | min-height: 76px; 664 | } 665 | 666 | header h1 { 667 | font: 28px/32px "Copse", "Helvetica Neue", Helvetica, Arial, sans-serif; 668 | color: #f3f3f3; 669 | text-shadow: 0px 2px 0px #235796; 670 | margin: 0px; 671 | white-space: nowrap; 672 | overflow: hidden; 673 | text-overflow: ellipsis; 674 | -o-text-overflow: ellipsis; 675 | -ms-text-overflow: ellipsis; 676 | } 677 | 678 | header p { 679 | color: #d8d8d8; 680 | text-shadow: rgba(0, 0, 0, 0.2) 0 1px 0; 681 | font-size: 18px; 682 | margin: 0px; 683 | } 684 | 685 | #banner { 686 | z-index: 100; 687 | left: 0; 688 | width: 100%; 689 | height: 50px; 690 | margin-right: -382px; 691 | position: fixed; 692 | top: 100px; 693 | background: #ffcc00; 694 | border: 1px solid #f0b500; 695 | -moz-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25); 696 | -webkit-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25); 697 | -o-box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25); 698 | box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.25); 699 | -moz-border-radius: 0px 2px 2px 0px; 700 | -webkit-border-radius: 0px 2px 2px 0px; 701 | -o-border-radius: 0px 2px 2px 0px; 702 | -ms-border-radius: 0px 2px 2px 0px; 703 | -khtml-border-radius: 0px 2px 2px 0px; 704 | border-radius: 0px 2px 2px 0px; 705 | padding-right: 10px; 706 | } 707 | 708 | #banner .banner-inner { 709 | margin: 0 auto; 710 | max-width: 650px; 711 | overflow: hidden; 712 | width: 50%; 713 | } 714 | 715 | #banner .button { 716 | border: 1px solid #dba500; 717 | background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffe788), color-stop(100%, #ffce38)); 718 | background: -webkit-linear-gradient(#ffe788, #ffce38); 719 | background: -moz-linear-gradient(#ffe788, #ffce38); 720 | background: -o-linear-gradient(#ffe788, #ffce38); 721 | background: -ms-linear-gradient(#ffe788, #ffce38); 722 | background: linear-gradient(#ffe788, #ffce38); 723 | -moz-border-radius: 2px; 724 | -webkit-border-radius: 2px; 725 | -o-border-radius: 2px; 726 | -ms-border-radius: 2px; 727 | -khtml-border-radius: 2px; 728 | border-radius: 2px; 729 | -moz-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.4), 0px 1px 1px rgba(0, 0, 0, 0.1); 730 | -webkit-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.4), 0px 1px 1px rgba(0, 0, 0, 0.1); 731 | -o-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.4), 0px 1px 1px rgba(0, 0, 0, 0.1); 732 | box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.4), 0px 1px 1px rgba(0, 0, 0, 0.1); 733 | background-color: #FFE788; 734 | margin-left: 5px; 735 | padding: 10px 12px; 736 | margin-top: 6px; 737 | line-height: 14px; 738 | font-size: 14px; 739 | color: #333; 740 | font-weight: bold; 741 | display: inline-block; 742 | text-align: center; 743 | } 744 | 745 | #banner .button:hover { 746 | background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffe788), color-stop(100%, #ffe788)); 747 | background: -webkit-linear-gradient(#ffe788, #ffe788); 748 | background: -moz-linear-gradient(#ffe788, #ffe788); 749 | background: -o-linear-gradient(#ffe788, #ffe788); 750 | background: -ms-linear-gradient(#ffe788, #ffe788); 751 | background: linear-gradient(#ffe788, #ffe788); 752 | background-color: #ffeca0; 753 | } 754 | 755 | #banner .fork { 756 | padding: 10px 12px; 757 | margin-top: 6px; 758 | line-height: 14px; 759 | font-size: 14px; 760 | float: left; 761 | background-color: #FFE788; 762 | } 763 | 764 | #banner .downloads { 765 | float: right; 766 | margin: 0 45px 0 0; 767 | } 768 | 769 | #banner .downloads span { 770 | float: left; 771 | line-height: 52px; 772 | font-size: 90%; 773 | color: #9d7f0d; 774 | text-transform: uppercase; 775 | text-shadow: rgba(255, 255, 255, 0.2) 0 1px 0; 776 | } 777 | 778 | #banner ul { 779 | list-style: none; 780 | height: 40px; 781 | padding: 0; 782 | float: left; 783 | margin-left: 10px; 784 | } 785 | 786 | #banner ul li { 787 | display: inline; 788 | } 789 | 790 | #banner ul li a.button { 791 | background-color: #FFE788; 792 | } 793 | 794 | #banner #logo { 795 | position: relative; 796 | float: left; 797 | height: 36px; 798 | width: 36px; 799 | left: 0%; 800 | top: 7px; 801 | margin-right: 10px; 802 | background: url(../images/octocat-logo.png); 803 | } 804 | 805 | section { 806 | width: 690px; 807 | padding: 30px 30px 50px 30px; 808 | margin: 20px 0; 809 | margin-top: 190px; 810 | position: relative; 811 | background: #fbfbfb; 812 | -moz-border-radius: 3px; 813 | -webkit-border-radius: 3px; 814 | -o-border-radius: 3px; 815 | -ms-border-radius: 3px; 816 | -khtml-border-radius: 3px; 817 | border-radius: 3px; 818 | border: 1px solid #cbcbcb; 819 | -moz-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.09), inset 0px 0px 2px 2px rgba(255, 255, 255, 0.5), inset 0 0 5px 5px rgba(255, 255, 255, 0.4); 820 | -webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.09), inset 0px 0px 2px 2px rgba(255, 255, 255, 0.5), inset 0 0 5px 5px rgba(255, 255, 255, 0.4); 821 | -o-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.09), inset 0px 0px 2px 2px rgba(255, 255, 255, 0.5), inset 0 0 5px 5px rgba(255, 255, 255, 0.4); 822 | box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.09), inset 0px 0px 2px 2px rgba(255, 255, 255, 0.5), inset 0 0 5px 5px rgba(255, 255, 255, 0.4); 823 | } 824 | 825 | small { 826 | font-size: 12px; 827 | } 828 | 829 | nav { 830 | width: 240px; 831 | position: fixed; 832 | left: 50%; 833 | margin-left: -640px; 834 | text-align: right; 835 | } 836 | 837 | nav ul { 838 | list-style: none; 839 | list-style-image: none; 840 | font-size: 14px; 841 | line-height: 24px; 842 | } 843 | 844 | nav ul li { 845 | padding: 5px 0px; 846 | line-height: 16px; 847 | } 848 | 849 | nav ul li.tag-h1 { 850 | font-size: 1.2em; 851 | } 852 | 853 | nav ul li.tag-h1 a { 854 | font-weight: bold; 855 | color: #333; 856 | } 857 | 858 | nav ul li.tag-h2 + .tag-h1 { 859 | margin-top: 10px; 860 | } 861 | 862 | nav ul a { 863 | color: #666; 864 | } 865 | 866 | nav ul a:hover { 867 | color: #999; 868 | } 869 | 870 | footer { 871 | width: 180px; 872 | position: fixed; 873 | left: 50%; 874 | margin-left: -600px; 875 | bottom: 20px; 876 | text-align: right; 877 | line-height: 16px; 878 | } 879 | 880 | @media print, screen and (max-width: 1060px) { 881 | div.wrapper { 882 | width: auto; 883 | margin: 0; 884 | } 885 | 886 | nav { 887 | display: none; 888 | } 889 | 890 | header, section, footer { 891 | float: none; 892 | } 893 | 894 | header h1, section h1, footer h1 { 895 | white-space: nowrap; 896 | overflow: hidden; 897 | text-overflow: ellipsis; 898 | -o-text-overflow: ellipsis; 899 | -ms-text-overflow: ellipsis; 900 | } 901 | 902 | #banner { 903 | width: 100%; 904 | } 905 | 906 | #banner .downloads { 907 | margin-right: 60px; 908 | } 909 | 910 | #banner #logo { 911 | margin-right: 15px; 912 | } 913 | 914 | section { 915 | border: 1px solid #e5e5e5; 916 | border-width: 1px 0; 917 | padding: 20px auto; 918 | margin: 190px auto 20px; 919 | max-width: 600px; 920 | } 921 | 922 | footer { 923 | text-align: center; 924 | margin: 20px auto; 925 | position: relative; 926 | left: auto; 927 | bottom: auto; 928 | width: auto; 929 | } 930 | } 931 | 932 | @media print, screen and (max-width: 720px) { 933 | body { 934 | word-wrap: break-word; 935 | } 936 | 937 | header { 938 | padding: 10px 10px; 939 | margin: 0; 940 | } 941 | 942 | header h1 { 943 | font-size: 28px; 944 | white-space: nowrap; 945 | overflow: hidden; 946 | text-overflow: ellipsis; 947 | -o-text-overflow: ellipsis; 948 | -ms-text-overflow: ellipsis; 949 | } 950 | 951 | header p { 952 | display: none; 953 | } 954 | 955 | #banner { 956 | top: 80px; 957 | } 958 | 959 | #banner .fork { 960 | float: left; 961 | display: inline-block; 962 | margin-left: 0px; 963 | position: fixed; 964 | left: 20px; 965 | } 966 | 967 | section { 968 | margin-top: 130px; 969 | margin-bottom: 0px; 970 | width: auto; 971 | } 972 | 973 | header ul, header p.view { 974 | position: static; 975 | } 976 | } 977 | 978 | @media print, screen and (max-width: 480px) { 979 | header { 980 | position: relative; 981 | padding: 5px 0px; 982 | min-height: 0px; 983 | } 984 | 985 | header h1 { 986 | font-size: 24px; 987 | white-space: nowrap; 988 | overflow: hidden; 989 | text-overflow: ellipsis; 990 | -o-text-overflow: ellipsis; 991 | -ms-text-overflow: ellipsis; 992 | } 993 | 994 | section { 995 | margin-top: 5px; 996 | } 997 | 998 | #banner { 999 | display: none; 1000 | } 1001 | 1002 | header ul { 1003 | display: none; 1004 | } 1005 | } 1006 | 1007 | @media print { 1008 | body { 1009 | padding: 0.4in; 1010 | font-size: 12pt; 1011 | color: #444; 1012 | } 1013 | } 1014 | 1015 | @media print, screen and (max-height: 680px) { 1016 | footer { 1017 | text-align: center; 1018 | margin: 20px auto; 1019 | position: relative; 1020 | left: auto; 1021 | bottom: auto; 1022 | width: auto; 1023 | } 1024 | } 1025 | 1026 | @media print, screen and (max-height: 480px) { 1027 | nav { 1028 | display: none; 1029 | } 1030 | 1031 | footer { 1032 | text-align: center; 1033 | margin: 20px auto; 1034 | position: relative; 1035 | left: auto; 1036 | bottom: auto; 1037 | width: auto; 1038 | } 1039 | } 1040 | 1041 | #BallsLeft { 1042 | text-align: center; 1043 | background-color: grey; 1044 | color: white; 1045 | padding: 5px; 1046 | width: 25%; 1047 | display: inline-block; 1048 | margin-bottom: 3px; 1049 | border-radius: 2%; 1050 | border-color: grey; 1051 | border-width: 2px; 1052 | border-style: solid; 1053 | 1054 | } 1055 | 1056 | #BallsAllocated { 1057 | text-align: center; 1058 | float: right; 1059 | background-color: grey; 1060 | color: white; 1061 | padding: 5px; 1062 | margin-bottom: 3px; 1063 | width: 25%; 1064 | border-radius: 2%; 1065 | border-color: grey; 1066 | border-width: 2px; 1067 | border-style: solid; 1068 | } -------------------------------------------------------------------------------- /lib/distributionbuilder.css: -------------------------------------------------------------------------------- 1 | /* Distbuilder */ 2 | 3 | .distbuilder { 4 | border-width: 2px; 5 | border-color: grey; 6 | border-style: solid; 7 | padding: 2px; 8 | } 9 | 10 | /* Grid */ 11 | 12 | .distbuilder > .grid { 13 | background-color: lightsteelblue; 14 | width: 100%; 15 | padding-bottom: 2px; 16 | padding-top: 5px; 17 | } 18 | 19 | .distbuilder > .grid > .distrow { 20 | display: flex; 21 | } 22 | 23 | .distbuilder > .grid > .distrow > .cell { 24 | flex: 1; 25 | display: flex; 26 | justify-content: center; 27 | } 28 | 29 | .distbuilder > .grid > .distrow > .cell > .ball { 30 | margin-bottom: 1px; 31 | height: 10px; 32 | width: 10px; 33 | border: 1px solid white; 34 | border-radius: 50%; 35 | align-self: center; 36 | } 37 | 38 | .distbuilder > .grid > .distrow > .filled > .ball { 39 | background-color: #FF4500; 40 | } 41 | 42 | /* Labels */ 43 | 44 | .distbuilder > .labels { 45 | background-color: steelblue; 46 | width: 100%; 47 | } 48 | 49 | .distbuilder > .labels > .distrow { 50 | display: flex; 51 | width: 100%; 52 | } 53 | 54 | .distbuilder > .labels > .distrow > .label { 55 | color: white; 56 | float: left; 57 | text-align: center; 58 | font-size: 0.8em; 59 | flex: 1; 60 | min-width: 0; 61 | } 62 | 63 | /* Buttons */ 64 | 65 | .distbuilder > .buttons { 66 | background-color: steelblue; 67 | width: 100%; 68 | } 69 | 70 | .distbuilder > .buttons > .distrow { 71 | display: flex; 72 | width: 100%; 73 | } 74 | 75 | .distbuilder > .buttons > .distrow > .buttongroup { 76 | text-align: center; 77 | flex: 1; 78 | padding-bottom: 5px; 79 | } 80 | 81 | .distbuilder > .buttons > .distrow > .buttongroup > .distbutton { 82 | border-radius: 10%; 83 | width: 60%; 84 | margin: 0 auto !important; 85 | padding: 0 !important; 86 | line-height: 1 !important; 87 | background-color: lightgrey; 88 | } 89 | 90 | /* Totals */ 91 | 92 | .totals { 93 | background-color: #FF4500; 94 | } 95 | 96 | .totals > .distrow { 97 | display: flex; 98 | } 99 | 100 | .distbuilder > .totals > .distrow > .total { 101 | color: white; 102 | float: left; 103 | text-align: center; 104 | font-size: 0.8em; 105 | flex: 1; 106 | } 107 | 108 | /* Totals */ 109 | 110 | .totals { 111 | background-color: #FF4500; 112 | } 113 | 114 | .totals > .distrow { 115 | display: flex; 116 | } 117 | 118 | .distbuilder > .totals > .distrow > .total { 119 | color: white; 120 | float: left; 121 | text-align: center; 122 | font-size: 0.8em; 123 | flex: 1; 124 | } 125 | -------------------------------------------------------------------------------- /lib/distributionbuilder.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Quentin André on 07/10/2016. 3 | */ 4 | import "jquery"; 5 | import './distributionbuilder.css'; 6 | import "bootstrap-webpack!./bootstrap.config.js"; 7 | interface InitConfigObject { 8 | minVal?: number; 9 | maxVal?: number; 10 | nBalls?: number; 11 | nRows?: number; 12 | nBuckets?: number; 13 | onTouch?: Function; 14 | onChange?: Function; 15 | addTotals?: boolean; 16 | toggleGridClick?: boolean; 17 | } 18 | interface LabelizeConfigObject { 19 | labels?: Array; 20 | prefix?: string; 21 | suffix?: string; 22 | } 23 | declare global { 24 | interface JQuery { 25 | mousehold(timestart: number, timeout: number, callback: Function): any; 26 | } 27 | } 28 | declare type ValidRenderOrder = "buttons-grid-labels" | "grid-labels-buttons" | "labels-grid-buttons" | "labels-buttons-grid" | "grid-buttons-labels" | "buttons-labels-grid"; 29 | declare type ValidButtonAction = "increment" | "decrement"; 30 | declare class DistributionBuilder { 31 | min: number; 32 | max: number; 33 | nBalls: number; 34 | nRows: number; 35 | nBuckets: number; 36 | remainingBalls: number; 37 | distribution: Array; 38 | _$target: JQuery; 39 | onTouch: Function; 40 | onChange: Function; 41 | toggleGridClick: Boolean; 42 | addTotals: Boolean; 43 | constructor(o: InitConfigObject); 44 | render(target: string, order: ValidRenderOrder, resize: boolean): void; 45 | labelize(o: LabelizeConfigObject): void; 46 | isComplete(): boolean; 47 | getRemainingBalls(): number; 48 | getDistribution(): Array; 49 | setDistribution(dist: Array): void; 50 | _setLabels(labels: Array): void; 51 | _buttonActionCreator(action: ValidButtonAction): Function; 52 | _gridActionCreator(row: number): Function; 53 | _createGrid(): JQuery; 54 | _createButtons(): JQuery; 55 | _createLabels(): JQuery; 56 | _createTotals(): JQuery; 57 | _updateTotals(): void; 58 | _updateGridCol(col: number, silent?: boolean): void; 59 | } 60 | export default DistributionBuilder; 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "distbuilder", 3 | "version": "1.4.1", 4 | "description": "A distribution builder for economic and psychology experiments. Based on Goldstein and Rothschild's 2014 paper 'Lay understanding of probability distributions'.", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "build": "webpack --progress --color", 8 | "start": "webpack-dev-server" 9 | }, 10 | "author": "Quentin ANDRE", 11 | "license": "ISC", 12 | "dependencies": { 13 | "bootstrap": "3", 14 | "expose-loader": "^0.7.5", 15 | "jquery": "^3.3.1" 16 | }, 17 | "devDependencies": { 18 | "@types/jquery": "^3.3.6", 19 | "autoprefixer": "^8.6.3", 20 | "babel-core": "^6.26.3", 21 | "babel-loader": "^7.1.4", 22 | "babel-preset-env": "^1.7.0", 23 | "bootstrap-webpack": "^0.0.6", 24 | "clean-webpack-plugin": "^0.1.19", 25 | "css-loader": "^0.28.11", 26 | "exports-loader": "^0.7.0", 27 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 28 | "file-loader": "^1.1.11", 29 | "image-webpack-loader": "^4.3.1", 30 | "imports-loader": "^0.8.0", 31 | "less": "^3.8.1", 32 | "less-loader": "^4.1.0", 33 | "mini-css-extract-plugin": "^0.4.0", 34 | "node-sass": "^4.9.0", 35 | "postcss-loader": "^2.1.5", 36 | "sass-loader": "^7.0.3", 37 | "style-loader": "^0.21.0", 38 | "ts-loader": "^5.1.0", 39 | "typescript": "^3.0.3", 40 | "url-loader": "^1.1.1", 41 | "webpack": "^4.12.0", 42 | "webpack-cli": "^4.5.0", 43 | "webpack-dev-server": "^3.1.4", 44 | "webpack-md5-hash": "^0.0.6" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /reference.bib: -------------------------------------------------------------------------------- 1 | @misc{, 2 | title = {{distBuilder} ({Version} 1.4.1) [{Computer} {Software}]}, 3 | url = {https://quentinandre.github.io/DistributionBuilder/}, 4 | abstract = {A Javascript library adding distribution builders to your experiments.}, 5 | author = {André, Quentin}, 6 | year = {2016} 7 | } -------------------------------------------------------------------------------- /src/bootstrap.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | scripts: { 3 | 'transition': false, 4 | 'alert': false, 5 | 'button': true, 6 | 'carousel': false, 7 | 'collapse': false, 8 | 'dropdown': false, 9 | 'modal': false, 10 | 'tooltip': false, 11 | 'popover': false, 12 | 'scrollspy': false, 13 | 'tab': false, 14 | 'affix': false 15 | }, 16 | styles: { 17 | "mixins": true, 18 | 19 | "normalize": true, 20 | "print": true, 21 | 22 | "scaffolding": false, 23 | "type": false, 24 | "code": false, 25 | "grid": false, 26 | "tables": false, 27 | "forms": false, 28 | "buttons": true, 29 | 30 | "component-animations": false, 31 | "glyphicons": true, 32 | "dropdowns": false, 33 | "button-groups": false, 34 | "input-groups": false, 35 | "navs": false, 36 | "navbar": false, 37 | "breadcrumbs": false, 38 | "pagination": false, 39 | "pager": false, 40 | "labels": false, 41 | "badges": false, 42 | "jumbotron": false, 43 | "thumbnails": false, 44 | "alerts": false, 45 | "progress-bars": false, 46 | "media": false, 47 | "list-group": false, 48 | "panels": false, 49 | "wells": false, 50 | "close": false, 51 | 52 | "modals": false, 53 | "tooltip": false, 54 | "popovers": false, 55 | "carousel": false, 56 | 57 | "utilities": false, 58 | "responsive-utilities": false 59 | } 60 | }; 61 | 62 | -------------------------------------------------------------------------------- /src/bootstrap.config.less: -------------------------------------------------------------------------------- 1 | @btn-default-color: #444; 2 | @btn-default-bg: #eee; -------------------------------------------------------------------------------- /src/demo.js: -------------------------------------------------------------------------------- 1 | import DistributionBuilder from './distributionbuilder.ts' 2 | import jQuery from 'jquery'; 3 | 4 | var $j = jQuery.noConflict(); 5 | 6 | $j(document).ready(function () { 7 | var distbuilder0 = new DistributionBuilder({}); 8 | distbuilder0.render("targetdiv0"); 9 | distbuilder0.labelize({}); 10 | 11 | var distbuilder1 = new DistributionBuilder({ 12 | minVal: 0, 13 | maxVal: 100, 14 | nRows: 20, 15 | nBuckets: 20, 16 | nBalls: 50, 17 | onTouch: function () { 18 | console.log("Distbuilder was touched!") 19 | }, 20 | onChange: function () { 21 | console.log("Distbuilder was updated!"); 22 | }, 23 | addTotals: true, 24 | toggleGridClick: true 25 | }); 26 | 27 | var distbuilder2 = new DistributionBuilder({ 28 | minVal: 0, 29 | maxVal: 100, 30 | nRows: 10, 31 | nBuckets: 20, 32 | nBalls: 10, 33 | onTouch: function () { 34 | console.log("Distbuilder was touched!") 35 | }, 36 | onChange: function () { 37 | console.log("Distbuilder was updated!"); 38 | } 39 | }); 40 | 41 | var distbuilder3 = new DistributionBuilder({ 42 | minVal: 0, 43 | maxVal: 100, 44 | nRows: 10, 45 | nBuckets: 20, 46 | nBalls: 10, 47 | onTouch: function () { 48 | console.log("Distbuilder was touched!") 49 | }, 50 | onChange: function () { 51 | console.log("Distbuilder was updated!"); 52 | } 53 | }); 54 | 55 | 56 | distbuilder1.render("targetdiv1"); 57 | distbuilder1.labelize({}); 58 | distbuilder2.render("targetdiv2", "labels-grid-buttons"); 59 | distbuilder2.labelize({}); 60 | 61 | distbuilder3.render("targetdiv3"); 62 | distbuilder3.labelize({ 63 | prefix: '~', 64 | suffix: '€' 65 | }); 66 | 67 | var distbuilder4 = new DistributionBuilder({ 68 | minVal: 0, 69 | maxVal: 100, 70 | nRows: 10, 71 | nBuckets: 10, 72 | nBalls: 60 73 | }); 74 | var dist = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 75 | distbuilder4.render("targetdiv4"); 76 | distbuilder4.labelize(); 77 | distbuilder4.setDistribution(dist); 78 | 79 | 80 | var n_balls = 10; 81 | $j('#BallsLeft').text("You have " + n_balls + " balls left."); 82 | $j('#BallsAllocated').text("You have allocated " + 0 + " balls."); 83 | var distbuilder5 = new DistributionBuilder({ 84 | minVal: 0, 85 | maxVal: 100, 86 | nRows: 10, 87 | nBuckets: 20, 88 | nBalls: n_balls, 89 | onChange: function () { 90 | var remainingballs = this.getRemainingBalls(); 91 | var ballsallocated = n_balls - this.getRemainingBalls(); 92 | $j('#BallsLeft').text("You have " + remainingballs 93 | + " balls left."); 94 | $j('#BallsAllocated').text("You have allocated " + 95 | ballsallocated + " balls."); 96 | } 97 | }); 98 | 99 | distbuilder5.render("targetdiv5"); 100 | distbuilder5.labelize({}); 101 | 102 | var distbuilder6 = new DistributionBuilder({ 103 | minVal: 0, 104 | maxVal: 100, 105 | nRows: 10, 106 | nBuckets: 20, 107 | nBalls: 10, 108 | onChange: function () { 109 | if (this.isComplete()) { 110 | $j("#SubmitDistribution1").attr("disabled", false) 111 | } else { 112 | $j("#SubmitDistribution1").attr("disabled", true) 113 | } 114 | } 115 | }); 116 | distbuilder6.render("targetdiv6"); 117 | distbuilder6.labelize({}); 118 | $j("#SubmitDistribution1").click(function () { 119 | alert("Distribution Validated!") 120 | }); 121 | 122 | var distbuilder7 = new DistributionBuilder({ 123 | minVal: 0, 124 | maxVal: 100, 125 | nRows: 10, 126 | nBuckets: 20, 127 | nBalls: 10, 128 | onChange: function () { 129 | if (this.isComplete()) { 130 | $j("#SubmitDistribution2").attr("disabled", false) 131 | } else { 132 | $j("#SubmitDistribution2").attr("disabled", true) 133 | } 134 | } 135 | }); 136 | distbuilder7.render("targetdiv7"); 137 | distbuilder7.labelize({}); 138 | $j("#SubmitDistribution2").click(function () { 139 | var message = "The distribution specified by the user is: " + distbuilder7.getDistribution(); 140 | alert(message) 141 | }); 142 | 143 | var distbuilder8 = new DistributionBuilder({ 144 | minVal: 0, 145 | maxVal: 100, 146 | nRows: 10, 147 | nBuckets: 20, 148 | nBalls: 10, 149 | onChange: function () { 150 | if (this.isComplete()) { 151 | $j("#SubmitDistribution3").attr("disabled", false) 152 | } else { 153 | $j("#SubmitDistribution3").attr("disabled", true) 154 | } 155 | } 156 | }); 157 | distbuilder8.render("targetdiv8"); 158 | distbuilder8.labelize({}); 159 | $j("#SubmitDistribution3").click(function () { 160 | var message = 'The function "Qualtrics.SurveyEngine.setEmbeddedData("MyDistributionResult", '; 161 | message += distbuilder8.getDistribution().join() + ')" was called. Your data would have been stored in Qualtrics!'; 162 | alert(message) 163 | }); 164 | 165 | } 166 | ); 167 | -------------------------------------------------------------------------------- /src/distributionbuilder.css: -------------------------------------------------------------------------------- 1 | /* Distbuilder */ 2 | 3 | .distbuilder { 4 | border-width: 2px; 5 | border-color: grey; 6 | border-style: solid; 7 | padding: 2px; 8 | } 9 | 10 | /* Grid */ 11 | 12 | .distbuilder > .grid { 13 | background-color: lightsteelblue; 14 | width: 100%; 15 | padding-bottom: 2px; 16 | padding-top: 5px; 17 | } 18 | 19 | .distbuilder > .grid > .distrow { 20 | display: flex; 21 | } 22 | 23 | .distbuilder > .grid > .distrow > .cell { 24 | flex: 1; 25 | display: flex; 26 | justify-content: center; 27 | } 28 | 29 | .distbuilder > .grid > .distrow > .cell > .ball { 30 | margin-bottom: 1px; 31 | height: 10px; 32 | width: 10px; 33 | border: 1px solid white; 34 | border-radius: 50%; 35 | align-self: center; 36 | } 37 | 38 | .distbuilder > .grid > .distrow > .filled > .ball { 39 | background-color: #FF4500; 40 | } 41 | 42 | /* Labels */ 43 | 44 | .distbuilder > .labels { 45 | background-color: steelblue; 46 | width: 100%; 47 | } 48 | 49 | .distbuilder > .labels > .distrow { 50 | display: flex; 51 | width: 100%; 52 | } 53 | 54 | .distbuilder > .labels > .distrow > .label { 55 | color: white; 56 | float: left; 57 | text-align: center; 58 | font-size: 0.8em; 59 | flex: 1; 60 | min-width: 0; 61 | } 62 | 63 | /* Buttons */ 64 | 65 | .distbuilder > .buttons { 66 | background-color: steelblue; 67 | width: 100%; 68 | } 69 | 70 | .distbuilder > .buttons > .distrow { 71 | display: flex; 72 | width: 100%; 73 | } 74 | 75 | .distbuilder > .buttons > .distrow > .buttongroup { 76 | text-align: center; 77 | flex: 1; 78 | padding-bottom: 5px; 79 | } 80 | 81 | .distbuilder > .buttons > .distrow > .buttongroup > .distbutton { 82 | border-radius: 10%; 83 | width: 60%; 84 | margin: 0 auto !important; 85 | padding: 0 !important; 86 | line-height: 1 !important; 87 | background-color: lightgrey; 88 | } 89 | 90 | /* Totals */ 91 | 92 | .totals { 93 | background-color: #FF4500; 94 | } 95 | 96 | .totals > .distrow { 97 | display: flex; 98 | } 99 | 100 | .distbuilder > .totals > .distrow > .total { 101 | color: white; 102 | float: left; 103 | text-align: center; 104 | font-size: 0.8em; 105 | flex: 1; 106 | } 107 | 108 | /* Totals */ 109 | 110 | .totals { 111 | background-color: #FF4500; 112 | } 113 | 114 | .totals > .distrow { 115 | display: flex; 116 | } 117 | 118 | .distbuilder > .totals > .distrow > .total { 119 | color: white; 120 | float: left; 121 | text-align: center; 122 | font-size: 0.8em; 123 | flex: 1; 124 | } -------------------------------------------------------------------------------- /src/distributionbuilder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Quentin André on 07/10/2016. 3 | */ 4 | import * as jQuery from "jquery"; 5 | import './distributionbuilder.css'; 6 | import "bootstrap-webpack!./bootstrap.config.js"; 7 | var $j = jQuery.noConflict(); 8 | import MouseHold from './../dependencies/mousehold'; 9 | MouseHold($j); 10 | class DistributionBuilder { 11 | constructor(o) { 12 | let obj = o ? o : {}; 13 | this.min = obj.hasOwnProperty('minVal') ? obj.minVal : 0; 14 | this.max = obj.hasOwnProperty('maxVal') ? obj.maxVal : 10; 15 | this.nBalls = obj.hasOwnProperty('nBalls') ? obj.nBalls : 10; 16 | this.nRows = obj.hasOwnProperty('nRows') ? obj.nRows : 10; 17 | this.nBuckets = obj.hasOwnProperty('nBuckets') ? obj.nBuckets : 10; 18 | this.onTouch = obj.hasOwnProperty('onTouch') ? obj.onTouch : () => { 19 | }; 20 | this.onChange = obj.hasOwnProperty('onChange') ? obj.onChange : () => { 21 | }; 22 | this.remainingBalls = this.nBalls; 23 | this.distribution = new Array(this.nBuckets).fill(0); 24 | this._$target = $j('
    '); 25 | } 26 | render(target, order, r) { 27 | if (r) { 28 | console.warn("The 'resize' argument has been deprecated."); 29 | } 30 | if ((this._$target)) { // Has already been rendered 31 | this._$target.html(''); 32 | this._$target.removeClass('distbuilder'); 33 | } 34 | let $target = $j('#' + target); // Target Div of Grid 35 | let parts = { 36 | 'grid': this._createGrid($target), 37 | 'labels': this._createLabels($target), 38 | 'buttons': this._createButtons($target) 39 | }; 40 | let validOrder = new RegExp('(buttons-grid-labels)|(grid-labels-buttons)|(labels-grid-buttons)|(labels-buttons-grid)|(grid-buttons-labels)|(buttons-labels-grid)', 'g'); 41 | this._$target = $target; 42 | $target.addClass('distbuilder'); 43 | let o = order ? order : "grid-labels-buttons"; 44 | if (!validOrder.test(o)) { 45 | throw ("The order '" + o + "' could not be understood. Make sure " + 46 | "that the order is any combination of 'labels', 'grid', and " + 47 | "'button, separated by '-'."); 48 | } 49 | else { 50 | let renderorder = order.split('-'); 51 | renderorder.forEach((e) => $target.append(parts[e])); 52 | } 53 | } 54 | labelize(o) { 55 | let obj = o ? o : {}; 56 | let values = []; 57 | if (obj.hasOwnProperty('labels')) { 58 | values = obj.labels; 59 | } 60 | else { 61 | let step = (this.max - this.min) / this.nBuckets; 62 | values = Array.from({ length: this.nBuckets }, (value, key) => this.min + key * step + step / 2); 63 | } 64 | let prefix = obj.hasOwnProperty('prefix') ? obj.prefix : ''; 65 | let suffix = obj.hasOwnProperty('suffix') ? obj.suffix : ''; 66 | let labels = values.map((v) => prefix + v + suffix); 67 | this._setLabels(labels); 68 | } 69 | isComplete() { 70 | return (this.remainingBalls == 0); 71 | } 72 | getRemainingBalls() { 73 | return this.remainingBalls; 74 | } 75 | getDistribution() { 76 | return this.distribution.slice(); 77 | } 78 | _setLabels(labels) { 79 | labels.forEach((l, i) => { 80 | let label = this._$target.find('.label' + i); 81 | label.html(l); 82 | }); 83 | } 84 | _actionCreator(action) { 85 | if (action == 'increment') { 86 | return (bucket) => { 87 | return () => { 88 | this.onTouch(); 89 | if ((this.distribution[bucket] < (this.nRows)) && (this.remainingBalls > 0)) { 90 | let rowIndex = this.distribution[bucket]; 91 | this._$target.find(".row" + rowIndex + ">.col" + bucket).addClass("filled"); 92 | this.distribution[bucket]++; 93 | this.remainingBalls--; 94 | this.onChange(); 95 | } 96 | }; 97 | }; 98 | } 99 | else { 100 | return (bucket) => { 101 | return () => { 102 | this.onTouch(); 103 | if (this.distribution[bucket] > 0) { 104 | this.distribution[bucket]--; 105 | let rowIndex = this.distribution[bucket]; 106 | this._$target.find(".row" + rowIndex + ">.col" + bucket).removeClass("filled"); 107 | this.remainingBalls++; 108 | this.onChange(); 109 | } 110 | }; 111 | }; 112 | } 113 | } 114 | _createGrid($target) { 115 | let nRows = this.nRows; 116 | let nBuckets = this.nBuckets; 117 | let $grid = $j('
    ', { class: "grid" }); //Div holding the grid 118 | for (let row = 0; row < nRows; row++) { // Create as many rows as needed 119 | let rowIndex = (nRows - row - 1); // Row number 0 is the bottom-most row. 120 | let $lineDiv = $j('
    ', { class: "distrow row" + rowIndex }); 121 | for (let col = 0; col < nBuckets; col++) { // Create as many cells as needed 122 | let $colDiv = $j("
    ", { "class": "cell " + "col" + col }); 123 | let $ball = $j("
    ", { "class": "ball " + "col" + col }); 124 | $colDiv.append($ball); 125 | $lineDiv.append($colDiv); // Add each cell to the row 126 | } 127 | $grid.append($lineDiv); // Add each row to the grid div 128 | } 129 | return $grid; 130 | } 131 | _createButtons($target) { 132 | let incrementAction = this._actionCreator('increment'); //Currying functions 133 | let decrementAction = this._actionCreator('decrement'); //Currying functions 134 | let $lineDivButtons = $j("
    ", { class: "distrow" }); 135 | let $buttons = $j('
    ', { class: "buttons" }); //Div holding the buttons 136 | for (let col = 0; col < this.nBuckets; col++) { 137 | let $divButtons = $j("
    ", { "class": "buttongroup" }); 138 | let $addButton = $j('', { class: "btn btn-default distbutton glyphicon glyphicon-plus" }); 139 | let $removeButton = $j('', { class: "btn btn-default distbutton glyphicon glyphicon-minus" }); 140 | $divButtons.append($addButton 141 | .mousehold(200, 100, incrementAction(col)) 142 | .click(incrementAction(col))); 143 | $divButtons.append($removeButton 144 | .mousehold(200, 100, decrementAction(col)) 145 | .click(decrementAction(col))); 146 | $lineDivButtons.append($divButtons); 147 | } 148 | $buttons.append($lineDivButtons); 149 | return $buttons; 150 | } 151 | _createLabels($target) { 152 | let $labels = $j('
    ', { class: "labels" }); //Div holding the buttons 153 | let $lineDivLabels = $j("
    ", { "class": "distrow" }); 154 | for (let col = 0; col < this.nBuckets; col++) { 155 | let $divLabel = $j("
    ", { "class": "label" + " label" + col }); 156 | $lineDivLabels.append($divLabel); 157 | } 158 | $labels.append($lineDivLabels); // Add each row to the grid div 159 | return $labels; 160 | } 161 | } 162 | 163 | export default DistributionBuilder -------------------------------------------------------------------------------- /src/distributionbuilder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Quentin André on 07/10/2016. 3 | */ 4 | 5 | import * as jQuery from "jquery"; 6 | import "jquery"; 7 | import './distributionbuilder.css'; 8 | import "bootstrap-webpack!./bootstrap.config.js"; 9 | 10 | let $j = jQuery.noConflict(); 11 | import MouseHold from './../dependencies/mousehold' 12 | 13 | MouseHold($j); 14 | 15 | interface InitConfigObject { 16 | minVal?: number 17 | maxVal?: number 18 | nBalls?: number 19 | nRows?: number 20 | nBuckets?: number 21 | onTouch?: Function 22 | onChange?: Function 23 | addTotals?: boolean 24 | toggleGridClick?: boolean 25 | } 26 | 27 | interface LabelizeConfigObject { 28 | labels?: Array 29 | prefix?: string 30 | suffix?: string 31 | } 32 | 33 | declare global { 34 | interface JQuery { 35 | mousehold(timestart: number, timeout: number, callback: Function): any; 36 | } 37 | } 38 | 39 | type ValidRenderOrder = 40 | "buttons-grid-labels" 41 | | "grid-labels-buttons" 42 | | "labels-grid-buttons" 43 | | "labels-buttons-grid" 44 | | "grid-buttons-labels" 45 | | "buttons-labels-grid"; 46 | 47 | type ValidButtonAction = "increment" | "decrement"; 48 | 49 | class DistributionBuilder { 50 | min: number; 51 | max: number; 52 | nBalls: number; 53 | nRows: number; 54 | nBuckets: number; 55 | remainingBalls: number; 56 | distribution: Array; 57 | _$target: JQuery; 58 | onTouch: Function; 59 | onChange: Function; 60 | toggleGridClick: Boolean; 61 | addTotals: Boolean; 62 | 63 | constructor(o: InitConfigObject) { 64 | let obj = o ? o : {}; 65 | this.min = obj.hasOwnProperty('minVal') ? obj.minVal : 0; 66 | this.max = obj.hasOwnProperty('maxVal') ? obj.maxVal : 10; 67 | this.nBalls = obj.hasOwnProperty('nBalls') ? obj.nBalls : 10; 68 | this.nRows = obj.hasOwnProperty('nRows') ? obj.nRows : 10; 69 | this.nBuckets = obj.hasOwnProperty('nBuckets') ? obj.nBuckets : 10; 70 | this.addTotals = obj.hasOwnProperty('addTotals') ? obj.addTotals : false; 71 | this.onTouch = obj.hasOwnProperty('onTouch') ? obj.onTouch : () => { 72 | }; 73 | this.onChange = obj.hasOwnProperty('onChange') ? obj.onChange : () => {}; 74 | this.toggleGridClick = obj.hasOwnProperty('toggleGridClick') ? obj.toggleGridClick : false; 75 | this.remainingBalls = this.nBalls; 76 | this.distribution = new Array(this.nBuckets).fill(0); 77 | this._$target = $j('
    '); 78 | } 79 | 80 | render(target: string, order: ValidRenderOrder, resize: boolean): void { 81 | if (resize) { 82 | console.warn("The 'resize' argument has been deprecated."); 83 | } 84 | if ((this._$target)) { // Has already been rendered 85 | this._$target.html(''); 86 | this._$target.removeClass('distbuilder'); 87 | } 88 | let $target = $j('#' + target); // Target Div of Grid 89 | let parts = { 90 | 'grid': this._createGrid(), 91 | 'labels': this._createLabels(), 92 | 'buttons': this._createButtons() 93 | }; 94 | let validOrder = new RegExp('(buttons-grid-labels)|(grid-labels-buttons)|(labels-grid-buttons)|(labels-buttons-grid)|(grid-buttons-labels)|(buttons-labels-grid)', 'g'); 95 | this._$target = $target; 96 | $target.addClass('distbuilder'); 97 | let o = order ? order : "grid-labels-buttons"; 98 | if (!validOrder.test(o)) { 99 | throw ("The order '" + o + "' could not be understood. Make sure " + 100 | "that the order is any combination of 'labels', 'grid', and " + 101 | "'button, separated by '-'.") 102 | } else { 103 | let renderorder = o.split('-'); 104 | renderorder.forEach((e: string) => $target.append(parts[e])) 105 | } 106 | if (this.addTotals){ 107 | $target.append(this._createTotals()) 108 | } 109 | } 110 | 111 | labelize(o: LabelizeConfigObject): void { 112 | let obj = o ? o : {}; 113 | let values = []; 114 | if (obj.hasOwnProperty('labels')) { 115 | values = obj.labels 116 | } else { 117 | let step = (this.max - this.min) / this.nBuckets; 118 | values = Array.from({length: this.nBuckets}, (value, key) => this.min + key * step + step / 2); 119 | } 120 | let prefix = obj.hasOwnProperty('prefix') ? obj.prefix : ''; 121 | let suffix = obj.hasOwnProperty('suffix') ? obj.suffix : ''; 122 | let labels = values.map((v) => prefix + v + suffix); 123 | this._setLabels(labels) 124 | } 125 | 126 | isComplete(): boolean { 127 | return (this.remainingBalls == 0); 128 | } 129 | 130 | getRemainingBalls(): number { 131 | return this.remainingBalls; 132 | } 133 | 134 | getDistribution(): Array { 135 | return this.distribution.slice(); 136 | } 137 | 138 | setDistribution(dist: Array): void { 139 | if (dist.length != this.nBuckets) { 140 | throw ("The length of the entered distribution does not match the number of buckets") 141 | } 142 | 143 | let sumVals = dist.reduce((a, b) => a + b); 144 | if (sumVals > this.nBalls) { 145 | throw ("The number of balls in the distribution exceeds the number of balls.") 146 | } 147 | 148 | let maxVal = dist.reduce((a, b) => a >= b ? a : b); 149 | if (maxVal > this.nRows) { 150 | throw ("The number of balls in one or several buckets is greater than the number of rows.") 151 | } 152 | this.distribution = dist; 153 | this.remainingBalls = this.remainingBalls - sumVals; 154 | for (let col = 0; col < this.nBuckets; col++) { 155 | this._updateGridCol(col, true); 156 | } 157 | this._updateTotals(); 158 | } 159 | 160 | _setLabels(labels: Array): void { 161 | labels.forEach((l, i) => { 162 | let label = this._$target.find('.label' + i); 163 | label.html(l); 164 | }) 165 | } 166 | 167 | _buttonActionCreator(action: ValidButtonAction): Function { 168 | if (action == 'increment') { 169 | return (col: number) => { 170 | return () => { 171 | this.onTouch(); 172 | if ((this.distribution[col] < (this.nRows)) && (this.remainingBalls > 0)) { 173 | this.distribution[col]++; 174 | this.remainingBalls--; 175 | this._updateGridCol(col); 176 | } 177 | } 178 | } 179 | } else { 180 | return (col: number) => { 181 | return () => { 182 | this.onTouch(); 183 | if (this.distribution[col] > 0) { 184 | this.distribution[col]--; 185 | this.remainingBalls++; 186 | this._updateGridCol(col); 187 | } 188 | } 189 | } 190 | } 191 | } 192 | 193 | _gridActionCreator(row: number): Function { 194 | return (col: number) => { 195 | return () => { 196 | this.onTouch(); 197 | let startRow = this.distribution[col] 198 | let targetRow = row+1 199 | let deltaRow = targetRow - startRow 200 | if (deltaRow < 0) { // We are removing balls 201 | this.remainingBalls = this.remainingBalls - deltaRow; 202 | this.distribution[col] = targetRow; 203 | this._updateGridCol(col) 204 | } else if (deltaRow > 0) { // Adding balls 205 | deltaRow = Math.min(this.remainingBalls, deltaRow) 206 | this.remainingBalls = this.remainingBalls - deltaRow; 207 | this.distribution[col] = startRow+deltaRow; 208 | this._updateGridCol(col) 209 | } else { // Toggle current ball on/off 210 | this.remainingBalls++ 211 | this.distribution[col]-- 212 | this._updateGridCol(col) 213 | } 214 | } 215 | } 216 | } 217 | 218 | _createGrid(): JQuery { 219 | let nRows = this.nRows; 220 | let nBuckets = this.nBuckets; 221 | let $grid = $j('
    ', {class: "grid"}); //Div holding the grid 222 | for (let row = 0; row < nRows; row++) { // Create as many rows as needed 223 | let rowIndex = (nRows - row - 1); // Row number 0 is the bottom-most row. 224 | let $lineDiv = $j('
    ', {class: "distrow row" + rowIndex}); 225 | for (let col = 0; col < nBuckets; col++) { // Create as many cells as needed 226 | let clickAction = this._gridActionCreator(rowIndex)(col) 227 | let $colDiv = $j("
    ", {"class": "cell " + "col" + col}); 228 | let $ball = $j("
    ", {"class": "ball " + "col" + col}); 229 | if (this.toggleGridClick) { 230 | $colDiv.click(clickAction) 231 | } 232 | $colDiv.append($ball); 233 | $lineDiv.append($colDiv); // Add each cell to the row 234 | } 235 | $grid.append($lineDiv); // Add each row to the grid div 236 | } 237 | return $grid 238 | } 239 | 240 | _createButtons(): JQuery { 241 | let incrementAction = this._buttonActionCreator('increment'); //Currying functions 242 | let decrementAction = this._buttonActionCreator('decrement'); //Currying functions 243 | let $lineDivButtons = $j("
    ", {class: "distrow"}); 244 | let $buttons = $j('
    ', {class: "buttons"}); //Div holding the buttons 245 | for (let col = 0; col < this.nBuckets; col++) { 246 | let $divButtons = $j("
    ", {"class": "buttongroup"}); 247 | let $addButton = $j('', {class: "btn btn-default distbutton glyphicon glyphicon-plus"}); 248 | let $removeButton = $j('', {class: "btn btn-default distbutton glyphicon glyphicon-minus"}); 249 | $divButtons.append($addButton 250 | .mousehold(200, 100, incrementAction(col)) 251 | .click(incrementAction(col)) 252 | ); 253 | $divButtons.append($removeButton 254 | .mousehold(200, 100, decrementAction(col)) 255 | .click(decrementAction(col)) 256 | ); 257 | $lineDivButtons.append($divButtons); 258 | } 259 | $buttons.append($lineDivButtons); 260 | return $buttons 261 | } 262 | 263 | _createLabels(): JQuery { 264 | let $labels = $j('
    ', {class: "labels"}); //Div holding the labels 265 | let $lineDivLabels = $j("
    ", {"class": "distrow"}); 266 | for (let col = 0; col < this.nBuckets; col++) { 267 | let $divLabel = $j("
    ", {"class": "label" + " label" + col}); 268 | $lineDivLabels.append($divLabel); 269 | } 270 | $labels.append($lineDivLabels); // Add the row to the div 271 | return $labels 272 | } 273 | 274 | _createTotals(): JQuery { 275 | let distrib = this.getDistribution(); 276 | let $totals = $j('
    ', {class: "totals"}); //Div holding the buttons 277 | let $lineDivTotals = $j("
    ", {"class": "distrow"}); 278 | for (let col = 0; col < this.nBuckets; col++) { 279 | let $divLabel = $j("
    ", {"class": "total" + " col" + col}); 280 | $divLabel.text(distrib[col]); 281 | $lineDivTotals.append($divLabel); 282 | } 283 | $totals.append($lineDivTotals); // Add the row to the div 284 | return $totals 285 | } 286 | 287 | _updateTotals(){ 288 | let $totals = this._$target.find(".totals > .distrow > .total"); 289 | let distrib = this.getDistribution(); 290 | $totals.each(function(i: number) { 291 | $j(this).text(distrib[i]); 292 | }) 293 | } 294 | 295 | _updateGridCol(col: number, silent=false){ 296 | let val = this.getDistribution()[col]; 297 | let maxVal = this.nRows 298 | let $ballsDiv = this._$target.find(".cell.col"+col); 299 | $ballsDiv.removeClass("filled") 300 | $ballsDiv.slice(maxVal-val, maxVal).addClass("filled") 301 | if (!silent){ 302 | this.onChange(); 303 | this._updateTotals(); 304 | } 305 | } 306 | 307 | } 308 | 309 | export default DistributionBuilder; -------------------------------------------------------------------------------- /src/entry.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./distributionbuilder.ts').default; -------------------------------------------------------------------------------- /src/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "lib": ["dom", "es6"], 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "noEmitOnError": true, 8 | "noUnusedLocals": true, 9 | "outDir": "./lib", 10 | "target": "es6", 11 | "strict": true, 12 | "strictNullChecks": false, 13 | "suppressImplicitAnyIndexErrors": true, 14 | "types": [] 15 | }, 16 | "include": ["src/*"] 17 | } 18 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const WebpackMd5Hash = require("webpack-md5-hash"); 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | const CleanWebpackPlugin = require("clean-webpack-plugin"); 5 | const webpack = require("webpack"); 6 | 7 | module.exports = { 8 | entry: { 9 | 'docs/demo/js/main': ["./src/demo.js"], 10 | 'lib/distributionbuilder': ["./src/entry.js"], 11 | 'lib/distributionbuilder.min': ["./src/entry.js"] 12 | }, 13 | output: { 14 | path: path.resolve(__dirname), 15 | filename: "[name].js" 16 | }, 17 | devServer: { 18 | contentBase: "./dist", 19 | port: 7700 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.js$/, 25 | exclude: /node_modules/, 26 | use: { 27 | loader: 'babel-loader', 28 | } 29 | }, 30 | { 31 | test: require.resolve('jquery'), 32 | use: [{ 33 | loader: 'expose-loader', 34 | options: 'jQuery' 35 | }, { 36 | loader: 'expose-loader', 37 | options: '$j' 38 | }] 39 | }, 40 | { 41 | test: require.resolve('./src/entry.js'), 42 | use: [{ 43 | loader: 'expose-loader', 44 | options: 'DistributionBuilder' 45 | }] 46 | }, 47 | { 48 | test: /\.css$/, 49 | use: [ 50 | "style-loader", 51 | MiniCssExtractPlugin.loader, 52 | "css-loader", 53 | {loader: "postcss-loader", options: {}} 54 | ], 55 | }, 56 | 57 | { 58 | test: /\.tsx?$/, 59 | loader: "ts-loader" 60 | }, 61 | { 62 | test: /\.svg$/, 63 | loader: 'url-loader?limit=65000&mimetype=image/svg+xml&name=public/fonts/[name].[ext]' 64 | }, 65 | { 66 | test: /\.woff$/, 67 | loader: 'url-loader?limit=65000&mimetype=application/font-woff&name=public/fonts/[name].[ext]' 68 | }, 69 | { 70 | test: /\.woff2$/, 71 | loader: 'url-loader?limit=65000&mimetype=application/font-woff2&name=public/fonts/[name].[ext]' 72 | }, 73 | { 74 | test: /\.[ot]tf$/, 75 | loader: 'url-loader?limit=65000&mimetype=application/octet-stream&name=public/fonts/[name].[ext]' 76 | }, 77 | { 78 | test: /\.eot$/, 79 | loader: 'url-loader?limit=65000&mimetype=application/vnd.ms-fontobject&name=public/fonts/[name].[ext]' 80 | }, 81 | /* 82 | { 83 | test: /\.(woff(2)?|ttf|eot|otf)(\?v=\d+\.\d+\.\d+)?$/, 84 | use: [ 85 | { 86 | loader: "file-loader", 87 | options: { 88 | limit: 5000000, 89 | name: "[name].[ext]", 90 | outputPath: "fonts/" 91 | } 92 | } 93 | ] 94 | }, 95 | { 96 | test: /\.(gif|png|jpe?g|svg)$/i, 97 | use: [ 98 | "file-loader", 99 | { 100 | loader: "image-webpack-loader", 101 | options: { 102 | bypassOnDebug: true 103 | } 104 | } 105 | ] 106 | }*/ 107 | ] 108 | }, 109 | plugins: [ 110 | new CleanWebpackPlugin("dist", {}), 111 | new MiniCssExtractPlugin({ 112 | filename: "/lib/distributionbuilder.css" 113 | }), 114 | new WebpackMd5Hash() 115 | ] 116 | }; 117 | --------------------------------------------------------------------------------