"
61 | ].join(''),
62 | markup = (function () {
63 |
64 | // IE does not support gradients with multiple stops, so we need to simulate
65 | // that for the rainbow slider with 8 divs that each have a single gradient
66 | var gradientFix = "";
67 | if (IE) {
68 | for (var i = 1; i <= 6; i++) {
69 | gradientFix += "";
70 | }
71 | }
72 |
73 | return [
74 | "
",
75 | "
",
76 | "",
77 | "
",
78 | "
",
79 | "
",
80 | "",
81 | "
",
82 | "
",
83 | "
",
84 | "
",
85 | "",
86 | "
",
87 | "
",
88 | "
",
89 | "
",
90 | "
",
91 | "
",
92 | "",
93 | gradientFix,
94 | "
",
95 | "
",
96 | "
",
97 | "
",
98 | "
",
99 | "",
100 | "
",
101 | "",
102 | "
",
103 | "",
104 | "",
105 | "
",
106 | "
",
107 | "
"
108 | ].join("");
109 | })();
110 |
111 | function paletteTemplate (p, color, className) {
112 | var html = [];
113 | for (var i = 0; i < p.length; i++) {
114 | var current = p[i];
115 | if(current) {
116 | var tiny = tinycolor(current);
117 | var c = tiny.toHsl().l < 0.5 ? "sp-thumb-el sp-thumb-dark" : "sp-thumb-el sp-thumb-light";
118 | c += (tinycolor.equals(color, current)) ? " sp-thumb-active" : "";
119 |
120 | var swatchStyle = rgbaSupport ? ("background-color:" + tiny.toRgbString()) : "filter:" + tiny.toFilter();
121 | html.push('');
122 | } else {
123 | var cls = 'sp-clear-display';
124 | html.push('');
125 | }
126 | }
127 | return "
" + html.join('') + "
";
128 | }
129 |
130 | function hideAll() {
131 | for (var i = 0; i < spectrums.length; i++) {
132 | if (spectrums[i]) {
133 | spectrums[i].hide();
134 | }
135 | }
136 | }
137 |
138 | function instanceOptions(o, callbackContext) {
139 | var opts = $.extend({}, defaultOpts, o);
140 | opts.callbacks = {
141 | 'move': bind(opts.move, callbackContext),
142 | 'change': bind(opts.change, callbackContext),
143 | 'show': bind(opts.show, callbackContext),
144 | 'hide': bind(opts.hide, callbackContext),
145 | 'beforeShow': bind(opts.beforeShow, callbackContext)
146 | };
147 |
148 | return opts;
149 | }
150 |
151 | function spectrum(element, o) {
152 |
153 | var opts = instanceOptions(o, element),
154 | flat = opts.flat,
155 | showSelectionPalette = opts.showSelectionPalette,
156 | localStorageKey = opts.localStorageKey,
157 | theme = opts.theme,
158 | callbacks = opts.callbacks,
159 | resize = throttle(reflow, 10),
160 | visible = false,
161 | dragWidth = 0,
162 | dragHeight = 0,
163 | dragHelperHeight = 0,
164 | slideHeight = 0,
165 | slideWidth = 0,
166 | alphaWidth = 0,
167 | alphaSlideHelperWidth = 0,
168 | slideHelperHeight = 0,
169 | currentHue = 0,
170 | currentSaturation = 0,
171 | currentValue = 0,
172 | currentAlpha = 1,
173 | palette = opts.palette.slice(0),
174 | paletteArray = $.isArray(palette[0]) ? palette : [palette],
175 | selectionPalette = opts.selectionPalette.slice(0),
176 | maxSelectionSize = opts.maxSelectionSize,
177 | draggingClass = "sp-dragging",
178 | shiftMovementDirection = null;
179 |
180 | var doc = element.ownerDocument,
181 | body = doc.body,
182 | boundElement = $(element),
183 | disabled = false,
184 | container = $(markup, doc).addClass(theme),
185 | dragger = container.find(".sp-color"),
186 | dragHelper = container.find(".sp-dragger"),
187 | slider = container.find(".sp-hue"),
188 | slideHelper = container.find(".sp-slider"),
189 | alphaSliderInner = container.find(".sp-alpha-inner"),
190 | alphaSlider = container.find(".sp-alpha"),
191 | alphaSlideHelper = container.find(".sp-alpha-handle"),
192 | textInput = container.find(".sp-input"),
193 | paletteContainer = container.find(".sp-palette"),
194 | initialColorContainer = container.find(".sp-initial"),
195 | cancelButton = container.find(".sp-cancel"),
196 | clearButton = container.find(".sp-clear"),
197 | chooseButton = container.find(".sp-choose"),
198 | isInput = boundElement.is("input"),
199 | isInputTypeColor = isInput && inputTypeColorSupport && boundElement.attr("type") === "color",
200 | shouldReplace = isInput && !flat,
201 | replacer = (shouldReplace) ? $(replaceInput).addClass(theme).addClass(opts.className) : $([]),
202 | offsetElement = (shouldReplace) ? replacer : boundElement,
203 | previewElement = replacer.find(".sp-preview-inner"),
204 | initialColor = opts.color || (isInput && boundElement.val()),
205 | colorOnShow = false,
206 | preferredFormat = opts.preferredFormat,
207 | currentPreferredFormat = preferredFormat,
208 | clickoutFiresChange = !opts.showButtons || opts.clickoutFiresChange,
209 | isEmpty = !initialColor,
210 | allowEmpty = opts.allowEmpty && !isInputTypeColor;
211 |
212 | function applyOptions() {
213 |
214 | if (opts.showPaletteOnly) {
215 | opts.showPalette = true;
216 | }
217 |
218 | container.toggleClass("sp-flat", flat);
219 | container.toggleClass("sp-input-disabled", !opts.showInput);
220 | container.toggleClass("sp-alpha-enabled", opts.showAlpha);
221 | container.toggleClass("sp-clear-enabled", allowEmpty);
222 | container.toggleClass("sp-buttons-disabled", !opts.showButtons);
223 | container.toggleClass("sp-palette-disabled", !opts.showPalette);
224 | container.toggleClass("sp-palette-only", opts.showPaletteOnly);
225 | container.toggleClass("sp-initial-disabled", !opts.showInitial);
226 | container.addClass(opts.className);
227 |
228 | reflow();
229 | }
230 |
231 | function initialize() {
232 |
233 | if (IE) {
234 | container.find("*:not(input)").attr("unselectable", "on");
235 | }
236 |
237 | applyOptions();
238 |
239 | if (shouldReplace) {
240 | boundElement.after(replacer).hide();
241 | }
242 |
243 | if (!allowEmpty) {
244 | clearButton.hide();
245 | }
246 |
247 | if (flat) {
248 | boundElement.after(container).hide();
249 | }
250 | else {
251 |
252 | var appendTo = opts.appendTo === "parent" ? boundElement.parent() : $(opts.appendTo);
253 | if (appendTo.length !== 1) {
254 | appendTo = $("body");
255 | }
256 |
257 | appendTo.append(container);
258 | }
259 |
260 | if (localStorageKey && window.localStorage) {
261 |
262 | // Migrate old palettes over to new format. May want to remove this eventually.
263 | try {
264 | var oldPalette = window.localStorage[localStorageKey].split(",#");
265 | if (oldPalette.length > 1) {
266 | delete window.localStorage[localStorageKey];
267 | $.each(oldPalette, function(i, c) {
268 | addColorToSelectionPalette(c);
269 | });
270 | }
271 | }
272 | catch(e) { }
273 |
274 | try {
275 | selectionPalette = window.localStorage[localStorageKey].split(";");
276 | }
277 | catch (e) { }
278 | }
279 |
280 | offsetElement.bind("click.spectrum touchstart.spectrum", function (e) {
281 | if (!disabled) {
282 | toggle();
283 | }
284 |
285 | e.stopPropagation();
286 |
287 | if (!$(e.target).is("input")) {
288 | e.preventDefault();
289 | }
290 | });
291 |
292 | if(boundElement.is(":disabled") || (opts.disabled === true)) {
293 | disable();
294 | }
295 |
296 | // Prevent clicks from bubbling up to document. This would cause it to be hidden.
297 | container.click(stopPropagation);
298 |
299 | // Handle user typed input
300 | textInput.change(setFromTextInput);
301 | textInput.bind("paste", function () {
302 | setTimeout(setFromTextInput, 1);
303 | });
304 | textInput.keydown(function (e) { if (e.keyCode == 13) { setFromTextInput(); } });
305 |
306 | cancelButton.text(opts.cancelText);
307 | cancelButton.bind("click.spectrum", function (e) {
308 | e.stopPropagation();
309 | e.preventDefault();
310 | hide("cancel");
311 | });
312 |
313 |
314 | clearButton.bind("click.spectrum", function (e) {
315 | e.stopPropagation();
316 | e.preventDefault();
317 |
318 | isEmpty = true;
319 |
320 | move();
321 | if(flat) {
322 | //for the flat style, this is a change event
323 | updateOriginalInput(true);
324 | }
325 | });
326 |
327 |
328 | chooseButton.text(opts.chooseText);
329 | chooseButton.bind("click.spectrum", function (e) {
330 | e.stopPropagation();
331 | e.preventDefault();
332 |
333 | if (isValid()) {
334 | updateOriginalInput(true);
335 | hide();
336 | }
337 | });
338 |
339 | draggable(alphaSlider, function (dragX, dragY, e) {
340 | currentAlpha = (dragX / alphaWidth);
341 | isEmpty = false;
342 | if (e.shiftKey) {
343 | currentAlpha = Math.round(currentAlpha * 10) / 10;
344 | }
345 |
346 | move();
347 | });
348 |
349 | draggable(slider, function (dragX, dragY) {
350 | currentHue = parseFloat(dragY / slideHeight);
351 | isEmpty = false;
352 | move();
353 | }, dragStart, dragStop);
354 |
355 | draggable(dragger, function (dragX, dragY, e) {
356 |
357 | // shift+drag should snap the movement to either the x or y axis.
358 | if (!e.shiftKey) {
359 | shiftMovementDirection = null;
360 | }
361 | else if (!shiftMovementDirection) {
362 | var oldDragX = currentSaturation * dragWidth;
363 | var oldDragY = dragHeight - (currentValue * dragHeight);
364 | var furtherFromX = Math.abs(dragX - oldDragX) > Math.abs(dragY - oldDragY);
365 |
366 | shiftMovementDirection = furtherFromX ? "x" : "y";
367 | }
368 |
369 | var setSaturation = !shiftMovementDirection || shiftMovementDirection === "x";
370 | var setValue = !shiftMovementDirection || shiftMovementDirection === "y";
371 |
372 | if (setSaturation) {
373 | currentSaturation = parseFloat(dragX / dragWidth);
374 | }
375 | if (setValue) {
376 | currentValue = parseFloat((dragHeight - dragY) / dragHeight);
377 | }
378 |
379 | isEmpty = false;
380 |
381 | move();
382 |
383 | }, dragStart, dragStop);
384 |
385 | if (!!initialColor) {
386 | set(initialColor);
387 |
388 | // In case color was black - update the preview UI and set the format
389 | // since the set function will not run (default color is black).
390 | updateUI();
391 | currentPreferredFormat = preferredFormat || tinycolor(initialColor).format;
392 |
393 | addColorToSelectionPalette(initialColor);
394 | }
395 | else {
396 | updateUI();
397 | }
398 |
399 | if (flat) {
400 | show();
401 | }
402 |
403 | function palletElementClick(e) {
404 | if (e.data && e.data.ignore) {
405 | set($(this).data("color"));
406 | move();
407 | }
408 | else {
409 | set($(this).data("color"));
410 | updateOriginalInput(true);
411 | move();
412 | hide();
413 | }
414 |
415 | return false;
416 | }
417 |
418 | var paletteEvent = IE ? "mousedown.spectrum" : "click.spectrum touchstart.spectrum";
419 | paletteContainer.delegate(".sp-thumb-el", paletteEvent, palletElementClick);
420 | initialColorContainer.delegate(".sp-thumb-el:nth-child(1)", paletteEvent, { ignore: true }, palletElementClick);
421 | }
422 |
423 | function addColorToSelectionPalette(color) {
424 | if (showSelectionPalette) {
425 | var colorRgb = tinycolor(color).toRgbString();
426 | if ($.inArray(colorRgb, selectionPalette) === -1) {
427 | selectionPalette.push(colorRgb);
428 | while(selectionPalette.length > maxSelectionSize) {
429 | selectionPalette.shift();
430 | }
431 | }
432 |
433 | if (localStorageKey && window.localStorage) {
434 | try {
435 | window.localStorage[localStorageKey] = selectionPalette.join(";");
436 | }
437 | catch(e) { }
438 | }
439 | }
440 | }
441 |
442 | function getUniqueSelectionPalette() {
443 | var unique = [];
444 | var p = selectionPalette;
445 | var paletteLookup = {};
446 | var rgb;
447 |
448 | if (opts.showPalette) {
449 |
450 | for (var i = 0; i < paletteArray.length; i++) {
451 | for (var j = 0; j < paletteArray[i].length; j++) {
452 | rgb = tinycolor(paletteArray[i][j]).toRgbString();
453 | paletteLookup[rgb] = true;
454 | }
455 | }
456 |
457 | for (i = 0; i < p.length; i++) {
458 | rgb = tinycolor(p[i]).toRgbString();
459 |
460 | if (!paletteLookup.hasOwnProperty(rgb)) {
461 | unique.push(p[i]);
462 | paletteLookup[rgb] = true;
463 | }
464 | }
465 | }
466 |
467 | return unique.reverse().slice(0, opts.maxSelectionSize);
468 | }
469 |
470 | function drawPalette() {
471 |
472 | var currentColor = get();
473 |
474 | var html = $.map(paletteArray, function (palette, i) {
475 | return paletteTemplate(palette, currentColor, "sp-palette-row sp-palette-row-" + i);
476 | });
477 |
478 | if (selectionPalette) {
479 | html.push(paletteTemplate(getUniqueSelectionPalette(), currentColor, "sp-palette-row sp-palette-row-selection"));
480 | }
481 |
482 | paletteContainer.html(html.join(""));
483 | }
484 |
485 | function drawInitial() {
486 | if (opts.showInitial) {
487 | var initial = colorOnShow;
488 | var current = get();
489 | initialColorContainer.html(paletteTemplate([initial, current], current, "sp-palette-row-initial"));
490 | }
491 | }
492 |
493 | function dragStart() {
494 | if (dragHeight <= 0 || dragWidth <= 0 || slideHeight <= 0) {
495 | reflow();
496 | }
497 | container.addClass(draggingClass);
498 | shiftMovementDirection = null;
499 | }
500 |
501 | function dragStop() {
502 | container.removeClass(draggingClass);
503 | }
504 |
505 | function setFromTextInput() {
506 |
507 | var value = textInput.val();
508 |
509 | if ((value === null || value === "") && allowEmpty) {
510 | set(null);
511 | }
512 | else {
513 | var tiny = tinycolor(value);
514 | if (tiny.ok) {
515 | set(tiny);
516 | }
517 | else {
518 | textInput.addClass("sp-validation-error");
519 | }
520 | }
521 | }
522 |
523 | function toggle() {
524 | if (visible) {
525 | hide();
526 | }
527 | else {
528 | show();
529 | }
530 | }
531 |
532 | function show() {
533 | var event = $.Event('beforeShow.spectrum');
534 |
535 | if (visible) {
536 | reflow();
537 | return;
538 | }
539 |
540 | boundElement.trigger(event, [ get() ]);
541 |
542 | if (callbacks.beforeShow(get()) === false || event.isDefaultPrevented()) {
543 | return;
544 | }
545 |
546 | hideAll();
547 | visible = true;
548 |
549 | $(doc).bind("click.spectrum", hide);
550 | $(window).bind("resize.spectrum", resize);
551 | replacer.addClass("sp-active");
552 | container.removeClass("sp-hidden");
553 |
554 | if (opts.showPalette) {
555 | drawPalette();
556 | }
557 | reflow();
558 | updateUI();
559 |
560 | colorOnShow = get();
561 |
562 | drawInitial();
563 | callbacks.show(colorOnShow);
564 | boundElement.trigger('show.spectrum', [ colorOnShow ]);
565 | }
566 |
567 | function hide(e) {
568 |
569 | // Return on right click
570 | if (e && e.type == "click" && e.button == 2) { return; }
571 |
572 | // Return if hiding is unnecessary
573 | if (!visible || flat) { return; }
574 | visible = false;
575 |
576 | $(doc).unbind("click.spectrum", hide);
577 | $(window).unbind("resize.spectrum", resize);
578 |
579 | replacer.removeClass("sp-active");
580 | container.addClass("sp-hidden");
581 |
582 | var colorHasChanged = !tinycolor.equals(get(), colorOnShow);
583 |
584 | if (colorHasChanged) {
585 | if (clickoutFiresChange && e !== "cancel") {
586 | updateOriginalInput(true);
587 | }
588 | else {
589 | revert();
590 | }
591 | }
592 |
593 | callbacks.hide(get());
594 | boundElement.trigger('hide.spectrum', [ get() ]);
595 | }
596 |
597 | function revert() {
598 | set(colorOnShow, true);
599 | }
600 |
601 | function set(color, ignoreFormatChange) {
602 | if (tinycolor.equals(color, get())) {
603 | return;
604 | }
605 |
606 | var newColor;
607 | if (!color && allowEmpty) {
608 | isEmpty = true;
609 | } else {
610 | isEmpty = false;
611 | newColor = tinycolor(color);
612 | var newHsv = newColor.toHsv();
613 |
614 | currentHue = (newHsv.h % 360) / 360;
615 | currentSaturation = newHsv.s;
616 | currentValue = newHsv.v;
617 | currentAlpha = newHsv.a;
618 | }
619 | updateUI();
620 |
621 | if (newColor && newColor.ok && !ignoreFormatChange) {
622 | currentPreferredFormat = preferredFormat || newColor.format;
623 | }
624 | }
625 |
626 | function get(opts) {
627 | opts = opts || { };
628 |
629 | if (allowEmpty && isEmpty) {
630 | return null;
631 | }
632 |
633 | return tinycolor.fromRatio({
634 | h: currentHue,
635 | s: currentSaturation,
636 | v: currentValue,
637 | a: Math.round(currentAlpha * 100) / 100
638 | }, { format: opts.format || currentPreferredFormat });
639 | }
640 |
641 | function isValid() {
642 | return !textInput.hasClass("sp-validation-error");
643 | }
644 |
645 | function move() {
646 | updateUI();
647 |
648 | callbacks.move(get());
649 | boundElement.trigger('move.spectrum', [ get() ]);
650 | }
651 |
652 | function updateUI() {
653 |
654 | textInput.removeClass("sp-validation-error");
655 |
656 | updateHelperLocations();
657 |
658 | // Update dragger background color (gradients take care of saturation and value).
659 | var flatColor = tinycolor.fromRatio({ h: currentHue, s: 1, v: 1 });
660 | dragger.css("background-color", flatColor.toHexString());
661 |
662 | // Get a format that alpha will be included in (hex and names ignore alpha)
663 | var format = currentPreferredFormat;
664 | if (currentAlpha < 1) {
665 | if (format === "hex" || format === "hex3" || format === "hex6" || format === "name") {
666 | format = "rgb";
667 | }
668 | }
669 |
670 | var realColor = get({ format: format }),
671 | displayColor = '';
672 |
673 | //reset background info for preview element
674 | previewElement.removeClass("sp-clear-display");
675 | previewElement.css('background-color', 'transparent');
676 |
677 | if (!realColor && allowEmpty) {
678 | // Update the replaced elements background with icon indicating no color selection
679 | previewElement.addClass("sp-clear-display");
680 | }
681 | else {
682 | var realHex = realColor.toHexString(),
683 | realRgb = realColor.toRgbString();
684 |
685 | // Update the replaced elements background color (with actual selected color)
686 | if (rgbaSupport || realColor.alpha === 1) {
687 | previewElement.css("background-color", realRgb);
688 | }
689 | else {
690 | previewElement.css("background-color", "transparent");
691 | previewElement.css("filter", realColor.toFilter());
692 | }
693 |
694 | if (opts.showAlpha) {
695 | var rgb = realColor.toRgb();
696 | rgb.a = 0;
697 | var realAlpha = tinycolor(rgb).toRgbString();
698 | var gradient = "linear-gradient(left, " + realAlpha + ", " + realHex + ")";
699 |
700 | if (IE) {
701 | alphaSliderInner.css("filter", tinycolor(realAlpha).toFilter({ gradientType: 1 }, realHex));
702 | }
703 | else {
704 | alphaSliderInner.css("background", "-webkit-" + gradient);
705 | alphaSliderInner.css("background", "-moz-" + gradient);
706 | alphaSliderInner.css("background", "-ms-" + gradient);
707 | alphaSliderInner.css("background", gradient);
708 | }
709 | }
710 |
711 | displayColor = realColor.toString(format);
712 | }
713 | // Update the text entry input as it changes happen
714 | if (opts.showInput) {
715 | textInput.val(displayColor);
716 | }
717 |
718 | if (opts.showPalette) {
719 | drawPalette();
720 | }
721 |
722 | drawInitial();
723 | }
724 |
725 | function updateHelperLocations() {
726 | var s = currentSaturation;
727 | var v = currentValue;
728 |
729 | if(allowEmpty && isEmpty) {
730 | //if selected color is empty, hide the helpers
731 | alphaSlideHelper.hide();
732 | slideHelper.hide();
733 | dragHelper.hide();
734 | }
735 | else {
736 | //make sure helpers are visible
737 | alphaSlideHelper.show();
738 | slideHelper.show();
739 | dragHelper.show();
740 |
741 | // Where to show the little circle in that displays your current selected color
742 | var dragX = s * dragWidth;
743 | var dragY = dragHeight - (v * dragHeight);
744 | dragX = Math.max(
745 | -dragHelperHeight,
746 | Math.min(dragWidth - dragHelperHeight, dragX - dragHelperHeight)
747 | );
748 | dragY = Math.max(
749 | -dragHelperHeight,
750 | Math.min(dragHeight - dragHelperHeight, dragY - dragHelperHeight)
751 | );
752 | dragHelper.css({
753 | "top": dragY,
754 | "left": dragX
755 | });
756 |
757 | var alphaX = currentAlpha * alphaWidth;
758 | alphaSlideHelper.css({
759 | "left": alphaX - (alphaSlideHelperWidth / 2)
760 | });
761 |
762 | // Where to show the bar that displays your current selected hue
763 | var slideY = (currentHue) * slideHeight;
764 | slideHelper.css({
765 | "top": slideY - slideHelperHeight
766 | });
767 | }
768 | }
769 |
770 | function updateOriginalInput(fireCallback) {
771 | var color = get(),
772 | displayColor = '',
773 | hasChanged = !tinycolor.equals(color, colorOnShow);
774 |
775 | if(color) {
776 | displayColor = color.toString(currentPreferredFormat);
777 | // Update the selection palette with the current color
778 | addColorToSelectionPalette(color);
779 | }
780 |
781 | if (isInput) {
782 | boundElement.val(displayColor);
783 | }
784 |
785 | colorOnShow = color;
786 |
787 | if (fireCallback && hasChanged) {
788 | callbacks.change(color);
789 | boundElement.trigger('change', [ color ]);
790 | }
791 | }
792 |
793 | function reflow() {
794 | dragWidth = dragger.width();
795 | dragHeight = dragger.height();
796 | dragHelperHeight = dragHelper.height();
797 | slideWidth = slider.width();
798 | slideHeight = slider.height();
799 | slideHelperHeight = slideHelper.height();
800 | alphaWidth = alphaSlider.width();
801 | alphaSlideHelperWidth = alphaSlideHelper.width();
802 |
803 | if (!flat) {
804 | container.css("position", "absolute");
805 | container.offset(getOffset(container, offsetElement));
806 | }
807 |
808 | updateHelperLocations();
809 | }
810 |
811 | function destroy() {
812 | boundElement.show();
813 | offsetElement.unbind("click.spectrum touchstart.spectrum");
814 | container.remove();
815 | replacer.remove();
816 | spectrums[spect.id] = null;
817 | }
818 |
819 | function option(optionName, optionValue) {
820 | if (optionName === undefined) {
821 | return $.extend({}, opts);
822 | }
823 | if (optionValue === undefined) {
824 | return opts[optionName];
825 | }
826 |
827 | opts[optionName] = optionValue;
828 | applyOptions();
829 | }
830 |
831 | function enable() {
832 | disabled = false;
833 | boundElement.attr("disabled", false);
834 | offsetElement.removeClass("sp-disabled");
835 | }
836 |
837 | function disable() {
838 | hide();
839 | disabled = true;
840 | boundElement.attr("disabled", true);
841 | offsetElement.addClass("sp-disabled");
842 | }
843 |
844 | initialize();
845 |
846 | var spect = {
847 | show: show,
848 | hide: hide,
849 | toggle: toggle,
850 | reflow: reflow,
851 | option: option,
852 | enable: enable,
853 | disable: disable,
854 | set: function (c) {
855 | set(c);
856 | updateOriginalInput();
857 | },
858 | get: get,
859 | destroy: destroy,
860 | container: container
861 | };
862 |
863 | spect.id = spectrums.push(spect) - 1;
864 |
865 | return spect;
866 | }
867 |
868 | /**
869 | * checkOffset - get the offset below/above and left/right element depending on screen position
870 | * Thanks https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.datepicker.js
871 | */
872 | function getOffset(picker, input) {
873 | var extraY = 0;
874 | var dpWidth = picker.outerWidth();
875 | var dpHeight = picker.outerHeight();
876 | var inputHeight = input.outerHeight();
877 | var doc = picker[0].ownerDocument;
878 | var docElem = doc.documentElement;
879 | var viewWidth = docElem.clientWidth + $(doc).scrollLeft();
880 | var viewHeight = docElem.clientHeight + $(doc).scrollTop();
881 | var offset = input.offset();
882 | offset.top += inputHeight;
883 |
884 | offset.left -=
885 | Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ?
886 | Math.abs(offset.left + dpWidth - viewWidth) : 0);
887 |
888 | offset.top -=
889 | Math.min(offset.top, ((offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ?
890 | Math.abs(dpHeight + inputHeight - extraY) : extraY));
891 |
892 | return offset;
893 | }
894 |
895 | /**
896 | * noop - do nothing
897 | */
898 | function noop() {
899 |
900 | }
901 |
902 | /**
903 | * stopPropagation - makes the code only doing this a little easier to read in line
904 | */
905 | function stopPropagation(e) {
906 | e.stopPropagation();
907 | }
908 |
909 | /**
910 | * Create a function bound to a given object
911 | * Thanks to underscore.js
912 | */
913 | function bind(func, obj) {
914 | var slice = Array.prototype.slice;
915 | var args = slice.call(arguments, 2);
916 | return function () {
917 | return func.apply(obj, args.concat(slice.call(arguments)));
918 | };
919 | }
920 |
921 | /**
922 | * Lightweight drag helper. Handles containment within the element, so that
923 | * when dragging, the x is within [0,element.width] and y is within [0,element.height]
924 | */
925 | function draggable(element, onmove, onstart, onstop) {
926 | onmove = onmove || function () { };
927 | onstart = onstart || function () { };
928 | onstop = onstop || function () { };
929 | var doc = element.ownerDocument || document;
930 | var dragging = false;
931 | var offset = {};
932 | var maxHeight = 0;
933 | var maxWidth = 0;
934 | var hasTouch = ('ontouchstart' in window);
935 |
936 | var duringDragEvents = {};
937 | duringDragEvents["selectstart"] = prevent;
938 | duringDragEvents["dragstart"] = prevent;
939 | duringDragEvents["touchmove mousemove"] = move;
940 | duringDragEvents["touchend mouseup"] = stop;
941 |
942 | function prevent(e) {
943 | if (e.stopPropagation) {
944 | e.stopPropagation();
945 | }
946 | if (e.preventDefault) {
947 | e.preventDefault();
948 | }
949 | e.returnValue = false;
950 | }
951 |
952 | function move(e) {
953 | if (dragging) {
954 | // Mouseup happened outside of window
955 | if (IE && document.documentMode < 9 && !e.button) {
956 | return stop();
957 | }
958 |
959 | var touches = e.originalEvent.touches;
960 | var pageX = touches ? touches[0].pageX : e.pageX;
961 | var pageY = touches ? touches[0].pageY : e.pageY;
962 |
963 | var dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth));
964 | var dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight));
965 |
966 | if (hasTouch) {
967 | // Stop scrolling in iOS
968 | prevent(e);
969 | }
970 |
971 | onmove.apply(element, [dragX, dragY, e]);
972 | }
973 | }
974 | function start(e) {
975 | var rightclick = (e.which) ? (e.which == 3) : (e.button == 2);
976 | var touches = e.originalEvent.touches;
977 |
978 | if (!rightclick && !dragging) {
979 | if (onstart.apply(element, arguments) !== false) {
980 | dragging = true;
981 | maxHeight = $(element).height();
982 | maxWidth = $(element).width();
983 | offset = $(element).offset();
984 |
985 | $(doc).bind(duringDragEvents);
986 | $(doc.body).addClass("sp-dragging");
987 |
988 | if (!hasTouch) {
989 | move(e);
990 | }
991 |
992 | prevent(e);
993 | }
994 | }
995 | }
996 | function stop() {
997 | if (dragging) {
998 | $(doc).unbind(duringDragEvents);
999 | $(doc.body).removeClass("sp-dragging");
1000 | onstop.apply(element, arguments);
1001 | }
1002 | dragging = false;
1003 | }
1004 |
1005 | $(element).bind("touchstart mousedown", start);
1006 | }
1007 |
1008 | function throttle(func, wait, debounce) {
1009 | var timeout;
1010 | return function () {
1011 | var context = this, args = arguments;
1012 | var throttler = function () {
1013 | timeout = null;
1014 | func.apply(context, args);
1015 | };
1016 | if (debounce) clearTimeout(timeout);
1017 | if (debounce || !timeout) timeout = setTimeout(throttler, wait);
1018 | };
1019 | }
1020 |
1021 |
1022 | function log(){/* jshint -W021 */if(window.console){if(Function.prototype.bind)log=Function.prototype.bind.call(console.log,console);else log=function(){Function.prototype.apply.call(console.log,console,arguments);};log.apply(this,arguments);}}
1023 |
1024 | /**
1025 | * Define a jQuery plugin
1026 | */
1027 | var dataID = "spectrum.id";
1028 | $.fn.spectrum = function (opts, extra) {
1029 |
1030 | if (typeof opts == "string") {
1031 |
1032 | var returnValue = this;
1033 | var args = Array.prototype.slice.call( arguments, 1 );
1034 |
1035 | this.each(function () {
1036 | var spect = spectrums[$(this).data(dataID)];
1037 | if (spect) {
1038 |
1039 | var method = spect[opts];
1040 | if (!method) {
1041 | throw new Error( "Spectrum: no such method: '" + opts + "'" );
1042 | }
1043 |
1044 | if (opts == "get") {
1045 | returnValue = spect.get();
1046 | }
1047 | else if (opts == "container") {
1048 | returnValue = spect.container;
1049 | }
1050 | else if (opts == "option") {
1051 | returnValue = spect.option.apply(spect, args);
1052 | }
1053 | else if (opts == "destroy") {
1054 | spect.destroy();
1055 | $(this).removeData(dataID);
1056 | }
1057 | else {
1058 | method.apply(spect, args);
1059 | }
1060 | }
1061 | });
1062 |
1063 | return returnValue;
1064 | }
1065 |
1066 | // Initializing a new instance of spectrum
1067 | return this.spectrum("destroy").each(function () {
1068 | var spect = spectrum(this, opts);
1069 | $(this).data(dataID, spect.id);
1070 | });
1071 | };
1072 |
1073 | $.fn.spectrum.load = true;
1074 | $.fn.spectrum.loadOpts = {};
1075 | $.fn.spectrum.draggable = draggable;
1076 | $.fn.spectrum.defaults = defaultOpts;
1077 |
1078 | $.spectrum = { };
1079 | $.spectrum.localization = { };
1080 | $.spectrum.palettes = { };
1081 |
1082 | $.fn.spectrum.processNativeColorInputs = function () {
1083 | if (!inputTypeColorSupport) {
1084 | $("input[type=color]").spectrum({
1085 | preferredFormat: "hex6"
1086 | });
1087 | }
1088 | };
1089 |
1090 | // TinyColor v0.9.16
1091 | // https://github.com/bgrins/TinyColor
1092 | // 2013-08-10, Brian Grinstead, MIT License
1093 |
1094 | (function() {
1095 |
1096 | var trimLeft = /^[\s,#]+/,
1097 | trimRight = /\s+$/,
1098 | tinyCounter = 0,
1099 | math = Math,
1100 | mathRound = math.round,
1101 | mathMin = math.min,
1102 | mathMax = math.max,
1103 | mathRandom = math.random;
1104 |
1105 | function tinycolor (color, opts) {
1106 |
1107 | color = (color) ? color : '';
1108 | opts = opts || { };
1109 |
1110 | // If input is already a tinycolor, return itself
1111 | if (typeof color == "object" && color.hasOwnProperty("_tc_id")) {
1112 | return color;
1113 | }
1114 |
1115 | var rgb = inputToRGB(color);
1116 | var r = rgb.r,
1117 | g = rgb.g,
1118 | b = rgb.b,
1119 | a = rgb.a,
1120 | roundA = mathRound(100*a) / 100,
1121 | format = opts.format || rgb.format;
1122 |
1123 | // Don't let the range of [0,255] come back in [0,1].
1124 | // Potentially lose a little bit of precision here, but will fix issues where
1125 | // .5 gets interpreted as half of the total, instead of half of 1
1126 | // If it was supposed to be 128, this was already taken care of by `inputToRgb`
1127 | if (r < 1) { r = mathRound(r); }
1128 | if (g < 1) { g = mathRound(g); }
1129 | if (b < 1) { b = mathRound(b); }
1130 |
1131 | return {
1132 | ok: rgb.ok,
1133 | format: format,
1134 | _tc_id: tinyCounter++,
1135 | alpha: a,
1136 | getAlpha: function() {
1137 | return a;
1138 | },
1139 | setAlpha: function(value) {
1140 | a = boundAlpha(value);
1141 | roundA = mathRound(100*a) / 100;
1142 | },
1143 | toHsv: function() {
1144 | var hsv = rgbToHsv(r, g, b);
1145 | return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: a };
1146 | },
1147 | toHsvString: function() {
1148 | var hsv = rgbToHsv(r, g, b);
1149 | var h = mathRound(hsv.h * 360), s = mathRound(hsv.s * 100), v = mathRound(hsv.v * 100);
1150 | return (a == 1) ?
1151 | "hsv(" + h + ", " + s + "%, " + v + "%)" :
1152 | "hsva(" + h + ", " + s + "%, " + v + "%, "+ roundA + ")";
1153 | },
1154 | toHsl: function() {
1155 | var hsl = rgbToHsl(r, g, b);
1156 | return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: a };
1157 | },
1158 | toHslString: function() {
1159 | var hsl = rgbToHsl(r, g, b);
1160 | var h = mathRound(hsl.h * 360), s = mathRound(hsl.s * 100), l = mathRound(hsl.l * 100);
1161 | return (a == 1) ?
1162 | "hsl(" + h + ", " + s + "%, " + l + "%)" :
1163 | "hsla(" + h + ", " + s + "%, " + l + "%, "+ roundA + ")";
1164 | },
1165 | toHex: function(allow3Char) {
1166 | return rgbToHex(r, g, b, allow3Char);
1167 | },
1168 | toHexString: function(allow3Char) {
1169 | return '#' + rgbToHex(r, g, b, allow3Char);
1170 | },
1171 | toRgb: function() {
1172 | return { r: mathRound(r), g: mathRound(g), b: mathRound(b), a: a };
1173 | },
1174 | toRgbString: function() {
1175 | return (a == 1) ?
1176 | "rgb(" + mathRound(r) + ", " + mathRound(g) + ", " + mathRound(b) + ")" :
1177 | "rgba(" + mathRound(r) + ", " + mathRound(g) + ", " + mathRound(b) + ", " + roundA + ")";
1178 | },
1179 | toPercentageRgb: function() {
1180 | return { r: mathRound(bound01(r, 255) * 100) + "%", g: mathRound(bound01(g, 255) * 100) + "%", b: mathRound(bound01(b, 255) * 100) + "%", a: a };
1181 | },
1182 | toPercentageRgbString: function() {
1183 | return (a == 1) ?
1184 | "rgb(" + mathRound(bound01(r, 255) * 100) + "%, " + mathRound(bound01(g, 255) * 100) + "%, " + mathRound(bound01(b, 255) * 100) + "%)" :
1185 | "rgba(" + mathRound(bound01(r, 255) * 100) + "%, " + mathRound(bound01(g, 255) * 100) + "%, " + mathRound(bound01(b, 255) * 100) + "%, " + roundA + ")";
1186 | },
1187 | toName: function() {
1188 | if (a === 0) {
1189 | return "transparent";
1190 | }
1191 |
1192 | return hexNames[rgbToHex(r, g, b, true)] || false;
1193 | },
1194 | toFilter: function(secondColor) {
1195 | var hex = rgbToHex(r, g, b);
1196 | var secondHex = hex;
1197 | var alphaHex = Math.round(parseFloat(a) * 255).toString(16);
1198 | var secondAlphaHex = alphaHex;
1199 | var gradientType = opts && opts.gradientType ? "GradientType = 1, " : "";
1200 |
1201 | if (secondColor) {
1202 | var s = tinycolor(secondColor);
1203 | secondHex = s.toHex();
1204 | secondAlphaHex = Math.round(parseFloat(s.alpha) * 255).toString(16);
1205 | }
1206 |
1207 | return "progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr=#" + pad2(alphaHex) + hex + ",endColorstr=#" + pad2(secondAlphaHex) + secondHex + ")";
1208 | },
1209 | toString: function(format) {
1210 | var formatSet = !!format;
1211 | format = format || this.format;
1212 |
1213 | var formattedString = false;
1214 | var hasAlphaAndFormatNotSet = !formatSet && a < 1 && a > 0;
1215 | var formatWithAlpha = hasAlphaAndFormatNotSet && (format === "hex" || format === "hex6" || format === "hex3" || format === "name");
1216 |
1217 | if (format === "rgb") {
1218 | formattedString = this.toRgbString();
1219 | }
1220 | if (format === "prgb") {
1221 | formattedString = this.toPercentageRgbString();
1222 | }
1223 | if (format === "hex" || format === "hex6") {
1224 | formattedString = this.toHexString();
1225 | }
1226 | if (format === "hex3") {
1227 | formattedString = this.toHexString(true);
1228 | }
1229 | if (format === "name") {
1230 | formattedString = this.toName();
1231 | }
1232 | if (format === "hsl") {
1233 | formattedString = this.toHslString();
1234 | }
1235 | if (format === "hsv") {
1236 | formattedString = this.toHsvString();
1237 | }
1238 |
1239 | if (formatWithAlpha) {
1240 | return this.toRgbString();
1241 | }
1242 |
1243 | return formattedString || this.toHexString();
1244 | }
1245 | };
1246 | }
1247 |
1248 | // If input is an object, force 1 into "1.0" to handle ratios properly
1249 | // String input requires "1.0" as input, so 1 will be treated as 1
1250 | tinycolor.fromRatio = function(color, opts) {
1251 | if (typeof color == "object") {
1252 | var newColor = {};
1253 | for (var i in color) {
1254 | if (color.hasOwnProperty(i)) {
1255 | if (i === "a") {
1256 | newColor[i] = color[i];
1257 | }
1258 | else {
1259 | newColor[i] = convertToPercentage(color[i]);
1260 | }
1261 | }
1262 | }
1263 | color = newColor;
1264 | }
1265 |
1266 | return tinycolor(color, opts);
1267 | };
1268 |
1269 | // Given a string or object, convert that input to RGB
1270 | // Possible string inputs:
1271 | //
1272 | // "red"
1273 | // "#f00" or "f00"
1274 | // "#ff0000" or "ff0000"
1275 | // "rgb 255 0 0" or "rgb (255, 0, 0)"
1276 | // "rgb 1.0 0 0" or "rgb (1, 0, 0)"
1277 | // "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
1278 | // "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
1279 | // "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
1280 | // "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
1281 | // "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
1282 | //
1283 | function inputToRGB(color) {
1284 |
1285 | var rgb = { r: 0, g: 0, b: 0 };
1286 | var a = 1;
1287 | var ok = false;
1288 | var format = false;
1289 |
1290 | if (typeof color == "string") {
1291 | color = stringInputToObject(color);
1292 | }
1293 |
1294 | if (typeof color == "object") {
1295 | if (color.hasOwnProperty("r") && color.hasOwnProperty("g") && color.hasOwnProperty("b")) {
1296 | rgb = rgbToRgb(color.r, color.g, color.b);
1297 | ok = true;
1298 | format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb";
1299 | }
1300 | else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("v")) {
1301 | color.s = convertToPercentage(color.s);
1302 | color.v = convertToPercentage(color.v);
1303 | rgb = hsvToRgb(color.h, color.s, color.v);
1304 | ok = true;
1305 | format = "hsv";
1306 | }
1307 | else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("l")) {
1308 | color.s = convertToPercentage(color.s);
1309 | color.l = convertToPercentage(color.l);
1310 | rgb = hslToRgb(color.h, color.s, color.l);
1311 | ok = true;
1312 | format = "hsl";
1313 | }
1314 |
1315 | if (color.hasOwnProperty("a")) {
1316 | a = color.a;
1317 | }
1318 | }
1319 |
1320 | a = boundAlpha(a);
1321 |
1322 | return {
1323 | ok: ok,
1324 | format: color.format || format,
1325 | r: mathMin(255, mathMax(rgb.r, 0)),
1326 | g: mathMin(255, mathMax(rgb.g, 0)),
1327 | b: mathMin(255, mathMax(rgb.b, 0)),
1328 | a: a
1329 | };
1330 | }
1331 |
1332 |
1333 | // Conversion Functions
1334 | // --------------------
1335 |
1336 | // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from:
1337 | //
1338 |
1339 | // `rgbToRgb`
1340 | // Handle bounds / percentage checking to conform to CSS color spec
1341 | //
1342 | // *Assumes:* r, g, b in [0, 255] or [0, 1]
1343 | // *Returns:* { r, g, b } in [0, 255]
1344 | function rgbToRgb(r, g, b){
1345 | return {
1346 | r: bound01(r, 255) * 255,
1347 | g: bound01(g, 255) * 255,
1348 | b: bound01(b, 255) * 255
1349 | };
1350 | }
1351 |
1352 | // `rgbToHsl`
1353 | // Converts an RGB color value to HSL.
1354 | // *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
1355 | // *Returns:* { h, s, l } in [0,1]
1356 | function rgbToHsl(r, g, b) {
1357 |
1358 | r = bound01(r, 255);
1359 | g = bound01(g, 255);
1360 | b = bound01(b, 255);
1361 |
1362 | var max = mathMax(r, g, b), min = mathMin(r, g, b);
1363 | var h, s, l = (max + min) / 2;
1364 |
1365 | if(max == min) {
1366 | h = s = 0; // achromatic
1367 | }
1368 | else {
1369 | var d = max - min;
1370 | s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
1371 | switch(max) {
1372 | case r: h = (g - b) / d + (g < b ? 6 : 0); break;
1373 | case g: h = (b - r) / d + 2; break;
1374 | case b: h = (r - g) / d + 4; break;
1375 | }
1376 |
1377 | h /= 6;
1378 | }
1379 |
1380 | return { h: h, s: s, l: l };
1381 | }
1382 |
1383 | // `hslToRgb`
1384 | // Converts an HSL color value to RGB.
1385 | // *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
1386 | // *Returns:* { r, g, b } in the set [0, 255]
1387 | function hslToRgb(h, s, l) {
1388 | var r, g, b;
1389 |
1390 | h = bound01(h, 360);
1391 | s = bound01(s, 100);
1392 | l = bound01(l, 100);
1393 |
1394 | function hue2rgb(p, q, t) {
1395 | if(t < 0) t += 1;
1396 | if(t > 1) t -= 1;
1397 | if(t < 1/6) return p + (q - p) * 6 * t;
1398 | if(t < 1/2) return q;
1399 | if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
1400 | return p;
1401 | }
1402 |
1403 | if(s === 0) {
1404 | r = g = b = l; // achromatic
1405 | }
1406 | else {
1407 | var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
1408 | var p = 2 * l - q;
1409 | r = hue2rgb(p, q, h + 1/3);
1410 | g = hue2rgb(p, q, h);
1411 | b = hue2rgb(p, q, h - 1/3);
1412 | }
1413 |
1414 | return { r: r * 255, g: g * 255, b: b * 255 };
1415 | }
1416 |
1417 | // `rgbToHsv`
1418 | // Converts an RGB color value to HSV
1419 | // *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
1420 | // *Returns:* { h, s, v } in [0,1]
1421 | function rgbToHsv(r, g, b) {
1422 |
1423 | r = bound01(r, 255);
1424 | g = bound01(g, 255);
1425 | b = bound01(b, 255);
1426 |
1427 | var max = mathMax(r, g, b), min = mathMin(r, g, b);
1428 | var h, s, v = max;
1429 |
1430 | var d = max - min;
1431 | s = max === 0 ? 0 : d / max;
1432 |
1433 | if(max == min) {
1434 | h = 0; // achromatic
1435 | }
1436 | else {
1437 | switch(max) {
1438 | case r: h = (g - b) / d + (g < b ? 6 : 0); break;
1439 | case g: h = (b - r) / d + 2; break;
1440 | case b: h = (r - g) / d + 4; break;
1441 | }
1442 | h /= 6;
1443 | }
1444 | return { h: h, s: s, v: v };
1445 | }
1446 |
1447 | // `hsvToRgb`
1448 | // Converts an HSV color value to RGB.
1449 | // *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
1450 | // *Returns:* { r, g, b } in the set [0, 255]
1451 | function hsvToRgb(h, s, v) {
1452 |
1453 | h = bound01(h, 360) * 6;
1454 | s = bound01(s, 100);
1455 | v = bound01(v, 100);
1456 |
1457 | var i = math.floor(h),
1458 | f = h - i,
1459 | p = v * (1 - s),
1460 | q = v * (1 - f * s),
1461 | t = v * (1 - (1 - f) * s),
1462 | mod = i % 6,
1463 | r = [v, q, p, p, t, v][mod],
1464 | g = [t, v, v, q, p, p][mod],
1465 | b = [p, p, t, v, v, q][mod];
1466 |
1467 | return { r: r * 255, g: g * 255, b: b * 255 };
1468 | }
1469 |
1470 | // `rgbToHex`
1471 | // Converts an RGB color to hex
1472 | // Assumes r, g, and b are contained in the set [0, 255]
1473 | // Returns a 3 or 6 character hex
1474 | function rgbToHex(r, g, b, allow3Char) {
1475 |
1476 | var hex = [
1477 | pad2(mathRound(r).toString(16)),
1478 | pad2(mathRound(g).toString(16)),
1479 | pad2(mathRound(b).toString(16))
1480 | ];
1481 |
1482 | // Return a 3 character hex if possible
1483 | if (allow3Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1)) {
1484 | return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
1485 | }
1486 |
1487 | return hex.join("");
1488 | }
1489 |
1490 | // `equals`
1491 | // Can be called with any tinycolor input
1492 | tinycolor.equals = function (color1, color2) {
1493 | if (!color1 || !color2) { return false; }
1494 | return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString();
1495 | };
1496 | tinycolor.random = function() {
1497 | return tinycolor.fromRatio({
1498 | r: mathRandom(),
1499 | g: mathRandom(),
1500 | b: mathRandom()
1501 | });
1502 | };
1503 |
1504 |
1505 | // Modification Functions
1506 | // ----------------------
1507 | // Thanks to less.js for some of the basics here
1508 | //
1509 |
1510 | tinycolor.desaturate = function (color, amount) {
1511 | amount = (amount === 0) ? 0 : (amount || 10);
1512 | var hsl = tinycolor(color).toHsl();
1513 | hsl.s -= amount / 100;
1514 | hsl.s = clamp01(hsl.s);
1515 | return tinycolor(hsl);
1516 | };
1517 | tinycolor.saturate = function (color, amount) {
1518 | amount = (amount === 0) ? 0 : (amount || 10);
1519 | var hsl = tinycolor(color).toHsl();
1520 | hsl.s += amount / 100;
1521 | hsl.s = clamp01(hsl.s);
1522 | return tinycolor(hsl);
1523 | };
1524 | tinycolor.greyscale = function(color) {
1525 | return tinycolor.desaturate(color, 100);
1526 | };
1527 | tinycolor.lighten = function(color, amount) {
1528 | amount = (amount === 0) ? 0 : (amount || 10);
1529 | var hsl = tinycolor(color).toHsl();
1530 | hsl.l += amount / 100;
1531 | hsl.l = clamp01(hsl.l);
1532 | return tinycolor(hsl);
1533 | };
1534 | tinycolor.darken = function (color, amount) {
1535 | amount = (amount === 0) ? 0 : (amount || 10);
1536 | var hsl = tinycolor(color).toHsl();
1537 | hsl.l -= amount / 100;
1538 | hsl.l = clamp01(hsl.l);
1539 | return tinycolor(hsl);
1540 | };
1541 | tinycolor.complement = function(color) {
1542 | var hsl = tinycolor(color).toHsl();
1543 | hsl.h = (hsl.h + 180) % 360;
1544 | return tinycolor(hsl);
1545 | };
1546 |
1547 |
1548 | // Combination Functions
1549 | // ---------------------
1550 | // Thanks to jQuery xColor for some of the ideas behind these
1551 | //
1552 |
1553 | tinycolor.triad = function(color) {
1554 | var hsl = tinycolor(color).toHsl();
1555 | var h = hsl.h;
1556 | return [
1557 | tinycolor(color),
1558 | tinycolor({ h: (h + 120) % 360, s: hsl.s, l: hsl.l }),
1559 | tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l })
1560 | ];
1561 | };
1562 | tinycolor.tetrad = function(color) {
1563 | var hsl = tinycolor(color).toHsl();
1564 | var h = hsl.h;
1565 | return [
1566 | tinycolor(color),
1567 | tinycolor({ h: (h + 90) % 360, s: hsl.s, l: hsl.l }),
1568 | tinycolor({ h: (h + 180) % 360, s: hsl.s, l: hsl.l }),
1569 | tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l })
1570 | ];
1571 | };
1572 | tinycolor.splitcomplement = function(color) {
1573 | var hsl = tinycolor(color).toHsl();
1574 | var h = hsl.h;
1575 | return [
1576 | tinycolor(color),
1577 | tinycolor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l}),
1578 | tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l})
1579 | ];
1580 | };
1581 | tinycolor.analogous = function(color, results, slices) {
1582 | results = results || 6;
1583 | slices = slices || 30;
1584 |
1585 | var hsl = tinycolor(color).toHsl();
1586 | var part = 360 / slices;
1587 | var ret = [tinycolor(color)];
1588 |
1589 | for (hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360; --results; ) {
1590 | hsl.h = (hsl.h + part) % 360;
1591 | ret.push(tinycolor(hsl));
1592 | }
1593 | return ret;
1594 | };
1595 | tinycolor.monochromatic = function(color, results) {
1596 | results = results || 6;
1597 | var hsv = tinycolor(color).toHsv();
1598 | var h = hsv.h, s = hsv.s, v = hsv.v;
1599 | var ret = [];
1600 | var modification = 1 / results;
1601 |
1602 | while (results--) {
1603 | ret.push(tinycolor({ h: h, s: s, v: v}));
1604 | v = (v + modification) % 1;
1605 | }
1606 |
1607 | return ret;
1608 | };
1609 |
1610 |
1611 | // Readability Functions
1612 | // ---------------------
1613 | //
1614 |
1615 | // `readability`
1616 | // Analyze the 2 colors and returns an object with the following properties:
1617 | // `brightness`: difference in brightness between the two colors
1618 | // `color`: difference in color/hue between the two colors
1619 | tinycolor.readability = function(color1, color2) {
1620 | var a = tinycolor(color1).toRgb();
1621 | var b = tinycolor(color2).toRgb();
1622 | var brightnessA = (a.r * 299 + a.g * 587 + a.b * 114) / 1000;
1623 | var brightnessB = (b.r * 299 + b.g * 587 + b.b * 114) / 1000;
1624 | var colorDiff = (
1625 | Math.max(a.r, b.r) - Math.min(a.r, b.r) +
1626 | Math.max(a.g, b.g) - Math.min(a.g, b.g) +
1627 | Math.max(a.b, b.b) - Math.min(a.b, b.b)
1628 | );
1629 |
1630 | return {
1631 | brightness: Math.abs(brightnessA - brightnessB),
1632 | color: colorDiff
1633 | };
1634 | };
1635 |
1636 | // `readable`
1637 | // http://www.w3.org/TR/AERT#color-contrast
1638 | // Ensure that foreground and background color combinations provide sufficient contrast.
1639 | // *Example*
1640 | // tinycolor.readable("#000", "#111") => false
1641 | tinycolor.readable = function(color1, color2) {
1642 | var readability = tinycolor.readability(color1, color2);
1643 | return readability.brightness > 125 && readability.color > 500;
1644 | };
1645 |
1646 | // `mostReadable`
1647 | // Given a base color and a list of possible foreground or background
1648 | // colors for that base, returns the most readable color.
1649 | // *Example*
1650 | // tinycolor.mostReadable("#123", ["#fff", "#000"]) => "#000"
1651 | tinycolor.mostReadable = function(baseColor, colorList) {
1652 | var bestColor = null;
1653 | var bestScore = 0;
1654 | var bestIsReadable = false;
1655 | for (var i=0; i < colorList.length; i++) {
1656 |
1657 | // We normalize both around the "acceptable" breaking point,
1658 | // but rank brightness constrast higher than hue.
1659 |
1660 | var readability = tinycolor.readability(baseColor, colorList[i]);
1661 | var readable = readability.brightness > 125 && readability.color > 500;
1662 | var score = 3 * (readability.brightness / 125) + (readability.color / 500);
1663 |
1664 | if ((readable && ! bestIsReadable) ||
1665 | (readable && bestIsReadable && score > bestScore) ||
1666 | ((! readable) && (! bestIsReadable) && score > bestScore)) {
1667 | bestIsReadable = readable;
1668 | bestScore = score;
1669 | bestColor = tinycolor(colorList[i]);
1670 | }
1671 | }
1672 | return bestColor;
1673 | };
1674 |
1675 |
1676 | // Big List of Colors
1677 | // ------------------
1678 | //
1679 | var names = tinycolor.names = {
1680 | aliceblue: "f0f8ff",
1681 | antiquewhite: "faebd7",
1682 | aqua: "0ff",
1683 | aquamarine: "7fffd4",
1684 | azure: "f0ffff",
1685 | beige: "f5f5dc",
1686 | bisque: "ffe4c4",
1687 | black: "000",
1688 | blanchedalmond: "ffebcd",
1689 | blue: "00f",
1690 | blueviolet: "8a2be2",
1691 | brown: "a52a2a",
1692 | burlywood: "deb887",
1693 | burntsienna: "ea7e5d",
1694 | cadetblue: "5f9ea0",
1695 | chartreuse: "7fff00",
1696 | chocolate: "d2691e",
1697 | coral: "ff7f50",
1698 | cornflowerblue: "6495ed",
1699 | cornsilk: "fff8dc",
1700 | crimson: "dc143c",
1701 | cyan: "0ff",
1702 | darkblue: "00008b",
1703 | darkcyan: "008b8b",
1704 | darkgoldenrod: "b8860b",
1705 | darkgray: "a9a9a9",
1706 | darkgreen: "006400",
1707 | darkgrey: "a9a9a9",
1708 | darkkhaki: "bdb76b",
1709 | darkmagenta: "8b008b",
1710 | darkolivegreen: "556b2f",
1711 | darkorange: "ff8c00",
1712 | darkorchid: "9932cc",
1713 | darkred: "8b0000",
1714 | darksalmon: "e9967a",
1715 | darkseagreen: "8fbc8f",
1716 | darkslateblue: "483d8b",
1717 | darkslategray: "2f4f4f",
1718 | darkslategrey: "2f4f4f",
1719 | darkturquoise: "00ced1",
1720 | darkviolet: "9400d3",
1721 | deeppink: "ff1493",
1722 | deepskyblue: "00bfff",
1723 | dimgray: "696969",
1724 | dimgrey: "696969",
1725 | dodgerblue: "1e90ff",
1726 | firebrick: "b22222",
1727 | floralwhite: "fffaf0",
1728 | forestgreen: "228b22",
1729 | fuchsia: "f0f",
1730 | gainsboro: "dcdcdc",
1731 | ghostwhite: "f8f8ff",
1732 | gold: "ffd700",
1733 | goldenrod: "daa520",
1734 | gray: "808080",
1735 | green: "008000",
1736 | greenyellow: "adff2f",
1737 | grey: "808080",
1738 | honeydew: "f0fff0",
1739 | hotpink: "ff69b4",
1740 | indianred: "cd5c5c",
1741 | indigo: "4b0082",
1742 | ivory: "fffff0",
1743 | khaki: "f0e68c",
1744 | lavender: "e6e6fa",
1745 | lavenderblush: "fff0f5",
1746 | lawngreen: "7cfc00",
1747 | lemonchiffon: "fffacd",
1748 | lightblue: "add8e6",
1749 | lightcoral: "f08080",
1750 | lightcyan: "e0ffff",
1751 | lightgoldenrodyellow: "fafad2",
1752 | lightgray: "d3d3d3",
1753 | lightgreen: "90ee90",
1754 | lightgrey: "d3d3d3",
1755 | lightpink: "ffb6c1",
1756 | lightsalmon: "ffa07a",
1757 | lightseagreen: "20b2aa",
1758 | lightskyblue: "87cefa",
1759 | lightslategray: "789",
1760 | lightslategrey: "789",
1761 | lightsteelblue: "b0c4de",
1762 | lightyellow: "ffffe0",
1763 | lime: "0f0",
1764 | limegreen: "32cd32",
1765 | linen: "faf0e6",
1766 | magenta: "f0f",
1767 | maroon: "800000",
1768 | mediumaquamarine: "66cdaa",
1769 | mediumblue: "0000cd",
1770 | mediumorchid: "ba55d3",
1771 | mediumpurple: "9370db",
1772 | mediumseagreen: "3cb371",
1773 | mediumslateblue: "7b68ee",
1774 | mediumspringgreen: "00fa9a",
1775 | mediumturquoise: "48d1cc",
1776 | mediumvioletred: "c71585",
1777 | midnightblue: "191970",
1778 | mintcream: "f5fffa",
1779 | mistyrose: "ffe4e1",
1780 | moccasin: "ffe4b5",
1781 | navajowhite: "ffdead",
1782 | navy: "000080",
1783 | oldlace: "fdf5e6",
1784 | olive: "808000",
1785 | olivedrab: "6b8e23",
1786 | orange: "ffa500",
1787 | orangered: "ff4500",
1788 | orchid: "da70d6",
1789 | palegoldenrod: "eee8aa",
1790 | palegreen: "98fb98",
1791 | paleturquoise: "afeeee",
1792 | palevioletred: "db7093",
1793 | papayawhip: "ffefd5",
1794 | peachpuff: "ffdab9",
1795 | peru: "cd853f",
1796 | pink: "ffc0cb",
1797 | plum: "dda0dd",
1798 | powderblue: "b0e0e6",
1799 | purple: "800080",
1800 | red: "f00",
1801 | rosybrown: "bc8f8f",
1802 | royalblue: "4169e1",
1803 | saddlebrown: "8b4513",
1804 | salmon: "fa8072",
1805 | sandybrown: "f4a460",
1806 | seagreen: "2e8b57",
1807 | seashell: "fff5ee",
1808 | sienna: "a0522d",
1809 | silver: "c0c0c0",
1810 | skyblue: "87ceeb",
1811 | slateblue: "6a5acd",
1812 | slategray: "708090",
1813 | slategrey: "708090",
1814 | snow: "fffafa",
1815 | springgreen: "00ff7f",
1816 | steelblue: "4682b4",
1817 | tan: "d2b48c",
1818 | teal: "008080",
1819 | thistle: "d8bfd8",
1820 | tomato: "ff6347",
1821 | turquoise: "40e0d0",
1822 | violet: "ee82ee",
1823 | wheat: "f5deb3",
1824 | white: "fff",
1825 | whitesmoke: "f5f5f5",
1826 | yellow: "ff0",
1827 | yellowgreen: "9acd32"
1828 | };
1829 |
1830 | // Make it easy to access colors via `hexNames[hex]`
1831 | var hexNames = tinycolor.hexNames = flip(names);
1832 |
1833 |
1834 | // Utilities
1835 | // ---------
1836 |
1837 | // `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }`
1838 | function flip(o) {
1839 | var flipped = { };
1840 | for (var i in o) {
1841 | if (o.hasOwnProperty(i)) {
1842 | flipped[o[i]] = i;
1843 | }
1844 | }
1845 | return flipped;
1846 | }
1847 |
1848 | // Return a valid alpha value [0,1] with all invalid values being set to 1
1849 | function boundAlpha(a) {
1850 | a = parseFloat(a);
1851 |
1852 | if (isNaN(a) || a < 0 || a > 1) {
1853 | a = 1;
1854 | }
1855 |
1856 | return a;
1857 | }
1858 |
1859 | // Take input from [0, n] and return it as [0, 1]
1860 | function bound01(n, max) {
1861 | if (isOnePointZero(n)) { n = "100%"; }
1862 |
1863 | var processPercent = isPercentage(n);
1864 | n = mathMin(max, mathMax(0, parseFloat(n)));
1865 |
1866 | // Automatically convert percentage into number
1867 | if (processPercent) {
1868 | n = parseInt(n * max, 10) / 100;
1869 | }
1870 |
1871 | // Handle floating point rounding errors
1872 | if ((math.abs(n - max) < 0.000001)) {
1873 | return 1;
1874 | }
1875 |
1876 | // Convert into [0, 1] range if it isn't already
1877 | return (n % max) / parseFloat(max);
1878 | }
1879 |
1880 | // Force a number between 0 and 1
1881 | function clamp01(val) {
1882 | return mathMin(1, mathMax(0, val));
1883 | }
1884 |
1885 | // Parse an integer into hex
1886 | function parseHex(val) {
1887 | return parseInt(val, 16);
1888 | }
1889 |
1890 | // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
1891 | //
1892 | function isOnePointZero(n) {
1893 | return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1;
1894 | }
1895 |
1896 | // Check to see if string passed in is a percentage
1897 | function isPercentage(n) {
1898 | return typeof n === "string" && n.indexOf('%') != -1;
1899 | }
1900 |
1901 | // Force a hex value to have 2 characters
1902 | function pad2(c) {
1903 | return c.length == 1 ? '0' + c : '' + c;
1904 | }
1905 |
1906 | // Replace a decimal with it's percentage value
1907 | function convertToPercentage(n) {
1908 | if (n <= 1) {
1909 | n = (n * 100) + "%";
1910 | }
1911 |
1912 | return n;
1913 | }
1914 |
1915 | var matchers = (function() {
1916 |
1917 | //
1918 | var CSS_INTEGER = "[-\\+]?\\d+%?";
1919 |
1920 | //
1921 | var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?";
1922 |
1923 | // Allow positive/negative integer/number. Don't capture the either/or, just the entire outcome.
1924 | var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")";
1925 |
1926 | // Actual matching.
1927 | // Parentheses and commas are optional, but not required.
1928 | // Whitespace can take the place of commas or opening paren
1929 | var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
1930 | var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
1931 |
1932 | return {
1933 | rgb: new RegExp("rgb" + PERMISSIVE_MATCH3),
1934 | rgba: new RegExp("rgba" + PERMISSIVE_MATCH4),
1935 | hsl: new RegExp("hsl" + PERMISSIVE_MATCH3),
1936 | hsla: new RegExp("hsla" + PERMISSIVE_MATCH4),
1937 | hsv: new RegExp("hsv" + PERMISSIVE_MATCH3),
1938 | hex3: /^([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
1939 | hex6: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/
1940 | };
1941 | })();
1942 |
1943 | // `stringInputToObject`
1944 | // Permissive string parsing. Take in a number of formats, and output an object
1945 | // based on detected format. Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
1946 | function stringInputToObject(color) {
1947 |
1948 | color = color.replace(trimLeft,'').replace(trimRight, '').toLowerCase();
1949 | var named = false;
1950 | if (names[color]) {
1951 | color = names[color];
1952 | named = true;
1953 | }
1954 | else if (color == 'transparent') {
1955 | return { r: 0, g: 0, b: 0, a: 0, format: "name" };
1956 | }
1957 |
1958 | // Try to match string input using regular expressions.
1959 | // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360]
1960 | // Just return an object and let the conversion functions handle that.
1961 | // This way the result will be the same whether the tinycolor is initialized with string or object.
1962 | var match;
1963 | if ((match = matchers.rgb.exec(color))) {
1964 | return { r: match[1], g: match[2], b: match[3] };
1965 | }
1966 | if ((match = matchers.rgba.exec(color))) {
1967 | return { r: match[1], g: match[2], b: match[3], a: match[4] };
1968 | }
1969 | if ((match = matchers.hsl.exec(color))) {
1970 | return { h: match[1], s: match[2], l: match[3] };
1971 | }
1972 | if ((match = matchers.hsla.exec(color))) {
1973 | return { h: match[1], s: match[2], l: match[3], a: match[4] };
1974 | }
1975 | if ((match = matchers.hsv.exec(color))) {
1976 | return { h: match[1], s: match[2], v: match[3] };
1977 | }
1978 | if ((match = matchers.hex6.exec(color))) {
1979 | return {
1980 | r: parseHex(match[1]),
1981 | g: parseHex(match[2]),
1982 | b: parseHex(match[3]),
1983 | format: named ? "name" : "hex"
1984 | };
1985 | }
1986 | if ((match = matchers.hex3.exec(color))) {
1987 | return {
1988 | r: parseHex(match[1] + '' + match[1]),
1989 | g: parseHex(match[2] + '' + match[2]),
1990 | b: parseHex(match[3] + '' + match[3]),
1991 | format: named ? "name" : "hex"
1992 | };
1993 | }
1994 |
1995 | return false;
1996 | }
1997 |
1998 | // Expose tinycolor to window, does not need to run in non-browser context.
1999 | window.tinycolor = tinycolor;
2000 |
2001 | })();
2002 |
2003 |
2004 | $(function () {
2005 | if ($.fn.spectrum.load) {
2006 | $.fn.spectrum.processNativeColorInputs();
2007 | }
2008 | });
2009 |
2010 | })(window, jQuery);
2011 |
--------------------------------------------------------------------------------
/public/javascripts/underscore.js:
--------------------------------------------------------------------------------
1 | // Underscore.js 1.5.2
2 | // http://underscorejs.org
3 | // (c) 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
4 | // Underscore may be freely distributed under the MIT license.
5 |
6 | (function() {
7 |
8 | // Baseline setup
9 | // --------------
10 |
11 | // Establish the root object, `window` in the browser, or `exports` on the server.
12 | var root = this;
13 |
14 | // Save the previous value of the `_` variable.
15 | var previousUnderscore = root._;
16 |
17 | // Establish the object that gets returned to break out of a loop iteration.
18 | var breaker = {};
19 |
20 | // Save bytes in the minified (but not gzipped) version:
21 | var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;
22 |
23 | //use the faster Date.now if available.
24 | var getTime = (Date.now || function() {
25 | return new Date().getTime();
26 | });
27 |
28 | // Create quick reference variables for speed access to core prototypes.
29 | var
30 | push = ArrayProto.push,
31 | slice = ArrayProto.slice,
32 | concat = ArrayProto.concat,
33 | toString = ObjProto.toString,
34 | hasOwnProperty = ObjProto.hasOwnProperty;
35 |
36 | // All **ECMAScript 5** native function implementations that we hope to use
37 | // are declared here.
38 | var
39 | nativeForEach = ArrayProto.forEach,
40 | nativeMap = ArrayProto.map,
41 | nativeReduce = ArrayProto.reduce,
42 | nativeReduceRight = ArrayProto.reduceRight,
43 | nativeFilter = ArrayProto.filter,
44 | nativeEvery = ArrayProto.every,
45 | nativeSome = ArrayProto.some,
46 | nativeIndexOf = ArrayProto.indexOf,
47 | nativeLastIndexOf = ArrayProto.lastIndexOf,
48 | nativeIsArray = Array.isArray,
49 | nativeKeys = Object.keys,
50 | nativeBind = FuncProto.bind;
51 |
52 | // Create a safe reference to the Underscore object for use below.
53 | var _ = function(obj) {
54 | if (obj instanceof _) return obj;
55 | if (!(this instanceof _)) return new _(obj);
56 | this._wrapped = obj;
57 | };
58 |
59 | // Export the Underscore object for **Node.js**, with
60 | // backwards-compatibility for the old `require()` API. If we're in
61 | // the browser, add `_` as a global object via a string identifier,
62 | // for Closure Compiler "advanced" mode.
63 | if (typeof exports !== 'undefined') {
64 | if (typeof module !== 'undefined' && module.exports) {
65 | exports = module.exports = _;
66 | }
67 | exports._ = _;
68 | } else {
69 | root._ = _;
70 | }
71 |
72 | // Current version.
73 | _.VERSION = '1.5.2';
74 |
75 | // Collection Functions
76 | // --------------------
77 |
78 | // The cornerstone, an `each` implementation, aka `forEach`.
79 | // Handles objects with the built-in `forEach`, arrays, and raw objects.
80 | // Delegates to **ECMAScript 5**'s native `forEach` if available.
81 | var each = _.each = _.forEach = function(obj, iterator, context) {
82 | if (obj == null) return;
83 | if (nativeForEach && obj.forEach === nativeForEach) {
84 | obj.forEach(iterator, context);
85 | } else if (obj.length === +obj.length) {
86 | for (var i = 0, length = obj.length; i < length; i++) {
87 | if (iterator.call(context, obj[i], i, obj) === breaker) return;
88 | }
89 | } else {
90 | var keys = _.keys(obj);
91 | for (var i = 0, length = keys.length; i < length; i++) {
92 | if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return;
93 | }
94 | }
95 | };
96 |
97 | // Return the results of applying the iterator to each element.
98 | // Delegates to **ECMAScript 5**'s native `map` if available.
99 | _.map = _.collect = function(obj, iterator, context) {
100 | var results = [];
101 | if (obj == null) return results;
102 | if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context);
103 | each(obj, function(value, index, list) {
104 | results.push(iterator.call(context, value, index, list));
105 | });
106 | return results;
107 | };
108 |
109 | var reduceError = 'Reduce of empty array with no initial value';
110 |
111 | // **Reduce** builds up a single result from a list of values, aka `inject`,
112 | // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
113 | _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
114 | var initial = arguments.length > 2;
115 | if (obj == null) obj = [];
116 | if (nativeReduce && obj.reduce === nativeReduce) {
117 | if (context) iterator = _.bind(iterator, context);
118 | return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator);
119 | }
120 | each(obj, function(value, index, list) {
121 | if (!initial) {
122 | memo = value;
123 | initial = true;
124 | } else {
125 | memo = iterator.call(context, memo, value, index, list);
126 | }
127 | });
128 | if (!initial) throw new TypeError(reduceError);
129 | return memo;
130 | };
131 |
132 | // The right-associative version of reduce, also known as `foldr`.
133 | // Delegates to **ECMAScript 5**'s native `reduceRight` if available.
134 | _.reduceRight = _.foldr = function(obj, iterator, memo, context) {
135 | var initial = arguments.length > 2;
136 | if (obj == null) obj = [];
137 | if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
138 | if (context) iterator = _.bind(iterator, context);
139 | return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
140 | }
141 | var length = obj.length;
142 | if (length !== +length) {
143 | var keys = _.keys(obj);
144 | length = keys.length;
145 | }
146 | each(obj, function(value, index, list) {
147 | index = keys ? keys[--length] : --length;
148 | if (!initial) {
149 | memo = obj[index];
150 | initial = true;
151 | } else {
152 | memo = iterator.call(context, memo, obj[index], index, list);
153 | }
154 | });
155 | if (!initial) throw new TypeError(reduceError);
156 | return memo;
157 | };
158 |
159 | // Return the first value which passes a truth test. Aliased as `detect`.
160 | _.find = _.detect = function(obj, iterator, context) {
161 | var result;
162 | any(obj, function(value, index, list) {
163 | if (iterator.call(context, value, index, list)) {
164 | result = value;
165 | return true;
166 | }
167 | });
168 | return result;
169 | };
170 |
171 | // Return all the elements that pass a truth test.
172 | // Delegates to **ECMAScript 5**'s native `filter` if available.
173 | // Aliased as `select`.
174 | _.filter = _.select = function(obj, iterator, context) {
175 | var results = [];
176 | if (obj == null) return results;
177 | if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context);
178 | each(obj, function(value, index, list) {
179 | if (iterator.call(context, value, index, list)) results.push(value);
180 | });
181 | return results;
182 | };
183 |
184 | // Return all the elements for which a truth test fails.
185 | _.reject = function(obj, iterator, context) {
186 | return _.filter(obj, function(value, index, list) {
187 | return !iterator.call(context, value, index, list);
188 | }, context);
189 | };
190 |
191 | // Determine whether all of the elements match a truth test.
192 | // Delegates to **ECMAScript 5**'s native `every` if available.
193 | // Aliased as `all`.
194 | _.every = _.all = function(obj, iterator, context) {
195 | iterator || (iterator = _.identity);
196 | var result = true;
197 | if (obj == null) return result;
198 | if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context);
199 | each(obj, function(value, index, list) {
200 | if (!(result = result && iterator.call(context, value, index, list))) return breaker;
201 | });
202 | return !!result;
203 | };
204 |
205 | // Determine if at least one element in the object matches a truth test.
206 | // Delegates to **ECMAScript 5**'s native `some` if available.
207 | // Aliased as `any`.
208 | var any = _.some = _.any = function(obj, iterator, context) {
209 | iterator || (iterator = _.identity);
210 | var result = false;
211 | if (obj == null) return result;
212 | if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
213 | each(obj, function(value, index, list) {
214 | if (result || (result = iterator.call(context, value, index, list))) return breaker;
215 | });
216 | return !!result;
217 | };
218 |
219 | // Determine if the array or object contains a given value (using `===`).
220 | // Aliased as `include`.
221 | _.contains = _.include = function(obj, target) {
222 | if (obj == null) return false;
223 | if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1;
224 | return any(obj, function(value) {
225 | return value === target;
226 | });
227 | };
228 |
229 | // Invoke a method (with arguments) on every item in a collection.
230 | _.invoke = function(obj, method) {
231 | var args = slice.call(arguments, 2);
232 | var isFunc = _.isFunction(method);
233 | return _.map(obj, function(value) {
234 | return (isFunc ? method : value[method]).apply(value, args);
235 | });
236 | };
237 |
238 | // Convenience version of a common use case of `map`: fetching a property.
239 | _.pluck = function(obj, key) {
240 | return _.map(obj, _.property(key));
241 | };
242 |
243 | // Convenience version of a common use case of `filter`: selecting only objects
244 | // containing specific `key:value` pairs.
245 | _.where = function(obj, attrs, first) {
246 | if (_.isEmpty(attrs)) return first ? void 0 : [];
247 | return _[first ? 'find' : 'filter'](obj, function(value) {
248 | for (var key in attrs) {
249 | if (attrs[key] !== value[key]) return false;
250 | }
251 | return true;
252 | });
253 | };
254 |
255 | // Convenience version of a common use case of `find`: getting the first object
256 | // containing specific `key:value` pairs.
257 | _.findWhere = function(obj, attrs) {
258 | return _.where(obj, attrs, true);
259 | };
260 |
261 | // Return the maximum element or (element-based computation).
262 | // Can't optimize arrays of integers longer than 65,535 elements.
263 | // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797)
264 | _.max = function(obj, iterator, context) {
265 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
266 | return Math.max.apply(Math, obj);
267 | }
268 | if (!iterator && _.isEmpty(obj)) return -Infinity;
269 | var result = {computed : -Infinity, value: -Infinity};
270 | each(obj, function(value, index, list) {
271 | var computed = iterator ? iterator.call(context, value, index, list) : value;
272 | computed > result.computed && (result = {value : value, computed : computed});
273 | });
274 | return result.value;
275 | };
276 |
277 | // Return the minimum element (or element-based computation).
278 | _.min = function(obj, iterator, context) {
279 | if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) {
280 | return Math.min.apply(Math, obj);
281 | }
282 | if (!iterator && _.isEmpty(obj)) return Infinity;
283 | var result = {computed : Infinity, value: Infinity};
284 | each(obj, function(value, index, list) {
285 | var computed = iterator ? iterator.call(context, value, index, list) : value;
286 | computed < result.computed && (result = {value : value, computed : computed});
287 | });
288 | return result.value;
289 | };
290 |
291 | // Shuffle an array, using the modern version of the
292 | // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
293 | _.shuffle = function(obj) {
294 | var rand;
295 | var index = 0;
296 | var shuffled = [];
297 | each(obj, function(value) {
298 | rand = _.random(index++);
299 | shuffled[index - 1] = shuffled[rand];
300 | shuffled[rand] = value;
301 | });
302 | return shuffled;
303 | };
304 |
305 | // Sample **n** random values from a collection.
306 | // If **n** is not specified, returns a single random element.
307 | // The internal `guard` argument allows it to work with `map`.
308 | _.sample = function(obj, n, guard) {
309 | if (n == null || guard) {
310 | if (obj.length !== +obj.length) obj = _.values(obj);
311 | return obj[_.random(obj.length - 1)];
312 | }
313 | return _.shuffle(obj).slice(0, Math.max(0, n));
314 | };
315 |
316 | // An internal function to generate lookup iterators.
317 | var lookupIterator = function(value) {
318 | if (value == null) return _.identity;
319 | if (_.isFunction(value)) return value;
320 | return _.property(value);
321 | };
322 |
323 | // Sort the object's values by a criterion produced by an iterator.
324 | _.sortBy = function(obj, iterator, context) {
325 | iterator = lookupIterator(iterator);
326 | return _.pluck(_.map(obj, function(value, index, list) {
327 | return {
328 | value: value,
329 | index: index,
330 | criteria: iterator.call(context, value, index, list)
331 | };
332 | }).sort(function(left, right) {
333 | var a = left.criteria;
334 | var b = right.criteria;
335 | if (a !== b) {
336 | if (a > b || a === void 0) return 1;
337 | if (a < b || b === void 0) return -1;
338 | }
339 | return left.index - right.index;
340 | }), 'value');
341 | };
342 |
343 | // An internal function used for aggregate "group by" operations.
344 | var group = function(behavior) {
345 | return function(obj, iterator, context) {
346 | var result = {};
347 | iterator = lookupIterator(iterator);
348 | each(obj, function(value, index) {
349 | var key = iterator.call(context, value, index, obj);
350 | behavior(result, key, value);
351 | });
352 | return result;
353 | };
354 | };
355 |
356 | // Groups the object's values by a criterion. Pass either a string attribute
357 | // to group by, or a function that returns the criterion.
358 | _.groupBy = group(function(result, key, value) {
359 | (_.has(result, key) ? result[key] : (result[key] = [])).push(value);
360 | });
361 |
362 | // Indexes the object's values by a criterion, similar to `groupBy`, but for
363 | // when you know that your index values will be unique.
364 | _.indexBy = group(function(result, key, value) {
365 | result[key] = value;
366 | });
367 |
368 | // Counts instances of an object that group by a certain criterion. Pass
369 | // either a string attribute to count by, or a function that returns the
370 | // criterion.
371 | _.countBy = group(function(result, key) {
372 | _.has(result, key) ? result[key]++ : result[key] = 1;
373 | });
374 |
375 | // Use a comparator function to figure out the smallest index at which
376 | // an object should be inserted so as to maintain order. Uses binary search.
377 | _.sortedIndex = function(array, obj, iterator, context) {
378 | iterator = lookupIterator(iterator);
379 | var value = iterator.call(context, obj);
380 | var low = 0, high = array.length;
381 | while (low < high) {
382 | var mid = (low + high) >>> 1;
383 | iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid;
384 | }
385 | return low;
386 | };
387 |
388 | // Safely create a real, live array from anything iterable.
389 | _.toArray = function(obj) {
390 | if (!obj) return [];
391 | if (_.isArray(obj)) return slice.call(obj);
392 | if (obj.length === +obj.length) return _.map(obj, _.identity);
393 | return _.values(obj);
394 | };
395 |
396 | // Return the number of elements in an object.
397 | _.size = function(obj) {
398 | if (obj == null) return 0;
399 | return (obj.length === +obj.length) ? obj.length : _.keys(obj).length;
400 | };
401 |
402 | // Array Functions
403 | // ---------------
404 |
405 | // Get the first element of an array. Passing **n** will return the first N
406 | // values in the array. Aliased as `head` and `take`. The **guard** check
407 | // allows it to work with `_.map`.
408 | _.first = _.head = _.take = function(array, n, guard) {
409 | if (array == null) return void 0;
410 | if ((n == null) || guard) return array[0];
411 | if (n < 0) return [];
412 | return slice.call(array, 0, n);
413 | };
414 |
415 | // Returns everything but the last entry of the array. Especially useful on
416 | // the arguments object. Passing **n** will return all the values in
417 | // the array, excluding the last N. The **guard** check allows it to work with
418 | // `_.map`.
419 | _.initial = function(array, n, guard) {
420 | return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n));
421 | };
422 |
423 | // Get the last element of an array. Passing **n** will return the last N
424 | // values in the array. The **guard** check allows it to work with `_.map`.
425 | _.last = function(array, n, guard) {
426 | if (array == null) return void 0;
427 | if ((n == null) || guard) return array[array.length - 1];
428 | return slice.call(array, Math.max(array.length - n, 0));
429 | };
430 |
431 | // Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
432 | // Especially useful on the arguments object. Passing an **n** will return
433 | // the rest N values in the array. The **guard**
434 | // check allows it to work with `_.map`.
435 | _.rest = _.tail = _.drop = function(array, n, guard) {
436 | return slice.call(array, (n == null) || guard ? 1 : n);
437 | };
438 |
439 | // Trim out all falsy values from an array.
440 | _.compact = function(array) {
441 | return _.filter(array, _.identity);
442 | };
443 |
444 | // Internal implementation of a recursive `flatten` function.
445 | var flatten = function(input, shallow, output) {
446 | if (shallow && _.every(input, _.isArray)) {
447 | return concat.apply(output, input);
448 | }
449 | each(input, function(value) {
450 | if (_.isArray(value) || _.isArguments(value)) {
451 | shallow ? push.apply(output, value) : flatten(value, shallow, output);
452 | } else {
453 | output.push(value);
454 | }
455 | });
456 | return output;
457 | };
458 |
459 | // Flatten out an array, either recursively (by default), or just one level.
460 | _.flatten = function(array, shallow) {
461 | return flatten(array, shallow, []);
462 | };
463 |
464 | // Return a version of the array that does not contain the specified value(s).
465 | _.without = function(array) {
466 | return _.difference(array, slice.call(arguments, 1));
467 | };
468 |
469 | // Produce a duplicate-free version of the array. If the array has already
470 | // been sorted, you have the option of using a faster algorithm.
471 | // Aliased as `unique`.
472 | _.uniq = _.unique = function(array, isSorted, iterator, context) {
473 | if (_.isFunction(isSorted)) {
474 | context = iterator;
475 | iterator = isSorted;
476 | isSorted = false;
477 | }
478 | var initial = iterator ? _.map(array, iterator, context) : array;
479 | var results = [];
480 | var seen = [];
481 | each(initial, function(value, index) {
482 | if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) {
483 | seen.push(value);
484 | results.push(array[index]);
485 | }
486 | });
487 | return results;
488 | };
489 |
490 | // Produce an array that contains the union: each distinct element from all of
491 | // the passed-in arrays.
492 | _.union = function() {
493 | return _.uniq(_.flatten(arguments, true));
494 | };
495 |
496 | // Produce an array that contains every item shared between all the
497 | // passed-in arrays.
498 | _.intersection = function(array) {
499 | var rest = slice.call(arguments, 1);
500 | return _.filter(_.uniq(array), function(item) {
501 | return _.every(rest, function(other) {
502 | return _.indexOf(other, item) >= 0;
503 | });
504 | });
505 | };
506 |
507 | // Take the difference between one array and a number of other arrays.
508 | // Only the elements present in just the first array will remain.
509 | _.difference = function(array) {
510 | var rest = concat.apply(ArrayProto, slice.call(arguments, 1));
511 | return _.filter(array, function(value){ return !_.contains(rest, value); });
512 | };
513 |
514 | // Zip together multiple lists into a single array -- elements that share
515 | // an index go together.
516 | _.zip = function() {
517 | var length = _.max(_.pluck(arguments, "length").concat(0));
518 | var results = new Array(length);
519 | for (var i = 0; i < length; i++) {
520 | results[i] = _.pluck(arguments, '' + i);
521 | }
522 | return results;
523 | };
524 |
525 | // Converts lists into objects. Pass either a single array of `[key, value]`
526 | // pairs, or two parallel arrays of the same length -- one of keys, and one of
527 | // the corresponding values.
528 | _.object = function(list, values) {
529 | if (list == null) return {};
530 | var result = {};
531 | for (var i = 0, length = list.length; i < length; i++) {
532 | if (values) {
533 | result[list[i]] = values[i];
534 | } else {
535 | result[list[i][0]] = list[i][1];
536 | }
537 | }
538 | return result;
539 | };
540 |
541 | // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**),
542 | // we need this function. Return the position of the first occurrence of an
543 | // item in an array, or -1 if the item is not included in the array.
544 | // Delegates to **ECMAScript 5**'s native `indexOf` if available.
545 | // If the array is large and already in sort order, pass `true`
546 | // for **isSorted** to use binary search.
547 | _.indexOf = function(array, item, isSorted) {
548 | if (array == null) return -1;
549 | var i = 0, length = array.length;
550 | if (isSorted) {
551 | if (typeof isSorted == 'number') {
552 | i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted);
553 | } else {
554 | i = _.sortedIndex(array, item);
555 | return array[i] === item ? i : -1;
556 | }
557 | }
558 | if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted);
559 | for (; i < length; i++) if (array[i] === item) return i;
560 | return -1;
561 | };
562 |
563 | // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available.
564 | _.lastIndexOf = function(array, item, from) {
565 | if (array == null) return -1;
566 | var hasIndex = from != null;
567 | if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) {
568 | return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item);
569 | }
570 | var i = (hasIndex ? from : array.length);
571 | while (i--) if (array[i] === item) return i;
572 | return -1;
573 | };
574 |
575 | // Generate an integer Array containing an arithmetic progression. A port of
576 | // the native Python `range()` function. See
577 | // [the Python documentation](http://docs.python.org/library/functions.html#range).
578 | _.range = function(start, stop, step) {
579 | if (arguments.length <= 1) {
580 | stop = start || 0;
581 | start = 0;
582 | }
583 | step = arguments[2] || 1;
584 |
585 | var length = Math.max(Math.ceil((stop - start) / step), 0);
586 | var idx = 0;
587 | var range = new Array(length);
588 |
589 | while(idx < length) {
590 | range[idx++] = start;
591 | start += step;
592 | }
593 |
594 | return range;
595 | };
596 |
597 | // Function (ahem) Functions
598 | // ------------------
599 |
600 | // Reusable constructor function for prototype setting.
601 | var ctor = function(){};
602 |
603 | // Create a function bound to a given object (assigning `this`, and arguments,
604 | // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
605 | // available.
606 | _.bind = function(func, context) {
607 | var args, bound;
608 | if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
609 | if (!_.isFunction(func)) throw new TypeError;
610 | args = slice.call(arguments, 2);
611 | return bound = function() {
612 | if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
613 | ctor.prototype = func.prototype;
614 | var self = new ctor;
615 | ctor.prototype = null;
616 | var result = func.apply(self, args.concat(slice.call(arguments)));
617 | if (Object(result) === result) return result;
618 | return self;
619 | };
620 | };
621 |
622 | // Partially apply a function by creating a version that has had some of its
623 | // arguments pre-filled, without changing its dynamic `this` context. _ acts
624 | // as a placeholder, allowing any combination of arguments to be pre-filled.
625 | _.partial = function(func) {
626 | var boundArgs = slice.call(arguments, 1);
627 | return function() {
628 | var args = slice.call(boundArgs);
629 | _.each(arguments, function(arg) {
630 | var index = args.indexOf(_);
631 | args[index >= 0 ? index : args.length] = arg;
632 | });
633 | return func.apply(this, _.map(args, function(value) {
634 | return value === _ ? void 0 : value;
635 | }));
636 | };
637 | };
638 |
639 | // Bind a number of an object's methods to that object. Remaining arguments
640 | // are the method names to be bound. Useful for ensuring that all callbacks
641 | // defined on an object belong to it.
642 | _.bindAll = function(obj) {
643 | var funcs = slice.call(arguments, 1);
644 | if (funcs.length === 0) throw new Error("bindAll must be passed function names");
645 | each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); });
646 | return obj;
647 | };
648 |
649 | // Memoize an expensive function by storing its results.
650 | _.memoize = function(func, hasher) {
651 | var memo = {};
652 | hasher || (hasher = _.identity);
653 | return function() {
654 | var key = hasher.apply(this, arguments);
655 | return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments));
656 | };
657 | };
658 |
659 | // Delays a function for the given number of milliseconds, and then calls
660 | // it with the arguments supplied.
661 | _.delay = function(func, wait) {
662 | var args = slice.call(arguments, 2);
663 | return setTimeout(function(){ return func.apply(null, args); }, wait);
664 | };
665 |
666 | // Defers a function, scheduling it to run after the current call stack has
667 | // cleared.
668 | _.defer = function(func) {
669 | return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1)));
670 | };
671 |
672 | // Returns a function, that, when invoked, will only be triggered at most once
673 | // during a given window of time. Normally, the throttled function will run
674 | // as much as it can, without ever going more than once per `wait` duration;
675 | // but if you'd like to disable the execution on the leading edge, pass
676 | // `{leading: false}`. To disable execution on the trailing edge, ditto.
677 | _.throttle = function(func, wait, options) {
678 | var context, args, result;
679 | var timeout = null;
680 | var previous = 0;
681 | options || (options = {});
682 | var later = function() {
683 | previous = options.leading === false ? 0 : getTime();
684 | timeout = null;
685 | result = func.apply(context, args);
686 | context = args = null;
687 | };
688 | return function() {
689 | var now = getTime();
690 | if (!previous && options.leading === false) previous = now;
691 | var remaining = wait - (now - previous);
692 | context = this;
693 | args = arguments;
694 | if (remaining <= 0) {
695 | clearTimeout(timeout);
696 | timeout = null;
697 | previous = now;
698 | result = func.apply(context, args);
699 | context = args = null;
700 | } else if (!timeout && options.trailing !== false) {
701 | timeout = setTimeout(later, remaining);
702 | }
703 | return result;
704 | };
705 | };
706 |
707 | // Returns a function, that, as long as it continues to be invoked, will not
708 | // be triggered. The function will be called after it stops being called for
709 | // N milliseconds. If `immediate` is passed, trigger the function on the
710 | // leading edge, instead of the trailing.
711 | _.debounce = function(func, wait, immediate) {
712 | var timeout, args, context, timestamp, result;
713 | return function() {
714 | context = this;
715 | args = arguments;
716 | timestamp = getTime();
717 | var later = function() {
718 | var last = getTime() - timestamp;
719 | if (last < wait) {
720 | timeout = setTimeout(later, wait - last);
721 | } else {
722 | timeout = null;
723 | if (!immediate) {
724 | result = func.apply(context, args);
725 | context = args = null;
726 | }
727 | }
728 | };
729 | var callNow = immediate && !timeout;
730 | if (!timeout) {
731 | timeout = setTimeout(later, wait);
732 | }
733 | if (callNow) {
734 | result = func.apply(context, args);
735 | context = args = null;
736 | }
737 |
738 | return result;
739 | };
740 | };
741 |
742 | // Returns a function that will be executed at most one time, no matter how
743 | // often you call it. Useful for lazy initialization.
744 | _.once = function(func) {
745 | var ran = false, memo;
746 | return function() {
747 | if (ran) return memo;
748 | ran = true;
749 | memo = func.apply(this, arguments);
750 | func = null;
751 | return memo;
752 | };
753 | };
754 |
755 | // Returns the first function passed as an argument to the second,
756 | // allowing you to adjust arguments, run code before and after, and
757 | // conditionally execute the original function.
758 | _.wrap = function(func, wrapper) {
759 | return _.partial(wrapper, func);
760 | };
761 |
762 | // Returns a function that is the composition of a list of functions, each
763 | // consuming the return value of the function that follows.
764 | _.compose = function() {
765 | var funcs = arguments;
766 | return function() {
767 | var args = arguments;
768 | for (var i = funcs.length - 1; i >= 0; i--) {
769 | args = [funcs[i].apply(this, args)];
770 | }
771 | return args[0];
772 | };
773 | };
774 |
775 | // Returns a function that will only be executed after being called N times.
776 | _.after = function(times, func) {
777 | return function() {
778 | if (--times < 1) {
779 | return func.apply(this, arguments);
780 | }
781 | };
782 | };
783 |
784 | // Object Functions
785 | // ----------------
786 |
787 | // Retrieve the names of an object's properties.
788 | // Delegates to **ECMAScript 5**'s native `Object.keys`
789 | _.keys = nativeKeys || function(obj) {
790 | if (obj !== Object(obj)) throw new TypeError('Invalid object');
791 | var keys = [];
792 | for (var key in obj) if (_.has(obj, key)) keys.push(key);
793 | return keys;
794 | };
795 |
796 | // Retrieve the values of an object's properties.
797 | _.values = function(obj) {
798 | var keys = _.keys(obj);
799 | var length = keys.length;
800 | var values = new Array(length);
801 | for (var i = 0; i < length; i++) {
802 | values[i] = obj[keys[i]];
803 | }
804 | return values;
805 | };
806 |
807 | // Convert an object into a list of `[key, value]` pairs.
808 | _.pairs = function(obj) {
809 | var keys = _.keys(obj);
810 | var length = keys.length;
811 | var pairs = new Array(length);
812 | for (var i = 0; i < length; i++) {
813 | pairs[i] = [keys[i], obj[keys[i]]];
814 | }
815 | return pairs;
816 | };
817 |
818 | // Invert the keys and values of an object. The values must be serializable.
819 | _.invert = function(obj) {
820 | var result = {};
821 | var keys = _.keys(obj);
822 | for (var i = 0, length = keys.length; i < length; i++) {
823 | result[obj[keys[i]]] = keys[i];
824 | }
825 | return result;
826 | };
827 |
828 | // Return a sorted list of the function names available on the object.
829 | // Aliased as `methods`
830 | _.functions = _.methods = function(obj) {
831 | var names = [];
832 | for (var key in obj) {
833 | if (_.isFunction(obj[key])) names.push(key);
834 | }
835 | return names.sort();
836 | };
837 |
838 | // Extend a given object with all the properties in passed-in object(s).
839 | _.extend = function(obj) {
840 | each(slice.call(arguments, 1), function(source) {
841 | if (source) {
842 | for (var prop in source) {
843 | obj[prop] = source[prop];
844 | }
845 | }
846 | });
847 | return obj;
848 | };
849 |
850 | // Return a copy of the object only containing the whitelisted properties.
851 | _.pick = function(obj) {
852 | var copy = {};
853 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
854 | each(keys, function(key) {
855 | if (key in obj) copy[key] = obj[key];
856 | });
857 | return copy;
858 | };
859 |
860 | // Return a copy of the object without the blacklisted properties.
861 | _.omit = function(obj) {
862 | var copy = {};
863 | var keys = concat.apply(ArrayProto, slice.call(arguments, 1));
864 | for (var key in obj) {
865 | if (!_.contains(keys, key)) copy[key] = obj[key];
866 | }
867 | return copy;
868 | };
869 |
870 | // Fill in a given object with default properties.
871 | _.defaults = function(obj) {
872 | each(slice.call(arguments, 1), function(source) {
873 | if (source) {
874 | for (var prop in source) {
875 | if (obj[prop] === void 0) obj[prop] = source[prop];
876 | }
877 | }
878 | });
879 | return obj;
880 | };
881 |
882 | // Create a (shallow-cloned) duplicate of an object.
883 | _.clone = function(obj) {
884 | if (!_.isObject(obj)) return obj;
885 | return _.isArray(obj) ? obj.slice() : _.extend({}, obj);
886 | };
887 |
888 | // Invokes interceptor with the obj, and then returns obj.
889 | // The primary purpose of this method is to "tap into" a method chain, in
890 | // order to perform operations on intermediate results within the chain.
891 | _.tap = function(obj, interceptor) {
892 | interceptor(obj);
893 | return obj;
894 | };
895 |
896 | // Internal recursive comparison function for `isEqual`.
897 | var eq = function(a, b, aStack, bStack) {
898 | // Identical objects are equal. `0 === -0`, but they aren't identical.
899 | // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
900 | if (a === b) return a !== 0 || 1 / a == 1 / b;
901 | // A strict comparison is necessary because `null == undefined`.
902 | if (a == null || b == null) return a === b;
903 | // Unwrap any wrapped objects.
904 | if (a instanceof _) a = a._wrapped;
905 | if (b instanceof _) b = b._wrapped;
906 | // Compare `[[Class]]` names.
907 | var className = toString.call(a);
908 | if (className != toString.call(b)) return false;
909 | switch (className) {
910 | // Strings, numbers, dates, and booleans are compared by value.
911 | case '[object String]':
912 | // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
913 | // equivalent to `new String("5")`.
914 | return a == String(b);
915 | case '[object Number]':
916 | // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
917 | // other numeric values.
918 | return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
919 | case '[object Date]':
920 | case '[object Boolean]':
921 | // Coerce dates and booleans to numeric primitive values. Dates are compared by their
922 | // millisecond representations. Note that invalid dates with millisecond representations
923 | // of `NaN` are not equivalent.
924 | return +a == +b;
925 | // RegExps are compared by their source patterns and flags.
926 | case '[object RegExp]':
927 | return a.source == b.source &&
928 | a.global == b.global &&
929 | a.multiline == b.multiline &&
930 | a.ignoreCase == b.ignoreCase;
931 | }
932 | if (typeof a != 'object' || typeof b != 'object') return false;
933 | // Assume equality for cyclic structures. The algorithm for detecting cyclic
934 | // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
935 | var length = aStack.length;
936 | while (length--) {
937 | // Linear search. Performance is inversely proportional to the number of
938 | // unique nested structures.
939 | if (aStack[length] == a) return bStack[length] == b;
940 | }
941 | // Objects with different constructors are not equivalent, but `Object`s
942 | // from different frames are.
943 | var aCtor = a.constructor, bCtor = b.constructor;
944 | if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) &&
945 | _.isFunction(bCtor) && (bCtor instanceof bCtor))
946 | && ('constructor' in a && 'constructor' in b)) {
947 | return false;
948 | }
949 | // Add the first object to the stack of traversed objects.
950 | aStack.push(a);
951 | bStack.push(b);
952 | var size = 0, result = true;
953 | // Recursively compare objects and arrays.
954 | if (className == '[object Array]') {
955 | // Compare array lengths to determine if a deep comparison is necessary.
956 | size = a.length;
957 | result = size == b.length;
958 | if (result) {
959 | // Deep compare the contents, ignoring non-numeric properties.
960 | while (size--) {
961 | if (!(result = eq(a[size], b[size], aStack, bStack))) break;
962 | }
963 | }
964 | } else {
965 | // Deep compare objects.
966 | for (var key in a) {
967 | if (_.has(a, key)) {
968 | // Count the expected number of properties.
969 | size++;
970 | // Deep compare each member.
971 | if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break;
972 | }
973 | }
974 | // Ensure that both objects contain the same number of properties.
975 | if (result) {
976 | for (key in b) {
977 | if (_.has(b, key) && !(size--)) break;
978 | }
979 | result = !size;
980 | }
981 | }
982 | // Remove the first object from the stack of traversed objects.
983 | aStack.pop();
984 | bStack.pop();
985 | return result;
986 | };
987 |
988 | // Perform a deep comparison to check if two objects are equal.
989 | _.isEqual = function(a, b) {
990 | return eq(a, b, [], []);
991 | };
992 |
993 | // Is a given array, string, or object empty?
994 | // An "empty" object has no enumerable own-properties.
995 | _.isEmpty = function(obj) {
996 | if (obj == null) return true;
997 | if (_.isArray(obj) || _.isString(obj)) return obj.length === 0;
998 | for (var key in obj) if (_.has(obj, key)) return false;
999 | return true;
1000 | };
1001 |
1002 | // Is a given value a DOM element?
1003 | _.isElement = function(obj) {
1004 | return !!(obj && obj.nodeType === 1);
1005 | };
1006 |
1007 | // Is a given value an array?
1008 | // Delegates to ECMA5's native Array.isArray
1009 | _.isArray = nativeIsArray || function(obj) {
1010 | return toString.call(obj) == '[object Array]';
1011 | };
1012 |
1013 | // Is a given variable an object?
1014 | _.isObject = function(obj) {
1015 | return obj === Object(obj);
1016 | };
1017 |
1018 | // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp.
1019 | each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) {
1020 | _['is' + name] = function(obj) {
1021 | return toString.call(obj) == '[object ' + name + ']';
1022 | };
1023 | });
1024 |
1025 | // Define a fallback version of the method in browsers (ahem, IE), where
1026 | // there isn't any inspectable "Arguments" type.
1027 | if (!_.isArguments(arguments)) {
1028 | _.isArguments = function(obj) {
1029 | return !!(obj && _.has(obj, 'callee'));
1030 | };
1031 | }
1032 |
1033 | // Optimize `isFunction` if appropriate.
1034 | if (typeof (/./) !== 'function') {
1035 | _.isFunction = function(obj) {
1036 | return typeof obj === 'function';
1037 | };
1038 | }
1039 |
1040 | // Is a given object a finite number?
1041 | _.isFinite = function(obj) {
1042 | return isFinite(obj) && !isNaN(parseFloat(obj));
1043 | };
1044 |
1045 | // Is the given value `NaN`? (NaN is the only number which does not equal itself).
1046 | _.isNaN = function(obj) {
1047 | return _.isNumber(obj) && obj != +obj;
1048 | };
1049 |
1050 | // Is a given value a boolean?
1051 | _.isBoolean = function(obj) {
1052 | return obj === true || obj === false || toString.call(obj) == '[object Boolean]';
1053 | };
1054 |
1055 | // Is a given value equal to null?
1056 | _.isNull = function(obj) {
1057 | return obj === null;
1058 | };
1059 |
1060 | // Is a given variable undefined?
1061 | _.isUndefined = function(obj) {
1062 | return obj === void 0;
1063 | };
1064 |
1065 | // Shortcut function for checking if an object has a given property directly
1066 | // on itself (in other words, not on a prototype).
1067 | _.has = function(obj, key) {
1068 | return hasOwnProperty.call(obj, key);
1069 | };
1070 |
1071 | // Utility Functions
1072 | // -----------------
1073 |
1074 | // Run Underscore.js in *noConflict* mode, returning the `_` variable to its
1075 | // previous owner. Returns a reference to the Underscore object.
1076 | _.noConflict = function() {
1077 | root._ = previousUnderscore;
1078 | return this;
1079 | };
1080 |
1081 | // Keep the identity function around for default iterators.
1082 | _.identity = function(value) {
1083 | return value;
1084 | };
1085 |
1086 | _.constant = function(value) {
1087 | return function () {
1088 | return value;
1089 | };
1090 | };
1091 |
1092 | _.property = function(key) {
1093 | return function(obj) {
1094 | return obj[key];
1095 | };
1096 | };
1097 |
1098 | // Run a function **n** times.
1099 | _.times = function(n, iterator, context) {
1100 | var accum = Array(Math.max(0, n));
1101 | for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i);
1102 | return accum;
1103 | };
1104 |
1105 | // Return a random integer between min and max (inclusive).
1106 | _.random = function(min, max) {
1107 | if (max == null) {
1108 | max = min;
1109 | min = 0;
1110 | }
1111 | return min + Math.floor(Math.random() * (max - min + 1));
1112 | };
1113 |
1114 | // List of HTML entities for escaping.
1115 | var entityMap = {
1116 | escape: {
1117 | '&': '&',
1118 | '<': '<',
1119 | '>': '>',
1120 | '"': '"',
1121 | "'": '''
1122 | }
1123 | };
1124 | entityMap.unescape = _.invert(entityMap.escape);
1125 |
1126 | // Regexes containing the keys and values listed immediately above.
1127 | var entityRegexes = {
1128 | escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'),
1129 | unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g')
1130 | };
1131 |
1132 | // Functions for escaping and unescaping strings to/from HTML interpolation.
1133 | _.each(['escape', 'unescape'], function(method) {
1134 | _[method] = function(string) {
1135 | if (string == null) return '';
1136 | return ('' + string).replace(entityRegexes[method], function(match) {
1137 | return entityMap[method][match];
1138 | });
1139 | };
1140 | });
1141 |
1142 | // If the value of the named `property` is a function then invoke it with the
1143 | // `object` as context; otherwise, return it.
1144 | _.result = function(object, property) {
1145 | if (object == null) return void 0;
1146 | var value = object[property];
1147 | return _.isFunction(value) ? value.call(object) : value;
1148 | };
1149 |
1150 | // Add your own custom functions to the Underscore object.
1151 | _.mixin = function(obj) {
1152 | each(_.functions(obj), function(name) {
1153 | var func = _[name] = obj[name];
1154 | _.prototype[name] = function() {
1155 | var args = [this._wrapped];
1156 | push.apply(args, arguments);
1157 | return result.call(this, func.apply(_, args));
1158 | };
1159 | });
1160 | };
1161 |
1162 | // Generate a unique integer id (unique within the entire client session).
1163 | // Useful for temporary DOM ids.
1164 | var idCounter = 0;
1165 | _.uniqueId = function(prefix) {
1166 | var id = ++idCounter + '';
1167 | return prefix ? prefix + id : id;
1168 | };
1169 |
1170 | // By default, Underscore uses ERB-style template delimiters, change the
1171 | // following template settings to use alternative delimiters.
1172 | _.templateSettings = {
1173 | evaluate : /<%([\s\S]+?)%>/g,
1174 | interpolate : /<%=([\s\S]+?)%>/g,
1175 | escape : /<%-([\s\S]+?)%>/g
1176 | };
1177 |
1178 | // When customizing `templateSettings`, if you don't want to define an
1179 | // interpolation, evaluation or escaping regex, we need one that is
1180 | // guaranteed not to match.
1181 | var noMatch = /(.)^/;
1182 |
1183 | // Certain characters need to be escaped so that they can be put into a
1184 | // string literal.
1185 | var escapes = {
1186 | "'": "'",
1187 | '\\': '\\',
1188 | '\r': 'r',
1189 | '\n': 'n',
1190 | '\t': 't',
1191 | '\u2028': 'u2028',
1192 | '\u2029': 'u2029'
1193 | };
1194 |
1195 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
1196 |
1197 | // JavaScript micro-templating, similar to John Resig's implementation.
1198 | // Underscore templating handles arbitrary delimiters, preserves whitespace,
1199 | // and correctly escapes quotes within interpolated code.
1200 | _.template = function(text, data, settings) {
1201 | var render;
1202 | settings = _.defaults({}, settings, _.templateSettings);
1203 |
1204 | // Combine delimiters into one regular expression via alternation.
1205 | var matcher = new RegExp([
1206 | (settings.escape || noMatch).source,
1207 | (settings.interpolate || noMatch).source,
1208 | (settings.evaluate || noMatch).source
1209 | ].join('|') + '|$', 'g');
1210 |
1211 | // Compile the template source, escaping string literals appropriately.
1212 | var index = 0;
1213 | var source = "__p+='";
1214 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
1215 | source += text.slice(index, offset)
1216 | .replace(escaper, function(match) { return '\\' + escapes[match]; });
1217 |
1218 | if (escape) {
1219 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
1220 | }
1221 | if (interpolate) {
1222 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
1223 | }
1224 | if (evaluate) {
1225 | source += "';\n" + evaluate + "\n__p+='";
1226 | }
1227 | index = offset + match.length;
1228 | return match;
1229 | });
1230 | source += "';\n";
1231 |
1232 | // If a variable is not specified, place data values in local scope.
1233 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
1234 |
1235 | source = "var __t,__p='',__j=Array.prototype.join," +
1236 | "print=function(){__p+=__j.call(arguments,'');};\n" +
1237 | source + "return __p;\n";
1238 |
1239 | try {
1240 | render = new Function(settings.variable || 'obj', '_', source);
1241 | } catch (e) {
1242 | e.source = source;
1243 | throw e;
1244 | }
1245 |
1246 | if (data) return render(data, _);
1247 | var template = function(data) {
1248 | return render.call(this, data, _);
1249 | };
1250 |
1251 | // Provide the compiled function source as a convenience for precompilation.
1252 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
1253 |
1254 | return template;
1255 | };
1256 |
1257 | // Add a "chain" function, which will delegate to the wrapper.
1258 | _.chain = function(obj) {
1259 | return _(obj).chain();
1260 | };
1261 |
1262 | // OOP
1263 | // ---------------
1264 | // If Underscore is called as a function, it returns a wrapped object that
1265 | // can be used OO-style. This wrapper holds altered versions of all the
1266 | // underscore functions. Wrapped objects may be chained.
1267 |
1268 | // Helper function to continue chaining intermediate results.
1269 | var result = function(obj) {
1270 | return this._chain ? _(obj).chain() : obj;
1271 | };
1272 |
1273 | // Add all of the Underscore functions to the wrapper object.
1274 | _.mixin(_);
1275 |
1276 | // Add all mutator Array functions to the wrapper.
1277 | each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
1278 | var method = ArrayProto[name];
1279 | _.prototype[name] = function() {
1280 | var obj = this._wrapped;
1281 | method.apply(obj, arguments);
1282 | if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0];
1283 | return result.call(this, obj);
1284 | };
1285 | });
1286 |
1287 | // Add all accessor Array functions to the wrapper.
1288 | each(['concat', 'join', 'slice'], function(name) {
1289 | var method = ArrayProto[name];
1290 | _.prototype[name] = function() {
1291 | return result.call(this, method.apply(this._wrapped, arguments));
1292 | };
1293 | });
1294 |
1295 | _.extend(_.prototype, {
1296 |
1297 | // Start chaining a wrapped Underscore object.
1298 | chain: function() {
1299 | this._chain = true;
1300 | return this;
1301 | },
1302 |
1303 | // Extracts the result from a wrapped and chained object.
1304 | value: function() {
1305 | return this._wrapped;
1306 | }
1307 |
1308 | });
1309 |
1310 | // AMD registration happens at the end for compatibility with AMD loaders
1311 | // that may not enforce next-turn semantics on modules. Even though general
1312 | // practice for AMD registration is to be anonymous, underscore registers
1313 | // as a named module because, like jQuery, it is a base library that is
1314 | // popular enough to be bundled in a third party lib, but not be part of
1315 | // an AMD load request. Those cases could generate an error when an
1316 | // anonymous define() is called outside of a loader request.
1317 | if (typeof define === 'function' && define.amd) {
1318 | define('underscore', [], function() {
1319 | return _;
1320 | });
1321 | }
1322 | }).call(this);
1323 |
--------------------------------------------------------------------------------
/public/stylesheets/style.less:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: 'Alegreya Sans', sans-serif;
3 | }
4 | @board-border: #964B00;
5 |
6 | #game-board {
7 | padding: 10px;
8 | display: inline-block;
9 | background-color: @board-border;
10 | border-radius: 4px;
11 |
12 | }
13 | @font-face {
14 | font-family: "Dice";
15 | src: url(../fonts/dice.ttf) format("truetype");
16 | }
17 |
18 | @beize: beige;
19 | .background rect {
20 | fill: @beize;
21 | }
22 | @col:#5C3F39;
23 | @col2: darken(@col, 20%);
24 |
25 | .border rect {
26 | fill: darken(@board-border, 20%);
27 | }
28 | .places {
29 |
30 | polyline:nth-child(even) {
31 | fill: @col2;
32 | }
33 |
34 | polyline:nth-child(odd) {
35 | fill: @col;
36 | }
37 | polyline {
38 | stroke: @col;
39 | stroke-width: 2px;
40 | stroke-style: solid;
41 | }
42 |
43 | }
44 |
45 | .bar rect {
46 | fill: @beize;
47 | stroke: @col;
48 | stroke-width: 2px;
49 | stroke-style: solid;
50 | }
51 |
52 | .home rect {
53 | fill: @beize;
54 | stroke: @col;
55 | stroke-width: 2px;
56 | stroke-style: solid;
57 | }
58 |
59 | #pieces {
60 | padding: 10px;
61 | circle {
62 | fill: lighten(black, 45%);
63 | stroke: black;
64 | stroke-width: 2px;
65 | stroke-style: solid;
66 | r: 25;
67 | transition: cx 1s, cy 1s;
68 | }
69 | circle.red {
70 | fill: lighten(red, 15%);
71 | }
72 | circle.selected {
73 | fill: yellow;
74 | }
75 | circle:hover {
76 | stroke: yellow;
77 | }
78 | }
79 |
80 | a.dice {
81 | color: black;
82 | font-family: "Dice";
83 | font-size: 60px;
84 | margin: 2px;
85 | &.used {
86 | color: lighten(grey, 20%);
87 | }
88 | &:hover {
89 | color: yellow;
90 | cursor: pointer;
91 | }
92 | }
93 | #dice {
94 | margin:8px;
95 | }
96 | #diceroll {
97 | font-size: 20px;
98 | color: white;
99 | }
100 | #playlink {
101 | margin:8px;
102 | font-size: 20px;
103 | color: white;
104 | }
105 |
106 | text {
107 | display:inline-block;
108 | vertical-align:middle;
109 | }
110 | span #chat{
111 | display: inline-block;
112 | }
113 | div #chat{
114 | padding: 10px;
115 | }
116 | #input {
117 | width: 200px;
118 | padding: 5px;
119 | }
120 |
--------------------------------------------------------------------------------
/routes/index.js:
--------------------------------------------------------------------------------
1 |
2 | /*
3 | * GET home page.
4 | */
5 |
6 | exports.index = function(req, res){
7 | res.render('index', { title: 'Express' });
8 | };
9 |
--------------------------------------------------------------------------------
/src/game.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var _ = require('underscore')
3 |
4 | _.any = function(ls){
5 | return _.contains(ls, true);
6 | }
7 |
8 | function Board(redState, blackState, bar, home) {
9 | var bs = typeof blackState !== 'undefined' ? blackState : {};
10 | var rs = typeof redState !== 'undefined' ? redState : {};
11 | blankState = _.object(
12 | _.range(1, 25),
13 | _.times(24,function(){return 0} )
14 | )
15 | this.blackState = _.extend({}, blankState, bs)
16 | this.redState = _.extend({}, blankState, rs)
17 | this.bar = typeof bar !== 'undefined' ? bar : {
18 | black: 0,
19 | red: 0
20 | };
21 | this.home = typeof home !== 'undefined' ? home : {
22 | black: 0,
23 | red: 0
24 | };
25 | }
26 |
27 | String.prototype.opponent = function(){
28 | if (this == 'red'){
29 | return 'black'
30 | } else if (this == 'black'){
31 | return 'red'
32 | }
33 | }
34 |
35 | function Player(color, board) {
36 | this.color = color;
37 | this.board = board;
38 | }
39 |
40 | Player.prototype = {
41 | get ownedPositions(){
42 | var t = this;
43 | var positions = _.filter(
44 | _.map(
45 | _.keys(this.state),
46 | function(i){return parseInt(i)}
47 | ),
48 | function(d){
49 | return t.piecesAt(d) > 0;
50 | }
51 | );
52 | var barPositions = this.bar > 0 ? ['bar'] : [];
53 | return positions.concat(barPositions);
54 | },
55 | get state() {
56 | if (this.color == 'red'){
57 | return this.board.redState;
58 | } else {
59 | return this.board.blackState;
60 | }
61 | },
62 | get opponent(){
63 | return this.board[this.color.opponent()];
64 | },
65 | get bar(){
66 | return this.board.bar[this.color];
67 | },
68 | set bar(val){
69 | this.board.bar[this.color] = val;
70 | },
71 | get home(){
72 | return this.board.home[this.color];
73 | },
74 | set home(val){
75 | this.board.home[this.color] = val;
76 | },
77 | canMovePieceAt: function(pos){
78 | if (pos == 'bar'){
79 | return true;
80 | } else if (pos == 'home') {
81 | return false;
82 | } else {
83 | return this.bar == 0;
84 | }
85 | },
86 | canMoveWith: function(roll){
87 | var that = this
88 | return _.any(_.map(
89 | this.ownedPositions,
90 | function(d){return that.validMove(d, roll)}
91 | ))
92 | },
93 | canMoveToTarget: function(target){
94 | return this.opponent.piecesAt(target) < 2;
95 | },
96 | canBearOff: function(pos, roll){
97 | var nonHomeIndices = this.color == 'red'? _.range(1, 19) : _.range(7, 25);
98 | var nonHomeValues = _.values(_.pick(this.state, nonHomeIndices));
99 | var nonHomeCount = _.reduce(nonHomeValues, function(x, y){return x + y}, 0);
100 | var homeIndices = this.color == 'red'? _.range(19, 25) : _.range(1, 7);
101 | var homeSubBoard = _.pick(this.state, homeIndices);
102 |
103 | var homeValues = [];
104 |
105 | for (var key in homeSubBoard){
106 | var o = homeSubBoard[key];
107 | if (o){
108 | homeValues.push(key);
109 | }
110 |
111 | }
112 | f = this.color == 'red' ? _.min : _.max;
113 | furthestPip = f(homeValues);
114 | requiredRollToHome = this.color == 'red' ? 25 - pos : pos
115 |
116 | return nonHomeCount == 0 && this.bar == 0 && (furthestPip == pos || requiredRollToHome == roll);
117 |
118 | },
119 | placePiece: function(target, roll){
120 | if (this.board.wouldBearOff(target)){
121 | // bearing off
122 | this.home += 1;
123 | } else {
124 | if (this.opponent.piecesAt(target) == 1){
125 | this.opponent.state[target] = 0;
126 | this.opponent.bar += 1;
127 | }
128 | this.state[target] = this.piecesAt(target) + 1;
129 | }
130 | },
131 | liftPiece: function(pos){
132 | if (pos === 'bar'){
133 | this.bar -= 1;
134 | } else {
135 | this.state[pos] -= 1;
136 | }
137 | },
138 | piecesAt: function(pos){
139 | if (pos === 'bar'){
140 | return this.bar
141 | } else {
142 | if (typeof this.state[pos] === "undefined"){
143 | this.state[pos] = 0;
144 | }
145 | return this.state[pos];
146 | }
147 | },
148 | validMove: function(pos, roll){
149 | var target = this.targetPosition(pos, roll);
150 | var canBearOff = this.canBearOff(pos, roll);
151 | var notBearingOff = !this.board.wouldBearOff(target)
152 | var validIfBearOff = notBearingOff || canBearOff;
153 | var canMoveTo = this.canMoveToTarget(target);
154 | var canMoveFrom = this.canMovePieceAt(pos);
155 |
156 | return (
157 | canMoveTo &&
158 | canMoveFrom &&
159 | validIfBearOff
160 | )
161 | },
162 | targetPosition: function(pos, roll){
163 | var p = pos;
164 | if (pos == 'bar'){
165 | p = this.color == 'red' ? 0: 25;
166 | }
167 | if (this.color == 'red'){
168 | return p + roll;
169 | } else if (this.color == 'black'){
170 | return p - roll;
171 | }
172 | },
173 | progressPiece: function(pos, roll){
174 | if (this.validMove(pos, roll)){
175 | var target = this.targetPosition(pos, roll);
176 | this.liftPiece(pos);
177 | this.placePiece(target, roll);
178 | return true;
179 | } else {
180 | return false;
181 | }
182 | }
183 |
184 | }
185 |
186 | Board.prototype = {
187 | get red(){
188 | return new Player('red', this);
189 | },
190 | get black(){
191 | return new Player('black', this);
192 | },
193 | toString: function(){
194 | return 'Red: ' + JSON.stringify(this.red.state) + '\nBlack: ' + JSON.stringify(this.black.state);
195 | },
196 | state: function(){
197 | var redBar = _(this.red.bar).times(function(){return {position: 'bar', color: 'red'}})
198 | var blackBar = _(this.black.bar).times(function(){return {position: 'bar', color: 'black'}})
199 | var redHome= _(this.red.home).times(function(){return {position: 'home', color: 'red'}})
200 | var blackHome = _(this.black.home).times(function(){return {position: 'home', color: 'black'}})
201 |
202 | var pieceGenerator = function(color, count, position){
203 | return _(count).times(
204 | function(){return {position:parseInt(position), color:color}}
205 | )
206 | }
207 | var redPieces = _.flatten(_.map(
208 | this.red.state,
209 | _.partial(pieceGenerator, 'red')
210 | ))
211 |
212 | var blackPieces = _.flatten(_.map(
213 | this.black.state,
214 | _.partial(pieceGenerator, 'black')
215 | ))
216 |
217 | return [].concat(redBar, blackBar, redHome, blackHome, redPieces, blackPieces)
218 | },
219 | owner: function(pos){
220 | if (this.red.piecesAt(pos) > 0){
221 | return this.red;
222 | } else if (this.black.piecesAt(pos) > 0){
223 | return this.black;
224 | }
225 | },
226 | wouldBearOff: function(target){
227 | return (target <= 0 || target > 24);
228 | },
229 | }
230 |
231 | var initialBoard = function() {
232 | return new Board(
233 | {1: 2, 12: 5, 17: 3, 19: 5,},
234 | {24: 2, 13: 5, 8: 3, 6: 5,}
235 | );
236 | }
237 |
238 | module.exports.Board = Board;
239 | module.exports.initialBoard = initialBoard;
240 |
--------------------------------------------------------------------------------
/src/run.js:
--------------------------------------------------------------------------------
1 | var sys = require('sys')
2 | var Board = require('../src/game.js')
3 |
4 | console.log(Board)
5 | var express = require('express');
6 | var app = express();
7 |
8 | app.get('/', function(req, res){
9 | res.send('hello world');
10 | });
11 |
12 | app.listen(3000);
13 |
--------------------------------------------------------------------------------
/src/server.js:
--------------------------------------------------------------------------------
1 | var game = require('./game.js')
2 | var Chance = require('chance');
3 | var _ = require('underscore');
4 |
5 | currentGame = undefined;
6 | currentPlayer = undefined;
7 | currentDice = undefined;
8 | autoDiceRoll = false;
9 |
10 | rollDice = function(){
11 | var x = chance.d6();
12 | var y = chance.d6();
13 | if (x === y){
14 | return [
15 | {'val':x, 'rolled':false},
16 | {'val':y, 'rolled':false},
17 | {'val':x, 'rolled':false},
18 | {'val':y, 'rolled':false},
19 | ]
20 | } else {
21 | return [
22 | {'val':x, 'rolled':false},
23 | {'val':y, 'rolled':false},
24 | ]
25 | }
26 | }
27 | resetDice = function(seed, performAutoDiceRoll){
28 | console.log('Seed', seed )
29 | if (seed === parseInt(seed)){
30 | chance = new Chance(seed);
31 | } else {
32 | chance = new Chance();
33 | }
34 | currentDice = undefined;
35 | autoDiceRoll = typeof performAutoDiceRoll != 'undefined'? performAutoDiceRoll : true;
36 | if (autoDiceRoll){
37 | currentDice = rollDice();
38 | }
39 | }
40 | resetGame = function(){
41 | currentGame = game.initialBoard();
42 | currentPlayer = 'red';
43 | }
44 | newGame = function(seed, performAutoDiceRoll){
45 | resetDice(seed, performAutoDiceRoll);
46 | resetGame();
47 | }
48 |
49 | module.exports.newGame = newGame;
50 |
51 | var ioModule = require('socket.io')
52 | , lessMiddleware = require('less-middleware')
53 | , express = require('express')
54 | , exphbs = require('express3-handlebars')
55 | , http = require('http')
56 | , path = require('path')
57 |
58 | loadApp = function(){
59 | var app = express();
60 | // all environments
61 | app.use(lessMiddleware(__dirname + '/../public'));
62 | app.use(express.static(__dirname + '/../public'));
63 |
64 | app.engine('html', exphbs({defaultLayout: 'main', extname: '.html'}));
65 | app.set('view engine', 'html');
66 |
67 | app.get('/', function (req, res) {
68 | res.render('index');
69 | });
70 |
71 | var logger = require('morgan');
72 | var bodyParser = require('body-parser')
73 | var methodOverride = require('method-override');
74 | var errorHandler = require('errorhandler');
75 | app.use(logger('dev'));
76 | app.use(bodyParser.json());
77 | app.use(bodyParser.urlencoded({extended: true}));
78 | app.use(methodOverride());
79 | //app.use(app.router);
80 | app.use(express.static(path.join(__dirname, '../public')));
81 | // development only
82 | if ('development' == app.get('env')) {
83 | app.use(errorHandler());
84 | }
85 | return app
86 | }
87 |
88 | launchApp = function(app, port){
89 | port = process.env.PORT || port || 5000;
90 | return app.listen(port)
91 | }
92 |
93 | switchControl = function(){
94 | console.log('switching')
95 | currentPlayer = currentPlayer.opponent();
96 | console.log('roll again')
97 | if (autoDiceRoll){
98 | currentDice = rollDice();
99 | } else {
100 | currentDice = undefined;
101 | }
102 | announcePlayer();
103 | }
104 | announceDice = function(){
105 | return io.sockets.emit("dice", {dice:currentDice, playable:canMove()});
106 | }
107 | announcePlayer = function(){
108 | return io.sockets.emit("player", currentPlayer)
109 | }
110 | announcePlayable = function(){
111 | return io.sockets.emit("playable", canMove())
112 | }
113 | announceState = function(){
114 | return io.sockets.emit("status", currentGame.state())
115 | }
116 | performRoll = function(){
117 | console.log("performing roll")
118 | if (typeof currentDice == 'undefined'){
119 | currentDice = rollDice();
120 | }
121 | announceDice();
122 | }
123 | performPass = function(){
124 | console.log('Can move', canMove())
125 | if (!canMove()){
126 | switchControl();
127 | announcePlayer();
128 | announceState();
129 | announceDice();
130 | return true;
131 | } else {
132 | return false;
133 | }
134 |
135 | }
136 | performMove = function(pos, rollIndex){
137 | var selectedDice = currentDice[rollIndex];
138 | var currentPlayerPieceSelected = currentGame.owner(pos).color == currentPlayer;
139 | if (!selectedDice.rolled && currentPlayerPieceSelected){
140 | var roll = selectedDice.val;
141 | var success = currentGame[currentPlayer].progressPiece(pos, roll);
142 | if (success){
143 | currentDice[rollIndex].rolled = true;
144 | }
145 | var incomplete = _.contains(
146 | _.pluck(currentDice, 'rolled'),
147 | false
148 | );
149 | if (!incomplete){
150 | switchControl();
151 | }
152 | }
153 | announceState();
154 | return announceDice();
155 | }
156 |
157 | loadIO = function(server){
158 | var io = require('socket.io').listen(server);
159 | io.sockets.on('connection', function (socket) {
160 | socket.on("pass", performPass);
161 | socket.on("status", announceState);
162 | socket.on("player", announcePlayer);
163 | socket.on("playable", announcePlayable);
164 | socket.on("move", performMove);
165 | socket.on("dice", announceDice);
166 | socket.on("roll", performRoll);
167 | });
168 | return io;
169 | }
170 |
171 | canMove = function(){
172 | if (currentDice){
173 | var dice = _.pluck(_.where(currentDice, {rolled:false}), 'val');
174 | var moveable = _.map(
175 | dice,
176 | function(d){
177 | return currentGame[currentPlayer].canMoveWith(d);
178 | }
179 | );
180 | return _.contains(moveable, true);
181 | } else {
182 | return true
183 | }
184 | }
185 | start = function(port, cb, seed, performAutoDiceRoll){
186 | server = launchApp(loadApp(), port);
187 | io = loadIO(server);
188 | newGame(seed, performAutoDiceRoll);
189 | }
190 |
191 | dropAllClients = function(){
192 | io.sockets.clients().forEach(function(socket){socket.disconnect(true)});
193 | }
194 |
195 | stop = function(cb){
196 | dropAllClients();
197 | server.close();
198 | cb()
199 | }
200 | module.exports.start = start;
201 | module.exports.resetServer = function(seed, performAutoDiceRoll){
202 | dropAllClients();
203 | newGame(seed, performAutoDiceRoll);
204 | }
205 | module.exports.stop = stop;
206 | module.exports.board = function(){return currentGame};
207 | module.exports.dice = function(){return currentDice};
208 | module.exports.setDice = function(d){currentDice = d;};
209 | module.exports.canMove = canMove;
210 | module.exports.player = currentPlayer;
211 | module.exports.performPass = function(){return performPass()};
212 | module.exports.io = function(){return io};
213 |
--------------------------------------------------------------------------------
/test/game.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | var should = require('should')
3 | game = require('../src/game.js')
4 | var Board = game.Board
5 | var initialBoard = game.initialBoard
6 | describe('Board', function(){
7 | describe('#start', function(){
8 | it('should have the correct starting positions for red', function(){
9 | var board = initialBoard();
10 | board.red.piecesAt(1).should.equal(2);
11 | board.red.piecesAt(12).should.equal(5);
12 | board.red.piecesAt(17).should.equal(3);
13 | board.red.piecesAt(19).should.equal(5);
14 | }),
15 | it('should have the correct starting positions for black', function(){
16 | var board = initialBoard();
17 | board.black.piecesAt(24).should.equal(2);
18 | board.black.piecesAt(13).should.equal(5);
19 | board.black.piecesAt(8).should.equal(3);
20 | board.black.piecesAt(6).should.equal(5);
21 | })
22 | }),
23 | describe('#move', function(){
24 | it('should be able to move ', function(){
25 | var board = new Board();
26 | board.redState = {1:1};
27 | board.red.piecesAt(1).should.equal(1);
28 | board.red.progressPiece(1, 1);
29 | board.red.piecesAt(1).should.equal(0);
30 | board.red.piecesAt(2).should.equal(1)
31 | }),
32 | it('should be able to move two points', function(){
33 | var board = new Board();
34 | board.redState = {1:1};
35 | assert.equal(board.red.piecesAt(1), 1);
36 | board.red.progressPiece(1, 2);
37 | assert.equal(board.red.piecesAt(1), 0);
38 | assert.equal(board.red.piecesAt(3), 1);
39 | }),
40 | it('should be able to move splitting a stack', function(){
41 | var board = new Board();
42 | board.redState = {1:2};
43 | board.red.progressPiece(1, 1);
44 | board.red.piecesAt(1).should.equal(1);
45 | board.red.piecesAt(2).should.equal(1);
46 | }),
47 | it('should not be able to move when an opposing stack is blocking', function(){
48 | var board = new Board();
49 | board.blackState = {2:2};
50 | board.redState = {1:1};
51 | board.red.validMove(1, 1).should.not.be.okay;
52 | board.red.canMoveToTarget(2).should.not.be.okay;
53 | board.red.progressPiece(1, 1);
54 | board.red.piecesAt(1).should.equal(1);
55 | board.red.piecesAt(2).should.equal(0);
56 | board.black.piecesAt(2).should.equal(2)
57 | }),
58 | it('should not be an invlid move returns false', function(){
59 | var board = new Board();
60 | board.blackState = {2:2};
61 | board.redState = {1:1};
62 | board.red.progressPiece(1, 1).should.be.false;
63 | }),
64 | it('should be able to determine what positions each player occupies', function(){
65 | var board = new Board();
66 | board.redState = {1:1};
67 | board.blackState = {2:2, 10:2};
68 | board.bar = {red:0 , black: 1};
69 | board.red.ownedPositions.should.be.eql([1]);
70 |
71 | board.black.ownedPositions.should.be.eql([2, 10, 'bar']);
72 | }),
73 | it('should be able to determine if a player has no moves', function(){
74 | var board = new Board();
75 | board.blackState = {2:2, 10:2};
76 | board.redState = {1:2};
77 | board.red.canMoveWith(1).should.be.false;
78 | board.black.canMoveWith(1).should.be.true;
79 | }),
80 | it('should be able to move if off the bar', function(){
81 | var board = new Board();
82 | board.blackState = {};
83 | board.redState = {};
84 | board.bar = {red:0 , black: 1};
85 | board.red.canMoveWith(3).should.be.false;
86 | board.black.canMoveWith(3).should.be.true;
87 | })
88 | it('should be able to move if stack blocking', function(){
89 | var board = new Board();
90 | board.blackState = {4:2, 13:0};
91 | board.redState = {1:2, 10:2};
92 | board.bar = {red:0 , black: 0};
93 | board.red.canMoveWith(3).should.be.true;
94 | board.black.canMoveWith(3).should.be.false;
95 | }),
96 | it('should be able to move if stack blocking from bar', function(){
97 | var board = new Board();
98 | board.blackState = {4:2};
99 | board.redState = {10:2};
100 | board.bar = {red:1, black: 0};
101 | board.red.canMoveWith(4).should.be.false;
102 | })
103 | }),
104 | describe('#player', function(){
105 | it('should be possible from a player string to retreive the opponent', function(){
106 | 'red'.opponent().should.equal('black');
107 | 'black'.opponent().should.equal('red');
108 | should.strictEqual('dave'.opponent(), undefined);
109 | })
110 | })
111 | describe('#validMove', function(){
112 | it('should be invalid to move into a stack of opposing pieces', function(){
113 | var board = new Board();
114 | board.blackState = {2:2};
115 | board.red.canMoveToTarget(2).should.be.false;
116 |
117 | })
118 | it('should be invalid to move into a stack of opposing pieces', function(){
119 | var board = new Board();
120 | board.redState = {2:2};
121 | board.black.canMoveToTarget(2).should.be.false;
122 |
123 | })
124 | it('should be valid to move against a single piece', function(){
125 | var board = new Board();
126 | board.blackState = {2:1};
127 | board.red.canMoveToTarget(2).should.be.true;
128 | })
129 | it('should be valid to move into an empty space', function(){
130 | var board = new Board();
131 | board.blackState = {2:0};
132 | board.red.canMoveToTarget(2).should.be.true;
133 | })
134 | it('should be invalid to move anything but bar moves if available', function(){
135 | var board = new Board();
136 | board.bar = {red:1, black:0};
137 | board.red.canMovePieceAt(2).should.be.false;
138 | board.red.canMovePieceAt('bar').should.be.true;
139 | })
140 | it('should be valid to move anything if bar is free', function(){
141 | var board = new Board();
142 | board.redState = {1:1};
143 | board.bar = {red:0, black:0};
144 | board.red.canMovePieceAt(1).should.be.true;
145 | })
146 | })
147 | describe('#hitting', function(){
148 | it('should be possible to hit a blot', function(){
149 | var board = new Board();
150 | board.blackState = {12:1};
151 | board.redState = {11:1};
152 | board.red.progressPiece(11, 1);
153 | board.red.piecesAt(11).should.equal(0);
154 | board.bar.black.should.equal(1)
155 | })
156 | it('should be possible for black to hit a blot', function(){
157 | var board = new Board();
158 | board.blackState = {12:1};
159 | board.redState = {11:1};
160 | board.black.progressPiece(12, 1);
161 | board.red.piecesAt(11).should.equal(0);
162 | board.black.piecesAt(11).should.equal(1);
163 | board.bar.red.should.equal(1);
164 | })
165 | }),
166 | describe('#bar', function(){
167 | it('should empty at startup', function() {
168 | var board = initialBoard();
169 | assert.equal(board.bar.black, 0);
170 | board.bar.black.should.equal(0);
171 | board.bar.red.should.equal(0);
172 | })
173 | it('should be able to move a red piece off of the bar to create a stack', function(){
174 | var board = new Board();
175 | board.redState = {1:1};
176 | board.bar = {red:1, black:0};
177 | board.red.progressPiece('bar', 1).should.be.true;
178 | board.bar.red.should.equal(0);
179 | board.red.piecesAt(1).should.equal(2)
180 | })
181 | it('should be able to move a red piece off of the bar', function(){
182 | var board = new Board();
183 | board.bar = {red:1, black: 0};
184 | board.red.progressPiece('bar', 1).should.be.true;
185 | board.bar.red.should.equal(0);
186 | board.red.piecesAt(1).should.equal(1);
187 | }),
188 | it('should be able to move a red piece off of the bar using lift', function(){
189 | var board = new Board();
190 | board.bar = {red:1, black: 0};
191 | board.red.progressPiece('bar', 1).should.be.true;
192 | board.bar.red.should.equal(0);
193 | board.red.piecesAt(1).should.equal(1);
194 | }),
195 | it('should be able to move a red piece off of the bar to a different first six point', function(){
196 | var board = new Board();
197 | board.bar = {red: 1, black: 0};
198 | board.red.progressPiece('bar', 6).should.be.true
199 | board.bar.red.should.equal(0);
200 | board.red.piecesAt(6).should.equal(1);
201 | }),
202 | it('should be possible to hit a blot from entering', function(){
203 | var board = new Board();
204 | board.blackState = {1:1};;
205 | board.bar = {red:1, black:0};
206 | board.red.progressPiece('bar', 1).should.be.true;
207 | board.red.piecesAt(1).should.equal(1, 'Pip successfully entered');
208 | board.black.piecesAt(1).should.equal(0, 'Hit pip removed');
209 | board.bar.black.should.equal(1, 'Hit pip on bar');
210 | }),
211 | it('should not be able to pop a piece when a black stack is blocking', function(){
212 | var board = new Board();
213 | board.blackState = {1:2};
214 | board.bar = {red:1, black:0};
215 | board.red.progressPiece('bar', 1).should.be.false;
216 | board.bar.red.should.equal(1);
217 | board.red.piecesAt(1).should.equal(0);
218 | board.black.piecesAt(1).should.equal(2);
219 | }),
220 | it('should be possible to pop a pip from the black bar', function(){
221 | var board = new Board();
222 | board.redState = {19:1};
223 | board.bar = {red:2, black:2};
224 | board.black.progressPiece('bar', 6);
225 | board.black.piecesAt(19).should.equal(1);
226 | board.red.piecesAt(19).should.equal(0);
227 | board.bar.red.should.equal(3);
228 | })
229 | }),
230 | describe('#bearingoff', function(){
231 | /*Once you have moved all of your checkers onto any of the last six points on the board, you may begin bearing off - unless your opponent knocks one of your checkers to the bar, and then you must stop bearing off until you return that checker to one of the last six points.
232 | *
233 | * To bear off, move a piece into the rectangular goal at the end of the board.
234 | * If you can move into the goal using an exact number, and you have no checkers on the bar or points further back than the last 6, then you may bear that checker off. (On the final point, a roll of one is needed to bear off that checker.)
235 | * You may choose to make any other move instead of bearing off, if you are able.
236 | * If you do not have a number that allows you to bear a checker off with an exact move, you may not bear off and must move another checker on the board instead.
237 | * However, If there are no checkers to move because they are all closer to the goal than the number you rolled, you may then bear off with the checker that is farthest away from your goal, even if the number you rolled is too large for an exact move.
238 | */
239 | it('should be able to bear off black at the end of a game', function() {
240 | var board = new Board();
241 | board.blackState = {1:1};
242 | board.black.canBearOff(1, 1).should.be.ok;
243 | board.black.progressPiece(1, 1);
244 | board.black.piecesAt(1).should.equal(0);
245 | board.home.black.should.equal(1);
246 | }),
247 | it('should not be able to bear off black with pieces outside the home zone', function() {
248 | var board = new Board();
249 | board.blackState = {1:1, 8:2};
250 | board.black.canBearOff(1, 1).should.not.be.ok;
251 | }),
252 | it('should not be able to bear off black with a larger move to play', function() {
253 | var board = new Board();
254 | board.blackState = {1:1, 2:2};
255 | board.black.canBearOff(1, 2).should.not.be.ok;
256 | board.black.canBearOff(2, 2).should.be.ok;
257 | }),
258 | it('should not be able to bear off black with a larger move to play, with a large dice roll', function() {
259 | var board = new Board();
260 | board.blackState = {1:1, 2:2};
261 | board.black.canBearOff(1, 6).should.not.be.ok;
262 | board.black.canBearOff(2, 6).should.be.ok;
263 | }),
264 | it('should be able to bear off red at the end of a game', function() {
265 | var board = new Board();
266 | board.redState = {24:1};
267 | board.red.canBearOff(24, 1).should.be.ok;
268 | board.red.progressPiece(24, 1);
269 | board.red.piecesAt(24).should.equal(0);
270 | board.home.red.should.equal(1);
271 | }),
272 | it('should be able to bear off successively', function() {
273 | var board = new Board();
274 | board.redState = {24:2};
275 | board.blackState = {1:2};
276 |
277 | board.black.progressPiece(1, 1);
278 | board.black.piecesAt(1).should.equal(1);
279 | board.black.canBearOff(1, 1).should.be.true;
280 | board.home.black.should.equal(1);
281 |
282 | board.red.progressPiece(24, 1);
283 | board.red.canBearOff(24, 1).should.be.true;
284 | board.red.piecesAt(24).should.equal(1);
285 | board.home.red.should.equal(1);
286 | }),
287 | it('should not be able to bear off if there are pieces outside of the home board', function(){
288 | var board = new Board();
289 | board.redState = {1: 1, 24:1};
290 | board.red.canBearOff(24, 2).should.not.be.ok;
291 | board.red.validMove(24, 2).should.not.be.ok;
292 | }),
293 | it('should not be able to bear off if there are pieces on the bar', function(){
294 | var board = new Board();
295 | board.redState = {1: 0, 24:1};
296 | board.bar.red = 1;
297 | board.red.canBearOff(24, 1).should.not.be.ok;
298 | board.red.progressPiece(24, 1);
299 | board.home.red.should.equal(0);
300 | }),
301 | it('should not be able to bear off a piece when another piece should be moved first', function(){
302 | var board = new Board();
303 | board.redState = {23:1, 24:1};
304 | board.red.validMove(24, 2).should.not.be.ok;
305 | board.red.validMove(23, 2).should.be.ok;
306 | }),
307 | it('should be able to bear off an exact move', function(){
308 | var board = new Board();
309 | board.redState = {22: 1, 23:1, 24:1};
310 | board.red.validMove(24, 2).should.not.be.ok;
311 | board.red.validMove(23, 2).should.be.ok;
312 | board.red.validMove(22, 2).should.be.ok;
313 | }),
314 | it('should not be able to bear off a piece when another piece could be moved without bearing off', function(){
315 | var board = new Board();
316 | board.redState = {21:0, 22:1, 24:1};
317 | board.red.validMove(24, 2).should.not.be.ok;
318 | board.red.validMove(22, 2).should.be.ok;
319 | }),
320 | it('should be able to bear off a piece with a larger number if it is the only piece remaining', function(){
321 | var board = new Board();
322 | board.redState = {21:0, 22:1, 24:1};
323 | board.red.validMove(22, 5).should.be.ok;
324 | }),
325 | it('should not be able to be able to move a piece which is on the home col', function(){
326 | var board = new Board()
327 | board.home = {red: 1, black: 3};
328 | board.red.validMove('home', 1).should.not.be.ok;
329 | })
330 | }),
331 | describe('#display-state', function(){
332 | it('should be able to produce a displayable summary of a board', function(){
333 | var board = new Board();
334 | board.redState = {2:2, 3:1};
335 | board.blackState = {23:1};
336 | board.home = {red: 1, black: 3};
337 | board.bar = {red: 2, black:4};
338 | assert.deepEqual(
339 | board.state(),
340 | [
341 | {position: 'bar', color:'red'},
342 | {position: 'bar', color:'red'},
343 | {position: 'bar', color:'black'},
344 | {position: 'bar', color:'black'},
345 | {position: 'bar', color:'black'},
346 | {position: 'bar', color:'black'},
347 | {position: 'home', color:'red'},
348 | {position: 'home', color:'black'},
349 | {position: 'home', color:'black'},
350 | {position: 'home', color:'black'},
351 | {position: 2, color:'red'},
352 | {position: 2, color:'red'},
353 | {position: 3, color:'red'},
354 | {position: 23, color:'black'}
355 | ]
356 | )
357 | })
358 | })
359 | })
360 |
361 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --require should
2 | --reporter spec
3 | --ui bdd
4 | --recursive
5 | --growl
6 |
--------------------------------------------------------------------------------
/test/server.js:
--------------------------------------------------------------------------------
1 | var assert = require('assert')
2 | , should = require('should')
3 | , _ = require('underscore')
4 | , app_module = require('../src/server.js')
5 | , jQuery = require('jquery')
6 | , should = require('should')
7 | , ioClient= require('socket.io-client')
8 | , Browser = require("zombie")
9 |
10 | var browser = undefined
11 | var socketURL = 'http://0.0.0.0:5000'
12 | var options = {
13 | transports: ['websocket'],
14 | 'force new connection': true
15 | };
16 |
17 | Browser.prototype.keydown = function(targetSelector, keyCode) {
18 | keyCode = keyCode.charCodeAt(0)
19 | var event = this.window.document.createEvent('HTMLEvents');
20 | event.initEvent('keydown', true, true);
21 | event.which = keyCode;
22 | if (targetSelector === this.window){
23 | var target = this.window
24 | } else{
25 | var target = this.window.document.querySelector(targetSelector);
26 | }
27 | target && target.dispatchEvent(event);
28 | };
29 |
30 | waitFor = function(browser, precondition, callback){
31 | var condition = precondition(browser);
32 | if (precondition(browser)){
33 | callback(browser)
34 | return;
35 | }
36 | setTimeout(
37 | function() {
38 | waitFor(browser, precondition, callback)
39 | },
40 | 100
41 | );
42 | }
43 | loadPage = function(cb, skipWaitingDice){
44 | var browser = new Browser();
45 | browser.debug = true;
46 | browser.visit("http://0.0.0.0:5000")
47 | waitFor(
48 | browser,
49 | function(b){
50 | var piecesOK = false;
51 | var diceOK = false;
52 | var pageLoaded = b.success
53 |
54 | if (pageLoaded){
55 | piecesOK = (b.queryAll("circle")|| []).length > 0;
56 | diceOK = (b.queryAll("#dice a") || []).length > 0;
57 | if (skipWaitingDice){
58 | diceOK = true;
59 | }
60 | }
61 | return pageLoaded && diceOK && piecesOK
62 | }, function(){
63 | $ = jQuery(browser.window)
64 | $.fn.d3Click = function () {
65 | this.each(function (i, e) {
66 | var evt = browser.window.document.createEvent("MouseEvents");
67 | evt.initMouseEvent("click", true, true, browser.window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
68 |
69 | e.dispatchEvent(evt);
70 | });
71 | };
72 | $.fn.pieceAt = function(pos, index){
73 | return $('circle[pos="'+pos+'"][index="'+index+'"]').first()
74 | };
75 | $.fn.diceAt = function(pos){
76 | return $('#dice a').eq(pos)
77 | }
78 | if (cb){
79 | cb();
80 | }
81 | }
82 | )
83 | return browser
84 | }
85 | describe('Game', function(){
86 | describe('#launchapp', function(){
87 | it.skip("Should be possible to restart and have a different intial dice", function(done){
88 | require('../app.js')
89 | t = chance.seed === parseInt(chance.seed)
90 | t.should.equal.true;
91 | app_module.stop(done);
92 | })
93 | }),
94 | describe('#spacedicetrigger', function(){
95 | before(function(){
96 | app_module.start(5000);
97 | app_module.io().set('log level', 0);
98 | }),
99 | beforeEach(function(done){
100 | this.timeout(5000);
101 | app_module.resetServer(seed=5, autodiceroll=false);
102 | // setup the browser and jQuery
103 | browser = loadPage(done, skipWaitingDice=true);
104 | }),
105 | after(function(done){
106 | app_module.stop(done);
107 | }),
108 | it("Should be possible to force a player to roll the dice, clicking a link", function(done){
109 | $('#dice a').length.should.equal(1);
110 | $('#dice a').text().should.equal("Perform roll");
111 | $('#playable a').length.should.equal(0)
112 | browser.clickLink("#diceroll");
113 |
114 | // roll should have been performed
115 | waitFor(
116 | browser,
117 | function(){
118 | return $('#dice a').text() == '21'
119 | },
120 | function(){
121 | client = ioClient.connect(socketURL, options);
122 | client.emit("move", 1, 0);
123 | client.emit("move", 1, 1);
124 | waitFor(
125 | browser,
126 | function(){
127 | return $('#dice a').text() == "Perform roll";
128 | },
129 | function(){done();}
130 | )
131 | }
132 | )
133 | }),
134 | it("Cannot pass if you haven't rolled", function(){
135 | app_module.performPass().should.equal.false
136 |
137 | }),
138 | it("Should be possible to force a player to roll the dice, using spacebar", function(done){
139 | browser.keydown(browser.window, ' ');
140 | waitFor(
141 | browser,
142 | function(){
143 | return $('#dice a').text() == '21'
144 | },
145 | function(){done()}
146 | )
147 |
148 | }),
149 | it("Should be able to see a notification that a roll is neccessary", function(){
150 | $('#dice a').text().should.equal("Perform roll")
151 | })
152 | }),
153 | describe('#diceselect', function(){
154 | before(function(){
155 | app_module.start(5000);
156 | app_module.io().set('log level', 0);
157 | }),
158 | beforeEach(function(done){
159 | this.timeout(5000);
160 | app_module.resetServer(seed=5);
161 | // setup the browser and jQuery
162 | browser = loadPage(done);
163 | }),
164 | after(function(done){
165 | app_module.stop(done);
166 | }),
167 | it("should be possible to trigger a roll with a key press", function(done){
168 | $('body').pieceAt(1, 1).d3Click()
169 | browser.keydown(browser.window, '2');
170 |
171 | firstDiceSelected = function(){
172 | return $('body').diceAt(0).hasClass('used')
173 | }
174 | firstDiceSelected().should.be.false;
175 | waitFor(browser, firstDiceSelected, function(){done()});
176 | }),
177 | it("should be possible to trigger a roll with a key press, for none dupe dice", function(done){
178 | $('body').pieceAt(1, 1).d3Click()
179 | browser.keydown(browser.window, '1');
180 | secondDiceSelected = function(){
181 | return $('body').diceAt(1).hasClass('used')
182 | }
183 | waitFor(browser, secondDiceSelected, function(){done()});
184 | }),
185 | it("Shouldn't display a notification for rolling when a roll isn't neccessary", function(){
186 | $('#dice a').text().should.not.equal("Perform roll")
187 | })
188 | }),
189 | describe('#play', function(){
190 | before(function(){
191 | app_module.start(5000);
192 | app_module.io().set('log level', 0);
193 | }),
194 | beforeEach(function(done){
195 | this.timeout(5000);
196 | app_module.resetServer(seed=4);
197 | // setup the browser and jQuery
198 | browser = loadPage(done);
199 | }),
200 | after(function(done){
201 | app_module.stop(done);
202 | }),
203 | it('should be able to access main page', function(){
204 | browser.success.should.be.ok;
205 | }),
206 | it('should be able to see main board', function(){
207 | browser.queryAll("circle").length.should.be.equal(30);
208 | browser.queryAll("#dice a").length.should.be.above(0);
209 | }),
210 | it('should be able to select a selectable piece on the board', function(){
211 | var circle= $('circle[pos="1"][index="1"]').first();
212 | circle.attr('class').should.equal('red');
213 | circle.d3Click();
214 | circle.attr('class').should.equal('red selected');
215 | }),
216 | it('should not be possible to select a piece which is not on the top of its stack', function(){
217 | var circle= $('circle[pos="1"][index="0"]').first();
218 | circle.attr('class').should.equal('red');
219 | circle.d3Click();
220 | circle.attr('class').should.equal('red');
221 | }),
222 | it("should be possible to read who's move it is", function(){
223 | $('#player').text().should.equal('Red');
224 | $('#playable a').length.should.equal(0);
225 | }),
226 | it.skip('should be possible to select any piece on the bar', function(){
227 | }),
228 | it.skip("shouldn't be able to select a piece which isn't yours", function(){
229 | }),
230 | it("should be possible to skip a turn if no piece can be moved", function(done){
231 | this.timeout(5000);
232 | var c = app_module.board();
233 | c.redState = {1:15};
234 | c.blackState = {7:15};
235 | app_module.canMove().should.be.false;
236 | browser = loadPage(function(){
237 | $('#playable a').length.should.equal(1);
238 | $('#playable').text().should.equal('Cannot move - skip turn');
239 | browser.click('#playlink');
240 | browser = loadPage(function(){
241 | $('#playable a').length.should.equal(0);
242 | $('#player').text().should.equal('Black');
243 | $('#dice a').text().should.equal('42');
244 | done();
245 | });
246 | });
247 | }),
248 | it("should be possible to skip a turn if no - server direct", function(done){
249 | this.timeout(5000);
250 | var c = app_module.board();
251 | c.redState = {1:15};
252 | c.blackState = {7:15};
253 | app_module.performPass().should.equal.true
254 | browser = loadPage(function(){
255 | $('#dice a').text().should.equal('42');
256 | $('#playable').text().should.equal('')
257 | app_module.performPass().should.equal.false;
258 | done();
259 | });
260 | }),
261 | it("should be possible to skip a turn if no piece can be moved but only one dice is left", function(done){
262 | this.timeout(5000);
263 | var c = app_module.board();
264 | c.redState = {1:15};
265 | c.blackState = {7:15};
266 | app_module.setDice([{val:3, rolled:true}, {val:6, rolled:false}])
267 | app_module.canMove().should.be.false;
268 | browser = loadPage(function(){
269 | $('#playable a').length.should.equal(1);
270 | $('#playable').text().should.equal('Cannot move - skip turn');
271 | done();
272 | });
273 | }),
274 | it('should be possible to watch a full move complete and have the UI update the the next player', function(done){
275 | client = ioClient.connect(socketURL, options);
276 | checkGameState = function(){
277 | var diceStr = '#dice a';
278 | waitFor(
279 | browser,
280 | function(b){
281 | return $(diceStr).text() == '42'; // dice have updated
282 | },
283 | function(){
284 | // confirm that the UI has updated, new player
285 |
286 | $('#player').text().should.equal('Black')
287 | done();
288 | }
289 | );
290 | }
291 |
292 | checkAfter4Moves = _.after(4, checkGameState);
293 | client.on('status', checkAfter4Moves);
294 | client.emit("move", 1, 0);
295 | client.emit("move", 1, 1);
296 | client.emit("move", 12, 2);
297 | client.emit("move", 12, 3);
298 |
299 | // check the dice have updated to the next set
300 | // check that the player has updated
301 | }),
302 | it('should be possible to move a piece', function(done){
303 | this.timeout(3000)
304 | locationToMoveFrom = 'circle[pos="1"][index="1"]'
305 | locationToMoveTo = 'circle[pos="7"][index="0"]'
306 | diceStr = '#dice a'
307 |
308 | // target should be empty
309 | $(locationToMoveTo).length.should.equal(0)
310 |
311 | var piece = $(locationToMoveFrom).first().d3Click()
312 | var die = $(diceStr).first().d3Click()
313 | $(diceStr).text().should.equal('6666')
314 |
315 | waitFor(
316 | browser,
317 | function(b){return $(locationToMoveFrom).length === 0}, // wait for the refresh
318 | function(){
319 | $(locationToMoveTo).length.should.equal(1) // assert the correct move has happened
320 | $(diceStr).text().should.equal('6666')
321 | $(diceStr).eq(0).hasClass('used').should.be.true
322 | // should not be actionable
323 | $(diceStr).eq(1).hasClass('used').should.be.false
324 | $(diceStr).eq(2).hasClass('used').should.be.false
325 | $(diceStr).eq(3).hasClass('used').should.be.false
326 | done()
327 | }
328 | );
329 | }),
330 | it("should be possible to trigger a roll with a key press, for dupe dice", function(done){
331 | this.timeout(5000);
332 | $('body').pieceAt(1, 1).d3Click()
333 | diceStr = '#dice a';
334 | browser.keydown(browser.window, '6');
335 |
336 | firstDiceSelected = function(){
337 | return $('body').diceAt(0).hasClass('used')
338 | }
339 | secondDiceSelected = function(){
340 | return $('body').diceAt(1).hasClass('used')
341 | }
342 | firstDiceSelected().should.be.false;
343 | waitFor(browser, firstDiceSelected, function(){
344 | $('body').pieceAt(1, 0).d3Click()
345 | browser.keydown(browser.window, '6');
346 | waitFor(browser, secondDiceSelected, function(){done()})
347 | });
348 | })
349 | }),
350 | describe('#view', function(){
351 | before(function(done){
352 | app_module.start(5000);
353 | app_module.io().set('log level', 0);
354 | done();
355 | }),
356 | beforeEach(function(){
357 | app_module.resetServer(seed=4);
358 | }),
359 | after(function(done){
360 | app_module.stop(done);
361 | }),
362 | it('should be possible to connect to a server', function(done){
363 | var client = ioClient.connect(socketURL, options);
364 | client.on("connect", done);
365 | })
366 | it('should be possible to retreive an initial game state', function(done){
367 | var client = ioClient.connect(socketURL, options);
368 | game = require('../src/game.js');
369 |
370 | client.on("connect", function(data){
371 | client.emit("status");
372 | });
373 | client.on("status", function(data){
374 | var positionSummary = _.countBy(data, _.values)
375 | assert.deepEqual(
376 | data,
377 | game.initialBoard().state()
378 | );
379 | positionSummary['1,red'].should.equal(2);
380 | done();
381 | });
382 | }),
383 | it('should be possible to retrieve the current dice roll', function(done){
384 |
385 | var client = ioClient.connect(socketURL, options);
386 | client.on("connect", function(data){
387 | client.emit("dice");
388 | client.on("dice", function(dice){
389 | dice.should.eql(
390 | {dice:[
391 | {"val":6,"rolled":false},
392 | {"val":6,"rolled":false},
393 | {"val":6,"rolled":false},
394 | {"val":6,"rolled":false}
395 | ], playable: true}
396 | )
397 | done();
398 | });
399 | });
400 | }),
401 | it('should be the case that all players are notified about a status change', function(done){
402 | this.timeout(10000);
403 | var client1 = ioClient.connect(socketURL, options);
404 |
405 | client1.on("connect", function(data){
406 | var client2 = ioClient.connect(socketURL, options);
407 |
408 | client2.on("connect", function(data){
409 | // this test will only complete when done has been called twice
410 | success = _.after(2, done)
411 | client2.on("status", function(data){
412 | success();
413 | });
414 | client1.on("status", function(data){
415 | success();
416 | });
417 | client1.emit("move", 1, 2)
418 | });
419 | });
420 | }),
421 | it('should be Red to play first', function(done){
422 | var client = ioClient.connect(socketURL, options);
423 | client.on("connect", function(data){
424 | client.emit("player");
425 | client.on("player", function(player){
426 | player.should.equal('red')
427 | done();
428 | });
429 | });
430 | }),
431 | it('should not be able to use the same die twice', function(done){
432 | var client = ioClient.connect(socketURL, options);
433 | client.on("connect", function(){
434 | client.emit("move", 1, 0);
435 | client.once("status", function(data1){
436 | var pos1 = _.countBy(data1, _.values);
437 | pos1['1,red'].should.equal(1);
438 | pos1['7,red'].should.equal(1);
439 | client.emit("move", 1, 0);
440 |
441 | client.once("status", function(data2){
442 | var pos2 = _.countBy(data2, _.values);
443 | pos2['1,red'].should.equal(1);
444 | pos2['7,red'].should.equal(1);
445 | done();
446 | })
447 | })
448 | })
449 | }),
450 | it('should not be able to move a black piece when it is red to move', function(done){
451 | var client = ioClient.connect(socketURL, options);
452 | client.on("connect", function(){
453 | client.emit("move", 24, 0);
454 | client.on("status", function(board){
455 | var pos = _.countBy(board, _.values);
456 | pos.should.eql(
457 | {
458 | '1,red': 2,
459 | '12,red': 5,
460 | '17,red': 3,
461 | '19,red': 5,
462 | '6,black': 5,
463 | '8,black': 3,
464 | '13,black': 5,
465 | '24,black': 2
466 | }
467 | )
468 | done();
469 | });
470 |
471 | })
472 | }),
473 | it('should be Black to play after a move', function(done){
474 | var client = ioClient.connect(socketURL, options);
475 | client.on("connect", function(data){
476 | client.emit("move", 1, 0)
477 | client.once("status", function(data){
478 | // check the update of the last move
479 | var positionSummary = _.countBy(data, _.values);
480 | positionSummary['1,red'].should.equal(1);
481 | positionSummary['7,red'].should.equal(1);
482 |
483 | // make some more moves
484 | client.emit("move", 1, 1)
485 | client.emit("move", 12, 2)
486 | client.emit("move", 12, 3)
487 |
488 | // check if the player changed
489 | client.on("player", function(player){
490 | player.should.equal('black');
491 | done();
492 | });
493 |
494 |
495 | });
496 | });
497 | }),
498 | it('should roll dice between moves completed', function(done){
499 | var client = ioClient.connect(socketURL, options);
500 | // 6,6,6,6 - move(1,6) RED
501 | // 4,2 - move(13,4) BLACK
502 | // 6,6,6,6 - move(24,6) RED
503 | // 5,4 - move(9, 5) BLACK
504 | // 5,4 - move(7, 4) RED
505 | // 2,1 - move(2, 1) BLACK
506 | // 6,2 ->
507 | this.timeout(10000);
508 | var i = 0
509 | doneAfter5 = _.after(5, done)
510 | diceCallback = function(dice){
511 | var targets = [
512 | {dice:[{'val':6, 'rolled':true}, {'val':6, 'rolled':false}, {'val':6, 'rolled':false}, {'val':6, 'rolled':false}], playable:true},
513 | {dice:[{'val':6, 'rolled':true}, {'val':6, 'rolled':true}, {'val':6, 'rolled':false}, {'val':6, 'rolled':false}], playable:true},
514 | {dice:[{'val':6, 'rolled':true}, {'val':6, 'rolled':true}, {'val':6, 'rolled':true}, {'val':6, 'rolled':false}], playable:true},
515 | {dice:[{'val':4, 'rolled':false},{'val':2, 'rolled':false}], playable:true },
516 | {dice:[{'val':4, 'rolled':false},{'val':2, 'rolled':true} ], playable:true }
517 | ]
518 | dice.should.eql(targets[i])
519 | i += 1;
520 | doneAfter5()
521 | }
522 | client.on("dice", diceCallback)
523 | client.on("connect", function(data){
524 | client.emit("move", 1, 0);
525 | client.emit("move", 1, 1);
526 | client.emit("move", 12, 2);
527 | client.emit("move", 12, 3);
528 | client.emit("move", 13, 1);
529 | });
530 | }),
531 | it('should make no dice change if the move was invalid', function(done){
532 | var client = ioClient.connect(socketURL, options);
533 | client.on("connect", function(){
534 | client.emit("move", 19, 0);
535 | client.on("dice", function(dice){
536 | dice.should.eql(
537 | {
538 | dice:[
539 | {'val':6, 'rolled':false},
540 | {'val':6, 'rolled':false},
541 | {'val':6, 'rolled':false},
542 | {'val':6, 'rolled':false}
543 | ],
544 | playable: true
545 | }
546 | )
547 | done();
548 | })
549 | })
550 | }),
551 | it('should be possible determine that a move can be made', function(){
552 | var c = app_module.board();
553 | c.redState = {2:15};
554 | c.blackState = {7:15};
555 | console.log(app_module.board());
556 | console.log(app_module.dice());
557 | app_module.canMove().should.be.true;
558 | })
559 | it('should be possible determine that no move can be made', function(){
560 | var c = app_module.board();
561 | c.redState = {1:15};
562 | c.blackState = {7:15};
563 | console.log(app_module.board());
564 | console.log(app_module.dice());
565 | app_module.canMove().should.be.false;
566 | })
567 | it.skip('should be possible to capture Black piece', function(done){
568 | var client = ioClient.connect(socketURL, options);
569 | display = function(data){console.log(data)}
570 | client.on('connect', function(){
571 | client.emit("move", 1, 6)
572 | client.once("status", function(data){
573 | client.emit("move", 13, 6)
574 | client.once("status", function(data){
575 | client.emit("move", 1, 6)
576 | client.once("status", function(data){
577 | done();
578 | })
579 | })
580 | })
581 | })
582 | })
583 | })
584 | })
585 |
--------------------------------------------------------------------------------
/views/index.html:
--------------------------------------------------------------------------------
1 |