From 8fc622b0217bdce0a17683123511810ed1d3d487 Mon Sep 17 00:00:00 2001 From: Lakhan Mandloi Date: Thu, 8 Aug 2019 13:07:10 +0530 Subject: [PATCH] Issue #SB-13915 task: Fork and cleanup code --- .editorconfig | 14 + angular.json | 137 + demo/src/api-docs.ts | 3672 ++++++ demo/src/app/app.component.html | 44 + demo/src/app/app.component.ts | 43 + demo/src/app/app.module.ts | 62 + demo/src/app/app.routing.ts | 56 + .../components/accordion/accordion.module.ts | 88 + .../demos/basic/accordion-basic.html | 35 + .../demos/basic/accordion-basic.module.ts | 13 + .../accordion/demos/basic/accordion-basic.ts | 8 + .../demos/config/accordion-config.html | 24 + .../demos/config/accordion-config.module.ts | 13 + .../demos/config/accordion-config.ts | 15 + .../demos/header/accordion-header.html | 56 + .../demos/header/accordion-header.module.ts | 13 + .../demos/header/accordion-header.ts | 9 + .../accordion-preventchange.html | 32 + .../accordion-preventchange.module.ts | 13 + .../preventchange/accordion-preventchange.ts | 19 + .../demos/static/accordion-static.html | 35 + .../demos/static/accordion-static.module.ts | 13 + .../demos/static/accordion-static.ts | 8 + .../demos/toggle/accordion-toggle.html | 28 + .../demos/toggle/accordion-toggle.module.ts | 13 + .../demos/toggle/accordion-toggle.ts | 8 + demo/src/app/components/alert/alert.module.ts | 119 + .../alert/demos/basic/alert-basic.html | 5 + .../alert/demos/basic/alert-basic.module.ts | 13 + .../alert/demos/basic/alert-basic.ts | 5 + .../demos/closeable/alert-closeable.html | 6 + .../demos/closeable/alert-closeable.module.ts | 13 + .../alert/demos/closeable/alert-closeable.ts | 54 + .../alert/demos/config/alert-config.html | 5 + .../alert/demos/config/alert-config.module.ts | 13 + .../alert/demos/config/alert-config.ts | 18 + .../alert/demos/custom/alert-custom.html | 3 + .../alert/demos/custom/alert-custom.module.ts | 13 + .../alert/demos/custom/alert-custom.ts | 14 + .../demos/selfclosing/alert-selfclosing.html | 14 + .../selfclosing/alert-selfclosing.module.ts | 13 + .../demos/selfclosing/alert-selfclosing.ts | 27 + .../app/components/buttons/buttons.module.ts | 70 + .../demos/checkbox/buttons-checkbox.html | 13 + .../demos/checkbox/buttons-checkbox.module.ts | 14 + .../demos/checkbox/buttons-checkbox.ts | 13 + .../buttons-checkbox-reactive.module.ts | 14 + .../buttons-checkboxreactive.html | 15 + .../buttons-checkboxreactive.ts | 20 + .../buttons/demos/radio/buttons-radio.html | 13 + .../demos/radio/buttons-radio.module.ts | 14 + .../buttons/demos/radio/buttons-radio.ts | 9 + .../buttons-radio-reactive.module.ts | 14 + .../radioreactive/buttons-radioreactive.html | 15 + .../radioreactive/buttons-radioreactive.ts | 18 + .../components/carousel/carousel.module.ts | 70 + .../carousel/demos/basic/carousel-basic.html | 29 + .../demos/basic/carousel-basic.module.ts | 13 + .../carousel/demos/basic/carousel-basic.ts | 6 + .../demos/config/carousel-config.html | 38 + .../demos/config/carousel-config.module.ts | 13 + .../carousel/demos/config/carousel-config.ts | 19 + .../demos/navigation/carousel-navigation.html | 18 + .../navigation/carousel-navigation.module.ts | 13 + .../demos/navigation/carousel-navigation.ts | 19 + .../carousel/demos/pause/carousel-pause.html | 30 + .../demos/pause/carousel-pause.module.ts | 14 + .../carousel/demos/pause/carousel-pause.ts | 34 + .../components/collapse/collapse.module.ts | 43 + .../collapse/demos/basic/collapse-basic.html | 13 + .../demos/basic/collapse-basic.module.ts | 13 + .../collapse/demos/basic/collapse-basic.ts | 9 + .../datepicker-calendars.component.ts | 113 + .../datepicker/datepicker.module.ts | 162 + .../demos/adapter/datepicker-adapter.html | 38 + .../demos/adapter/datepicker-adapter.ts | 20 + .../adapter/datepicker-adpater.module.ts | 14 + .../demos/basic/datepicker-basic.html | 14 + .../demos/basic/datepicker-basic.module.ts | 14 + .../demos/basic/datepicker-basic.ts | 19 + .../demos/config/datepicker-config.html | 3 + .../demos/config/datepicker-config.module.ts | 14 + .../demos/config/datepicker-config.ts | 24 + .../demos/customday/datepicker-customday.html | 20 + .../customday/datepicker-customday.module.ts | 14 + .../demos/customday/datepicker-customday.ts | 36 + .../demos/disabled/datepicker-disabled.html | 9 + .../disabled/datepicker-disabled.module.ts | 14 + .../demos/disabled/datepicker-disabled.ts | 16 + .../datepicker-footer-template.module.ts | 14 + .../datepicker-footertemplate.html | 19 + .../datepicker-footertemplate.ts | 13 + .../demos/hebrew/datepicker-hebrew.html | 24 + .../demos/hebrew/datepicker-hebrew.module.ts | 14 + .../demos/hebrew/datepicker-hebrew.ts | 59 + .../demos/i18n/datepicker-i18n.html | 12 + .../demos/i18n/datepicker-i18n.module.ts | 14 + .../datepicker/demos/i18n/datepicker-i18n.ts | 49 + .../datepicker-islamic-civil.module.ts | 14 + .../islamiccivil/datepicker-islamiccivil.html | 17 + .../islamiccivil/datepicker-islamiccivil.ts | 47 + .../datepicker-islamic-umalqura.module.ts | 14 + .../datepicker-islamicumalqura.html | 17 + .../datepicker-islamicumalqura.ts | 47 + .../demos/jalali/datepicker-jalali.html | 16 + .../demos/jalali/datepicker-jalali.module.ts | 14 + .../demos/jalali/datepicker-jalali.ts | 34 + .../demos/multiple/datepicker-multiple.html | 47 + .../multiple/datepicker-multiple.module.ts | 14 + .../demos/multiple/datepicker-multiple.ts | 19 + .../demos/popup/datepicker-popup.html | 14 + .../demos/popup/datepicker-popup.module.ts | 14 + .../demos/popup/datepicker-popup.ts | 9 + .../datepicker-position-target.module.ts | 14 + .../datepicker-positiontarget.html | 28 + .../datepicker-positiontarget.ts | 11 + .../demos/range/datepicker-range.html | 20 + .../demos/range/datepicker-range.module.ts | 13 + .../demos/range/datepicker-range.ts | 61 + .../datepicker-overview.component.html | 371 + .../overview/datepicker-overview.component.ts | 186 + .../datepicker-overview-demo.component.ts | 154 + .../dropdown/demos/basic/dropdown-basic.html | 23 + .../demos/basic/dropdown-basic.module.ts | 13 + .../dropdown/demos/basic/dropdown-basic.ts | 8 + .../demos/config/dropdown-config.html | 10 + .../demos/config/dropdown-config.module.ts | 13 + .../dropdown/demos/config/dropdown-config.ts | 15 + .../dropdown/demos/form/dropdown-form.html | 29 + .../demos/form/dropdown-form.module.ts | 13 + .../dropdown/demos/form/dropdown-form.ts | 8 + .../demos/manual/dropdown-manual.html | 13 + .../demos/manual/dropdown-manual.module.ts | 13 + .../dropdown/demos/manual/dropdown-manual.ts | 8 + .../demos/navbar/dropdown-navbar.html | 69 + .../demos/navbar/dropdown-navbar.module.ts | 14 + .../dropdown/demos/navbar/dropdown-navbar.ts | 9 + .../dropdown/demos/split/dropdown-split.html | 44 + .../demos/split/dropdown-split.module.ts | 13 + .../dropdown/demos/split/dropdown-split.ts | 8 + .../components/dropdown/dropdown.module.ts | 88 + .../modal/demos/basic/modal-basic.html | 30 + .../modal/demos/basic/modal-basic.module.ts | 13 + .../modal/demos/basic/modal-basic.ts | 31 + .../demos/component/modal-component.html | 4 + .../demos/component/modal-component.module.ts | 14 + .../modal/demos/component/modal-component.ts | 38 + .../modal/demos/config/modal-config.html | 16 + .../modal/demos/config/modal-config.module.ts | 13 + .../modal/demos/config/modal-config.ts | 21 + .../modal/demos/focus/modal-focus.html | 17 + .../modal/demos/focus/modal-focus.module.ts | 18 + .../modal/demos/focus/modal-focus.ts | 72 + .../modal/demos/options/modal-options.html | 54 + .../demos/options/modal-options.module.ts | 13 + .../modal/demos/options/modal-options.ts | 51 + .../modal/demos/stacked/modal-stacked.html | 1 + .../demos/stacked/modal-stacked.module.ts | 18 + .../modal/demos/stacked/modal-stacked.ts | 61 + demo/src/app/components/modal/modal.module.ts | 88 + .../demos/advanced/pagination-advanced.html | 12 + .../advanced/pagination-advanced.module.ts | 13 + .../demos/advanced/pagination-advanced.ts | 9 + .../demos/basic/pagination-basic.html | 12 + .../demos/basic/pagination-basic.module.ts | 13 + .../demos/basic/pagination-basic.ts | 9 + .../demos/config/pagination-config.html | 2 + .../demos/config/pagination-config.module.ts | 13 + .../demos/config/pagination-config.ts | 17 + .../pagination-customization.html | 9 + .../pagination-customization.module.ts | 13 + .../customization/pagination-customization.ts | 13 + .../demos/disabled/pagination-disabled.html | 6 + .../disabled/pagination-disabled.module.ts | 13 + .../demos/disabled/pagination-disabled.ts | 15 + .../demos/justify/pagination-justify.html | 6 + .../justify/pagination-justify.module.ts | 13 + .../demos/justify/pagination-justify.ts | 9 + .../demos/size/pagination-size.html | 3 + .../demos/size/pagination-size.module.ts | 13 + .../pagination/demos/size/pagination-size.ts | 9 + .../pagination-overview.component.html | 86 + .../overview/pagination-overview.component.ts | 70 + .../pagination/pagination.module.ts | 106 + .../demos/autoclose/popover-autoclose.html | 39 + .../autoclose/popover-autoclose.module.ts | 13 + .../demos/autoclose/popover-autoclose.ts | 9 + .../popover/demos/basic/popover-basic.html | 19 + .../demos/basic/popover-basic.module.ts | 13 + .../popover/demos/basic/popover-basic.ts | 8 + .../popover/demos/config/popover-config.html | 4 + .../demos/config/popover-config.module.ts | 13 + .../popover/demos/config/popover-config.ts | 15 + .../demos/container/popover-container.html | 16 + .../container/popover-container.module.ts | 13 + .../demos/container/popover-container.ts | 9 + .../popover-custom-class.module.ts | 13 + .../customclass/popover-customclass.html | 8 + .../demos/customclass/popover-customclass.ts | 18 + .../popover/demos/delay/popover-delay.html | 16 + .../demos/delay/popover-delay.module.ts | 13 + .../popover/demos/delay/popover-delay.ts | 8 + .../tplcontent/popover-tpl-content.module.ts | 13 + .../demos/tplcontent/popover-tplcontent.html | 10 + .../demos/tplcontent/popover-tplcontent.ts | 9 + .../popover-tpl-with-context.module.ts | 13 + .../popover-tplwithcontext.html | 27 + .../tplwithcontext/popover-tplwithcontext.ts | 17 + .../demos/triggers/popover-triggers.html | 19 + .../demos/triggers/popover-triggers.module.ts | 13 + .../demos/triggers/popover-triggers.ts | 8 + .../demos/visibility/popover-visibility.html | 13 + .../visibility/popover-visibility.module.ts | 13 + .../demos/visibility/popover-visibility.ts | 18 + .../app/components/popover/popover.module.ts | 124 + .../demos/basic/progressbar-basic.html | 4 + .../demos/basic/progressbar-basic.module.ts | 13 + .../demos/basic/progressbar-basic.ts | 13 + .../demos/config/progressbar-config.html | 5 + .../demos/config/progressbar-config.module.ts | 13 + .../demos/config/progressbar-config.ts | 18 + .../demos/height/progressbar-height.html | 4 + .../demos/height/progressbar-height.module.ts | 13 + .../demos/height/progressbar-height.ts | 14 + .../demos/labels/progressbar-labels.html | 4 + .../demos/labels/progressbar-labels.module.ts | 13 + .../demos/labels/progressbar-labels.ts | 13 + .../progressbar-show-value.module.ts | 13 + .../showvalue/progressbar-showvalue.html | 4 + .../demos/showvalue/progressbar-showvalue.ts | 13 + .../demos/striped/progressbar-striped.html | 4 + .../striped/progressbar-striped.module.ts | 13 + .../demos/striped/progressbar-striped.ts | 8 + .../progressbar/progressbar.module.ts | 88 + .../rating/demos/basic/rating-basic.html | 3 + .../rating/demos/basic/rating-basic.module.ts | 13 + .../rating/demos/basic/rating-basic.ts | 9 + .../rating/demos/config/rating-config.html | 3 + .../demos/config/rating-config.module.ts | 13 + .../rating/demos/config/rating-config.ts | 15 + .../rating/demos/decimal/rating-decimal.html | 14 + .../demos/decimal/rating-decimal.module.ts | 13 + .../rating/demos/decimal/rating-decimal.ts | 26 + .../rating/demos/events/rating-events.html | 9 + .../demos/events/rating-events.module.ts | 13 + .../rating/demos/events/rating-events.ts | 11 + .../rating/demos/form/rating-form.html | 16 + .../rating/demos/form/rating-form.module.ts | 14 + .../rating/demos/form/rating-form.ts | 18 + .../demos/template/rating-template.html | 9 + .../demos/template/rating-template.module.ts | 13 + .../rating/demos/template/rating-template.ts | 24 + .../app/components/rating/rating.module.ts | 88 + .../api-docs/api-docs-badge.component.ts | 37 + .../api-docs/api-docs-class.component.html | 82 + .../api-docs/api-docs-class.component.ts | 39 + .../api-docs/api-docs-config.component.html | 40 + .../api-docs/api-docs-config.component.ts | 34 + .../shared/api-docs/api-docs.component.html | 108 + .../shared/api-docs/api-docs.component.ts | 77 + .../shared/api-docs/api-docs.model.ts | 55 + .../app/components/shared/api-docs/index.ts | 4 + .../shared/api-page/api.component.ts | 54 + demo/src/app/components/shared/demo-list.ts | 40 + .../shared/examples-page/demo.component.html | 62 + .../shared/examples-page/demo.component.ts | 59 + .../examples-page/examples.component.ts | 40 + demo/src/app/components/shared/index.ts | 37 + .../app/components/shared/overview/index.ts | 3 + .../overview/overview-section.component.ts | 24 + .../shared/overview/overview.directive.ts | 7 + .../components/shared/overview/overview.ts | 8 + .../table/demos/basic/table-basic.html | 23 + .../table/demos/basic/table-basic.module.ts | 14 + .../table/demos/basic/table-basic.ts | 44 + .../table/demos/complete/countries.ts | 95 + .../table/demos/complete/country.service.ts | 107 + .../table/demos/complete/country.ts | 7 + .../demos/complete/sortable.directive.ts | 29 + .../table/demos/complete/table-complete.html | 50 + .../demos/complete/table-complete.module.ts | 22 + .../table/demos/complete/table-complete.ts | 34 + .../demos/filtering/table-filtering.html | 29 + .../demos/filtering/table-filtering.module.ts | 21 + .../table/demos/filtering/table-filtering.ts | 67 + .../demos/pagination/table-pagination.html | 34 + .../pagination/table-pagination.module.ts | 15 + .../demos/pagination/table-pagination.ts | 107 + .../table/demos/sortable/table-sortable.html | 23 + .../demos/sortable/table-sortable.module.ts | 14 + .../table/demos/sortable/table-sortable.ts | 101 + .../demo/table-overview-demo.component.ts | 58 + .../overview/table-overview.component.html | 67 + .../overview/table-overview.component.ts | 25 + demo/src/app/components/table/table.module.ts | 143 + .../tabset/demos/basic/tabset-basic.html | 26 + .../tabset/demos/basic/tabset-basic.module.ts | 13 + .../tabset/demos/basic/tabset-basic.ts | 7 + .../tabset/demos/config/tabset-config.html | 12 + .../demos/config/tabset-config.module.ts | 13 + .../tabset/demos/config/tabset-config.ts | 15 + .../tabset/demos/justify/tabset-justify.html | 44 + .../demos/justify/tabset-justify.module.ts | 14 + .../tabset/demos/justify/tabset-justify.ts | 9 + .../demos/orientation/tabset-orientation.html | 35 + .../orientation/tabset-orientation.module.ts | 14 + .../demos/orientation/tabset-orientation.ts | 9 + .../tabset/demos/pills/tabset-pills.html | 26 + .../tabset/demos/pills/tabset-pills.module.ts | 13 + .../tabset/demos/pills/tabset-pills.ts | 7 + .../tabset-prevent-change.module.ts | 13 + .../preventchange/tabset-preventchange.html | 25 + .../preventchange/tabset-preventchange.ts | 14 + .../demos/selectbyid/tabset-selectbyid.html | 25 + .../selectbyid/tabset-selectbyid.module.ts | 13 + .../demos/selectbyid/tabset-selectbyid.ts | 8 + .../app/components/tabset/tabset.module.ts | 97 + .../demos/adapter/timepicker-adapter.html | 6 + .../adapter/timepicker-adapter.module.ts | 14 + .../demos/adapter/timepicker-adapter.ts | 43 + .../demos/basic/timepicker-basic.html | 3 + .../demos/basic/timepicker-basic.module.ts | 14 + .../demos/basic/timepicker-basic.ts | 9 + .../demos/config/timepicker-config.html | 3 + .../demos/config/timepicker-config.module.ts | 14 + .../demos/config/timepicker-config.ts | 18 + .../demos/i18n/timepicker-i18n.html | 11 + .../demos/i18n/timepicker-i18n.module.ts | 15 + .../timepicker/demos/i18n/timepicker-i18n.ts | 34 + .../demos/meridian/timepicker-meridian.html | 6 + .../meridian/timepicker-meridian.module.ts | 14 + .../demos/meridian/timepicker-meridian.ts | 14 + .../demos/seconds/timepicker-seconds.html | 6 + .../seconds/timepicker-seconds.module.ts | 14 + .../demos/seconds/timepicker-seconds.ts | 15 + .../demos/spinners/timepicker-spinners.html | 7 + .../spinners/timepicker-spinners.module.ts | 14 + .../demos/spinners/timepicker-spinners.ts | 14 + .../demos/steps/timepicker-steps.html | 19 + .../demos/steps/timepicker-steps.module.ts | 14 + .../demos/steps/timepicker-steps.ts | 13 + .../validation/timepicker-validation.html | 14 + .../timepicker-validation.module.ts | 14 + .../demos/validation/timepicker-validation.ts | 26 + .../timepicker/timepicker.module.ts | 115 + .../demos/closeable/toast-closeable.html | 5 + .../demos/closeable/toast-closeable.module.ts | 10 + .../toast/demos/closeable/toast-closeable.ts | 12 + .../custom-header/toast-custom-header.html | 8 + .../toast-custom-header.module.ts | 13 + .../custom-header/toast-custom-header.ts | 4 + .../howto-global/toast-global.component.html | 11 + .../howto-global/toast-global.component.ts | 20 + .../demos/howto-global/toast-global.module.ts | 13 + .../toast/demos/howto-global/toast-service.ts | 14 + .../toasts-container.component.ts | 29 + .../toast/demos/inline/toast-inline.html | 10 + .../toast/demos/inline/toast-inline.module.ts | 8 + .../toast/demos/inline/toast-inline.ts | 4 + .../toast-prevent-autohide.html | 24 + .../toast-prevent-autohide.module.ts | 13 + .../toast-prevent-autohide.ts | 8 + .../overview/toast-overview.component.html | 98 + .../overview/toast-overview.component.ts | 104 + demo/src/app/components/toast/toast.module.ts | 103 + .../demos/autoclose/tooltip-autoclose.html | 41 + .../autoclose/tooltip-autoclose.module.ts | 14 + .../demos/autoclose/tooltip-autoclose.ts | 9 + .../tooltip/demos/basic/tooltip-basic.html | 12 + .../demos/basic/tooltip-basic.module.ts | 14 + .../tooltip/demos/basic/tooltip-basic.ts | 8 + .../tooltip/demos/config/tooltip-config.html | 3 + .../demos/config/tooltip-config.module.ts | 14 + .../tooltip/demos/config/tooltip-config.ts | 15 + .../demos/container/tooltip-container.html | 16 + .../container/tooltip-container.module.ts | 14 + .../demos/container/tooltip-container.ts | 9 + .../tooltip-custom-class.module.ts | 14 + .../customclass/tooltip-customclass.html | 8 + .../demos/customclass/tooltip-customclass.ts | 18 + .../tooltip/demos/delay/tooltip-delay.html | 15 + .../demos/delay/tooltip-delay.module.ts | 14 + .../tooltip/demos/delay/tooltip-delay.ts | 8 + .../tplcontent/tooltip-tpl-content.module.ts | 14 + .../demos/tplcontent/tooltip-tplcontent.html | 9 + .../demos/tplcontent/tooltip-tplcontent.ts | 9 + .../tooltip-tpl-with-context.module.ts | 13 + .../tooltip-tplwithcontext.html | 26 + .../tplwithcontext/tooltip-tplwithcontext.ts | 17 + .../demos/triggers/tooltip-triggers.html | 19 + .../demos/triggers/tooltip-triggers.module.ts | 14 + .../demos/triggers/tooltip-triggers.ts | 8 + .../app/components/tooltip/tooltip.module.ts | 115 + .../demos/basic/typeahead-basic.html | 11 + .../demos/basic/typeahead-basic.module.ts | 14 + .../typeahead/demos/basic/typeahead-basic.ts | 30 + .../demos/config/typeahead-config.html | 5 + .../demos/config/typeahead-config.module.ts | 14 + .../demos/config/typeahead-config.ts | 36 + .../demos/focus/typeahead-focus.html | 23 + .../demos/focus/typeahead-focus.module.ts | 14 + .../typeahead/demos/focus/typeahead-focus.ts | 37 + .../demos/format/typeahead-format.html | 6 + .../demos/format/typeahead-format.module.ts | 14 + .../demos/format/typeahead-format.ts | 31 + .../typeahead/demos/http/typeahead-http.html | 19 + .../demos/http/typeahead-http.module.ts | 15 + .../typeahead/demos/http/typeahead-http.ts | 59 + .../demos/template/typeahead-template.html | 14 + .../template/typeahead-template.module.ts | 14 + .../demos/template/typeahead-template.ts | 78 + .../components/typeahead/typeahead.module.ts | 88 + demo/src/app/default/default.component.html | 23 + demo/src/app/default/default.component.ts | 10 + demo/src/app/default/index.ts | 1 + .../getting-started.component.html | 139 + .../getting-started.component.ts | 51 + .../positioning/positioning.component.html | 125 + .../positioning/positioning.component.ts | 59 + demo/src/app/shared/analytics/analytics.ts | 42 + .../app/shared/code/code-highlight.service.ts | 20 + demo/src/app/shared/code/code.component.ts | 24 + demo/src/app/shared/code/snippet.ts | 36 + .../component-wrapper.component.html | 83 + .../component-wrapper.component.ts | 90 + .../app/shared/fragment/fragment.directive.ts | 12 + .../src/app/shared/icons/icons.component.html | 10 + demo/src/app/shared/icons/icons.component.ts | 17 + demo/src/app/shared/index.ts | 32 + .../page-wrapper/page-header.component.ts | 22 + .../page-wrapper/page-wrapper.component.html | 66 + .../page-wrapper/page-wrapper.component.ts | 26 + .../shared/side-nav/side-nav.component.html | 21 + .../app/shared/side-nav/side-nav.component.ts | 21 + demo/src/browserslist | 7 + demo/src/docs.json | 1 + demo/src/environments/environment.prod.ts | 7 + demo/src/environments/environment.ts | 20 + demo/src/environments/versions.ts | 8 + demo/src/main.ts | 12 + demo/src/polyfills.ts | 38 + demo/src/public/img/favicon.ico | Bin 0 -> 15086 bytes demo/src/public/img/github.svg | 1 + demo/src/public/img/link-symbol.svg | 3 + demo/src/public/img/logo-stack.png | Bin 0 -> 57958 bytes demo/src/public/img/logo-stack.svg | 39 + demo/src/public/img/logo.svg | 11 + demo/src/public/img/ngb-logo.png | Bin 0 -> 6252 bytes demo/src/public/img/ngb-logo.svg | 12 + demo/src/public/img/stackblitz-icon.svg | 5 + demo/src/public/img/sunbird-logo.png | Bin 0 -> 4410 bytes demo/src/public/index.html | 33 + demo/src/style/app.scss | 405 + demo/src/style/demos.css | 57 + demo/tsconfig.json | 14 + demo/tslint.json | 3 + package.json | 119 + src/accordion/accordion-config.spec.ts | 10 + src/accordion/accordion-config.ts | 13 + src/accordion/accordion.module.ts | 23 + src/accordion/accordion.spec.ts | 844 ++ src/accordion/accordion.ts | 344 + src/alert/alert-config.spec.ts | 10 + src/alert/alert-config.ts | 13 + src/alert/alert.module.ts | 11 + src/alert/alert.scss | 3 + src/alert/alert.spec.ts | 170 + src/alert/alert.ts | 73 + src/browserslist | 13 + src/buttons/buttons.module.ts | 15 + src/buttons/checkbox.spec.ts | 185 + src/buttons/checkbox.ts | 84 + src/buttons/label.ts | 12 + src/buttons/radio.spec.ts | 617 + src/buttons/radio.ts | 159 + src/carousel/carousel-config.spec.ts | 14 + src/carousel/carousel-config.ts | 17 + src/carousel/carousel.module.ts | 11 + src/carousel/carousel.spec.ts | 909 ++ src/carousel/carousel.ts | 342 + src/collapse/collapse.module.ts | 8 + src/collapse/collapse.spec.ts | 75 + src/collapse/collapse.ts | 16 + .../adapters/ngb-date-adapter.spec.ts | 56 + src/datepicker/adapters/ngb-date-adapter.ts | 53 + .../adapters/ngb-date-native-adapter.spec.ts | 57 + .../adapters/ngb-date-native-adapter.ts | 37 + .../ngb-date-native-utc-adapter.spec.ts | 57 + .../adapters/ngb-date-native-utc-adapter.ts | 22 + src/datepicker/datepicker-config.spec.ts | 19 + src/datepicker/datepicker-config.ts | 26 + .../datepicker-day-template-context.ts | 55 + src/datepicker/datepicker-day-view.scss | 12 + src/datepicker/datepicker-day-view.spec.ts | 95 + src/datepicker/datepicker-day-view.ts | 30 + src/datepicker/datepicker-i18n.spec.ts | 52 + src/datepicker/datepicker-i18n.ts | 100 + src/datepicker/datepicker-input.spec.ts | 931 ++ src/datepicker/datepicker-input.ts | 472 + src/datepicker/datepicker-integration.spec.ts | 113 + .../datepicker-keymap-service.spec.ts | 133 + src/datepicker/datepicker-keymap-service.ts | 62 + src/datepicker/datepicker-month-view.scss | 38 + src/datepicker/datepicker-month-view.spec.ts | 338 + src/datepicker/datepicker-month-view.ts | 51 + .../datepicker-navigation-select.scss | 6 + .../datepicker-navigation-select.spec.ts | 141 + .../datepicker-navigation-select.ts | 45 + src/datepicker/datepicker-navigation.scss | 70 + src/datepicker/datepicker-navigation.spec.ts | 137 + src/datepicker/datepicker-navigation.ts | 58 + src/datepicker/datepicker-service.spec.ts | 1431 +++ src/datepicker/datepicker-service.ts | 300 + src/datepicker/datepicker-tools.spec.ts | 608 + src/datepicker/datepicker-tools.ts | 215 + src/datepicker/datepicker-view-model.ts | 60 + src/datepicker/datepicker.module.ts | 42 + src/datepicker/datepicker.scss | 60 + src/datepicker/datepicker.spec.ts | 1217 ++ src/datepicker/datepicker.ts | 395 + .../hebrew/datepicker-i18n-hebrew.spec.ts | 76 + .../hebrew/datepicker-i18n-hebrew.ts | 34 + src/datepicker/hebrew/hebrew.spec.ts | 64 + src/datepicker/hebrew/hebrew.ts | 295 + .../hebrew/ngb-calendar-hebrew.spec.ts | 90 + src/datepicker/hebrew/ngb-calendar-hebrew.ts | 83 + src/datepicker/hijri/ngb-calendar-hijri.ts | 115 + .../hijri/ngb-calendar-islamic-civil.spec.ts | 434 + .../hijri/ngb-calendar-islamic-civil.ts | 132 + .../ngb-calendar-islamic-umalqura.spec.ts | 1007 ++ .../hijri/ngb-calendar-islamic-umalqura.ts | 222 + src/datepicker/jalali/jalali.ts | 227 + src/datepicker/jalali/ngb-calendar-persian.ts | 66 + src/datepicker/ngb-calendar.spec.ts | 97 + src/datepicker/ngb-calendar.ts | 161 + .../ngb-date-parser-formatter.spec.ts | 47 + src/datepicker/ngb-date-parser-formatter.ts | 64 + src/datepicker/ngb-date-struct.ts | 23 + src/datepicker/ngb-date.spec.ts | 86 + src/datepicker/ngb-date.ts | 98 + src/dropdown/dropdown-config.spec.ts | 21 + src/dropdown/dropdown-config.ts | 15 + src/dropdown/dropdown.module.ts | 19 + src/dropdown/dropdown.spec.ts | 492 + src/dropdown/dropdown.ts | 437 + src/index.ts | 138 + src/karma-ie.sauce.conf.js | 62 + src/karma.conf.js | 46 + src/karma.sauce.conf.js | 84 + src/modal/modal-backdrop.spec.ts | 15 + src/modal/modal-backdrop.ts | 11 + src/modal/modal-config.ts | 100 + src/modal/modal-dismiss-reasons.ts | 4 + src/modal/modal-ref.ts | 127 + src/modal/modal-stack.ts | 223 + src/modal/modal-window.spec.ts | 112 + src/modal/modal-window.ts | 93 + src/modal/modal.module.ts | 18 + src/modal/modal.scss | 7 + src/modal/modal.spec.ts | 1157 ++ src/modal/modal.ts | 46 + src/ng-package.json | 11 + src/ng-package.prod.json | 10 + src/package.json | 43 + src/pagination/pagination-config.spec.ts | 16 + src/pagination/pagination-config.ts | 19 + src/pagination/pagination.module.ts | 32 + src/pagination/pagination.spec.ts | 708 ++ src/pagination/pagination.ts | 386 + src/popover/popover-config.spec.ts | 16 + src/popover/popover-config.ts | 20 + src/popover/popover.module.ts | 17 + src/popover/popover.scss | 38 + src/popover/popover.spec.ts | 760 ++ src/popover/popover.ts | 293 + src/progressbar/progressbar-config.spec.ts | 13 + src/progressbar/progressbar-config.ts | 17 + src/progressbar/progressbar.module.ts | 11 + src/progressbar/progressbar.spec.ts | 275 + src/progressbar/progressbar.ts | 77 + src/rating/rating-config.spec.ts | 11 + src/rating/rating-config.ts | 14 + src/rating/rating.module.ts | 11 + src/rating/rating.spec.ts | 713 ++ src/rating/rating.ts | 231 + src/tabset/tabset-config.spec.ts | 10 + src/tabset/tabset-config.ts | 14 + src/tabset/tabset.module.ts | 13 + src/tabset/tabset.spec.ts | 593 + src/tabset/tabset.ts | 210 + src/test.ts | 18 + src/test/common.spec.ts | 54 + src/test/common.ts | 82 + src/test/datepicker/common.ts | 11 + src/test/global.spec.ts | 11 + src/test/jasmine.config.ts | 20 + src/test/typeahead/common.ts | 29 + src/test/typings/custom-jasmine.d.ts | 9 + src/timepicker/ngb-time-adapter.spec.ts | 48 + src/timepicker/ngb-time-adapter.ts | 54 + src/timepicker/ngb-time-struct.ts | 19 + src/timepicker/ngb-time.spec.ts | 222 + src/timepicker/ngb-time.ts | 51 + src/timepicker/timepicker-config.spec.ts | 17 + src/timepicker/timepicker-config.ts | 20 + src/timepicker/timepicker-i18n.spec.ts | 16 + src/timepicker/timepicker-i18n.ts | 39 + src/timepicker/timepicker.module.ts | 14 + src/timepicker/timepicker.scss | 52 + src/timepicker/timepicker.spec.ts | 1695 +++ src/timepicker/timepicker.ts | 284 + src/toast/toast-config.spec.ts | 11 + src/toast/toast-config.ts | 42 + src/toast/toast.module.ts | 11 + src/toast/toast.scss | 14 + src/toast/toast.spec.ts | 119 + src/toast/toast.ts | 137 + src/tooltip/tooltip-config.spec.ts | 16 + src/tooltip/tooltip-config.ts | 20 + src/tooltip/tooltip.module.ts | 11 + src/tooltip/tooltip.scss | 36 + src/tooltip/tooltip.spec.ts | 664 + src/tooltip/tooltip.ts | 265 + src/tsconfig-ie.spec.json | 6 + src/tsconfig.json | 17 + src/tsconfig.spec.json | 17 + src/tslint.json | 3 + src/typeahead/highlight.scss | 3 + src/typeahead/highlight.spec.ts | 165 + src/typeahead/highlight.ts | 51 + src/typeahead/typeahead-config.spec.ts | 12 + src/typeahead/typeahead-config.ts | 17 + src/typeahead/typeahead-window.spec.ts | 216 + src/typeahead/typeahead-window.ts | 123 + src/typeahead/typeahead.module.ts | 20 + src/typeahead/typeahead.spec.ts | 990 ++ src/typeahead/typeahead.ts | 414 + src/util/accessibility/live.spec.ts | 53 + src/util/accessibility/live.ts | 59 + src/util/autoclose.ts | 63 + src/util/focus-trap.ts | 66 + src/util/key.ts | 14 + src/util/popup.ts | 60 + src/util/positioning.spec.ts | 224 + src/util/positioning.ts | 254 + src/util/scrollbar.ts | 72 + src/util/triggers.spec.ts | 236 + src/util/triggers.ts | 112 + src/util/util.spec.ts | 112 + src/util/util.ts | 75 + tsconfig.json | 16 + tslint.json | 109 + yarn.lock | 10078 ++++++++++++++++ 653 files changed, 56730 insertions(+) create mode 100644 .editorconfig create mode 100644 angular.json create mode 100644 demo/src/api-docs.ts create mode 100644 demo/src/app/app.component.html create mode 100644 demo/src/app/app.component.ts create mode 100644 demo/src/app/app.module.ts create mode 100644 demo/src/app/app.routing.ts create mode 100644 demo/src/app/components/accordion/accordion.module.ts create mode 100644 demo/src/app/components/accordion/demos/basic/accordion-basic.html create mode 100644 demo/src/app/components/accordion/demos/basic/accordion-basic.module.ts create mode 100644 demo/src/app/components/accordion/demos/basic/accordion-basic.ts create mode 100644 demo/src/app/components/accordion/demos/config/accordion-config.html create mode 100644 demo/src/app/components/accordion/demos/config/accordion-config.module.ts create mode 100644 demo/src/app/components/accordion/demos/config/accordion-config.ts create mode 100644 demo/src/app/components/accordion/demos/header/accordion-header.html create mode 100644 demo/src/app/components/accordion/demos/header/accordion-header.module.ts create mode 100644 demo/src/app/components/accordion/demos/header/accordion-header.ts create mode 100644 demo/src/app/components/accordion/demos/preventchange/accordion-preventchange.html create mode 100644 demo/src/app/components/accordion/demos/preventchange/accordion-preventchange.module.ts create mode 100644 demo/src/app/components/accordion/demos/preventchange/accordion-preventchange.ts create mode 100644 demo/src/app/components/accordion/demos/static/accordion-static.html create mode 100644 demo/src/app/components/accordion/demos/static/accordion-static.module.ts create mode 100644 demo/src/app/components/accordion/demos/static/accordion-static.ts create mode 100644 demo/src/app/components/accordion/demos/toggle/accordion-toggle.html create mode 100644 demo/src/app/components/accordion/demos/toggle/accordion-toggle.module.ts create mode 100644 demo/src/app/components/accordion/demos/toggle/accordion-toggle.ts create mode 100644 demo/src/app/components/alert/alert.module.ts create mode 100644 demo/src/app/components/alert/demos/basic/alert-basic.html create mode 100644 demo/src/app/components/alert/demos/basic/alert-basic.module.ts create mode 100644 demo/src/app/components/alert/demos/basic/alert-basic.ts create mode 100644 demo/src/app/components/alert/demos/closeable/alert-closeable.html create mode 100644 demo/src/app/components/alert/demos/closeable/alert-closeable.module.ts create mode 100644 demo/src/app/components/alert/demos/closeable/alert-closeable.ts create mode 100644 demo/src/app/components/alert/demos/config/alert-config.html create mode 100644 demo/src/app/components/alert/demos/config/alert-config.module.ts create mode 100644 demo/src/app/components/alert/demos/config/alert-config.ts create mode 100644 demo/src/app/components/alert/demos/custom/alert-custom.html create mode 100644 demo/src/app/components/alert/demos/custom/alert-custom.module.ts create mode 100644 demo/src/app/components/alert/demos/custom/alert-custom.ts create mode 100644 demo/src/app/components/alert/demos/selfclosing/alert-selfclosing.html create mode 100644 demo/src/app/components/alert/demos/selfclosing/alert-selfclosing.module.ts create mode 100644 demo/src/app/components/alert/demos/selfclosing/alert-selfclosing.ts create mode 100644 demo/src/app/components/buttons/buttons.module.ts create mode 100644 demo/src/app/components/buttons/demos/checkbox/buttons-checkbox.html create mode 100644 demo/src/app/components/buttons/demos/checkbox/buttons-checkbox.module.ts create mode 100644 demo/src/app/components/buttons/demos/checkbox/buttons-checkbox.ts create mode 100644 demo/src/app/components/buttons/demos/checkboxreactive/buttons-checkbox-reactive.module.ts create mode 100644 demo/src/app/components/buttons/demos/checkboxreactive/buttons-checkboxreactive.html create mode 100644 demo/src/app/components/buttons/demos/checkboxreactive/buttons-checkboxreactive.ts create mode 100644 demo/src/app/components/buttons/demos/radio/buttons-radio.html create mode 100644 demo/src/app/components/buttons/demos/radio/buttons-radio.module.ts create mode 100644 demo/src/app/components/buttons/demos/radio/buttons-radio.ts create mode 100644 demo/src/app/components/buttons/demos/radioreactive/buttons-radio-reactive.module.ts create mode 100644 demo/src/app/components/buttons/demos/radioreactive/buttons-radioreactive.html create mode 100644 demo/src/app/components/buttons/demos/radioreactive/buttons-radioreactive.ts create mode 100644 demo/src/app/components/carousel/carousel.module.ts create mode 100644 demo/src/app/components/carousel/demos/basic/carousel-basic.html create mode 100644 demo/src/app/components/carousel/demos/basic/carousel-basic.module.ts create mode 100644 demo/src/app/components/carousel/demos/basic/carousel-basic.ts create mode 100644 demo/src/app/components/carousel/demos/config/carousel-config.html create mode 100644 demo/src/app/components/carousel/demos/config/carousel-config.module.ts create mode 100644 demo/src/app/components/carousel/demos/config/carousel-config.ts create mode 100644 demo/src/app/components/carousel/demos/navigation/carousel-navigation.html create mode 100644 demo/src/app/components/carousel/demos/navigation/carousel-navigation.module.ts create mode 100644 demo/src/app/components/carousel/demos/navigation/carousel-navigation.ts create mode 100644 demo/src/app/components/carousel/demos/pause/carousel-pause.html create mode 100644 demo/src/app/components/carousel/demos/pause/carousel-pause.module.ts create mode 100644 demo/src/app/components/carousel/demos/pause/carousel-pause.ts create mode 100644 demo/src/app/components/collapse/collapse.module.ts create mode 100644 demo/src/app/components/collapse/demos/basic/collapse-basic.html create mode 100644 demo/src/app/components/collapse/demos/basic/collapse-basic.module.ts create mode 100644 demo/src/app/components/collapse/demos/basic/collapse-basic.ts create mode 100644 demo/src/app/components/datepicker/calendars/datepicker-calendars.component.ts create mode 100644 demo/src/app/components/datepicker/datepicker.module.ts create mode 100644 demo/src/app/components/datepicker/demos/adapter/datepicker-adapter.html create mode 100644 demo/src/app/components/datepicker/demos/adapter/datepicker-adapter.ts create mode 100644 demo/src/app/components/datepicker/demos/adapter/datepicker-adpater.module.ts create mode 100644 demo/src/app/components/datepicker/demos/basic/datepicker-basic.html create mode 100644 demo/src/app/components/datepicker/demos/basic/datepicker-basic.module.ts create mode 100644 demo/src/app/components/datepicker/demos/basic/datepicker-basic.ts create mode 100644 demo/src/app/components/datepicker/demos/config/datepicker-config.html create mode 100644 demo/src/app/components/datepicker/demos/config/datepicker-config.module.ts create mode 100644 demo/src/app/components/datepicker/demos/config/datepicker-config.ts create mode 100644 demo/src/app/components/datepicker/demos/customday/datepicker-customday.html create mode 100644 demo/src/app/components/datepicker/demos/customday/datepicker-customday.module.ts create mode 100644 demo/src/app/components/datepicker/demos/customday/datepicker-customday.ts create mode 100644 demo/src/app/components/datepicker/demos/disabled/datepicker-disabled.html create mode 100644 demo/src/app/components/datepicker/demos/disabled/datepicker-disabled.module.ts create mode 100644 demo/src/app/components/datepicker/demos/disabled/datepicker-disabled.ts create mode 100644 demo/src/app/components/datepicker/demos/footertemplate/datepicker-footer-template.module.ts create mode 100644 demo/src/app/components/datepicker/demos/footertemplate/datepicker-footertemplate.html create mode 100644 demo/src/app/components/datepicker/demos/footertemplate/datepicker-footertemplate.ts create mode 100644 demo/src/app/components/datepicker/demos/hebrew/datepicker-hebrew.html create mode 100644 demo/src/app/components/datepicker/demos/hebrew/datepicker-hebrew.module.ts create mode 100644 demo/src/app/components/datepicker/demos/hebrew/datepicker-hebrew.ts create mode 100644 demo/src/app/components/datepicker/demos/i18n/datepicker-i18n.html create mode 100644 demo/src/app/components/datepicker/demos/i18n/datepicker-i18n.module.ts create mode 100644 demo/src/app/components/datepicker/demos/i18n/datepicker-i18n.ts create mode 100644 demo/src/app/components/datepicker/demos/islamiccivil/datepicker-islamic-civil.module.ts create mode 100644 demo/src/app/components/datepicker/demos/islamiccivil/datepicker-islamiccivil.html create mode 100644 demo/src/app/components/datepicker/demos/islamiccivil/datepicker-islamiccivil.ts create mode 100644 demo/src/app/components/datepicker/demos/islamicumalqura/datepicker-islamic-umalqura.module.ts create mode 100644 demo/src/app/components/datepicker/demos/islamicumalqura/datepicker-islamicumalqura.html create mode 100644 demo/src/app/components/datepicker/demos/islamicumalqura/datepicker-islamicumalqura.ts create mode 100644 demo/src/app/components/datepicker/demos/jalali/datepicker-jalali.html create mode 100644 demo/src/app/components/datepicker/demos/jalali/datepicker-jalali.module.ts create mode 100644 demo/src/app/components/datepicker/demos/jalali/datepicker-jalali.ts create mode 100644 demo/src/app/components/datepicker/demos/multiple/datepicker-multiple.html create mode 100644 demo/src/app/components/datepicker/demos/multiple/datepicker-multiple.module.ts create mode 100644 demo/src/app/components/datepicker/demos/multiple/datepicker-multiple.ts create mode 100644 demo/src/app/components/datepicker/demos/popup/datepicker-popup.html create mode 100644 demo/src/app/components/datepicker/demos/popup/datepicker-popup.module.ts create mode 100644 demo/src/app/components/datepicker/demos/popup/datepicker-popup.ts create mode 100644 demo/src/app/components/datepicker/demos/positiontarget/datepicker-position-target.module.ts create mode 100644 demo/src/app/components/datepicker/demos/positiontarget/datepicker-positiontarget.html create mode 100644 demo/src/app/components/datepicker/demos/positiontarget/datepicker-positiontarget.ts create mode 100644 demo/src/app/components/datepicker/demos/range/datepicker-range.html create mode 100644 demo/src/app/components/datepicker/demos/range/datepicker-range.module.ts create mode 100644 demo/src/app/components/datepicker/demos/range/datepicker-range.ts create mode 100644 demo/src/app/components/datepicker/overview/datepicker-overview.component.html create mode 100644 demo/src/app/components/datepicker/overview/datepicker-overview.component.ts create mode 100644 demo/src/app/components/datepicker/overview/demo/datepicker-overview-demo.component.ts create mode 100644 demo/src/app/components/dropdown/demos/basic/dropdown-basic.html create mode 100644 demo/src/app/components/dropdown/demos/basic/dropdown-basic.module.ts create mode 100644 demo/src/app/components/dropdown/demos/basic/dropdown-basic.ts create mode 100644 demo/src/app/components/dropdown/demos/config/dropdown-config.html create mode 100644 demo/src/app/components/dropdown/demos/config/dropdown-config.module.ts create mode 100644 demo/src/app/components/dropdown/demos/config/dropdown-config.ts create mode 100644 demo/src/app/components/dropdown/demos/form/dropdown-form.html create mode 100644 demo/src/app/components/dropdown/demos/form/dropdown-form.module.ts create mode 100644 demo/src/app/components/dropdown/demos/form/dropdown-form.ts create mode 100644 demo/src/app/components/dropdown/demos/manual/dropdown-manual.html create mode 100644 demo/src/app/components/dropdown/demos/manual/dropdown-manual.module.ts create mode 100644 demo/src/app/components/dropdown/demos/manual/dropdown-manual.ts create mode 100644 demo/src/app/components/dropdown/demos/navbar/dropdown-navbar.html create mode 100644 demo/src/app/components/dropdown/demos/navbar/dropdown-navbar.module.ts create mode 100644 demo/src/app/components/dropdown/demos/navbar/dropdown-navbar.ts create mode 100644 demo/src/app/components/dropdown/demos/split/dropdown-split.html create mode 100644 demo/src/app/components/dropdown/demos/split/dropdown-split.module.ts create mode 100644 demo/src/app/components/dropdown/demos/split/dropdown-split.ts create mode 100644 demo/src/app/components/dropdown/dropdown.module.ts create mode 100644 demo/src/app/components/modal/demos/basic/modal-basic.html create mode 100644 demo/src/app/components/modal/demos/basic/modal-basic.module.ts create mode 100644 demo/src/app/components/modal/demos/basic/modal-basic.ts create mode 100644 demo/src/app/components/modal/demos/component/modal-component.html create mode 100644 demo/src/app/components/modal/demos/component/modal-component.module.ts create mode 100644 demo/src/app/components/modal/demos/component/modal-component.ts create mode 100644 demo/src/app/components/modal/demos/config/modal-config.html create mode 100644 demo/src/app/components/modal/demos/config/modal-config.module.ts create mode 100644 demo/src/app/components/modal/demos/config/modal-config.ts create mode 100644 demo/src/app/components/modal/demos/focus/modal-focus.html create mode 100644 demo/src/app/components/modal/demos/focus/modal-focus.module.ts create mode 100644 demo/src/app/components/modal/demos/focus/modal-focus.ts create mode 100644 demo/src/app/components/modal/demos/options/modal-options.html create mode 100644 demo/src/app/components/modal/demos/options/modal-options.module.ts create mode 100644 demo/src/app/components/modal/demos/options/modal-options.ts create mode 100644 demo/src/app/components/modal/demos/stacked/modal-stacked.html create mode 100644 demo/src/app/components/modal/demos/stacked/modal-stacked.module.ts create mode 100644 demo/src/app/components/modal/demos/stacked/modal-stacked.ts create mode 100644 demo/src/app/components/modal/modal.module.ts create mode 100644 demo/src/app/components/pagination/demos/advanced/pagination-advanced.html create mode 100644 demo/src/app/components/pagination/demos/advanced/pagination-advanced.module.ts create mode 100644 demo/src/app/components/pagination/demos/advanced/pagination-advanced.ts create mode 100644 demo/src/app/components/pagination/demos/basic/pagination-basic.html create mode 100644 demo/src/app/components/pagination/demos/basic/pagination-basic.module.ts create mode 100644 demo/src/app/components/pagination/demos/basic/pagination-basic.ts create mode 100644 demo/src/app/components/pagination/demos/config/pagination-config.html create mode 100644 demo/src/app/components/pagination/demos/config/pagination-config.module.ts create mode 100644 demo/src/app/components/pagination/demos/config/pagination-config.ts create mode 100644 demo/src/app/components/pagination/demos/customization/pagination-customization.html create mode 100644 demo/src/app/components/pagination/demos/customization/pagination-customization.module.ts create mode 100644 demo/src/app/components/pagination/demos/customization/pagination-customization.ts create mode 100644 demo/src/app/components/pagination/demos/disabled/pagination-disabled.html create mode 100644 demo/src/app/components/pagination/demos/disabled/pagination-disabled.module.ts create mode 100644 demo/src/app/components/pagination/demos/disabled/pagination-disabled.ts create mode 100755 demo/src/app/components/pagination/demos/justify/pagination-justify.html create mode 100644 demo/src/app/components/pagination/demos/justify/pagination-justify.module.ts create mode 100755 demo/src/app/components/pagination/demos/justify/pagination-justify.ts create mode 100644 demo/src/app/components/pagination/demos/size/pagination-size.html create mode 100644 demo/src/app/components/pagination/demos/size/pagination-size.module.ts create mode 100644 demo/src/app/components/pagination/demos/size/pagination-size.ts create mode 100644 demo/src/app/components/pagination/overview/pagination-overview.component.html create mode 100644 demo/src/app/components/pagination/overview/pagination-overview.component.ts create mode 100644 demo/src/app/components/pagination/pagination.module.ts create mode 100644 demo/src/app/components/popover/demos/autoclose/popover-autoclose.html create mode 100644 demo/src/app/components/popover/demos/autoclose/popover-autoclose.module.ts create mode 100644 demo/src/app/components/popover/demos/autoclose/popover-autoclose.ts create mode 100644 demo/src/app/components/popover/demos/basic/popover-basic.html create mode 100644 demo/src/app/components/popover/demos/basic/popover-basic.module.ts create mode 100644 demo/src/app/components/popover/demos/basic/popover-basic.ts create mode 100644 demo/src/app/components/popover/demos/config/popover-config.html create mode 100644 demo/src/app/components/popover/demos/config/popover-config.module.ts create mode 100644 demo/src/app/components/popover/demos/config/popover-config.ts create mode 100644 demo/src/app/components/popover/demos/container/popover-container.html create mode 100644 demo/src/app/components/popover/demos/container/popover-container.module.ts create mode 100644 demo/src/app/components/popover/demos/container/popover-container.ts create mode 100644 demo/src/app/components/popover/demos/customclass/popover-custom-class.module.ts create mode 100644 demo/src/app/components/popover/demos/customclass/popover-customclass.html create mode 100644 demo/src/app/components/popover/demos/customclass/popover-customclass.ts create mode 100644 demo/src/app/components/popover/demos/delay/popover-delay.html create mode 100644 demo/src/app/components/popover/demos/delay/popover-delay.module.ts create mode 100644 demo/src/app/components/popover/demos/delay/popover-delay.ts create mode 100644 demo/src/app/components/popover/demos/tplcontent/popover-tpl-content.module.ts create mode 100644 demo/src/app/components/popover/demos/tplcontent/popover-tplcontent.html create mode 100644 demo/src/app/components/popover/demos/tplcontent/popover-tplcontent.ts create mode 100644 demo/src/app/components/popover/demos/tplwithcontext/popover-tpl-with-context.module.ts create mode 100644 demo/src/app/components/popover/demos/tplwithcontext/popover-tplwithcontext.html create mode 100644 demo/src/app/components/popover/demos/tplwithcontext/popover-tplwithcontext.ts create mode 100644 demo/src/app/components/popover/demos/triggers/popover-triggers.html create mode 100644 demo/src/app/components/popover/demos/triggers/popover-triggers.module.ts create mode 100644 demo/src/app/components/popover/demos/triggers/popover-triggers.ts create mode 100644 demo/src/app/components/popover/demos/visibility/popover-visibility.html create mode 100644 demo/src/app/components/popover/demos/visibility/popover-visibility.module.ts create mode 100644 demo/src/app/components/popover/demos/visibility/popover-visibility.ts create mode 100644 demo/src/app/components/popover/popover.module.ts create mode 100644 demo/src/app/components/progressbar/demos/basic/progressbar-basic.html create mode 100644 demo/src/app/components/progressbar/demos/basic/progressbar-basic.module.ts create mode 100644 demo/src/app/components/progressbar/demos/basic/progressbar-basic.ts create mode 100644 demo/src/app/components/progressbar/demos/config/progressbar-config.html create mode 100644 demo/src/app/components/progressbar/demos/config/progressbar-config.module.ts create mode 100644 demo/src/app/components/progressbar/demos/config/progressbar-config.ts create mode 100644 demo/src/app/components/progressbar/demos/height/progressbar-height.html create mode 100644 demo/src/app/components/progressbar/demos/height/progressbar-height.module.ts create mode 100644 demo/src/app/components/progressbar/demos/height/progressbar-height.ts create mode 100644 demo/src/app/components/progressbar/demos/labels/progressbar-labels.html create mode 100644 demo/src/app/components/progressbar/demos/labels/progressbar-labels.module.ts create mode 100644 demo/src/app/components/progressbar/demos/labels/progressbar-labels.ts create mode 100644 demo/src/app/components/progressbar/demos/showvalue/progressbar-show-value.module.ts create mode 100644 demo/src/app/components/progressbar/demos/showvalue/progressbar-showvalue.html create mode 100644 demo/src/app/components/progressbar/demos/showvalue/progressbar-showvalue.ts create mode 100644 demo/src/app/components/progressbar/demos/striped/progressbar-striped.html create mode 100644 demo/src/app/components/progressbar/demos/striped/progressbar-striped.module.ts create mode 100644 demo/src/app/components/progressbar/demos/striped/progressbar-striped.ts create mode 100644 demo/src/app/components/progressbar/progressbar.module.ts create mode 100644 demo/src/app/components/rating/demos/basic/rating-basic.html create mode 100644 demo/src/app/components/rating/demos/basic/rating-basic.module.ts create mode 100644 demo/src/app/components/rating/demos/basic/rating-basic.ts create mode 100644 demo/src/app/components/rating/demos/config/rating-config.html create mode 100644 demo/src/app/components/rating/demos/config/rating-config.module.ts create mode 100644 demo/src/app/components/rating/demos/config/rating-config.ts create mode 100644 demo/src/app/components/rating/demos/decimal/rating-decimal.html create mode 100644 demo/src/app/components/rating/demos/decimal/rating-decimal.module.ts create mode 100644 demo/src/app/components/rating/demos/decimal/rating-decimal.ts create mode 100644 demo/src/app/components/rating/demos/events/rating-events.html create mode 100644 demo/src/app/components/rating/demos/events/rating-events.module.ts create mode 100644 demo/src/app/components/rating/demos/events/rating-events.ts create mode 100644 demo/src/app/components/rating/demos/form/rating-form.html create mode 100644 demo/src/app/components/rating/demos/form/rating-form.module.ts create mode 100644 demo/src/app/components/rating/demos/form/rating-form.ts create mode 100644 demo/src/app/components/rating/demos/template/rating-template.html create mode 100644 demo/src/app/components/rating/demos/template/rating-template.module.ts create mode 100644 demo/src/app/components/rating/demos/template/rating-template.ts create mode 100644 demo/src/app/components/rating/rating.module.ts create mode 100644 demo/src/app/components/shared/api-docs/api-docs-badge.component.ts create mode 100644 demo/src/app/components/shared/api-docs/api-docs-class.component.html create mode 100644 demo/src/app/components/shared/api-docs/api-docs-class.component.ts create mode 100644 demo/src/app/components/shared/api-docs/api-docs-config.component.html create mode 100644 demo/src/app/components/shared/api-docs/api-docs-config.component.ts create mode 100644 demo/src/app/components/shared/api-docs/api-docs.component.html create mode 100644 demo/src/app/components/shared/api-docs/api-docs.component.ts create mode 100644 demo/src/app/components/shared/api-docs/api-docs.model.ts create mode 100644 demo/src/app/components/shared/api-docs/index.ts create mode 100644 demo/src/app/components/shared/api-page/api.component.ts create mode 100644 demo/src/app/components/shared/demo-list.ts create mode 100644 demo/src/app/components/shared/examples-page/demo.component.html create mode 100644 demo/src/app/components/shared/examples-page/demo.component.ts create mode 100644 demo/src/app/components/shared/examples-page/examples.component.ts create mode 100644 demo/src/app/components/shared/index.ts create mode 100644 demo/src/app/components/shared/overview/index.ts create mode 100644 demo/src/app/components/shared/overview/overview-section.component.ts create mode 100644 demo/src/app/components/shared/overview/overview.directive.ts create mode 100644 demo/src/app/components/shared/overview/overview.ts create mode 100644 demo/src/app/components/table/demos/basic/table-basic.html create mode 100644 demo/src/app/components/table/demos/basic/table-basic.module.ts create mode 100644 demo/src/app/components/table/demos/basic/table-basic.ts create mode 100644 demo/src/app/components/table/demos/complete/countries.ts create mode 100644 demo/src/app/components/table/demos/complete/country.service.ts create mode 100644 demo/src/app/components/table/demos/complete/country.ts create mode 100644 demo/src/app/components/table/demos/complete/sortable.directive.ts create mode 100644 demo/src/app/components/table/demos/complete/table-complete.html create mode 100644 demo/src/app/components/table/demos/complete/table-complete.module.ts create mode 100644 demo/src/app/components/table/demos/complete/table-complete.ts create mode 100644 demo/src/app/components/table/demos/filtering/table-filtering.html create mode 100644 demo/src/app/components/table/demos/filtering/table-filtering.module.ts create mode 100644 demo/src/app/components/table/demos/filtering/table-filtering.ts create mode 100644 demo/src/app/components/table/demos/pagination/table-pagination.html create mode 100644 demo/src/app/components/table/demos/pagination/table-pagination.module.ts create mode 100644 demo/src/app/components/table/demos/pagination/table-pagination.ts create mode 100644 demo/src/app/components/table/demos/sortable/table-sortable.html create mode 100644 demo/src/app/components/table/demos/sortable/table-sortable.module.ts create mode 100644 demo/src/app/components/table/demos/sortable/table-sortable.ts create mode 100644 demo/src/app/components/table/overview/demo/table-overview-demo.component.ts create mode 100644 demo/src/app/components/table/overview/table-overview.component.html create mode 100644 demo/src/app/components/table/overview/table-overview.component.ts create mode 100644 demo/src/app/components/table/table.module.ts create mode 100644 demo/src/app/components/tabset/demos/basic/tabset-basic.html create mode 100644 demo/src/app/components/tabset/demos/basic/tabset-basic.module.ts create mode 100644 demo/src/app/components/tabset/demos/basic/tabset-basic.ts create mode 100644 demo/src/app/components/tabset/demos/config/tabset-config.html create mode 100644 demo/src/app/components/tabset/demos/config/tabset-config.module.ts create mode 100644 demo/src/app/components/tabset/demos/config/tabset-config.ts create mode 100644 demo/src/app/components/tabset/demos/justify/tabset-justify.html create mode 100644 demo/src/app/components/tabset/demos/justify/tabset-justify.module.ts create mode 100644 demo/src/app/components/tabset/demos/justify/tabset-justify.ts create mode 100644 demo/src/app/components/tabset/demos/orientation/tabset-orientation.html create mode 100644 demo/src/app/components/tabset/demos/orientation/tabset-orientation.module.ts create mode 100644 demo/src/app/components/tabset/demos/orientation/tabset-orientation.ts create mode 100644 demo/src/app/components/tabset/demos/pills/tabset-pills.html create mode 100644 demo/src/app/components/tabset/demos/pills/tabset-pills.module.ts create mode 100644 demo/src/app/components/tabset/demos/pills/tabset-pills.ts create mode 100644 demo/src/app/components/tabset/demos/preventchange/tabset-prevent-change.module.ts create mode 100644 demo/src/app/components/tabset/demos/preventchange/tabset-preventchange.html create mode 100644 demo/src/app/components/tabset/demos/preventchange/tabset-preventchange.ts create mode 100644 demo/src/app/components/tabset/demos/selectbyid/tabset-selectbyid.html create mode 100644 demo/src/app/components/tabset/demos/selectbyid/tabset-selectbyid.module.ts create mode 100644 demo/src/app/components/tabset/demos/selectbyid/tabset-selectbyid.ts create mode 100644 demo/src/app/components/tabset/tabset.module.ts create mode 100644 demo/src/app/components/timepicker/demos/adapter/timepicker-adapter.html create mode 100644 demo/src/app/components/timepicker/demos/adapter/timepicker-adapter.module.ts create mode 100644 demo/src/app/components/timepicker/demos/adapter/timepicker-adapter.ts create mode 100644 demo/src/app/components/timepicker/demos/basic/timepicker-basic.html create mode 100644 demo/src/app/components/timepicker/demos/basic/timepicker-basic.module.ts create mode 100644 demo/src/app/components/timepicker/demos/basic/timepicker-basic.ts create mode 100644 demo/src/app/components/timepicker/demos/config/timepicker-config.html create mode 100644 demo/src/app/components/timepicker/demos/config/timepicker-config.module.ts create mode 100644 demo/src/app/components/timepicker/demos/config/timepicker-config.ts create mode 100644 demo/src/app/components/timepicker/demos/i18n/timepicker-i18n.html create mode 100644 demo/src/app/components/timepicker/demos/i18n/timepicker-i18n.module.ts create mode 100644 demo/src/app/components/timepicker/demos/i18n/timepicker-i18n.ts create mode 100644 demo/src/app/components/timepicker/demos/meridian/timepicker-meridian.html create mode 100644 demo/src/app/components/timepicker/demos/meridian/timepicker-meridian.module.ts create mode 100644 demo/src/app/components/timepicker/demos/meridian/timepicker-meridian.ts create mode 100644 demo/src/app/components/timepicker/demos/seconds/timepicker-seconds.html create mode 100644 demo/src/app/components/timepicker/demos/seconds/timepicker-seconds.module.ts create mode 100644 demo/src/app/components/timepicker/demos/seconds/timepicker-seconds.ts create mode 100644 demo/src/app/components/timepicker/demos/spinners/timepicker-spinners.html create mode 100644 demo/src/app/components/timepicker/demos/spinners/timepicker-spinners.module.ts create mode 100644 demo/src/app/components/timepicker/demos/spinners/timepicker-spinners.ts create mode 100644 demo/src/app/components/timepicker/demos/steps/timepicker-steps.html create mode 100644 demo/src/app/components/timepicker/demos/steps/timepicker-steps.module.ts create mode 100644 demo/src/app/components/timepicker/demos/steps/timepicker-steps.ts create mode 100644 demo/src/app/components/timepicker/demos/validation/timepicker-validation.html create mode 100644 demo/src/app/components/timepicker/demos/validation/timepicker-validation.module.ts create mode 100644 demo/src/app/components/timepicker/demos/validation/timepicker-validation.ts create mode 100644 demo/src/app/components/timepicker/timepicker.module.ts create mode 100644 demo/src/app/components/toast/demos/closeable/toast-closeable.html create mode 100644 demo/src/app/components/toast/demos/closeable/toast-closeable.module.ts create mode 100644 demo/src/app/components/toast/demos/closeable/toast-closeable.ts create mode 100644 demo/src/app/components/toast/demos/custom-header/toast-custom-header.html create mode 100644 demo/src/app/components/toast/demos/custom-header/toast-custom-header.module.ts create mode 100644 demo/src/app/components/toast/demos/custom-header/toast-custom-header.ts create mode 100644 demo/src/app/components/toast/demos/howto-global/toast-global.component.html create mode 100644 demo/src/app/components/toast/demos/howto-global/toast-global.component.ts create mode 100644 demo/src/app/components/toast/demos/howto-global/toast-global.module.ts create mode 100644 demo/src/app/components/toast/demos/howto-global/toast-service.ts create mode 100644 demo/src/app/components/toast/demos/howto-global/toasts-container.component.ts create mode 100644 demo/src/app/components/toast/demos/inline/toast-inline.html create mode 100644 demo/src/app/components/toast/demos/inline/toast-inline.module.ts create mode 100644 demo/src/app/components/toast/demos/inline/toast-inline.ts create mode 100644 demo/src/app/components/toast/demos/prevent-autohide/toast-prevent-autohide.html create mode 100644 demo/src/app/components/toast/demos/prevent-autohide/toast-prevent-autohide.module.ts create mode 100644 demo/src/app/components/toast/demos/prevent-autohide/toast-prevent-autohide.ts create mode 100644 demo/src/app/components/toast/overview/toast-overview.component.html create mode 100644 demo/src/app/components/toast/overview/toast-overview.component.ts create mode 100644 demo/src/app/components/toast/toast.module.ts create mode 100644 demo/src/app/components/tooltip/demos/autoclose/tooltip-autoclose.html create mode 100644 demo/src/app/components/tooltip/demos/autoclose/tooltip-autoclose.module.ts create mode 100644 demo/src/app/components/tooltip/demos/autoclose/tooltip-autoclose.ts create mode 100644 demo/src/app/components/tooltip/demos/basic/tooltip-basic.html create mode 100644 demo/src/app/components/tooltip/demos/basic/tooltip-basic.module.ts create mode 100644 demo/src/app/components/tooltip/demos/basic/tooltip-basic.ts create mode 100644 demo/src/app/components/tooltip/demos/config/tooltip-config.html create mode 100644 demo/src/app/components/tooltip/demos/config/tooltip-config.module.ts create mode 100644 demo/src/app/components/tooltip/demos/config/tooltip-config.ts create mode 100644 demo/src/app/components/tooltip/demos/container/tooltip-container.html create mode 100644 demo/src/app/components/tooltip/demos/container/tooltip-container.module.ts create mode 100644 demo/src/app/components/tooltip/demos/container/tooltip-container.ts create mode 100644 demo/src/app/components/tooltip/demos/customclass/tooltip-custom-class.module.ts create mode 100644 demo/src/app/components/tooltip/demos/customclass/tooltip-customclass.html create mode 100644 demo/src/app/components/tooltip/demos/customclass/tooltip-customclass.ts create mode 100644 demo/src/app/components/tooltip/demos/delay/tooltip-delay.html create mode 100644 demo/src/app/components/tooltip/demos/delay/tooltip-delay.module.ts create mode 100644 demo/src/app/components/tooltip/demos/delay/tooltip-delay.ts create mode 100644 demo/src/app/components/tooltip/demos/tplcontent/tooltip-tpl-content.module.ts create mode 100644 demo/src/app/components/tooltip/demos/tplcontent/tooltip-tplcontent.html create mode 100644 demo/src/app/components/tooltip/demos/tplcontent/tooltip-tplcontent.ts create mode 100644 demo/src/app/components/tooltip/demos/tplwithcontext/tooltip-tpl-with-context.module.ts create mode 100644 demo/src/app/components/tooltip/demos/tplwithcontext/tooltip-tplwithcontext.html create mode 100644 demo/src/app/components/tooltip/demos/tplwithcontext/tooltip-tplwithcontext.ts create mode 100644 demo/src/app/components/tooltip/demos/triggers/tooltip-triggers.html create mode 100644 demo/src/app/components/tooltip/demos/triggers/tooltip-triggers.module.ts create mode 100644 demo/src/app/components/tooltip/demos/triggers/tooltip-triggers.ts create mode 100644 demo/src/app/components/tooltip/tooltip.module.ts create mode 100644 demo/src/app/components/typeahead/demos/basic/typeahead-basic.html create mode 100644 demo/src/app/components/typeahead/demos/basic/typeahead-basic.module.ts create mode 100644 demo/src/app/components/typeahead/demos/basic/typeahead-basic.ts create mode 100644 demo/src/app/components/typeahead/demos/config/typeahead-config.html create mode 100644 demo/src/app/components/typeahead/demos/config/typeahead-config.module.ts create mode 100644 demo/src/app/components/typeahead/demos/config/typeahead-config.ts create mode 100644 demo/src/app/components/typeahead/demos/focus/typeahead-focus.html create mode 100644 demo/src/app/components/typeahead/demos/focus/typeahead-focus.module.ts create mode 100644 demo/src/app/components/typeahead/demos/focus/typeahead-focus.ts create mode 100644 demo/src/app/components/typeahead/demos/format/typeahead-format.html create mode 100644 demo/src/app/components/typeahead/demos/format/typeahead-format.module.ts create mode 100644 demo/src/app/components/typeahead/demos/format/typeahead-format.ts create mode 100644 demo/src/app/components/typeahead/demos/http/typeahead-http.html create mode 100644 demo/src/app/components/typeahead/demos/http/typeahead-http.module.ts create mode 100644 demo/src/app/components/typeahead/demos/http/typeahead-http.ts create mode 100644 demo/src/app/components/typeahead/demos/template/typeahead-template.html create mode 100644 demo/src/app/components/typeahead/demos/template/typeahead-template.module.ts create mode 100644 demo/src/app/components/typeahead/demos/template/typeahead-template.ts create mode 100644 demo/src/app/components/typeahead/typeahead.module.ts create mode 100644 demo/src/app/default/default.component.html create mode 100644 demo/src/app/default/default.component.ts create mode 100644 demo/src/app/default/index.ts create mode 100644 demo/src/app/pages/getting-started/getting-started.component.html create mode 100644 demo/src/app/pages/getting-started/getting-started.component.ts create mode 100644 demo/src/app/pages/positioning/positioning.component.html create mode 100644 demo/src/app/pages/positioning/positioning.component.ts create mode 100644 demo/src/app/shared/analytics/analytics.ts create mode 100644 demo/src/app/shared/code/code-highlight.service.ts create mode 100644 demo/src/app/shared/code/code.component.ts create mode 100644 demo/src/app/shared/code/snippet.ts create mode 100644 demo/src/app/shared/component-wrapper/component-wrapper.component.html create mode 100644 demo/src/app/shared/component-wrapper/component-wrapper.component.ts create mode 100644 demo/src/app/shared/fragment/fragment.directive.ts create mode 100644 demo/src/app/shared/icons/icons.component.html create mode 100644 demo/src/app/shared/icons/icons.component.ts create mode 100644 demo/src/app/shared/index.ts create mode 100644 demo/src/app/shared/page-wrapper/page-header.component.ts create mode 100644 demo/src/app/shared/page-wrapper/page-wrapper.component.html create mode 100644 demo/src/app/shared/page-wrapper/page-wrapper.component.ts create mode 100644 demo/src/app/shared/side-nav/side-nav.component.html create mode 100644 demo/src/app/shared/side-nav/side-nav.component.ts create mode 100644 demo/src/browserslist create mode 100644 demo/src/docs.json create mode 100644 demo/src/environments/environment.prod.ts create mode 100644 demo/src/environments/environment.ts create mode 100644 demo/src/environments/versions.ts create mode 100644 demo/src/main.ts create mode 100644 demo/src/polyfills.ts create mode 100644 demo/src/public/img/favicon.ico create mode 100644 demo/src/public/img/github.svg create mode 100644 demo/src/public/img/link-symbol.svg create mode 100644 demo/src/public/img/logo-stack.png create mode 100644 demo/src/public/img/logo-stack.svg create mode 100644 demo/src/public/img/logo.svg create mode 100644 demo/src/public/img/ngb-logo.png create mode 100644 demo/src/public/img/ngb-logo.svg create mode 100644 demo/src/public/img/stackblitz-icon.svg create mode 100644 demo/src/public/img/sunbird-logo.png create mode 100644 demo/src/public/index.html create mode 100644 demo/src/style/app.scss create mode 100644 demo/src/style/demos.css create mode 100644 demo/tsconfig.json create mode 100644 demo/tslint.json create mode 100644 package.json create mode 100644 src/accordion/accordion-config.spec.ts create mode 100644 src/accordion/accordion-config.ts create mode 100644 src/accordion/accordion.module.ts create mode 100644 src/accordion/accordion.spec.ts create mode 100644 src/accordion/accordion.ts create mode 100644 src/alert/alert-config.spec.ts create mode 100644 src/alert/alert-config.ts create mode 100644 src/alert/alert.module.ts create mode 100644 src/alert/alert.scss create mode 100644 src/alert/alert.spec.ts create mode 100644 src/alert/alert.ts create mode 100644 src/browserslist create mode 100644 src/buttons/buttons.module.ts create mode 100644 src/buttons/checkbox.spec.ts create mode 100644 src/buttons/checkbox.ts create mode 100644 src/buttons/label.ts create mode 100644 src/buttons/radio.spec.ts create mode 100644 src/buttons/radio.ts create mode 100644 src/carousel/carousel-config.spec.ts create mode 100644 src/carousel/carousel-config.ts create mode 100644 src/carousel/carousel.module.ts create mode 100644 src/carousel/carousel.spec.ts create mode 100644 src/carousel/carousel.ts create mode 100644 src/collapse/collapse.module.ts create mode 100644 src/collapse/collapse.spec.ts create mode 100644 src/collapse/collapse.ts create mode 100644 src/datepicker/adapters/ngb-date-adapter.spec.ts create mode 100644 src/datepicker/adapters/ngb-date-adapter.ts create mode 100644 src/datepicker/adapters/ngb-date-native-adapter.spec.ts create mode 100644 src/datepicker/adapters/ngb-date-native-adapter.ts create mode 100644 src/datepicker/adapters/ngb-date-native-utc-adapter.spec.ts create mode 100644 src/datepicker/adapters/ngb-date-native-utc-adapter.ts create mode 100644 src/datepicker/datepicker-config.spec.ts create mode 100644 src/datepicker/datepicker-config.ts create mode 100644 src/datepicker/datepicker-day-template-context.ts create mode 100644 src/datepicker/datepicker-day-view.scss create mode 100644 src/datepicker/datepicker-day-view.spec.ts create mode 100644 src/datepicker/datepicker-day-view.ts create mode 100644 src/datepicker/datepicker-i18n.spec.ts create mode 100644 src/datepicker/datepicker-i18n.ts create mode 100644 src/datepicker/datepicker-input.spec.ts create mode 100644 src/datepicker/datepicker-input.ts create mode 100644 src/datepicker/datepicker-integration.spec.ts create mode 100644 src/datepicker/datepicker-keymap-service.spec.ts create mode 100644 src/datepicker/datepicker-keymap-service.ts create mode 100644 src/datepicker/datepicker-month-view.scss create mode 100644 src/datepicker/datepicker-month-view.spec.ts create mode 100644 src/datepicker/datepicker-month-view.ts create mode 100644 src/datepicker/datepicker-navigation-select.scss create mode 100644 src/datepicker/datepicker-navigation-select.spec.ts create mode 100644 src/datepicker/datepicker-navigation-select.ts create mode 100644 src/datepicker/datepicker-navigation.scss create mode 100644 src/datepicker/datepicker-navigation.spec.ts create mode 100644 src/datepicker/datepicker-navigation.ts create mode 100644 src/datepicker/datepicker-service.spec.ts create mode 100644 src/datepicker/datepicker-service.ts create mode 100644 src/datepicker/datepicker-tools.spec.ts create mode 100644 src/datepicker/datepicker-tools.ts create mode 100644 src/datepicker/datepicker-view-model.ts create mode 100644 src/datepicker/datepicker.module.ts create mode 100644 src/datepicker/datepicker.scss create mode 100644 src/datepicker/datepicker.spec.ts create mode 100644 src/datepicker/datepicker.ts create mode 100644 src/datepicker/hebrew/datepicker-i18n-hebrew.spec.ts create mode 100644 src/datepicker/hebrew/datepicker-i18n-hebrew.ts create mode 100644 src/datepicker/hebrew/hebrew.spec.ts create mode 100644 src/datepicker/hebrew/hebrew.ts create mode 100644 src/datepicker/hebrew/ngb-calendar-hebrew.spec.ts create mode 100644 src/datepicker/hebrew/ngb-calendar-hebrew.ts create mode 100644 src/datepicker/hijri/ngb-calendar-hijri.ts create mode 100644 src/datepicker/hijri/ngb-calendar-islamic-civil.spec.ts create mode 100644 src/datepicker/hijri/ngb-calendar-islamic-civil.ts create mode 100644 src/datepicker/hijri/ngb-calendar-islamic-umalqura.spec.ts create mode 100644 src/datepicker/hijri/ngb-calendar-islamic-umalqura.ts create mode 100644 src/datepicker/jalali/jalali.ts create mode 100644 src/datepicker/jalali/ngb-calendar-persian.ts create mode 100644 src/datepicker/ngb-calendar.spec.ts create mode 100644 src/datepicker/ngb-calendar.ts create mode 100644 src/datepicker/ngb-date-parser-formatter.spec.ts create mode 100644 src/datepicker/ngb-date-parser-formatter.ts create mode 100644 src/datepicker/ngb-date-struct.ts create mode 100644 src/datepicker/ngb-date.spec.ts create mode 100644 src/datepicker/ngb-date.ts create mode 100644 src/dropdown/dropdown-config.spec.ts create mode 100644 src/dropdown/dropdown-config.ts create mode 100644 src/dropdown/dropdown.module.ts create mode 100644 src/dropdown/dropdown.spec.ts create mode 100644 src/dropdown/dropdown.ts create mode 100644 src/index.ts create mode 100644 src/karma-ie.sauce.conf.js create mode 100644 src/karma.conf.js create mode 100644 src/karma.sauce.conf.js create mode 100644 src/modal/modal-backdrop.spec.ts create mode 100644 src/modal/modal-backdrop.ts create mode 100644 src/modal/modal-config.ts create mode 100644 src/modal/modal-dismiss-reasons.ts create mode 100644 src/modal/modal-ref.ts create mode 100644 src/modal/modal-stack.ts create mode 100644 src/modal/modal-window.spec.ts create mode 100644 src/modal/modal-window.ts create mode 100644 src/modal/modal.module.ts create mode 100644 src/modal/modal.scss create mode 100644 src/modal/modal.spec.ts create mode 100644 src/modal/modal.ts create mode 100644 src/ng-package.json create mode 100644 src/ng-package.prod.json create mode 100644 src/package.json create mode 100644 src/pagination/pagination-config.spec.ts create mode 100644 src/pagination/pagination-config.ts create mode 100644 src/pagination/pagination.module.ts create mode 100644 src/pagination/pagination.spec.ts create mode 100644 src/pagination/pagination.ts create mode 100644 src/popover/popover-config.spec.ts create mode 100644 src/popover/popover-config.ts create mode 100644 src/popover/popover.module.ts create mode 100644 src/popover/popover.scss create mode 100644 src/popover/popover.spec.ts create mode 100644 src/popover/popover.ts create mode 100644 src/progressbar/progressbar-config.spec.ts create mode 100644 src/progressbar/progressbar-config.ts create mode 100644 src/progressbar/progressbar.module.ts create mode 100644 src/progressbar/progressbar.spec.ts create mode 100644 src/progressbar/progressbar.ts create mode 100644 src/rating/rating-config.spec.ts create mode 100644 src/rating/rating-config.ts create mode 100644 src/rating/rating.module.ts create mode 100644 src/rating/rating.spec.ts create mode 100644 src/rating/rating.ts create mode 100644 src/tabset/tabset-config.spec.ts create mode 100644 src/tabset/tabset-config.ts create mode 100644 src/tabset/tabset.module.ts create mode 100644 src/tabset/tabset.spec.ts create mode 100644 src/tabset/tabset.ts create mode 100644 src/test.ts create mode 100644 src/test/common.spec.ts create mode 100644 src/test/common.ts create mode 100644 src/test/datepicker/common.ts create mode 100644 src/test/global.spec.ts create mode 100644 src/test/jasmine.config.ts create mode 100644 src/test/typeahead/common.ts create mode 100644 src/test/typings/custom-jasmine.d.ts create mode 100644 src/timepicker/ngb-time-adapter.spec.ts create mode 100644 src/timepicker/ngb-time-adapter.ts create mode 100644 src/timepicker/ngb-time-struct.ts create mode 100644 src/timepicker/ngb-time.spec.ts create mode 100644 src/timepicker/ngb-time.ts create mode 100644 src/timepicker/timepicker-config.spec.ts create mode 100644 src/timepicker/timepicker-config.ts create mode 100644 src/timepicker/timepicker-i18n.spec.ts create mode 100644 src/timepicker/timepicker-i18n.ts create mode 100644 src/timepicker/timepicker.module.ts create mode 100644 src/timepicker/timepicker.scss create mode 100644 src/timepicker/timepicker.spec.ts create mode 100644 src/timepicker/timepicker.ts create mode 100644 src/toast/toast-config.spec.ts create mode 100644 src/toast/toast-config.ts create mode 100644 src/toast/toast.module.ts create mode 100644 src/toast/toast.scss create mode 100644 src/toast/toast.spec.ts create mode 100644 src/toast/toast.ts create mode 100644 src/tooltip/tooltip-config.spec.ts create mode 100644 src/tooltip/tooltip-config.ts create mode 100644 src/tooltip/tooltip.module.ts create mode 100644 src/tooltip/tooltip.scss create mode 100644 src/tooltip/tooltip.spec.ts create mode 100644 src/tooltip/tooltip.ts create mode 100644 src/tsconfig-ie.spec.json create mode 100644 src/tsconfig.json create mode 100644 src/tsconfig.spec.json create mode 100644 src/tslint.json create mode 100644 src/typeahead/highlight.scss create mode 100644 src/typeahead/highlight.spec.ts create mode 100644 src/typeahead/highlight.ts create mode 100644 src/typeahead/typeahead-config.spec.ts create mode 100644 src/typeahead/typeahead-config.ts create mode 100644 src/typeahead/typeahead-window.spec.ts create mode 100644 src/typeahead/typeahead-window.ts create mode 100644 src/typeahead/typeahead.module.ts create mode 100644 src/typeahead/typeahead.spec.ts create mode 100644 src/typeahead/typeahead.ts create mode 100644 src/util/accessibility/live.spec.ts create mode 100644 src/util/accessibility/live.ts create mode 100644 src/util/autoclose.ts create mode 100644 src/util/focus-trap.ts create mode 100644 src/util/key.ts create mode 100644 src/util/popup.ts create mode 100644 src/util/positioning.spec.ts create mode 100644 src/util/positioning.ts create mode 100644 src/util/scrollbar.ts create mode 100644 src/util/triggers.spec.ts create mode 100644 src/util/triggers.ts create mode 100644 src/util/util.spec.ts create mode 100644 src/util/util.ts create mode 100644 tsconfig.json create mode 100644 tslint.json create mode 100644 yarn.lock diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e0f1dae --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/angular.json b/angular.json new file mode 100644 index 0000000..872ca52 --- /dev/null +++ b/angular.json @@ -0,0 +1,137 @@ +{ + "$schema": "./node_modules/@angular-devkit/core/src/workspace/workspace-schema.json", + "version": 1, + "newProjectRoot": "", + "projects": { + "sunbird-ui-components": { + "root": "", + "sourceRoot": "src", + "projectType": "library", + "architect": { + "build": { + "builder": "@angular-devkit/build-ng-packagr:build", + "options": { + "project": "src/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "src/tsconfig.json", + "project": "src/ng-package.prod.json" + } + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/test.ts", + "tsConfig": "src/tsconfig.spec.json", + "codeCoverageExclude": [ + "src/test.ts", + "src/test/**" + ], + "karmaConfig": "src/karma.conf.js" + }, + "configurations": { + "ie": { + "tsConfig": "src/tsconfig-ie.spec.json" + } + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "src/tsconfig.json" + ], + "exclude": [ + "**/node_modules/**" + ] + } + } + } + }, + "demo": { + "root": "demo", + "sourceRoot": "demo/src", + "projectType": "application", + "prefix": "sb", + "schematics": {}, + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "demo/dist", + "index": "demo/src/public/index.html", + "main": "demo/src/main.ts", + "polyfills": "demo/src/polyfills.ts", + "tsConfig": "demo/tsconfig.json", + "assets": [ + { + "glob": "favicon.ico", + "input": "src", + "output": "/" + }, + { + "glob": "**/*", + "input": "demo/src/public", + "output": "/" + } + ], + "styles": [ + "node_modules/bootstrap/dist/css/bootstrap.css", + "node_modules/prismjs/themes/prism.css", + "demo/src/style/app.scss", + "demo/src/style/demos.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "src": "demo/src/environments/environment.ts", + "replaceWith": "demo/src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "extractCss": true, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true + } + } + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "demo:build" + }, + "configurations": { + "production": { + "browserTarget": "demo:build:production" + } + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ + "demo/tsconfig.json" + ], + "exclude": [ + "**/node_modules/**", + "**/api-docs.ts" + ] + } + } + } + }, + }, + "cli": { + "packageManager": "yarn" + } +} diff --git a/demo/src/api-docs.ts b/demo/src/api-docs.ts new file mode 100644 index 0000000..50da092 --- /dev/null +++ b/demo/src/api-docs.ts @@ -0,0 +1,3672 @@ +const API_DOCS = { + "NgbAccordionConfig": { + "fileName": "src/accordion/accordion-config.ts", + "className": "NgbAccordionConfig", + "description": "

A configuration service for the NgbAccordion component.

\n

You can inject this service, typically in your root component, and customize its properties\nto provide default values for all accordions used in the application.

", + "type": "Service", + "methods": [], + "properties": [ + { + "name": "closeOthers", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "type", + "type": "string", + "description": "" + } + ] + }, + "NgbPanelHeaderContext": { + "fileName": "src/accordion/accordion.ts", + "className": "NgbPanelHeaderContext", + "description": "

The context for the NgbPanelHeader template

", + "since": { + "version": "4.1.0", + "description": "" + }, + "type": "Interface", + "methods": [], + "properties": [ + { + "name": "opened", + "type": "boolean", + "description": "

True if current panel is opened

" + } + ] + }, + "NgbPanelHeader": { + "fileName": "src/accordion/accordion.ts", + "className": "NgbPanelHeader", + "description": "

A directive that wraps an accordion panel header with any HTML markup and a toggling button\nmarked with NgbPanelToggle.\nSee the header customization demo for more details.

\n

You can also use NgbPanelTitle to customize only the panel title.

", + "since": { + "version": "4.1.0", + "description": "" + }, + "type": "Directive", + "selector": "ng-template[ngbPanelHeader]", + "inputs": [], + "outputs": [], + "properties": [], + "methods": [] + }, + "NgbPanelTitle": { + "fileName": "src/accordion/accordion.ts", + "className": "NgbPanelTitle", + "description": "

A directive that wraps only the panel title with HTML markup inside.

\n

You can also use NgbPanelHeader to customize the full panel header.

", + "type": "Directive", + "selector": "ng-template[ngbPanelTitle]", + "inputs": [], + "outputs": [], + "properties": [], + "methods": [] + }, + "NgbPanelContent": { + "fileName": "src/accordion/accordion.ts", + "className": "NgbPanelContent", + "description": "

A directive that wraps the accordion panel content.

", + "type": "Directive", + "selector": "ng-template[ngbPanelContent]", + "inputs": [], + "outputs": [], + "properties": [], + "methods": [] + }, + "NgbPanel": { + "fileName": "src/accordion/accordion.ts", + "className": "NgbPanel", + "description": "

A directive that wraps an individual accordion panel with title and collapsible content.

", + "type": "Directive", + "selector": "ngb-panel", + "inputs": [ + { + "name": "disabled", + "defaultValue": "false", + "type": "boolean", + "description": "

If true, the panel is disabled an can't be toggled.

" + }, + { + "name": "id", + "type": "string", + "description": "

An optional id for the panel that must be unique on the page.

\n

If not provided, it will be auto-generated in the ngb-panel-xxx format.

" + }, + { + "name": "title", + "type": "string", + "description": "

The panel title.

\n

You can alternatively use NgbPanelTitle to set panel title.

" + }, + { + "name": "type", + "type": "string", + "description": "

Type of the current panel.

\n

Bootstrap provides styles for the following types: 'success', 'info', 'warning', 'danger', 'primary',\n'secondary', 'light' and 'dark'.

" + } + ], + "outputs": [], + "properties": [ + { + "name": "contentTpl", + "type": "NgbPanelContent", + "description": "" + }, + { + "name": "contentTpls", + "type": "QueryList", + "description": "" + }, + { + "name": "headerTpl", + "type": "NgbPanelHeader", + "description": "" + }, + { + "name": "headerTpls", + "type": "QueryList", + "description": "" + }, + { + "name": "isOpen", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "titleTpl", + "type": "NgbPanelTitle", + "description": "" + }, + { + "name": "titleTpls", + "type": "QueryList", + "description": "" + } + ], + "methods": [] + }, + "NgbPanelChangeEvent": { + "fileName": "src/accordion/accordion.ts", + "className": "NgbPanelChangeEvent", + "description": "

An event emitted right before toggling an accordion panel.

", + "type": "Interface", + "methods": [], + "properties": [ + { + "name": "nextState", + "type": "boolean", + "description": "

The next state of the panel.

\n

true if it will be opened, false if closed.

" + }, + { + "name": "panelId", + "type": "string", + "description": "

The id of the accordion panel that is being toggled.

" + }, + { + "name": "preventDefault", + "type": "() => void", + "description": "

Calling this function will prevent panel toggling.

" + } + ] + }, + "NgbAccordion": { + "fileName": "src/accordion/accordion.ts", + "className": "NgbAccordion", + "description": "

Accordion is a collection of collapsible panels (bootstrap cards).

\n

It can ensure only one panel is opened at a time and allows to customize panel\nheaders.

", + "type": "Component", + "selector": "ngb-accordion", + "exportAs": "ngbAccordion", + "inputs": [ + { + "name": "activeIds", + "type": "string | string[]", + "description": "

An array or comma separated strings of panel ids that should be opened initially.

\n

For subsequent changes use methods like expand(), collapse(), etc. and\nthe (panelChange) event.

" + }, + { + "name": "closeOthers", + "type": "boolean", + "description": "

If true, only one panel could be opened at a time.

\n

Opening a new panel will close others.

" + }, + { + "name": "destroyOnHide", + "defaultValue": "true", + "type": "boolean", + "description": "

If true, panel content will be detached from DOM and not simply hidden when the panel is collapsed.

" + }, + { + "name": "type", + "type": "string", + "description": "

Type of panels.

\n

Bootstrap provides styles for the following types: 'success', 'info', 'warning', 'danger', 'primary',\n'secondary', 'light' and 'dark'.

" + } + ], + "outputs": [ + { + "name": "panelChange", + "description": "

Event emitted right before the panel toggle happens.

\n

See NgbPanelChangeEvent for payload details.

" + } + ], + "properties": [ + { + "name": "panels", + "type": "QueryList", + "description": "" + } + ], + "methods": [ + { + "name": "isExpanded", + "description": "

Checks if a panel with a given id is expanded.

", + "args": [ + { + "name": "panelId", + "type": "string" + } + ], + "returnType": "boolean" + }, + { + "name": "expand", + "description": "

Expands a panel with a given id.

\n

Has no effect if the panel is already expanded or disabled.

", + "args": [ + { + "name": "panelId", + "type": "string" + } + ], + "returnType": "void" + }, + { + "name": "expandAll", + "description": "

Expands all panels, if [closeOthers] is false.

\n

If [closeOthers] is true, it will expand the first panel, unless there is already a panel opened.

", + "args": [], + "returnType": "void" + }, + { + "name": "collapse", + "description": "

Collapses a panel with the given id.

\n

Has no effect if the panel is already collapsed or disabled.

", + "args": [ + { + "name": "panelId", + "type": "string" + } + ], + "returnType": "void" + }, + { + "name": "collapseAll", + "description": "

Collapses all opened panels.

", + "args": [], + "returnType": "void" + }, + { + "name": "toggle", + "description": "

Toggles a panel with the given id.

\n

Has no effect if the panel is disabled.

", + "args": [ + { + "name": "panelId", + "type": "string" + } + ], + "returnType": "void" + } + ] + }, + "NgbPanelToggle": { + "fileName": "src/accordion/accordion.ts", + "className": "NgbPanelToggle", + "description": "

A directive to put on a button that toggles panel opening and closing.

\n

To be used inside the NgbPanelHeader

", + "since": { + "version": "4.1.0", + "description": "" + }, + "type": "Directive", + "selector": "button[ngbPanelToggle]", + "inputs": [ + { + "name": "ngbPanelToggle", + "type": "NgbPanel", + "description": "" + } + ], + "outputs": [], + "properties": [], + "methods": [] + }, + "NgbAlertConfig": { + "fileName": "src/alert/alert-config.ts", + "className": "NgbAlertConfig", + "description": "

A configuration service for the NgbAlert component.

\n

You can inject this service, typically in your root component, and customize its properties\nto provide default values for all alerts used in the application.

", + "type": "Service", + "methods": [], + "properties": [ + { + "name": "dismissible", + "defaultValue": "true", + "type": "boolean", + "description": "" + }, + { + "name": "type", + "defaultValue": "warning", + "type": "string", + "description": "" + } + ] + }, + "NgbAlert": { + "fileName": "src/alert/alert.ts", + "className": "NgbAlert", + "description": "

Alert is a component to provide contextual feedback messages for user.

\n

It supports several alert types and can be dismissed.

", + "type": "Component", + "selector": "ngb-alert", + "inputs": [ + { + "name": "dismissible", + "type": "boolean", + "description": "

If true, alert can be dismissed by the user.

\n

The close button (×) will be displayed and you can be notified\nof the event with the (close) output.

" + }, + { + "name": "type", + "type": "string", + "description": "

Type of the alert.

\n

Bootstrap provides styles for the following types: 'success', 'info', 'warning', 'danger', 'primary',\n'secondary', 'light' and 'dark'.

" + } + ], + "outputs": [ + { + "name": "close", + "description": "

An event emitted when the close button is clicked. It has no payload and only relevant for dismissible alerts.

" + } + ], + "properties": [], + "methods": [] + }, + "NgbCheckBox": { + "fileName": "src/buttons/checkbox.ts", + "className": "NgbCheckBox", + "description": "

Allows to easily create Bootstrap-style checkbox buttons.

\n

Integrates with forms, so the value of a checked button is bound to the underlying form control\neither in a reactive or template-driven way.

", + "type": "Directive", + "selector": "[ngbButton][type=checkbox]", + "inputs": [ + { + "name": "disabled", + "defaultValue": "false", + "type": "boolean", + "description": "

If true, the checkbox button will be disabled

" + }, + { + "name": "valueChecked", + "defaultValue": "true", + "type": "boolean", + "description": "

The form control value when the checkbox is checked.

" + }, + { + "name": "valueUnChecked", + "defaultValue": "false", + "type": "boolean", + "description": "

The form control value when the checkbox is unchecked.

" + } + ], + "outputs": [], + "properties": [ + { + "name": "checked", + "type": "any", + "description": "" + }, + { + "name": "onChange", + "type": "(_: any) => void", + "description": "" + }, + { + "name": "onTouched", + "type": "() => void", + "description": "" + } + ], + "methods": [] + }, + "NgbRadioGroup": { + "fileName": "src/buttons/radio.ts", + "className": "NgbRadioGroup", + "description": "

Allows to easily create Bootstrap-style radio buttons.

\n

Integrates with forms, so the value of a checked button is bound to the underlying form control\neither in a reactive or template-driven way.

", + "type": "Directive", + "selector": "[ngbRadioGroup]", + "inputs": [ + { + "name": "name", + "type": "string", + "description": "

Name of the radio group applied to radio input elements.

\n

Will be applied to all radio input elements inside the group,\nunless NgbRadio's specify names themselves.

\n

If not provided, will be generated in the ngb-radio-xx format.

" + } + ], + "outputs": [], + "properties": [ + { + "name": "disabled", + "type": "boolean", + "description": "" + }, + { + "name": "onChange", + "type": "(_: any) => void", + "description": "" + }, + { + "name": "onTouched", + "type": "() => void", + "description": "" + } + ], + "methods": [] + }, + "NgbRadio": { + "fileName": "src/buttons/radio.ts", + "className": "NgbRadio", + "description": "

A directive that marks an input of type "radio" as a part of the\nNgbRadioGroup.

", + "type": "Directive", + "selector": "[ngbButton][type=radio]", + "inputs": [ + { + "name": "disabled", + "type": "boolean", + "description": "

If true, current radio button will be disabled.

" + }, + { + "name": "name", + "type": "string", + "description": "

The value for the 'name' property of the input element.

\n

All inputs of the radio group should have the same name. If not specified,\nthe name of the enclosing group is used.

" + }, + { + "name": "value", + "type": "any", + "description": "

The form control value when current radio button is checked.

" + } + ], + "outputs": [], + "properties": [ + { + "name": "checked", + "type": "boolean", + "description": "" + }, + { + "name": "disabled", + "type": "boolean", + "description": "

If true, current radio button will be disabled.

" + }, + { + "name": "nameAttr", + "type": "string", + "description": "" + }, + { + "name": "value", + "type": "any", + "description": "

The form control value when current radio button is checked.

" + } + ], + "methods": [] + }, + "NgbCarouselConfig": { + "fileName": "src/carousel/carousel-config.ts", + "className": "NgbCarouselConfig", + "description": "

A configuration service for the NgbCarousel component.

\n

You can inject this service, typically in your root component, and customize its properties\nto provide default values for all carousels used in the application.

", + "type": "Service", + "methods": [], + "properties": [ + { + "name": "interval", + "defaultValue": "5000", + "type": "number", + "description": "" + }, + { + "name": "keyboard", + "defaultValue": "true", + "type": "boolean", + "description": "" + }, + { + "name": "pauseOnHover", + "defaultValue": "true", + "type": "boolean", + "description": "" + }, + { + "name": "showNavigationArrows", + "defaultValue": "true", + "type": "boolean", + "description": "" + }, + { + "name": "showNavigationIndicators", + "defaultValue": "true", + "type": "boolean", + "description": "" + }, + { + "name": "wrap", + "defaultValue": "true", + "type": "boolean", + "description": "" + } + ] + }, + "NgbSlide": { + "fileName": "src/carousel/carousel.ts", + "className": "NgbSlide", + "description": "

A directive that wraps the individual carousel slide.

", + "type": "Directive", + "selector": "ng-template[ngbSlide]", + "inputs": [ + { + "name": "id", + "type": "string", + "description": "

Slide id that must be unique for the entire document.

\n

If not provided, will be generated in the ngb-slide-xx format.

" + } + ], + "outputs": [], + "properties": [], + "methods": [] + }, + "NgbCarousel": { + "fileName": "src/carousel/carousel.ts", + "className": "NgbCarousel", + "description": "

Carousel is a component to easily create and control slideshows.

\n

Allows to set intervals, change the way user interacts with the slides and provides a programmatic API.

", + "type": "Component", + "selector": "ngb-carousel", + "exportAs": "ngbCarousel", + "inputs": [ + { + "name": "activeId", + "type": "string", + "description": "

The slide id that should be displayed initially.

\n

For subsequent interactions use methods select(), next(), etc. and the (slide) output.

" + }, + { + "name": "interval", + "type": "number", + "description": "

Time in milliseconds before the next slide is shown.

" + }, + { + "name": "keyboard", + "type": "boolean", + "description": "

If true, allows to interact with carousel using keyboard 'arrow left' and 'arrow right'.

" + }, + { + "name": "pauseOnHover", + "type": "boolean", + "description": "

If true, will pause slide switching when mouse cursor hovers the slide.

", + "since": { + "version": "2.2.0", + "description": "" + } + }, + { + "name": "showNavigationArrows", + "type": "boolean", + "description": "

If true, 'previous' and 'next' navigation arrows will be visible on the slide.

", + "since": { + "version": "2.2.0", + "description": "" + } + }, + { + "name": "showNavigationIndicators", + "type": "boolean", + "description": "

If true, navigation indicators at the bottom of the slide will be visible.

", + "since": { + "version": "2.2.0", + "description": "" + } + }, + { + "name": "wrap", + "type": "boolean", + "description": "

If true, will 'wrap' the carousel by switching from the last slide back to the first.

" + } + ], + "outputs": [ + { + "name": "slide", + "description": "

An event emitted right after the slide transition is completed.

\n

See NgbSlideEvent for payload details.

" + } + ], + "properties": [ + { + "name": "interval", + "type": "number", + "description": "

Time in milliseconds before the next slide is shown.

" + }, + { + "name": "NgbSlideEventSource", + "defaultValue": "NgbSlideEventSource", + "type": "typeof NgbSlideEventSource", + "description": "" + }, + { + "name": "pauseOnHover", + "type": "boolean", + "description": "

If true, will pause slide switching when mouse cursor hovers the slide.

", + "since": { + "version": "2.2.0", + "description": "" + } + }, + { + "name": "slides", + "type": "QueryList", + "description": "" + }, + { + "name": "wrap", + "type": "boolean", + "description": "

If true, will 'wrap' the carousel by switching from the last slide back to the first.

" + } + ], + "methods": [ + { + "name": "select", + "description": "

Navigates to a slide with the specified identifier.

", + "args": [ + { + "name": "slideId", + "type": "string" + }, + { + "name": "source", + "type": "NgbSlideEventSource" + } + ], + "returnType": "void" + }, + { + "name": "prev", + "description": "

Navigates to the previous slide.

", + "args": [ + { + "name": "source", + "type": "NgbSlideEventSource" + } + ], + "returnType": "void" + }, + { + "name": "next", + "description": "

Navigates to the next slide.

", + "args": [ + { + "name": "source", + "type": "NgbSlideEventSource" + } + ], + "returnType": "void" + }, + { + "name": "pause", + "description": "

Pauses cycling through the slides.

", + "args": [], + "returnType": "void" + }, + { + "name": "cycle", + "description": "

Restarts cycling through the slides from left to right.

", + "args": [], + "returnType": "void" + } + ] + }, + "NgbSlideEvent": { + "fileName": "src/carousel/carousel.ts", + "className": "NgbSlideEvent", + "description": "

A slide change event emitted right after the slide transition is completed.

", + "type": "Interface", + "methods": [], + "properties": [ + { + "name": "current", + "type": "string", + "description": "

The current slide id.

" + }, + { + "name": "direction", + "type": "NgbSlideEventDirection", + "description": "

The slide event direction.

\n

Possible values are 'left' | 'right'.

" + }, + { + "name": "paused", + "type": "boolean", + "description": "

Whether the pause() method was called (and no cycle() call was done afterwards).

", + "since": { + "version": "5.1.0", + "description": "" + } + }, + { + "name": "prev", + "type": "string", + "description": "

The previous slide id.

" + }, + { + "name": "source", + "type": "NgbSlideEventSource", + "description": "

Source triggering the slide change event.

\n

Possible values are 'timer' | 'arrowLeft' | 'arrowRight' | 'indicator'

", + "since": { + "version": "5.1.0", + "description": "" + } + } + ] + }, + "NgbCollapse": { + "fileName": "src/collapse/collapse.ts", + "className": "NgbCollapse", + "description": "

A directive to provide a simple way of hiding and showing elements on the page.

", + "type": "Directive", + "selector": "[ngbCollapse]", + "exportAs": "ngbCollapse", + "inputs": [ + { + "name": "ngbCollapse", + "defaultValue": "false", + "type": "boolean", + "description": "

If true, will collapse the element or show it otherwise.

" + } + ], + "outputs": [], + "properties": [], + "methods": [] + }, + "NgbDateAdapter": { + "fileName": "src/datepicker/adapters/ngb-date-adapter.ts", + "className": "NgbDateAdapter", + "description": "

An abstract service that does the conversion between the internal datepicker NgbDateStruct model and\nany provided user date model D, ex. a string, a native date, etc.

\n

The adapter is used only for conversion when binding datepicker to a form control,\nex. [(ngModel)]="userDateModel". Here userDateModel can be of any type.

\n

The default datepicker implementation assumes we use NgbDateStruct as a user model.

\n

See the date format overview for more details\nand the custom adapter demo for an example.

", + "typeParameter": "D", + "type": "Service", + "methods": [ + { + "name": "fromModel", + "description": "

Converts a user-model date of type D to an NgbDateStruct for internal use.

", + "args": [ + { + "name": "value", + "type": "D" + } + ], + "returnType": "NgbDateStruct" + }, + { + "name": "toModel", + "description": "

Converts an internal NgbDateStruct date to a user-model date of type D.

", + "args": [ + { + "name": "date", + "type": "NgbDateStruct" + } + ], + "returnType": "D" + } + ], + "properties": [] + }, + "NgbDateNativeAdapter": { + "fileName": "src/datepicker/adapters/ngb-date-native-adapter.ts", + "className": "NgbDateNativeAdapter", + "description": "

NgbDateAdapter implementation that uses\nnative javascript dates as a user date model.

", + "type": "Service", + "methods": [ + { + "name": "fromModel", + "description": "

Converts a native Date to a NgbDateStruct.

", + "args": [ + { + "name": "date", + "type": "Date" + } + ], + "returnType": "NgbDateStruct" + }, + { + "name": "toModel", + "description": "

Converts a NgbDateStruct to a native Date.

", + "args": [ + { + "name": "date", + "type": "NgbDateStruct" + } + ], + "returnType": "Date" + } + ], + "properties": [] + }, + "NgbDateNativeUTCAdapter": { + "fileName": "src/datepicker/adapters/ngb-date-native-utc-adapter.ts", + "className": "NgbDateNativeUTCAdapter", + "description": "

Same as NgbDateNativeAdapter, but with UTC dates.

", + "since": { + "version": "3.2.0", + "description": "" + }, + "type": "Service", + "methods": [], + "properties": [] + }, + "NgbDatepickerConfig": { + "fileName": "src/datepicker/datepicker-config.ts", + "className": "NgbDatepickerConfig", + "description": "

A configuration service for the NgbDatepicker component.

\n

You can inject this service, typically in your root component, and customize the values of its properties in\norder to provide default values for all the datepickers used in the application.

", + "type": "Service", + "methods": [], + "properties": [ + { + "name": "dayTemplate", + "type": "TemplateRef", + "description": "" + }, + { + "name": "dayTemplateData", + "type": "(date: NgbDateStruct, current: { year: number; month: number; }) => any", + "description": "" + }, + { + "name": "displayMonths", + "defaultValue": "1", + "type": "number", + "description": "" + }, + { + "name": "firstDayOfWeek", + "defaultValue": "1", + "type": "number", + "description": "" + }, + { + "name": "footerTemplate", + "type": "TemplateRef", + "description": "" + }, + { + "name": "markDisabled", + "type": "(date: NgbDateStruct, current: { year: number; month: number; }) => boolean", + "description": "" + }, + { + "name": "maxDate", + "type": "NgbDateStruct", + "description": "" + }, + { + "name": "minDate", + "type": "NgbDateStruct", + "description": "" + }, + { + "name": "navigation", + "defaultValue": "select", + "type": "\"select\" | \"arrows\" | \"none\"", + "description": "" + }, + { + "name": "outsideDays", + "defaultValue": "visible", + "type": "\"visible\" | \"collapsed\" | \"hidden\"", + "description": "" + }, + { + "name": "showWeekdays", + "defaultValue": "true", + "type": "boolean", + "description": "" + }, + { + "name": "showWeekNumbers", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "startDate", + "type": "{ year: number; month: number; }", + "description": "" + } + ] + }, + "DayTemplateContext": { + "fileName": "src/datepicker/datepicker-day-template-context.ts", + "className": "DayTemplateContext", + "description": "

The context for the datepicker 'day' template.

\n

You can override the way dates are displayed in the datepicker via the [dayTemplate] input.

", + "type": "Interface", + "methods": [], + "properties": [ + { + "name": "$implicit", + "type": "NgbDate", + "description": "

The date that corresponds to the template. Same as the date parameter.

\n

Can be used for convenience as a default template key, ex. let-d.

", + "since": { + "version": "3.3.0", + "description": "" + } + }, + { + "name": "currentMonth", + "type": "number", + "description": "

The month currently displayed by the datepicker.

" + }, + { + "name": "data", + "type": "any", + "description": "

Any data you pass using the [dayTemplateData] input in the datepicker.

", + "since": { + "version": "3.3.0", + "description": "" + } + }, + { + "name": "date", + "type": "NgbDate", + "description": "

The date that corresponds to the template.

" + }, + { + "name": "disabled", + "type": "boolean", + "description": "

True if the current date is disabled.

" + }, + { + "name": "focused", + "type": "boolean", + "description": "

True if the current date is focused.

" + }, + { + "name": "selected", + "type": "boolean", + "description": "

True if the current date is selected.

" + }, + { + "name": "today", + "type": "boolean", + "description": "

True if the current date is today (equal to NgbCalendar.getToday()).

", + "since": { + "version": "4.1.0", + "description": "" + } + } + ] + }, + "NgbDatepickerI18n": { + "fileName": "src/datepicker/datepicker-i18n.ts", + "className": "NgbDatepickerI18n", + "description": "

A service supplying i18n data to the datepicker component.

\n

The default implementation of this service uses the Angular locale and registered locale data for\nweekdays and month names (as explained in the Angular i18n guide).

\n

It also provides a way to i18n data that depends on calendar calculations, like aria labels, day, week and year\nnumerals. For other static labels the datepicker uses the default Angular i18n.

\n

See the i18n demo and\nHebrew calendar demo on how to extend this class and define\na custom provider for i18n.

", + "type": "Service", + "methods": [ + { + "name": "getWeekdayShortName", + "description": "

Returns the short weekday name to display in the heading of the month view.

\n

With default calendar we use ISO 8601: 'weekday' is 1=Mon ... 7=Sun.

", + "args": [ + { + "name": "weekday", + "type": "number" + } + ], + "returnType": "string" + }, + { + "name": "getMonthShortName", + "description": "

Returns the short month name to display in the date picker navigation.

\n

With default calendar we use ISO 8601: 'month' is 1=Jan ... 12=Dec.

", + "args": [ + { + "name": "month", + "type": "number" + }, + { + "name": "year", + "type": "number" + } + ], + "returnType": "string" + }, + { + "name": "getMonthFullName", + "description": "

Returns the full month name to display in the date picker navigation.

\n

With default calendar we use ISO 8601: 'month' is 1=Jan ... 12=Dec.

", + "args": [ + { + "name": "month", + "type": "number" + }, + { + "name": "year", + "type": "number" + } + ], + "returnType": "string" + }, + { + "name": "getDayAriaLabel", + "description": "

Returns the value of the aria-label attribute for a specific date.

", + "args": [ + { + "name": "date", + "type": "NgbDateStruct" + } + ], + "returnType": "string", + "since": { + "version": "2.0.0", + "description": "" + } + }, + { + "name": "getDayNumerals", + "description": "

Returns the textual representation of a day that is rendered in a day cell.

", + "args": [ + { + "name": "date", + "type": "NgbDateStruct" + } + ], + "returnType": "string", + "since": { + "version": "3.0.0", + "description": "" + } + }, + { + "name": "getWeekNumerals", + "description": "

Returns the textual representation of a week number rendered by datepicker.

", + "args": [ + { + "name": "weekNumber", + "type": "number" + } + ], + "returnType": "string", + "since": { + "version": "3.0.0", + "description": "" + } + }, + { + "name": "getYearNumerals", + "description": "

Returns the textual representation of a year that is rendered in the datepicker year select box.

", + "args": [ + { + "name": "year", + "type": "number" + } + ], + "returnType": "string", + "since": { + "version": "3.0.0", + "description": "" + } + } + ], + "properties": [] + }, + "NgbInputDatepicker": { + "fileName": "src/datepicker/datepicker-input.ts", + "className": "NgbInputDatepicker", + "description": "

A directive that allows to stick a datepicker popup to an input field.

\n

Manages interaction with the input field itself, does value formatting and provides forms integration.

", + "type": "Directive", + "selector": "input[ngbDatepicker]", + "exportAs": "ngbDatepicker", + "inputs": [ + { + "name": "autoClose", + "defaultValue": "true", + "type": "boolean | \"inside\" | \"outside\"", + "description": "

Indicates whether the datepicker popup should be closed automatically after date selection / outside click or not.

\n
    \n
  • true - the popup will close on both date selection and outside click.
  • \n
  • false - the popup can only be closed manually via close() or toggle() methods.
  • \n
  • "inside" - the popup will close on date selection, but not outside clicks.
  • \n
  • "outside" - the popup will close only on the outside click and not on date selection/inside clicks.
  • \n
", + "since": { + "version": "3.0.0", + "description": "" + } + }, + { + "name": "container", + "type": "string", + "description": "

A selector specifying the element the datepicker popup should be appended to.

\n

Currently only supports "body".

" + }, + { + "name": "dayTemplate", + "type": "TemplateRef", + "description": "

The reference to a custom template for the day.

\n

Allows to completely override the way a day 'cell' in the calendar is displayed.

\n

See DayTemplateContext for the data you get inside.

" + }, + { + "name": "dayTemplateData", + "type": "(date: NgbDate, current: { year: number; month: number; }) => any", + "description": "

The callback to pass any arbitrary data to the template cell via the\nDayTemplateContext's data parameter.

\n

current is the month that is currently displayed by the datepicker.

", + "since": { + "version": "3.3.0", + "description": "" + } + }, + { + "name": "disabled", + "type": "any", + "description": "" + }, + { + "name": "displayMonths", + "type": "number", + "description": "

The number of months to display.

" + }, + { + "name": "firstDayOfWeek", + "type": "number", + "description": "

The first day of the week.

\n

With default calendar we use ISO 8601: 'weekday' is 1=Mon ... 7=Sun.

" + }, + { + "name": "footerTemplate", + "type": "TemplateRef", + "description": "

The reference to the custom template for the datepicker footer.

", + "since": { + "version": "3.3.0", + "description": "" + } + }, + { + "name": "markDisabled", + "type": "(date: NgbDate, current: { year: number; month: number; }) => boolean", + "description": "

The callback to mark some dates as disabled.

\n

It is called for each new date when navigating to a different month.

\n

current is the month that is currently displayed by the datepicker.

" + }, + { + "name": "maxDate", + "type": "NgbDateStruct", + "description": "

The latest date that can be displayed or selected. Also used for form validation.

\n

If not provided, 'year' select box will display 10 years after the current month.

" + }, + { + "name": "minDate", + "type": "NgbDateStruct", + "description": "

The earliest date that can be displayed or selected. Also used for form validation.

\n

If not provided, 'year' select box will display 10 years before the current month.

" + }, + { + "name": "navigation", + "type": "\"select\" | \"arrows\" | \"none\"", + "description": "

Navigation type.

\n
    \n
  • "select" - select boxes for month and navigation arrows
  • \n
  • "arrows" - only navigation arrows
  • \n
  • "none" - no navigation visible at all
  • \n
" + }, + { + "name": "outsideDays", + "type": "\"visible\" | \"collapsed\" | \"hidden\"", + "description": "

The way of displaying days that don't belong to the current month.

\n
    \n
  • "visible" - days are visible
  • \n
  • "hidden" - days are hidden, white space preserved
  • \n
  • "collapsed" - days are collapsed, so the datepicker height might change between months
  • \n
\n

For the 2+ months view, days in between months are never shown.

" + }, + { + "name": "placement", + "type": "PlacementArray", + "description": "

The preferred placement of the datepicker popup.

\n

Possible values are "top", "top-left", "top-right", "bottom", "bottom-left",\n"bottom-right", "left", "left-top", "left-bottom", "right", "right-top",\n"right-bottom"

\n

Accepts an array of strings or a string with space separated possible values.

\n

The default order of preference is "bottom-left bottom-right top-left top-right"

\n

Please see the positioning overview for more details.

" + }, + { + "name": "positionTarget", + "type": "string | HTMLElement", + "description": "

A css selector or html element specifying the element the datepicker popup should be positioned against.

\n

By default the input is used as a target.

", + "since": { + "version": "4.2.0", + "description": "" + } + }, + { + "name": "showWeekdays", + "type": "boolean", + "description": "

If true, weekdays will be displayed.

" + }, + { + "name": "showWeekNumbers", + "type": "boolean", + "description": "

If true, week numbers will be displayed.

" + }, + { + "name": "startDate", + "type": "{ year: number; month: number; day?: number; }", + "description": "

The date to open calendar with.

\n

With the default calendar we use ISO 8601: 'month' is 1=Jan ... 12=Dec.\nIf nothing or invalid date is provided, calendar will open with current month.

\n

You could use navigateTo(date) method as an alternative.

" + } + ], + "outputs": [ + { + "name": "closed", + "description": "

An event fired after closing datepicker window.

", + "since": { + "version": "4.2.0", + "description": "" + } + }, + { + "name": "dateSelect", + "description": "

An event emitted when user selects a date using keyboard or mouse.

\n

The payload of the event is currently selected NgbDate.

", + "since": { + "version": "1.1.1", + "description": "" + } + }, + { + "name": "navigate", + "description": "

Event emitted right after the navigation happens and displayed month changes.

\n

See NgbDatepickerNavigateEvent for the payload info.

" + } + ], + "properties": [], + "methods": [ + { + "name": "open", + "description": "

Opens the datepicker popup.

\n

If the related form control contains a valid date, the corresponding month will be opened.

", + "args": [], + "returnType": "void" + }, + { + "name": "close", + "description": "

Closes the datepicker popup.

", + "args": [], + "returnType": "void" + }, + { + "name": "toggle", + "description": "

Toggles the datepicker popup.

", + "args": [], + "returnType": "void" + }, + { + "name": "navigateTo", + "description": "

Navigates to the provided date.

\n

With the default calendar we use ISO 8601: 'month' is 1=Jan ... 12=Dec.\nIf nothing or invalid date provided calendar will open current month.

\n

Use the [startDate] input as an alternative.

", + "args": [ + { + "name": "date", + "type": "{ year: number; month: number; day?: number; }" + } + ], + "returnType": "void" + } + ] + }, + "NgbDatepickerNavigateEvent": { + "fileName": "src/datepicker/datepicker.ts", + "className": "NgbDatepickerNavigateEvent", + "description": "

An event emitted right before the navigation happens and the month displayed by the datepicker changes.

", + "type": "Interface", + "methods": [], + "properties": [ + { + "name": "current", + "type": "{ year: number; month: number; }", + "description": "

The currently displayed month.

" + }, + { + "name": "next", + "type": "{ year: number; month: number; }", + "description": "

The month we're navigating to.

" + }, + { + "name": "preventDefault", + "type": "() => void", + "description": "

Calling this function will prevent navigation from happening.

", + "since": { + "version": "4.1.0", + "description": "" + } + } + ] + }, + "NgbDatepicker": { + "fileName": "src/datepicker/datepicker.ts", + "className": "NgbDatepicker", + "description": "

A highly configurable component that helps you with selecting calendar dates.

\n

NgbDatepicker is meant to be displayed inline on a page or put inside a popup.

", + "type": "Component", + "selector": "ngb-datepicker", + "exportAs": "ngbDatepicker", + "inputs": [ + { + "name": "dayTemplate", + "type": "TemplateRef", + "description": "

The reference to a custom template for the day.

\n

Allows to completely override the way a day 'cell' in the calendar is displayed.

\n

See DayTemplateContext for the data you get inside.

" + }, + { + "name": "dayTemplateData", + "type": "(date: NgbDate, current: { year: number; month: number; }) => any", + "description": "

The callback to pass any arbitrary data to the template cell via the\nDayTemplateContext's data parameter.

\n

current is the month that is currently displayed by the datepicker.

", + "since": { + "version": "3.3.0", + "description": "" + } + }, + { + "name": "displayMonths", + "type": "number", + "description": "

The number of months to display.

" + }, + { + "name": "firstDayOfWeek", + "type": "number", + "description": "

The first day of the week.

\n

With default calendar we use ISO 8601: 'weekday' is 1=Mon ... 7=Sun.

" + }, + { + "name": "footerTemplate", + "type": "TemplateRef", + "description": "

The reference to the custom template for the datepicker footer.

", + "since": { + "version": "3.3.0", + "description": "" + } + }, + { + "name": "markDisabled", + "type": "(date: NgbDate, current: { year: number; month: number; }) => boolean", + "description": "

The callback to mark some dates as disabled.

\n

It is called for each new date when navigating to a different month.

\n

current is the month that is currently displayed by the datepicker.

" + }, + { + "name": "maxDate", + "type": "NgbDateStruct", + "description": "

The latest date that can be displayed or selected.

\n

If not provided, 'year' select box will display 10 years after the current month.

" + }, + { + "name": "minDate", + "type": "NgbDateStruct", + "description": "

The earliest date that can be displayed or selected.

\n

If not provided, 'year' select box will display 10 years before the current month.

" + }, + { + "name": "navigation", + "type": "\"select\" | \"arrows\" | \"none\"", + "description": "

Navigation type.

\n
    \n
  • "select" - select boxes for month and navigation arrows
  • \n
  • "arrows" - only navigation arrows
  • \n
  • "none" - no navigation visible at all
  • \n
" + }, + { + "name": "outsideDays", + "type": "\"visible\" | \"collapsed\" | \"hidden\"", + "description": "

The way of displaying days that don't belong to the current month.

\n
    \n
  • "visible" - days are visible
  • \n
  • "hidden" - days are hidden, white space preserved
  • \n
  • "collapsed" - days are collapsed, so the datepicker height might change between months
  • \n
\n

For the 2+ months view, days in between months are never shown.

" + }, + { + "name": "showWeekdays", + "type": "boolean", + "description": "

If true, weekdays will be displayed.

" + }, + { + "name": "showWeekNumbers", + "type": "boolean", + "description": "

If true, week numbers will be displayed.

" + }, + { + "name": "startDate", + "type": "{ year: number; month: number; day?: number; }", + "description": "

The date to open calendar with.

\n

With the default calendar we use ISO 8601: 'month' is 1=Jan ... 12=Dec.\nIf nothing or invalid date is provided, calendar will open with current month.

\n

You could use navigateTo(date) method as an alternative.

" + } + ], + "outputs": [ + { + "name": "navigate", + "description": "

An event emitted right before the navigation happens and displayed month changes.

\n

See NgbDatepickerNavigateEvent for the payload info.

" + }, + { + "name": "select", + "description": "

An event emitted when user selects a date using keyboard or mouse.

\n

The payload of the event is currently selected NgbDate.

" + } + ], + "properties": [ + { + "name": "model", + "type": "DatepickerViewModel", + "description": "" + }, + { + "name": "onChange", + "type": "(_: any) => void", + "description": "" + }, + { + "name": "onTouched", + "type": "() => void", + "description": "" + } + ], + "methods": [ + { + "name": "navigateTo", + "description": "

Navigates to the provided date.

\n

With the default calendar we use ISO 8601: 'month' is 1=Jan ... 12=Dec.\nIf nothing or invalid date provided calendar will open current month.

\n

Use the [startDate] input as an alternative.

", + "args": [ + { + "name": "date", + "type": "{ year: number; month: number; day?: number; }" + } + ], + "returnType": "void" + } + ] + }, + "NgbCalendar": { + "fileName": "src/datepicker/ngb-calendar.ts", + "className": "NgbCalendar", + "description": "

A service that represents the calendar used by the datepicker.

\n

The default implementation uses the Gregorian calendar. You can inject it in your own\nimplementations if necessary to simplify NgbDate calculations.

", + "type": "Service", + "methods": [ + { + "name": "getDaysPerWeek", + "description": "

Returns the number of days per week.

", + "args": [], + "returnType": "number" + }, + { + "name": "getMonths", + "description": "

Returns an array of months per year.

\n

With default calendar we use ISO 8601 and return [1, 2, ..., 12];

", + "args": [ + { + "name": "year", + "type": "number" + } + ], + "returnType": "number[]" + }, + { + "name": "getWeeksPerMonth", + "description": "

Returns the number of weeks per month.

", + "args": [], + "returnType": "number" + }, + { + "name": "getWeekday", + "description": "

Returns the weekday number for a given day.

\n

With the default calendar we use ISO 8601: 'weekday' is 1=Mon ... 7=Sun

", + "args": [ + { + "name": "date", + "type": "NgbDate" + } + ], + "returnType": "number" + }, + { + "name": "getNext", + "description": "

Adds a number of years, months or days to a given date.

\n
    \n
  • period can be y, m or d and defaults to day.
  • \n
  • number defaults to 1.
  • \n
\n

Always returns a new date.

", + "args": [ + { + "name": "date", + "type": "NgbDate" + }, + { + "name": "period", + "type": "NgbPeriod" + }, + { + "name": "number", + "type": "number" + } + ], + "returnType": "NgbDate" + }, + { + "name": "getPrev", + "description": "

Subtracts a number of years, months or days from a given date.

\n
    \n
  • period can be y, m or d and defaults to day.
  • \n
  • number defaults to 1.
  • \n
\n

Always returns a new date.

", + "args": [ + { + "name": "date", + "type": "NgbDate" + }, + { + "name": "period", + "type": "NgbPeriod" + }, + { + "name": "number", + "type": "number" + } + ], + "returnType": "NgbDate" + }, + { + "name": "getWeekNumber", + "description": "

Returns the week number for a given week.

", + "args": [ + { + "name": "week", + "type": "NgbDate[]" + }, + { + "name": "firstDayOfWeek", + "type": "number" + } + ], + "returnType": "number" + }, + { + "name": "getToday", + "description": "

Returns the today's date.

", + "args": [], + "returnType": "NgbDate" + }, + { + "name": "isValid", + "description": "

Checks if a date is valid in the current calendar.

", + "args": [ + { + "name": "date", + "type": "NgbDate" + } + ], + "returnType": "boolean" + } + ], + "properties": [] + }, + "NgbDateParserFormatter": { + "fileName": "src/datepicker/ngb-date-parser-formatter.ts", + "className": "NgbDateParserFormatter", + "description": "

An abstract service for parsing and formatting dates for the\nNgbInputDatepicker directive.\nConverts between the internal NgbDateStruct model presentation and a string that is displayed in the\ninput element.

\n

When user types something in the input this service attempts to parse it into a NgbDateStruct object.\nAnd vice versa, when users selects a date in the calendar with the mouse, it must be displayed as a string\nin the input.

\n

Default implementation uses the ISO 8601 format, but you can provide another implementation via DI\nto use an alternative string format or a custom parsing logic.

\n

See the date format overview for more details.

", + "type": "Service", + "methods": [ + { + "name": "parse", + "description": "

Parses the given string to an NgbDateStruct.

\n

Implementations should try their best to provide a result, even\npartial. They must return null if the value can't be parsed.

", + "args": [ + { + "name": "value", + "type": "string" + } + ], + "returnType": "NgbDateStruct" + }, + { + "name": "format", + "description": "

Formats the given NgbDateStruct to a string.

\n

Implementations should return an empty string if the given date is null,\nand try their best to provide a partial result if the given date is incomplete or invalid.

", + "args": [ + { + "name": "date", + "type": "NgbDateStruct" + } + ], + "returnType": "string" + } + ], + "properties": [] + }, + "NgbDateStruct": { + "fileName": "src/datepicker/ngb-date-struct.ts", + "className": "NgbDateStruct", + "description": "

An interface of the date model used by the datepicker.

\n

All datepicker APIs consume NgbDateStruct, but return NgbDate.

\n

See the date format overview for more details.

", + "type": "Interface", + "methods": [], + "properties": [ + { + "name": "day", + "type": "number", + "description": "

The day of month, starting at 1

" + }, + { + "name": "month", + "type": "number", + "description": "

The month, for example 1=Jan ... 12=Dec

" + }, + { + "name": "year", + "type": "number", + "description": "

The year, for example 2016

" + } + ] + }, + "NgbDate": { + "fileName": "src/datepicker/ngb-date.ts", + "className": "NgbDate", + "description": "

A simple class that represents a date that datepicker also uses internally.

\n

It is the implementation of the NgbDateStruct interface that adds some convenience methods,\nlike .equals(), .before(), etc.

\n

All datepicker APIs consume NgbDateStruct, but return NgbDate.

\n

In many cases it is simpler to manipulate these objects together with\nNgbCalendar than native JS Dates.

\n

See the date format overview for more details.

", + "since": { + "version": "3.0.0", + "description": "" + }, + "type": "Class", + "methods": [ + { + "name": "from", + "description": "

A static method that creates a new date object from the NgbDateStruct,

\n

ex. NgbDate.from({year: 2000, month: 5, day: 1}).

\n

If the date is already of NgbDate type, the method will return the same object.

", + "args": [ + { + "name": "date", + "type": "NgbDateStruct" + } + ], + "returnType": "NgbDate" + }, + { + "name": "equals", + "description": "

Checks if the current date is equal to another date.

", + "args": [ + { + "name": "other", + "type": "NgbDateStruct" + } + ], + "returnType": "boolean" + }, + { + "name": "before", + "description": "

Checks if the current date is before another date.

", + "args": [ + { + "name": "other", + "type": "NgbDateStruct" + } + ], + "returnType": "boolean" + }, + { + "name": "after", + "description": "

Checks if the current date is after another date.

", + "args": [ + { + "name": "other", + "type": "NgbDateStruct" + } + ], + "returnType": "boolean" + } + ], + "properties": [ + { + "name": "day", + "type": "number", + "description": "

The day of month, starting with 1

" + }, + { + "name": "month", + "type": "number", + "description": "

The month, for example 1=Jan ... 12=Dec as in ISO 8601

" + }, + { + "name": "year", + "type": "number", + "description": "

The year, for example 2016

" + } + ] + }, + "NgbDropdownConfig": { + "fileName": "src/dropdown/dropdown-config.ts", + "className": "NgbDropdownConfig", + "description": "

A configuration service for the NgbDropdown component.

\n

You can inject this service, typically in your root component, and customize the values of its properties in\norder to provide default values for all the dropdowns used in the application.

", + "type": "Service", + "methods": [], + "properties": [ + { + "name": "autoClose", + "defaultValue": "true", + "type": "boolean | \"inside\" | \"outside\"", + "description": "" + }, + { + "name": "container", + "type": "\"body\"", + "description": "" + }, + { + "name": "placement", + "type": "PlacementArray", + "description": "" + } + ] + }, + "NgbDropdownItem": { + "fileName": "src/dropdown/dropdown.ts", + "className": "NgbDropdownItem", + "description": "

A directive you should put put on a dropdown item to enable keyboard navigation.\nArrow keys will move focus between items marked with this directive.

", + "since": { + "version": "4.1.0", + "description": "" + }, + "type": "Directive", + "selector": "[ngbDropdownItem]", + "inputs": [ + { + "name": "disabled", + "type": "boolean", + "description": "" + } + ], + "outputs": [], + "properties": [ + { + "name": "disabled", + "type": "boolean", + "description": "" + } + ], + "methods": [] + }, + "NgbDropdownMenu": { + "fileName": "src/dropdown/dropdown.ts", + "className": "NgbDropdownMenu", + "description": "

A directive that wraps dropdown menu content and dropdown items.

", + "type": "Directive", + "selector": "[ngbDropdownMenu]", + "inputs": [], + "outputs": [], + "properties": [ + { + "name": "isOpen", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "menuItems", + "type": "QueryList", + "description": "" + }, + { + "name": "placement", + "defaultValue": "bottom", + "type": "Placement", + "description": "" + } + ], + "methods": [] + }, + "NgbDropdownAnchor": { + "fileName": "src/dropdown/dropdown.ts", + "className": "NgbDropdownAnchor", + "description": "

A directive to mark an element to which dropdown menu will be anchored.

\n

This is a simple version of the NgbDropdownToggle directive.\nIt plays the same role, but doesn't listen to click events to toggle dropdown menu thus enabling support\nfor events other than click.

", + "since": { + "version": "1.1.0", + "description": "" + }, + "type": "Directive", + "selector": "[ngbDropdownAnchor]", + "inputs": [], + "outputs": [], + "properties": [ + { + "name": "anchorEl", + "type": "any", + "description": "" + } + ], + "methods": [] + }, + "NgbDropdownToggle": { + "fileName": "src/dropdown/dropdown.ts", + "className": "NgbDropdownToggle", + "description": "

A directive to mark an element that will toggle dropdown via the click event.

\n

You can also use NgbDropdownAnchor as an alternative.

", + "type": "Directive", + "selector": "[ngbDropdownToggle]", + "inputs": [], + "outputs": [], + "properties": [], + "methods": [] + }, + "NgbDropdown": { + "fileName": "src/dropdown/dropdown.ts", + "className": "NgbDropdown", + "description": "

A directive that provides contextual overlays for displaying lists of links and more.

", + "type": "Directive", + "selector": "[ngbDropdown]", + "exportAs": "ngbDropdown", + "inputs": [ + { + "name": "autoClose", + "type": "boolean | \"inside\" | \"outside\"", + "description": "

Indicates whether the dropdown should be closed when clicking one of dropdown items or pressing ESC.

\n
    \n
  • true - the dropdown will close on both outside and inside (menu) clicks.
  • \n
  • false - the dropdown can only be closed manually via close() or toggle() methods.
  • \n
  • "inside" - the dropdown will close on inside menu clicks, but not outside clicks.
  • \n
  • "outside" - the dropdown will close only on the outside clicks and not on menu clicks.
  • \n
" + }, + { + "name": "container", + "type": "\"body\"", + "description": "

A selector specifying the element the dropdown should be appended to.\nCurrently only supports "body".

", + "since": { + "version": "4.1.0", + "description": "" + } + }, + { + "name": "display", + "type": "\"dynamic\" | \"static\"", + "description": "

Enable or disable the dynamic positioning. The default value is dynamic unless the dropdown is used\ninside a Bootstrap navbar. If you need custom placement for a dropdown in a navbar, set it to\ndynamic explicitly. See the positioning of dropdown\nand the navbar demo for more details.

", + "since": { + "version": "4.2.0", + "description": "" + } + }, + { + "name": "open", + "defaultValue": "false", + "type": "boolean", + "description": "

Defines whether or not the dropdown menu is opened initially.

" + }, + { + "name": "placement", + "type": "PlacementArray", + "description": "

The preferred placement of the dropdown.

\n

Possible values are "top", "top-left", "top-right", "bottom", "bottom-left",\n"bottom-right", "left", "left-top", "left-bottom", "right", "right-top",\n"right-bottom"

\n

Accepts an array of strings or a string with space separated possible values.

\n

The default order of preference is "bottom-left bottom-right top-left top-right"

\n

Please see the positioning overview for more details.

" + } + ], + "outputs": [ + { + "name": "openChange", + "description": "

An event fired when the dropdown is opened or closed.

\n

The event payload is a boolean:

\n
    \n
  • true - the dropdown was opened
  • \n
  • false - the dropdown was closed
  • \n
" + } + ], + "properties": [], + "methods": [ + { + "name": "isOpen", + "description": "

Checks if the dropdown menu is open.

", + "args": [], + "returnType": "boolean" + }, + { + "name": "open", + "description": "

Opens the dropdown menu.

", + "args": [], + "returnType": "void" + }, + { + "name": "close", + "description": "

Closes the dropdown menu.

", + "args": [], + "returnType": "void" + }, + { + "name": "toggle", + "description": "

Toggles the dropdown menu.

", + "args": [], + "returnType": "void" + } + ] + }, + "NgbModalOptions": { + "fileName": "src/modal/modal-config.ts", + "className": "NgbModalOptions", + "description": "

Options available when opening new modal windows with NgbModal.open() method.

", + "type": "Interface", + "methods": [], + "properties": [ + { + "name": "ariaLabelledBy", + "type": "string", + "description": "

aria-labelledby attribute value to set on the modal window.

", + "since": { + "version": "2.2.0", + "description": "" + } + }, + { + "name": "backdrop", + "type": "boolean | \"static\"", + "description": "

If true, the backdrop element will be created for a given modal.

\n

Alternatively, specify 'static' for a backdrop which doesn't close the modal on click.

\n

Default value is true.

" + }, + { + "name": "backdropClass", + "type": "string", + "description": "

A custom class to append to the modal backdrop.

", + "since": { + "version": "1.1.0", + "description": "" + } + }, + { + "name": "beforeDismiss", + "type": "() => boolean | Promise", + "description": "

Callback right before the modal will be dismissed.

\n

If this function returns:

\n
    \n
  • false
  • \n
  • a promise resolved with false
  • \n
  • a promise that is rejected
  • \n
\n

then the modal won't be dismissed.

" + }, + { + "name": "centered", + "type": "boolean", + "description": "

If true, the modal will be centered vertically.

\n

Default value is false.

", + "since": { + "version": "1.1.0", + "description": "" + } + }, + { + "name": "container", + "type": "string", + "description": "

A selector specifying the element all new modal windows should be appended to.

\n

If not specified, will be body.

" + }, + { + "name": "injector", + "type": "Injector", + "description": "

The Injector to use for modal content.

" + }, + { + "name": "keyboard", + "type": "boolean", + "description": "

If true, the modal will be closed when Escape key is pressed

\n

Default value is true.

" + }, + { + "name": "scrollable", + "type": "boolean", + "description": "

Scrollable modal content (false by default).

", + "since": { + "version": "5.0.0", + "description": "" + } + }, + { + "name": "size", + "type": "\"sm\" | \"lg\" | \"xl\"", + "description": "

Size of a new modal window.

" + }, + { + "name": "windowClass", + "type": "string", + "description": "

A custom class to append to the modal window.

" + } + ] + }, + "NgbModalConfig": { + "fileName": "src/modal/modal-config.ts", + "className": "NgbModalConfig", + "description": "

A configuration service for the NgbModal service.

\n

You can inject this service, typically in your root component, and customize the values of its properties in\norder to provide default values for all modals used in the application.

", + "since": { + "version": "3.1.0", + "description": "" + }, + "type": "Service", + "methods": [], + "properties": [ + { + "name": "backdrop", + "defaultValue": "true", + "type": "boolean | \"static\"", + "description": "

If true, the backdrop element will be created for a given modal.

\n

Alternatively, specify 'static' for a backdrop which doesn't close the modal on click.

\n

Default value is true.

" + }, + { + "name": "keyboard", + "defaultValue": "true", + "type": "boolean", + "description": "

If true, the modal will be closed when Escape key is pressed

\n

Default value is true.

" + } + ] + }, + "NgbActiveModal": { + "fileName": "src/modal/modal-ref.ts", + "className": "NgbActiveModal", + "description": "

A reference to the currently opened (active) modal.

\n

Instances of this class can be injected into your component passed as modal content.\nSo you can .close() or .dismiss() the modal window from your component.

", + "type": "Class", + "methods": [ + { + "name": "close", + "description": "

Closes the modal with an optional result value.

\n

The NgbMobalRef.result promise will be resolved with the provided value.

", + "args": [ + { + "name": "result", + "type": "any" + } + ], + "returnType": "void" + }, + { + "name": "dismiss", + "description": "

Dismisses the modal with an optional reason value.

\n

The NgbModalRef.result promise will be rejected with the provided value.

", + "args": [ + { + "name": "reason", + "type": "any" + } + ], + "returnType": "void" + } + ], + "properties": [] + }, + "NgbModalRef": { + "fileName": "src/modal/modal-ref.ts", + "className": "NgbModalRef", + "description": "

A reference to the newly opened modal returned by the NgbModal.open() method.

", + "type": "Class", + "methods": [ + { + "name": "close", + "description": "

Closes the modal with an optional result value.

\n

The NgbMobalRef.result promise will be resolved with the provided value.

", + "args": [ + { + "name": "result", + "type": "any" + } + ], + "returnType": "void" + }, + { + "name": "dismiss", + "description": "

Dismisses the modal with an optional reason value.

\n

The NgbModalRef.result promise will be rejected with the provided value.

", + "args": [ + { + "name": "reason", + "type": "any" + } + ], + "returnType": "void" + } + ], + "properties": [ + { + "name": "componentInstance", + "type": "any", + "description": "

The instance of a component used for the modal content.

\n

When a TemplateRef is used as the content, will return undefined.

" + }, + { + "name": "result", + "type": "Promise", + "description": "

The promise that is resolved when the modal is closed and rejected when the modal is dismissed.

" + } + ] + }, + "NgbModal": { + "fileName": "src/modal/modal.ts", + "className": "NgbModal", + "description": "

A service for opening modal windows.

\n

Creating a modal is straightforward: create a component or a template and pass it as an argument to\nthe .open() method.

", + "type": "Service", + "methods": [ + { + "name": "open", + "description": "

Opens a new modal window with the specified content and supplied options.

\n

Content can be provided as a TemplateRef or a component type. If you pass a component type as content,\nthen instances of those components can be injected with an instance of the NgbActiveModal class. You can then\nuse NgbActiveModal methods to close / dismiss modals from "inside" of your component.

\n

Also see the NgbModalOptions for the list of supported options.

", + "args": [ + { + "name": "content", + "type": "any" + }, + { + "name": "options", + "type": "NgbModalOptions" + } + ], + "returnType": "NgbModalRef" + }, + { + "name": "dismissAll", + "description": "

Dismisses all currently displayed modal windows with the supplied reason.

", + "args": [ + { + "name": "reason", + "type": "any" + } + ], + "returnType": "void", + "since": { + "version": "3.1.0", + "description": "" + } + }, + { + "name": "hasOpenModals", + "description": "

Indicates if there are currently any open modal windows in the application.

", + "args": [], + "returnType": "boolean", + "since": { + "version": "3.3.0", + "description": "" + } + } + ], + "properties": [] + }, + "NgbPaginationConfig": { + "fileName": "src/pagination/pagination-config.ts", + "className": "NgbPaginationConfig", + "description": "

A configuration service for the NgbPagination component.

\n

You can inject this service, typically in your root component, and customize the values of its properties in\norder to provide default values for all the paginations used in the application.

", + "type": "Service", + "methods": [], + "properties": [ + { + "name": "boundaryLinks", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "directionLinks", + "defaultValue": "true", + "type": "boolean", + "description": "" + }, + { + "name": "disabled", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "ellipses", + "defaultValue": "true", + "type": "boolean", + "description": "" + }, + { + "name": "maxSize", + "defaultValue": "0", + "type": "number", + "description": "" + }, + { + "name": "pageSize", + "defaultValue": "10", + "type": "number", + "description": "" + }, + { + "name": "rotate", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "size", + "type": "\"sm\" | \"lg\"", + "description": "" + } + ] + }, + "NgbPaginationLinkContext": { + "fileName": "src/pagination/pagination.ts", + "className": "NgbPaginationLinkContext", + "description": "

A context for the

\n
    \n
  • NgbPaginationFirst
  • \n
  • NgbPaginationPrevious
  • \n
  • NgbPaginationNext
  • \n
  • NgbPaginationLast
  • \n
  • NgbPaginationEllipsis
  • \n
\n

link templates in case you want to override one.

", + "since": { + "version": "4.1.0", + "description": "" + }, + "type": "Interface", + "methods": [], + "properties": [ + { + "name": "currentPage", + "type": "number", + "description": "

The currently selected page number

" + }, + { + "name": "disabled", + "type": "boolean", + "description": "

If true, the current link is disabled

" + } + ] + }, + "NgbPaginationNumberContext": { + "fileName": "src/pagination/pagination.ts", + "className": "NgbPaginationNumberContext", + "description": "

A context for the NgbPaginationNumber link template in case you want to override one.

\n

Extends NgbPaginationLinkContext.

", + "since": { + "version": "4.1.0", + "description": "" + }, + "type": "Interface", + "methods": [], + "properties": [ + { + "name": "$implicit", + "type": "number", + "description": "

The page number, displayed by the current page link.

" + } + ] + }, + "NgbPaginationEllipsis": { + "fileName": "src/pagination/pagination.ts", + "className": "NgbPaginationEllipsis", + "description": "

A directive to match the 'ellipsis' link template

", + "since": { + "version": "4.1.0", + "description": "" + }, + "type": "Directive", + "selector": "ng-template[ngbPaginationEllipsis]", + "inputs": [], + "outputs": [], + "properties": [], + "methods": [] + }, + "NgbPaginationFirst": { + "fileName": "src/pagination/pagination.ts", + "className": "NgbPaginationFirst", + "description": "

A directive to match the 'first' link template

", + "since": { + "version": "4.1.0", + "description": "" + }, + "type": "Directive", + "selector": "ng-template[ngbPaginationFirst]", + "inputs": [], + "outputs": [], + "properties": [], + "methods": [] + }, + "NgbPaginationLast": { + "fileName": "src/pagination/pagination.ts", + "className": "NgbPaginationLast", + "description": "

A directive to match the 'last' link template

", + "since": { + "version": "4.1.0", + "description": "" + }, + "type": "Directive", + "selector": "ng-template[ngbPaginationLast]", + "inputs": [], + "outputs": [], + "properties": [], + "methods": [] + }, + "NgbPaginationNext": { + "fileName": "src/pagination/pagination.ts", + "className": "NgbPaginationNext", + "description": "

A directive to match the 'next' link template

", + "since": { + "version": "4.1.0", + "description": "" + }, + "type": "Directive", + "selector": "ng-template[ngbPaginationNext]", + "inputs": [], + "outputs": [], + "properties": [], + "methods": [] + }, + "NgbPaginationNumber": { + "fileName": "src/pagination/pagination.ts", + "className": "NgbPaginationNumber", + "description": "

A directive to match the page 'number' link template

", + "since": { + "version": "4.1.0", + "description": "" + }, + "type": "Directive", + "selector": "ng-template[ngbPaginationNumber]", + "inputs": [], + "outputs": [], + "properties": [], + "methods": [] + }, + "NgbPaginationPrevious": { + "fileName": "src/pagination/pagination.ts", + "className": "NgbPaginationPrevious", + "description": "

A directive to match the 'previous' link template

", + "since": { + "version": "4.1.0", + "description": "" + }, + "type": "Directive", + "selector": "ng-template[ngbPaginationPrevious]", + "inputs": [], + "outputs": [], + "properties": [], + "methods": [] + }, + "NgbPagination": { + "fileName": "src/pagination/pagination.ts", + "className": "NgbPagination", + "description": "

A component that displays page numbers and allows to customize them in several ways.

", + "type": "Component", + "selector": "ngb-pagination", + "inputs": [ + { + "name": "boundaryLinks", + "type": "boolean", + "description": "

If true, the "First" and "Last" page links are shown.

" + }, + { + "name": "collectionSize", + "type": "number", + "description": "

The number of items in your paginated collection.

\n

Note, that this is not the number of pages. Page numbers are calculated dynamically based on\ncollectionSize and pageSize. Ex. if you have 100 items in your collection and displaying 20 items per page,\nyou'll end up with 5 pages.

" + }, + { + "name": "directionLinks", + "type": "boolean", + "description": "

If true, the "Next" and "Previous" page links are shown.

" + }, + { + "name": "disabled", + "type": "boolean", + "description": "

If true, pagination links will be disabled.

" + }, + { + "name": "ellipses", + "type": "boolean", + "description": "

If true, the ellipsis symbols and first/last page numbers will be shown when maxSize > number of pages.

" + }, + { + "name": "maxSize", + "type": "number", + "description": "

The maximum number of pages to display.

" + }, + { + "name": "page", + "defaultValue": "1", + "type": "number", + "description": "

The current page.

\n

Page numbers start with 1.

" + }, + { + "name": "pageSize", + "type": "number", + "description": "

The number of items per page.

" + }, + { + "name": "rotate", + "type": "boolean", + "description": "

Whether to rotate pages when maxSize > number of pages.

\n

The current page always stays in the middle if true.

" + }, + { + "name": "size", + "type": "\"sm\" | \"lg\"", + "description": "

The pagination display size.

\n

Bootstrap currently supports small and large sizes.

" + } + ], + "outputs": [ + { + "name": "pageChange", + "description": "

An event fired when the page is changed. Will fire only if collection size is set and all values are valid.

\n

Event payload is the number of the newly selected page.

\n

Page numbers start with 1.

" + } + ], + "properties": [ + { + "name": "pageCount", + "defaultValue": "0", + "type": "number", + "description": "" + }, + { + "name": "pages", + "type": "number[]", + "description": "" + }, + { + "name": "tplEllipsis", + "type": "NgbPaginationEllipsis", + "description": "" + }, + { + "name": "tplFirst", + "type": "NgbPaginationFirst", + "description": "" + }, + { + "name": "tplLast", + "type": "NgbPaginationLast", + "description": "" + }, + { + "name": "tplNext", + "type": "NgbPaginationNext", + "description": "" + }, + { + "name": "tplNumber", + "type": "NgbPaginationNumber", + "description": "" + }, + { + "name": "tplPrevious", + "type": "NgbPaginationPrevious", + "description": "" + } + ], + "methods": [] + }, + "NgbPopoverConfig": { + "fileName": "src/popover/popover-config.ts", + "className": "NgbPopoverConfig", + "description": "

A configuration service for the NgbPopover component.

\n

You can inject this service, typically in your root component, and customize the values of its properties in\norder to provide default values for all the popovers used in the application.

", + "type": "Service", + "methods": [], + "properties": [ + { + "name": "autoClose", + "defaultValue": "true", + "type": "boolean | \"inside\" | \"outside\"", + "description": "" + }, + { + "name": "closeDelay", + "defaultValue": "0", + "type": "number", + "description": "" + }, + { + "name": "container", + "type": "string", + "description": "" + }, + { + "name": "disablePopover", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "openDelay", + "defaultValue": "0", + "type": "number", + "description": "" + }, + { + "name": "placement", + "defaultValue": "auto", + "type": "PlacementArray", + "description": "" + }, + { + "name": "popoverClass", + "type": "string", + "description": "" + }, + { + "name": "triggers", + "defaultValue": "click", + "type": "string", + "description": "" + } + ] + }, + "NgbPopover": { + "fileName": "src/popover/popover.ts", + "className": "NgbPopover", + "description": "

A lightweight and extensible directive for fancy popover creation.

", + "type": "Directive", + "selector": "[ngbPopover]", + "exportAs": "ngbPopover", + "inputs": [ + { + "name": "autoClose", + "type": "boolean | \"inside\" | \"outside\"", + "description": "

Indicates whether the popover should be closed on Escape key and inside/outside clicks:

\n
    \n
  • true - closes on both outside and inside clicks as well as Escape presses
  • \n
  • false - disables the autoClose feature (NB: triggers still apply)
  • \n
  • "inside" - closes on inside clicks as well as Escape presses
  • \n
  • "outside" - closes on outside clicks (sometimes also achievable through triggers)\nas well as Escape presses
  • \n
", + "since": { + "version": "3.0.0", + "description": "" + } + }, + { + "name": "closeDelay", + "type": "number", + "description": "

The closing delay in ms. Works only for "non-manual" opening triggers defined by the triggers input.

", + "since": { + "version": "4.1.0", + "description": "" + } + }, + { + "name": "container", + "type": "string", + "description": "

A selector specifying the element the popover should be appended to.

\n

Currently only supports body.

" + }, + { + "name": "disablePopover", + "type": "boolean", + "description": "

If true, popover is disabled and won't be displayed.

", + "since": { + "version": "1.1.0", + "description": "" + } + }, + { + "name": "ngbPopover", + "type": "string | TemplateRef", + "description": "

The string content or a TemplateRef for the content to be displayed in the popover.

\n

If the title and the content are empty, the popover won't open.

" + }, + { + "name": "openDelay", + "type": "number", + "description": "

The opening delay in ms. Works only for "non-manual" opening triggers defined by the triggers input.

", + "since": { + "version": "4.1.0", + "description": "" + } + }, + { + "name": "placement", + "type": "PlacementArray", + "description": "

The preferred placement of the popover.

\n

Possible values are "top", "top-left", "top-right", "bottom", "bottom-left",\n"bottom-right", "left", "left-top", "left-bottom", "right", "right-top",\n"right-bottom"

\n

Accepts an array of strings or a string with space separated possible values.

\n

The default order of preference is "auto" (same as the sequence above).

\n

Please see the positioning overview for more details.

" + }, + { + "name": "popoverClass", + "type": "string", + "description": "

An optional class applied to the popover window element.

", + "since": { + "version": "2.2.0", + "description": "" + } + }, + { + "name": "popoverTitle", + "type": "string | TemplateRef", + "description": "

The title of the popover.

\n

If the title and the content are empty, the popover won't open.

" + }, + { + "name": "triggers", + "type": "string", + "description": "

Specifies events that should trigger the tooltip.

\n

Supports a space separated list of event names.\nFor more details see the triggers demo.

" + } + ], + "outputs": [ + { + "name": "hidden", + "description": "

An event emitted when the popover is hidden. Contains no payload.

" + }, + { + "name": "shown", + "description": "

An event emitted when the popover is shown. Contains no payload.

" + } + ], + "properties": [], + "methods": [ + { + "name": "open", + "description": "

Opens the popover.

\n

This is considered to be a "manual" triggering.\nThe context is an optional value to be injected into the popover template when it is created.

", + "args": [ + { + "name": "context", + "type": "any" + } + ], + "returnType": "void" + }, + { + "name": "close", + "description": "

Closes the popover.

\n

This is considered to be a "manual" triggering of the popover.

", + "args": [], + "returnType": "void" + }, + { + "name": "toggle", + "description": "

Toggles the popover.

\n

This is considered to be a "manual" triggering of the popover.

", + "args": [], + "returnType": "void" + }, + { + "name": "isOpen", + "description": "

Returns true, if the popover is currently shown.

", + "args": [], + "returnType": "boolean" + } + ] + }, + "NgbProgressbarConfig": { + "fileName": "src/progressbar/progressbar-config.ts", + "className": "NgbProgressbarConfig", + "description": "

A configuration service for the NgbProgressbar component.

\n

You can inject this service, typically in your root component, and customize the values of its properties in\norder to provide default values for all the progress bars used in the application.

", + "type": "Service", + "methods": [], + "properties": [ + { + "name": "animated", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "height", + "type": "string", + "description": "" + }, + { + "name": "max", + "defaultValue": "100", + "type": "number", + "description": "" + }, + { + "name": "showValue", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "striped", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "type", + "type": "string", + "description": "" + } + ] + }, + "NgbProgressbar": { + "fileName": "src/progressbar/progressbar.ts", + "className": "NgbProgressbar", + "description": "

A directive that provides feedback on the progress of a workflow or an action.

", + "type": "Component", + "selector": "ngb-progressbar", + "inputs": [ + { + "name": "animated", + "type": "boolean", + "description": "

If true, the stripes on the progressbar are animated.

\n

Takes effect only for browsers supporting CSS3 animations, and if striped is true.

" + }, + { + "name": "height", + "type": "string", + "description": "

THe height of the progress bar.

\n

Accepts any valid CSS height values, ex. "2rem"

" + }, + { + "name": "max", + "type": "number", + "description": "

The maximal value to be displayed in the progressbar.

" + }, + { + "name": "showValue", + "type": "boolean", + "description": "

If true, the current percentage will be shown in the xx% format.

" + }, + { + "name": "striped", + "type": "boolean", + "description": "

If true, the progress bars will be displayed as striped.

" + }, + { + "name": "type", + "type": "string", + "description": "

The type of the progress bar.

\n

Currently Bootstrap supports "success", "info", "warning" or "danger".

" + }, + { + "name": "value", + "defaultValue": "0", + "type": "number", + "description": "

The current value for the progress bar.

\n

Should be in the [0, max] range.

" + } + ], + "outputs": [], + "properties": [], + "methods": [] + }, + "NgbRatingConfig": { + "fileName": "src/rating/rating-config.ts", + "className": "NgbRatingConfig", + "description": "

A configuration service for the NgbRating component.

\n

You can inject this service, typically in your root component, and customize the values of its properties in\norder to provide default values for all the ratings used in the application.

", + "type": "Service", + "methods": [], + "properties": [ + { + "name": "max", + "defaultValue": "10", + "type": "number", + "description": "" + }, + { + "name": "readonly", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "resettable", + "defaultValue": "false", + "type": "boolean", + "description": "" + } + ] + }, + "StarTemplateContext": { + "fileName": "src/rating/rating.ts", + "className": "StarTemplateContext", + "description": "

The context for the custom star display template defined in the starTemplate.

", + "type": "Interface", + "methods": [], + "properties": [ + { + "name": "fill", + "type": "number", + "description": "

The star fill percentage, an integer in the [0, 100] range.

" + }, + { + "name": "index", + "type": "number", + "description": "

Index of the star, starts with 0.

" + } + ] + }, + "NgbRating": { + "fileName": "src/rating/rating.ts", + "className": "NgbRating", + "description": "

A directive that helps visualising and interacting with a star rating bar.

", + "type": "Component", + "selector": "ngb-rating", + "inputs": [ + { + "name": "max", + "type": "number", + "description": "

The maximal rating that can be given.

" + }, + { + "name": "rate", + "type": "number", + "description": "

The current rating. Could be a decimal value like 3.75.

" + }, + { + "name": "readonly", + "type": "boolean", + "description": "

If true, the rating can't be changed.

" + }, + { + "name": "resettable", + "type": "boolean", + "description": "

If true, the rating can be reset to 0 by mouse clicking currently set rating.

" + }, + { + "name": "starTemplate", + "type": "TemplateRef", + "description": "

The template to override the way each star is displayed.

\n

Alternatively put an <ng-template> as the only child of your <ngb-rating> element

" + } + ], + "outputs": [ + { + "name": "hover", + "description": "

An event emitted when the user is hovering over a given rating.

\n

Event payload equals to the rating being hovered over.

" + }, + { + "name": "leave", + "description": "

An event emitted when the user stops hovering over a given rating.

\n

Event payload equals to the rating of the last item being hovered over.

" + }, + { + "name": "rateChange", + "description": "

An event emitted when the user selects a new rating.

\n

Event payload equals to the newly selected rating.

" + } + ], + "properties": [ + { + "name": "contexts", + "type": "StarTemplateContext[]", + "description": "" + }, + { + "name": "disabled", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "nextRate", + "type": "number", + "description": "" + }, + { + "name": "onChange", + "type": "(_: any) => void", + "description": "" + }, + { + "name": "onTouched", + "type": "() => void", + "description": "" + }, + { + "name": "starTemplateFromContent", + "type": "TemplateRef", + "description": "" + } + ], + "methods": [] + }, + "NgbTabsetConfig": { + "fileName": "src/tabset/tabset-config.ts", + "className": "NgbTabsetConfig", + "description": "

A configuration service for the NgbTabset component.

\n

You can inject this service, typically in your root component, and customize the values of its properties in\norder to provide default values for all the tabsets used in the application.

", + "type": "Service", + "methods": [], + "properties": [ + { + "name": "justify", + "defaultValue": "start", + "type": "\"start\" | \"center\" | \"end\" | \"fill\" | \"justified\"", + "description": "" + }, + { + "name": "orientation", + "defaultValue": "horizontal", + "type": "\"horizontal\" | \"vertical\"", + "description": "" + }, + { + "name": "type", + "defaultValue": "tabs", + "type": "\"tabs\" | \"pills\"", + "description": "" + } + ] + }, + "NgbTabTitle": { + "fileName": "src/tabset/tabset.ts", + "className": "NgbTabTitle", + "description": "

A directive to wrap tab titles that need to contain HTML markup or other directives.

\n

Alternatively you could use the NgbTab.title input for string titles.

", + "type": "Directive", + "selector": "ng-template[ngbTabTitle]", + "inputs": [], + "outputs": [], + "properties": [], + "methods": [] + }, + "NgbTabContent": { + "fileName": "src/tabset/tabset.ts", + "className": "NgbTabContent", + "description": "

A directive to wrap content to be displayed in a tab.

", + "type": "Directive", + "selector": "ng-template[ngbTabContent]", + "inputs": [], + "outputs": [], + "properties": [], + "methods": [] + }, + "NgbTab": { + "fileName": "src/tabset/tabset.ts", + "className": "NgbTab", + "description": "

A directive representing an individual tab.

", + "type": "Directive", + "selector": "ngb-tab", + "inputs": [ + { + "name": "disabled", + "defaultValue": "false", + "type": "boolean", + "description": "

If true, the current tab is disabled and can't be toggled.

" + }, + { + "name": "id", + "type": "string", + "description": "

The tab identifier.

\n

Must be unique for the entire document for proper accessibility support.

" + }, + { + "name": "title", + "type": "string", + "description": "

The tab title.

\n

Use the NgbTabTitle directive for non-string titles.

" + } + ], + "outputs": [], + "properties": [ + { + "name": "contentTpl", + "type": "NgbTabContent", + "description": "" + }, + { + "name": "contentTpls", + "type": "QueryList", + "description": "" + }, + { + "name": "titleTpl", + "type": "NgbTabTitle", + "description": "" + }, + { + "name": "titleTpls", + "type": "QueryList", + "description": "" + } + ], + "methods": [] + }, + "NgbTabChangeEvent": { + "fileName": "src/tabset/tabset.ts", + "className": "NgbTabChangeEvent", + "description": "

The payload of the change event fired right before the tab change.

", + "type": "Interface", + "methods": [], + "properties": [ + { + "name": "activeId", + "type": "string", + "description": "

The id of the currently active tab.

" + }, + { + "name": "nextId", + "type": "string", + "description": "

The id of the newly selected tab.

" + }, + { + "name": "preventDefault", + "type": "() => void", + "description": "

Calling this function will prevent tab switching.

" + } + ] + }, + "NgbTabset": { + "fileName": "src/tabset/tabset.ts", + "className": "NgbTabset", + "description": "

A component that makes it easy to create tabbed interface.

", + "type": "Component", + "selector": "ngb-tabset", + "exportAs": "ngbTabset", + "inputs": [ + { + "name": "activeId", + "type": "string", + "description": "

The identifier of the tab that should be opened initially.

\n

For subsequent tab switches use the .select() method and the (tabChange) event.

" + }, + { + "name": "destroyOnHide", + "defaultValue": "true", + "type": "boolean", + "description": "

If true, non-visible tabs content will be removed from DOM. Otherwise it will just be hidden.

" + }, + { + "name": "justify", + "type": "\"start\" | \"center\" | \"end\" | \"fill\" | \"justified\"", + "description": "

The horizontal alignment of the tabs with flexbox utilities.

" + }, + { + "name": "orientation", + "type": "\"horizontal\" | \"vertical\"", + "description": "

The orientation of the tabset.

" + }, + { + "name": "type", + "type": "string", + "description": "

Type of navigation to be used for tabs.

\n

Currently Bootstrap supports only "tabs" and "pills".

\n

Since 3.0.0 can also be an arbitrary string (ex. for custom themes).

" + } + ], + "outputs": [ + { + "name": "tabChange", + "description": "

A tab change event emitted right before the tab change happens.

\n

See NgbTabChangeEvent for payload details.

" + } + ], + "properties": [ + { + "name": "justifyClass", + "type": "string", + "description": "" + }, + { + "name": "tabs", + "type": "QueryList", + "description": "" + } + ], + "methods": [ + { + "name": "select", + "description": "

Selects the tab with the given id and shows its associated content panel.

\n

Any other tab that was previously selected becomes unselected and its associated pane is removed from DOM or\nhidden depending on the destroyOnHide value.

", + "args": [ + { + "name": "tabId", + "type": "string" + } + ], + "returnType": "void" + } + ] + }, + "NgbTimeAdapter": { + "fileName": "src/timepicker/ngb-time-adapter.ts", + "className": "NgbTimeAdapter", + "description": "

An abstract service that does the conversion between the internal timepicker NgbTimeStruct model and\nany provided user time model T, ex. a string, a native date, etc.

\n

The adapter is used only for conversion when binding timepicker to a form control,\nex. [(ngModel)]="userTimeModel". Here userTimeModel can be of any type.

\n

The default timepicker implementation assumes we use NgbTimeStruct as a user model.

\n

See the custom time adapter demo for an example.

", + "since": { + "version": "2.2.0", + "description": "" + }, + "typeParameter": "T", + "type": "Service", + "methods": [ + { + "name": "fromModel", + "description": "

Converts a user-model time of type T to an NgbTimeStruct for internal use.

", + "args": [ + { + "name": "value", + "type": "T" + } + ], + "returnType": "NgbTimeStruct" + }, + { + "name": "toModel", + "description": "

Converts an internal NgbTimeStruct time to a user-model time of type T.

", + "args": [ + { + "name": "time", + "type": "NgbTimeStruct" + } + ], + "returnType": "T" + } + ], + "properties": [] + }, + "NgbTimeStruct": { + "fileName": "src/timepicker/ngb-time-struct.ts", + "className": "NgbTimeStruct", + "description": "

An interface for the time model used by the timepicker.

", + "type": "Interface", + "methods": [], + "properties": [ + { + "name": "hour", + "type": "number", + "description": "

The hour in the [0, 23] range.

" + }, + { + "name": "minute", + "type": "number", + "description": "

The minute in the [0, 59] range.

" + }, + { + "name": "second", + "type": "number", + "description": "

The second in the [0, 59] range.

" + } + ] + }, + "NgbTimepickerConfig": { + "fileName": "src/timepicker/timepicker-config.ts", + "className": "NgbTimepickerConfig", + "description": "

A configuration service for the NgbTimepicker component.

\n

You can inject this service, typically in your root component, and customize the values of its properties in\norder to provide default values for all the timepickers used in the application.

", + "type": "Service", + "methods": [], + "properties": [ + { + "name": "disabled", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "hourStep", + "defaultValue": "1", + "type": "number", + "description": "" + }, + { + "name": "meridian", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "minuteStep", + "defaultValue": "1", + "type": "number", + "description": "" + }, + { + "name": "readonlyInputs", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "seconds", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "secondStep", + "defaultValue": "1", + "type": "number", + "description": "" + }, + { + "name": "size", + "defaultValue": "medium", + "type": "\"small\" | \"medium\" | \"large\"", + "description": "" + }, + { + "name": "spinners", + "defaultValue": "true", + "type": "boolean", + "description": "" + } + ] + }, + "NgbTimepickerI18n": { + "fileName": "src/timepicker/timepicker-i18n.ts", + "className": "NgbTimepickerI18n", + "description": "

Type of the service supplying day periods (for example, 'AM' and 'PM') to NgbTimepicker component.\nThe default implementation of this service honors the Angular locale, and uses the registered locale data,\nas explained in the Angular i18n guide.

", + "type": "Service", + "methods": [ + { + "name": "getMorningPeriod", + "description": "

Returns the name for the period before midday.

", + "args": [], + "returnType": "string" + }, + { + "name": "getAfternoonPeriod", + "description": "

Returns the name for the period after midday.

", + "args": [], + "returnType": "string" + } + ], + "properties": [] + }, + "NgbTimepicker": { + "fileName": "src/timepicker/timepicker.ts", + "className": "NgbTimepicker", + "description": "

A directive that helps with wth picking hours, minutes and seconds.

", + "type": "Component", + "selector": "ngb-timepicker", + "inputs": [ + { + "name": "hourStep", + "type": "number", + "description": "

The number of hours to add/subtract when clicking hour spinners.

" + }, + { + "name": "meridian", + "type": "boolean", + "description": "

Whether to display 12H or 24H mode.

" + }, + { + "name": "minuteStep", + "type": "number", + "description": "

The number of minutes to add/subtract when clicking minute spinners.

" + }, + { + "name": "readonlyInputs", + "type": "boolean", + "description": "

If true, the timepicker is readonly and can't be changed.

" + }, + { + "name": "seconds", + "type": "boolean", + "description": "

If true, it is possible to select seconds.

" + }, + { + "name": "secondStep", + "type": "number", + "description": "

The number of seconds to add/subtract when clicking second spinners.

" + }, + { + "name": "size", + "type": "\"small\" | \"medium\" | \"large\"", + "description": "

The size of inputs and buttons.

" + }, + { + "name": "spinners", + "type": "boolean", + "description": "

If true, the spinners above and below inputs are visible.

" + } + ], + "outputs": [], + "properties": [ + { + "name": "disabled", + "type": "boolean", + "description": "" + }, + { + "name": "hourStep", + "type": "number", + "description": "

The number of hours to add/subtract when clicking hour spinners.

" + }, + { + "name": "isLargeSize", + "type": "boolean", + "description": "" + }, + { + "name": "isSmallSize", + "type": "boolean", + "description": "" + }, + { + "name": "minuteStep", + "type": "number", + "description": "

The number of minutes to add/subtract when clicking minute spinners.

" + }, + { + "name": "model", + "type": "NgbTime", + "description": "" + }, + { + "name": "onChange", + "type": "(_: any) => void", + "description": "" + }, + { + "name": "onTouched", + "type": "() => void", + "description": "" + }, + { + "name": "secondStep", + "type": "number", + "description": "

The number of seconds to add/subtract when clicking second spinners.

" + } + ], + "methods": [] + }, + "NgbToastOptions": { + "fileName": "src/toast/toast-config.ts", + "className": "NgbToastOptions", + "description": "

Interface used to type all toast config options. See NgbToastConfig.

", + "since": { + "version": "5.0.0", + "description": "" + }, + "type": "Interface", + "methods": [], + "properties": [ + { + "name": "ariaLive", + "type": "\"polite\" | \"alert\"", + "description": "

Type of aria-live attribute to be used.

\n

Could be one of these 2 values (as string):

\n
    \n
  • polite (default)
  • \n
  • alert
  • \n
" + }, + { + "name": "autohide", + "type": "boolean", + "description": "

Specify if the toast component should emit the hide() output\nafter a certain delay in ms.

" + }, + { + "name": "delay", + "type": "number", + "description": "

Delay in ms after which the hide() output should be emitted.

" + } + ] + }, + "NgbToastConfig": { + "fileName": "src/toast/toast-config.ts", + "className": "NgbToastConfig", + "description": "

Configuration service for the NgbToast component. You can inject this service, typically in your root component,\nand customize the values of its properties in order to provide default values for all the toasts used in the\napplication.

", + "since": { + "version": "5.0.0", + "description": "" + }, + "type": "Service", + "methods": [], + "properties": [ + { + "name": "ariaLive", + "defaultValue": "polite", + "type": "\"polite\" | \"alert\"", + "description": "

Type of aria-live attribute to be used.

\n

Could be one of these 2 values (as string):

\n
    \n
  • polite (default)
  • \n
  • alert
  • \n
" + }, + { + "name": "autohide", + "defaultValue": "true", + "type": "boolean", + "description": "

Specify if the toast component should emit the hide() output\nafter a certain delay in ms.

" + }, + { + "name": "delay", + "defaultValue": "500", + "type": "number", + "description": "

Delay in ms after which the hide() output should be emitted.

" + } + ] + }, + "NgbToastHeader": { + "fileName": "src/toast/toast.ts", + "className": "NgbToastHeader", + "description": "

This directive allows the usage of HTML markup or other directives\ninside of the toast's header.

", + "since": { + "version": "5.0.0", + "description": "" + }, + "type": "Directive", + "selector": "[ngbToastHeader]", + "inputs": [], + "outputs": [], + "properties": [], + "methods": [] + }, + "NgbToast": { + "fileName": "src/toast/toast.ts", + "className": "NgbToast", + "description": "

Toasts provide feedback messages as notifications to the user.\nGoal is to mimic the push notifications available both on mobile and desktop operating systems.

", + "since": { + "version": "5.0.0", + "description": "" + }, + "type": "Component", + "selector": "ngb-toast", + "exportAs": "ngbToast", + "inputs": [ + { + "name": "autohide", + "type": "boolean", + "description": "

Auto hide the toast after a delay in ms.\ndefault: true (inherited from NgbToastConfig)

" + }, + { + "name": "delay", + "type": "number", + "description": "

Delay after which the toast will hide (ms).\ndefault: 500 (ms) (inherited from NgbToastConfig)

" + }, + { + "name": "header", + "type": "string", + "description": "

Text to be used as toast's header.\nIgnored if a ContentChild template is specified at the same time.

" + } + ], + "outputs": [ + { + "name": "hide", + "description": "

An event fired immediately when toast's hide() method has been called.\nIt can only occur in 2 different scenarios:

\n
    \n
  • autohide timeout fires
  • \n
  • user clicks on a closing cross (&times)
  • \n
\n

Additionally this output is purely informative. The toast won't disappear. It's up to the user to take care of\nthat.

" + } + ], + "properties": [ + { + "name": "contentHeaderTpl", + "type": "TemplateRef", + "description": "

A template like <ng-template ngbToastHeader></ng-template> can be\nused in the projected content to allow markup usage.

" + } + ], + "methods": [] + }, + "NgbTooltipConfig": { + "fileName": "src/tooltip/tooltip-config.ts", + "className": "NgbTooltipConfig", + "description": "

A configuration service for the NgbTooltip component.

\n

You can inject this service, typically in your root component, and customize the values of its properties in\norder to provide default values for all the tooltips used in the application.

", + "type": "Service", + "methods": [], + "properties": [ + { + "name": "autoClose", + "defaultValue": "true", + "type": "boolean | \"inside\" | \"outside\"", + "description": "" + }, + { + "name": "closeDelay", + "defaultValue": "0", + "type": "number", + "description": "" + }, + { + "name": "container", + "type": "string", + "description": "" + }, + { + "name": "disableTooltip", + "defaultValue": "false", + "type": "boolean", + "description": "" + }, + { + "name": "openDelay", + "defaultValue": "0", + "type": "number", + "description": "" + }, + { + "name": "placement", + "defaultValue": "auto", + "type": "PlacementArray", + "description": "" + }, + { + "name": "tooltipClass", + "type": "string", + "description": "" + }, + { + "name": "triggers", + "defaultValue": "hover focus", + "type": "string", + "description": "" + } + ] + }, + "NgbTooltip": { + "fileName": "src/tooltip/tooltip.ts", + "className": "NgbTooltip", + "description": "

A lightweight and extensible directive for fancy tooltip creation.

", + "type": "Directive", + "selector": "[ngbTooltip]", + "exportAs": "ngbTooltip", + "inputs": [ + { + "name": "autoClose", + "type": "boolean | \"inside\" | \"outside\"", + "description": "

Indicates whether the tooltip should be closed on Escape key and inside/outside clicks:

\n
    \n
  • true - closes on both outside and inside clicks as well as Escape presses
  • \n
  • false - disables the autoClose feature (NB: triggers still apply)
  • \n
  • "inside" - closes on inside clicks as well as Escape presses
  • \n
  • "outside" - closes on outside clicks (sometimes also achievable through triggers)\nas well as Escape presses
  • \n
", + "since": { + "version": "3.0.0", + "description": "" + } + }, + { + "name": "closeDelay", + "type": "number", + "description": "

The closing delay in ms. Works only for "non-manual" opening triggers defined by the triggers input.

", + "since": { + "version": "4.1.0", + "description": "" + } + }, + { + "name": "container", + "type": "string", + "description": "

A selector specifying the element the tooltip should be appended to.

\n

Currently only supports "body".

" + }, + { + "name": "disableTooltip", + "type": "boolean", + "description": "

If true, tooltip is disabled and won't be displayed.

", + "since": { + "version": "1.1.0", + "description": "" + } + }, + { + "name": "ngbTooltip", + "type": "string | TemplateRef", + "description": "

The string content or a TemplateRef for the content to be displayed in the tooltip.

\n

If the content if falsy, the tooltip won't open.

" + }, + { + "name": "openDelay", + "type": "number", + "description": "

The opening delay in ms. Works only for "non-manual" opening triggers defined by the triggers input.

", + "since": { + "version": "4.1.0", + "description": "" + } + }, + { + "name": "placement", + "type": "PlacementArray", + "description": "

The preferred placement of the tooltip.

\n

Possible values are "top", "top-left", "top-right", "bottom", "bottom-left",\n"bottom-right", "left", "left-top", "left-bottom", "right", "right-top",\n"right-bottom"

\n

Accepts an array of strings or a string with space separated possible values.

\n

The default order of preference is "auto" (same as the sequence above).

\n

Please see the positioning overview for more details.

" + }, + { + "name": "tooltipClass", + "type": "string", + "description": "

An optional class applied to the tooltip window element.

", + "since": { + "version": "3.2.0", + "description": "" + } + }, + { + "name": "triggers", + "type": "string", + "description": "

Specifies events that should trigger the tooltip.

\n

Supports a space separated list of event names.\nFor more details see the triggers demo.

" + } + ], + "outputs": [ + { + "name": "hidden", + "description": "

An event emitted when the popover is hidden. Contains no payload.

" + }, + { + "name": "shown", + "description": "

An event emitted when the tooltip is shown. Contains no payload.

" + } + ], + "properties": [ + { + "name": "ngbTooltip", + "type": "string | TemplateRef", + "description": "

The string content or a TemplateRef for the content to be displayed in the tooltip.

\n

If the content if falsy, the tooltip won't open.

" + } + ], + "methods": [ + { + "name": "open", + "description": "

Opens the tooltip.

\n

This is considered to be a "manual" triggering.\nThe context is an optional value to be injected into the tooltip template when it is created.

", + "args": [ + { + "name": "context", + "type": "any" + } + ], + "returnType": "void" + }, + { + "name": "close", + "description": "

Closes the tooltip.

\n

This is considered to be a "manual" triggering of the tooltip.

", + "args": [], + "returnType": "void" + }, + { + "name": "toggle", + "description": "

Toggles the tooltip.

\n

This is considered to be a "manual" triggering of the tooltip.

", + "args": [], + "returnType": "void" + }, + { + "name": "isOpen", + "description": "

Returns true, if the popover is currently shown.

", + "args": [], + "returnType": "boolean" + } + ] + }, + "NgbHighlight": { + "fileName": "src/typeahead/highlight.ts", + "className": "NgbHighlight", + "description": "

A component that helps with text highlighting.

\n

If splits the result text into parts that contain the searched term and generates the HTML markup to simplify\nhighlighting:

\n

Ex. result="Alaska" and term="as" will produce Al<span class="ngb-highlight">as</span>ka.

", + "type": "Component", + "selector": "ngb-highlight", + "inputs": [ + { + "name": "highlightClass", + "defaultValue": "ngb-highlight", + "type": "string", + "description": "

The CSS class for <span> elements wrapping the term inside the result.

" + }, + { + "name": "result", + "type": "string", + "description": "

The text highlighting is added to.

\n

If the term is found inside this text, it will be highlighted.\nIf the term contains array then all the items from it will be highlighted inside the text.

" + }, + { + "name": "term", + "type": "string | string[]", + "description": "

The term or array of terms to be highlighted.\nSince version v4.2.0 term could be a string[]

" + } + ], + "outputs": [], + "properties": [ + { + "name": "parts", + "type": "string[]", + "description": "" + } + ], + "methods": [] + }, + "NgbTypeaheadConfig": { + "fileName": "src/typeahead/typeahead-config.ts", + "className": "NgbTypeaheadConfig", + "description": "

A configuration service for the NgbTypeahead component.

\n

You can inject this service, typically in your root component, and customize the values of its properties in\norder to provide default values for all the typeaheads used in the application.

", + "type": "Service", + "methods": [], + "properties": [ + { + "name": "container", + "type": "any", + "description": "" + }, + { + "name": "editable", + "defaultValue": "true", + "type": "boolean", + "description": "" + }, + { + "name": "focusFirst", + "defaultValue": "true", + "type": "boolean", + "description": "" + }, + { + "name": "placement", + "type": "PlacementArray", + "description": "" + }, + { + "name": "showHint", + "defaultValue": "false", + "type": "boolean", + "description": "" + } + ] + }, + "ResultTemplateContext": { + "fileName": "src/typeahead/typeahead-window.ts", + "className": "ResultTemplateContext", + "description": "

The context for the typeahead result template in case you want to override the default one.

", + "type": "Interface", + "methods": [], + "properties": [ + { + "name": "result", + "type": "any", + "description": "

Your typeahead result item.

" + }, + { + "name": "term", + "type": "string", + "description": "

Search term from the <input> used to get current result.

" + } + ] + }, + "NgbTypeaheadSelectItemEvent": { + "fileName": "src/typeahead/typeahead.ts", + "className": "NgbTypeaheadSelectItemEvent", + "description": "

An event emitted right before an item is selected from the result list.

", + "type": "Interface", + "methods": [], + "properties": [ + { + "name": "item", + "type": "any", + "description": "

The item from the result list about to be selected.

" + }, + { + "name": "preventDefault", + "type": "() => void", + "description": "

Calling this function will prevent item selection from happening.

" + } + ] + }, + "NgbTypeahead": { + "fileName": "src/typeahead/typeahead.ts", + "className": "NgbTypeahead", + "description": "

A directive providing a simple way of creating powerful typeaheads from any text input.

", + "type": "Directive", + "selector": "input[ngbTypeahead]", + "exportAs": "ngbTypeahead", + "inputs": [ + { + "name": "autocomplete", + "defaultValue": "off", + "type": "string", + "description": "

The value for the autocomplete attribute for the <input> element.

\n

Defaults to "off" to disable the native browser autocomplete, but you can override it if necessary.

", + "since": { + "version": "2.1.0", + "description": "" + } + }, + { + "name": "container", + "type": "string", + "description": "

A selector specifying the element the typeahead popup will be appended to.

\n

Currently only supports "body".

" + }, + { + "name": "editable", + "type": "boolean", + "description": "

If true, model values will not be restricted only to items selected from the popup.

" + }, + { + "name": "focusFirst", + "type": "boolean", + "description": "

If true, the first item in the result list will always stay focused while typing.

" + }, + { + "name": "inputFormatter", + "type": "(item: any) => string", + "description": "

The function that converts an item from the result list to a string to display in the <input> field.

\n

It is called when the user selects something in the popup or the model value changes, so the input needs to\nbe updated.

" + }, + { + "name": "ngbTypeahead", + "type": "(text: Observable) => Observable", + "description": "

The function that converts a stream of text values from the <input> element to the stream of the array of items\nto display in the typeahead popup.

\n

If the resulting observable emits a non-empty array - the popup will be shown. If it emits an empty array - the\npopup will be closed.

\n

See the basic example for more details.

\n

Note that the this argument is undefined so you need to explicitly bind it to a desired "this" target.

" + }, + { + "name": "placement", + "defaultValue": "bottom-left", + "type": "PlacementArray", + "description": "

The preferred placement of the typeahead.

\n

Possible values are "top", "top-left", "top-right", "bottom", "bottom-left",\n"bottom-right", "left", "left-top", "left-bottom", "right", "right-top",\n"right-bottom"

\n

Accepts an array of strings or a string with space separated possible values.

\n

The default order of preference is "bottom-left bottom-right top-left top-right"

\n

Please see the positioning overview for more details.

" + }, + { + "name": "resultFormatter", + "type": "(item: any) => string", + "description": "

The function that converts an item from the result list to a string to display in the popup.

\n

Must be provided, if your ngbTypeahead returns something other than Observable<string[]>.

\n

Alternatively for more complex markup in the popup you should use resultTemplate.

" + }, + { + "name": "resultTemplate", + "type": "TemplateRef", + "description": "

The template to override the way resulting items are displayed in the popup.

\n

See the ResultTemplateContext for the template context.

\n

Also see the template for results demo for more details.

" + }, + { + "name": "showHint", + "type": "boolean", + "description": "

If true, will show the hint in the <input> when an item in the result list matches.

" + } + ], + "outputs": [ + { + "name": "selectItem", + "description": "

An event emitted right before an item is selected from the result list.

\n

Event payload is of type NgbTypeaheadSelectItemEvent.

" + } + ], + "properties": [ + { + "name": "activeDescendant", + "type": "string", + "description": "" + }, + { + "name": "popupId", + "type": "string", + "description": "" + } + ], + "methods": [ + { + "name": "dismissPopup", + "description": "

Dismisses typeahead popup window

", + "args": [], + "returnType": "void" + }, + { + "name": "isPopupOpen", + "description": "

Returns true if the typeahead popup window is displayed

", + "args": [], + "returnType": "void" + } + ] + } +}; + +export default API_DOCS; \ No newline at end of file diff --git a/demo/src/app/app.component.html b/demo/src/app/app.component.html new file mode 100644 index 0000000..d250e81 --- /dev/null +++ b/demo/src/app/app.component.html @@ -0,0 +1,44 @@ + + +
+ +
+ +
+
+
+
diff --git a/demo/src/app/app.component.ts b/demo/src/app/app.component.ts new file mode 100644 index 0000000..ade7d73 --- /dev/null +++ b/demo/src/app/app.component.ts @@ -0,0 +1,43 @@ +import { DOCUMENT } from '@angular/common'; +import { Component, Inject, OnInit } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { filter } from 'rxjs/operators'; + +import { componentsList } from './shared'; +import { Analytics } from './shared/analytics/analytics'; + +@Component({ + selector: 'ngbd-app', + templateUrl: './app.component.html' +}) +export class AppComponent implements OnInit { + navbarCollapsed = true; + + components = componentsList; + + constructor( + private _analytics: Analytics, + router: Router, + @Inject(DOCUMENT) document: any + ) { + router.events + .pipe(filter(event => event instanceof NavigationEnd)) + .subscribe(event => { + const { fragment } = router.parseUrl(router.url); + if (fragment) { + setTimeout(() => { + const element = document.querySelector(`#${fragment}`); + if (element) { + element.scrollIntoView(); + } + }, 0); + } else { + window.scrollTo({ top: 0 }); + } + }); + } + + ngOnInit(): void { + this._analytics.trackPageViews(); + } +} diff --git a/demo/src/app/app.module.ts b/demo/src/app/app.module.ts new file mode 100644 index 0000000..1ec68a1 --- /dev/null +++ b/demo/src/app/app.module.ts @@ -0,0 +1,62 @@ +import {NgModule} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; + +import {AppComponent} from './app.component'; +import {routing} from './app.routing'; +import {NgbdAccordionModule} from './components/accordion/accordion.module'; +import {NgbdAlertModule} from './components/alert/alert.module'; +import {NgbdButtonsModule} from './components/buttons/buttons.module'; +import {NgbdCarouselModule} from './components/carousel/carousel.module'; +import {NgbdCollapseModule} from './components/collapse/collapse.module'; +import {NgbdDatepickerModule} from './components/datepicker/datepicker.module'; +import {NgbdDropdownModule} from './components/dropdown/dropdown.module'; +import {NgbdModalModule} from './components/modal/modal.module'; +import {NgbdPaginationModule} from './components/pagination/pagination.module'; +import {NgbdPopoverModule} from './components/popover/popover.module'; +import {NgbdProgressbarModule} from './components/progressbar/progressbar.module'; +import {NgbdRatingModule} from './components/rating/rating.module'; +import {NgbdTableModule} from './components/table/table.module'; +import {NgbdTabsetModule} from './components/tabset/tabset.module'; +import {NgbdTimepickerModule} from './components/timepicker/timepicker.module'; +import {NgbdToastModule} from './components/toast/toast.module'; +import {NgbdTooltipModule} from './components/tooltip/tooltip.module'; +import {NgbdTypeaheadModule} from './components/typeahead/typeahead.module'; +import {DefaultComponent} from './default'; +import {GettingStartedPage} from './pages/getting-started/getting-started.component'; +import {PositioningPage} from './pages/positioning/positioning.component'; +import {NgbdSharedModule} from './shared'; + + +const DEMOS = [ + NgbdAccordionModule, + NgbdAlertModule, + NgbdButtonsModule, + NgbdCarouselModule, + NgbdCollapseModule, + NgbdDatepickerModule, + NgbdDropdownModule, + NgbdModalModule, + NgbdPaginationModule, + NgbdPopoverModule, + NgbdProgressbarModule, + NgbdRatingModule, + NgbdTableModule, + NgbdTabsetModule, + NgbdTimepickerModule, + NgbdToastModule, + NgbdTooltipModule, + NgbdTypeaheadModule +]; + +const PAGES = [ + GettingStartedPage, + PositioningPage +]; + +@NgModule({ + declarations: [AppComponent, DefaultComponent, ...PAGES], + imports: [BrowserModule, routing, NgbModule, NgbdSharedModule, ...DEMOS], + bootstrap: [AppComponent] +}) +export class NgbdModule {} diff --git a/demo/src/app/app.routing.ts b/demo/src/app/app.routing.ts new file mode 100644 index 0000000..4311623 --- /dev/null +++ b/demo/src/app/app.routing.ts @@ -0,0 +1,56 @@ +import {ModuleWithProviders} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; + +import {ROUTES as ACCORDION_ROUTES} from './components/accordion/accordion.module'; +import {ROUTES as ALERT_ROUTES} from './components/alert/alert.module'; +import {ROUTES as BUTTONS_ROUTES} from './components/buttons/buttons.module'; +import {ROUTES as CAROUSEL_ROUTES} from './components/carousel/carousel.module'; +import {ROUTES as COLLAPSE_ROUTES} from './components/collapse/collapse.module'; +import {ROUTES as DATEPICKER_ROUTES} from './components/datepicker/datepicker.module'; +import {ROUTES as DROPDOWN_ROUTES} from './components/dropdown/dropdown.module'; +import {ROUTES as MODAL_ROUTES} from './components/modal/modal.module'; +import {ROUTES as PAGINATION_ROUTES} from './components/pagination/pagination.module'; +import {ROUTES as POPOVER_ROUTES} from './components/popover/popover.module'; +import {ROUTES as PROGRESSBAR_ROUTES} from './components/progressbar/progressbar.module'; +import {ROUTES as RATING_ROUTES} from './components/rating/rating.module'; +import {ROUTES as TABLE_ROUTES} from './components/table/table.module'; +import {ROUTES as TABSET_ROUTES} from './components/tabset/tabset.module'; +import {ROUTES as TIMEPICKER_ROUTES} from './components/timepicker/timepicker.module'; +import {ROUTES as TOAST_ROUTES} from './components/toast/toast.module'; +import {ROUTES as TOOLTIP_ROUTES} from './components/tooltip/tooltip.module'; +import {ROUTES as TYPEAHEAD_ROUTES} from './components/typeahead/typeahead.module'; +import {DefaultComponent} from './default'; +import {GettingStartedPage} from './pages/getting-started/getting-started.component'; +import {PositioningPage} from './pages/positioning/positioning.component'; + +const routes: Routes = [ + { path: '', pathMatch: 'full', redirectTo: 'home' }, + { path: 'home', component: DefaultComponent }, + { path: 'getting-started', component: GettingStartedPage }, + { path: 'positioning', component: PositioningPage }, + { path: 'components', pathMatch: 'full', redirectTo: 'components/alert' }, + { path: 'components/accordion', children: ACCORDION_ROUTES }, + { path: 'components/alert', children: ALERT_ROUTES }, + { path: 'components/buttons', children: BUTTONS_ROUTES }, + { path: 'components/carousel', children: CAROUSEL_ROUTES }, + { path: 'components/collapse', children: COLLAPSE_ROUTES }, + { path: 'components/datepicker', children: DATEPICKER_ROUTES }, + { path: 'components/dropdown', children: DROPDOWN_ROUTES }, + { path: 'components/modal', children: MODAL_ROUTES }, + { path: 'components/pagination', children: PAGINATION_ROUTES }, + { path: 'components/popover', children: POPOVER_ROUTES }, + { path: 'components/progressbar', children: PROGRESSBAR_ROUTES }, + { path: 'components/rating', children: RATING_ROUTES }, + { path: 'components/table', children: TABLE_ROUTES }, + { path: 'components/tabset', children: TABSET_ROUTES }, + { path: 'components/toast', children: TOAST_ROUTES }, + { path: 'components/timepicker', children: TIMEPICKER_ROUTES }, + { path: 'components/tooltip', children: TOOLTIP_ROUTES }, + { path: 'components/typeahead', children: TYPEAHEAD_ROUTES }, + { path: '**', redirectTo: 'home' } +]; + +export const routing: ModuleWithProviders = RouterModule.forRoot(routes, { + enableTracing: false, + useHash: true +}); diff --git a/demo/src/app/components/accordion/accordion.module.ts b/demo/src/app/components/accordion/accordion.module.ts new file mode 100644 index 0000000..4821e4e --- /dev/null +++ b/demo/src/app/components/accordion/accordion.module.ts @@ -0,0 +1,88 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component'; +import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared'; +import { NgbdApiPage } from '../shared/api-page/api.component'; +import { NgbdExamplesPage } from '../shared/examples-page/examples.component'; +import { NgbdAccordionBasic } from './demos/basic/accordion-basic'; +import { NgbdAccordionBasicModule } from './demos/basic/accordion-basic.module'; +import { NgbdAccordionConfig } from './demos/config/accordion-config'; +import { NgbdAccordionConfigModule } from './demos/config/accordion-config.module'; +import { NgbdAccordionHeader } from './demos/header/accordion-header'; +import { NgbdAccordionHeaderModule } from './demos/header/accordion-header.module'; +import { NgbdAccordionPreventchange } from './demos/preventchange/accordion-preventchange'; +import { NgbdAccordionPreventchangeModule } from './demos/preventchange/accordion-preventchange.module'; +import { NgbdAccordionStatic } from './demos/static/accordion-static'; +import { NgbdAccordionStaticModule } from './demos/static/accordion-static.module'; +import { NgbdAccordionToggle } from './demos/toggle/accordion-toggle'; +import { NgbdAccordionToggleModule } from './demos/toggle/accordion-toggle.module'; + +const DEMOS = { + basic: { + title: 'Accordion', + code: require('!raw-loader!./demos/basic/accordion-basic'), + markup: require('!raw-loader!./demos/basic/accordion-basic.html'), + type: NgbdAccordionBasic + }, + static: { + title: 'One open panel at a time', + code: require('!!raw-loader!./demos/static/accordion-static'), + markup: require('!!raw-loader!./demos/static/accordion-static.html'), + type: NgbdAccordionStatic + }, + toggle: { + title: 'Toggle panels', + code: require('!!raw-loader!./demos/toggle/accordion-toggle'), + markup: require('!!raw-loader!./demos/toggle/accordion-toggle.html'), + type: NgbdAccordionToggle + }, + header: { + title: 'Custom header', + code: require('!!raw-loader!./demos/header/accordion-header'), + markup: require('!!raw-loader!./demos/header/accordion-header.html'), + type: NgbdAccordionHeader + }, + preventchange: { + title: 'Prevent panel toggle', + code: require('!!raw-loader!./demos/preventchange/accordion-preventchange'), + markup: require('!!raw-loader!./demos/preventchange/accordion-preventchange.html'), + type: NgbdAccordionPreventchange + }, + config: { + title: 'Global configuration of accordions', + code: require('!!raw-loader!./demos/config/accordion-config'), + markup: require('!!raw-loader!./demos/config/accordion-config.html'), + type: NgbdAccordionConfig + } +}; + +export const ROUTES = [ + { path: '', pathMatch: 'full', redirectTo: 'examples' }, + { + path: '', + component: ComponentWrapper, + children: [ + { path: 'examples', component: NgbdExamplesPage }, + { path: 'api', component: NgbdApiPage } + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, + NgbdComponentsSharedModule, + NgbdAccordionBasicModule, + NgbdAccordionConfigModule, + NgbdAccordionHeaderModule, + NgbdAccordionToggleModule, + NgbdAccordionStaticModule, + NgbdAccordionPreventchangeModule + ] +}) +export class NgbdAccordionModule { + constructor(demoList: NgbdDemoList) { + demoList.register('accordion', DEMOS); + } +} diff --git a/demo/src/app/components/accordion/demos/basic/accordion-basic.html b/demo/src/app/components/accordion/demos/basic/accordion-basic.html new file mode 100644 index 0000000..29eb649 --- /dev/null +++ b/demo/src/app/components/accordion/demos/basic/accordion-basic.html @@ -0,0 +1,35 @@ + + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia + aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, + sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, + craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings + occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus + labore sustainable VHS. + + + + + Fancy title ★ + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia + aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, + sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, + craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings + occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus + labore sustainable VHS. + + + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia + aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, + sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, + craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings + occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus + labore sustainable VHS. + + + diff --git a/demo/src/app/components/accordion/demos/basic/accordion-basic.module.ts b/demo/src/app/components/accordion/demos/basic/accordion-basic.module.ts new file mode 100644 index 0000000..b330e81 --- /dev/null +++ b/demo/src/app/components/accordion/demos/basic/accordion-basic.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdAccordionBasic } from './accordion-basic'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdAccordionBasic], + exports: [NgbdAccordionBasic], + bootstrap: [NgbdAccordionBasic] +}) +export class NgbdAccordionBasicModule {} diff --git a/demo/src/app/components/accordion/demos/basic/accordion-basic.ts b/demo/src/app/components/accordion/demos/basic/accordion-basic.ts new file mode 100644 index 0000000..6cfdece --- /dev/null +++ b/demo/src/app/components/accordion/demos/basic/accordion-basic.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ngbd-accordion-basic', + templateUrl: './accordion-basic.html' +}) +export class NgbdAccordionBasic { +} diff --git a/demo/src/app/components/accordion/demos/config/accordion-config.html b/demo/src/app/components/accordion/demos/config/accordion-config.html new file mode 100644 index 0000000..f3d4ac5 --- /dev/null +++ b/demo/src/app/components/accordion/demos/config/accordion-config.html @@ -0,0 +1,24 @@ +

This accordion uses customized default values.

+ + + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia + aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, + sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, + craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings + occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus + labore sustainable VHS. + + + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia + aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, + sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, + craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings + occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus + labore sustainable VHS. + + + diff --git a/demo/src/app/components/accordion/demos/config/accordion-config.module.ts b/demo/src/app/components/accordion/demos/config/accordion-config.module.ts new file mode 100644 index 0000000..59e6c78 --- /dev/null +++ b/demo/src/app/components/accordion/demos/config/accordion-config.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdAccordionConfig } from './accordion-config'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdAccordionConfig], + exports: [NgbdAccordionConfig], + bootstrap: [NgbdAccordionConfig] +}) +export class NgbdAccordionConfigModule {} diff --git a/demo/src/app/components/accordion/demos/config/accordion-config.ts b/demo/src/app/components/accordion/demos/config/accordion-config.ts new file mode 100644 index 0000000..48a8be3 --- /dev/null +++ b/demo/src/app/components/accordion/demos/config/accordion-config.ts @@ -0,0 +1,15 @@ +import {Component} from '@angular/core'; +import {NgbAccordionConfig} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-accordion-config', + templateUrl: './accordion-config.html', + providers: [NgbAccordionConfig] // add the NgbAccordionConfig to the component providers +}) +export class NgbdAccordionConfig { + constructor(config: NgbAccordionConfig) { + // customize default values of accordions used by this component tree + config.closeOthers = true; + config.type = 'info'; + } +} diff --git a/demo/src/app/components/accordion/demos/header/accordion-header.html b/demo/src/app/components/accordion/demos/header/accordion-header.html new file mode 100644 index 0000000..795dd75 --- /dev/null +++ b/demo/src/app/components/accordion/demos/header/accordion-header.html @@ -0,0 +1,56 @@ + + + +
+
First panel - {{ opened ? 'opened' : 'collapsed' }}
+ +
+
+ + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia + aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, + sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, + craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings + occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus + labore sustainable VHS. + +
+ + +
+
Second panel
+
+ + + +
+
+
+ + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia + aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, + sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, + craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings + occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus + labore sustainable VHS. + +
+ + +
+ +

[I'm disabled]

+
+
+ + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia + aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, + sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, + craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings + occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus + labore sustainable VHS. + +
+
diff --git a/demo/src/app/components/accordion/demos/header/accordion-header.module.ts b/demo/src/app/components/accordion/demos/header/accordion-header.module.ts new file mode 100644 index 0000000..61f354c --- /dev/null +++ b/demo/src/app/components/accordion/demos/header/accordion-header.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdAccordionHeader } from './accordion-header'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdAccordionHeader], + exports: [NgbdAccordionHeader], + bootstrap: [NgbdAccordionHeader] +}) +export class NgbdAccordionHeaderModule {} diff --git a/demo/src/app/components/accordion/demos/header/accordion-header.ts b/demo/src/app/components/accordion/demos/header/accordion-header.ts new file mode 100644 index 0000000..01021fc --- /dev/null +++ b/demo/src/app/components/accordion/demos/header/accordion-header.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ngbd-accordion-header', + templateUrl: './accordion-header.html' +}) +export class NgbdAccordionHeader { + disabled = false; +} diff --git a/demo/src/app/components/accordion/demos/preventchange/accordion-preventchange.html b/demo/src/app/components/accordion/demos/preventchange/accordion-preventchange.html new file mode 100644 index 0000000..93ff3aa --- /dev/null +++ b/demo/src/app/components/accordion/demos/preventchange/accordion-preventchange.html @@ -0,0 +1,32 @@ + + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia + aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, + sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, + craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings + occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus + labore sustainable VHS. + + + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia + aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, + sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, + craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings + occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus + labore sustainable VHS. + + + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia + aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, + sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, + craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings + occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus + labore sustainable VHS. + + + diff --git a/demo/src/app/components/accordion/demos/preventchange/accordion-preventchange.module.ts b/demo/src/app/components/accordion/demos/preventchange/accordion-preventchange.module.ts new file mode 100644 index 0000000..3a948c2 --- /dev/null +++ b/demo/src/app/components/accordion/demos/preventchange/accordion-preventchange.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdAccordionPreventchange } from './accordion-preventchange'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdAccordionPreventchange], + exports: [NgbdAccordionPreventchange], + bootstrap: [NgbdAccordionPreventchange] +}) +export class NgbdAccordionPreventchangeModule {} diff --git a/demo/src/app/components/accordion/demos/preventchange/accordion-preventchange.ts b/demo/src/app/components/accordion/demos/preventchange/accordion-preventchange.ts new file mode 100644 index 0000000..2ce2c70 --- /dev/null +++ b/demo/src/app/components/accordion/demos/preventchange/accordion-preventchange.ts @@ -0,0 +1,19 @@ +import {Component} from '@angular/core'; +import {NgbPanelChangeEvent} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-accordion-preventchange', + templateUrl: './accordion-preventchange.html', +}) +export class NgbdAccordionPreventchange { + public beforeChange($event: NgbPanelChangeEvent) { + + if ($event.panelId === 'preventchange-2') { + $event.preventDefault(); + } + + if ($event.panelId === 'preventchange-3' && $event.nextState === false) { + $event.preventDefault(); + } + } +} diff --git a/demo/src/app/components/accordion/demos/static/accordion-static.html b/demo/src/app/components/accordion/demos/static/accordion-static.html new file mode 100644 index 0000000..f834c8b --- /dev/null +++ b/demo/src/app/components/accordion/demos/static/accordion-static.html @@ -0,0 +1,35 @@ + + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia + aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, + sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, + craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings + occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus + labore sustainable VHS. + + + + + Fancy title ★ + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia + aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, + sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, + craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings + occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus + labore sustainable VHS. + + + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia + aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, + sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, + craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings + occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus + labore sustainable VHS. + + + diff --git a/demo/src/app/components/accordion/demos/static/accordion-static.module.ts b/demo/src/app/components/accordion/demos/static/accordion-static.module.ts new file mode 100644 index 0000000..94aca3a --- /dev/null +++ b/demo/src/app/components/accordion/demos/static/accordion-static.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdAccordionStatic } from './accordion-static'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdAccordionStatic], + exports: [NgbdAccordionStatic], + bootstrap: [NgbdAccordionStatic] +}) +export class NgbdAccordionStaticModule {} diff --git a/demo/src/app/components/accordion/demos/static/accordion-static.ts b/demo/src/app/components/accordion/demos/static/accordion-static.ts new file mode 100644 index 0000000..32fa6e3 --- /dev/null +++ b/demo/src/app/components/accordion/demos/static/accordion-static.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ngbd-accordion-static', + templateUrl: './accordion-static.html' +}) +export class NgbdAccordionStatic { +} diff --git a/demo/src/app/components/accordion/demos/toggle/accordion-toggle.html b/demo/src/app/components/accordion/demos/toggle/accordion-toggle.html new file mode 100644 index 0000000..bf0d9b7 --- /dev/null +++ b/demo/src/app/components/accordion/demos/toggle/accordion-toggle.html @@ -0,0 +1,28 @@ + + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia + aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, + sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, + craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings + occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus + labore sustainable VHS. + + + + + Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon officia + aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf moon tempor, + sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim keffiyeh helvetica, + craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur butcher vice lomo. Leggings + occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably haven't heard of them accusamus + labore sustainable VHS. + + + + +
+ + + + diff --git a/demo/src/app/components/accordion/demos/toggle/accordion-toggle.module.ts b/demo/src/app/components/accordion/demos/toggle/accordion-toggle.module.ts new file mode 100644 index 0000000..fde1795 --- /dev/null +++ b/demo/src/app/components/accordion/demos/toggle/accordion-toggle.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdAccordionToggle } from './accordion-toggle'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdAccordionToggle], + exports: [NgbdAccordionToggle], + bootstrap: [NgbdAccordionToggle] +}) +export class NgbdAccordionToggleModule {} diff --git a/demo/src/app/components/accordion/demos/toggle/accordion-toggle.ts b/demo/src/app/components/accordion/demos/toggle/accordion-toggle.ts new file mode 100644 index 0000000..a514631 --- /dev/null +++ b/demo/src/app/components/accordion/demos/toggle/accordion-toggle.ts @@ -0,0 +1,8 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-accordion-toggle', + templateUrl: './accordion-toggle.html' +}) +export class NgbdAccordionToggle { +} diff --git a/demo/src/app/components/alert/alert.module.ts b/demo/src/app/components/alert/alert.module.ts new file mode 100644 index 0000000..01da256 --- /dev/null +++ b/demo/src/app/components/alert/alert.module.ts @@ -0,0 +1,119 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component'; +import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared'; +import { NgbdApiPage } from '../shared/api-page/api.component'; +import { NgbdExamplesPage } from '../shared/examples-page/examples.component'; +import { NgbdAlertBasic } from './demos/basic/alert-basic'; +import { NgbdAlertBasicModule } from './demos/basic/alert-basic.module'; +import { NgbdAlertCloseable } from './demos/closeable/alert-closeable'; +import { NgbdAlertCloseableModule } from './demos/closeable/alert-closeable.module'; +import { NgbdAlertConfig } from './demos/config/alert-config'; +import { NgbdAlertConfigModule } from './demos/config/alert-config.module'; +import { NgbdAlertCustom } from './demos/custom/alert-custom'; +import { NgbdAlertCustomModule } from './demos/custom/alert-custom.module'; +import { NgbdAlertSelfclosing } from './demos/selfclosing/alert-selfclosing'; +import { NgbdAlertSelfclosingModule } from './demos/selfclosing/alert-selfclosing.module'; + +const DEMOS = { + basic: { + title: 'Basic Alert', + type: NgbdAlertBasic, + files: [ + { + name: 'alert-basic.html', + source: require('!!raw-loader!./demos/basic/alert-basic.html') + }, + { + name: 'alert-basic.ts', + source: require('!!raw-loader!./demos/basic/alert-basic') + } + ] + }, + closeable: { + title: 'Closable Alert', + type: NgbdAlertCloseable, + files: [ + { + name: 'alert-closeable.html', + source: require('!!raw-loader!./demos/closeable/alert-closeable.html') + }, + { + name: 'alert-closeable.ts', + source: require('!!raw-loader!./demos/closeable/alert-closeable') + } + ] + }, + selfclosing: { + title: 'Self closing alert', + type: NgbdAlertSelfclosing, + files: [ + { + name: 'alert-selfclosing.html', + source: require('!!raw-loader!./demos/selfclosing/alert-selfclosing.html') + }, + { + name: 'alert-selfclosing.ts', + source: require('!!raw-loader!./demos/selfclosing/alert-selfclosing') + } + ] + }, + custom: { + title: 'Custom alert', + type: NgbdAlertCustom, + files: [ + { + name: 'alert-custom.html', + source: require('!!raw-loader!./demos/custom/alert-custom.html') + }, + { + name: 'alert-custom.ts', + source: require('!!raw-loader!./demos/custom/alert-custom') + } + ] + }, + config: { + title: 'Global configuration of alerts', + type: NgbdAlertConfig, + files: [ + { + name: 'alert-config.html', + source: require('!!raw-loader!./demos/config/alert-config.html') + }, + { + name: 'alert-config.ts', + source: require('!!raw-loader!./demos/config/alert-config') + } + ] + } +}; + +export const ROUTES = [ + { path: '', pathMatch: 'full', redirectTo: 'examples' }, + { + path: '', + component: ComponentWrapper, + children: [ + { path: 'examples', component: NgbdExamplesPage }, + { path: 'api', component: NgbdApiPage } + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, + NgbdComponentsSharedModule, + NgbdAlertBasicModule, + NgbdAlertCloseableModule, + NgbdAlertCustomModule, + NgbdAlertConfigModule, + NgbdAlertSelfclosingModule + ] +}) +export class NgbdAlertModule { + constructor(demoList: NgbdDemoList) { + demoList.register('alert', DEMOS); + } +} diff --git a/demo/src/app/components/alert/demos/basic/alert-basic.html b/demo/src/app/components/alert/demos/basic/alert-basic.html new file mode 100644 index 0000000..a59f478 --- /dev/null +++ b/demo/src/app/components/alert/demos/basic/alert-basic.html @@ -0,0 +1,5 @@ +

+ + Warning! Better check yourself, you're not looking too good. + +

diff --git a/demo/src/app/components/alert/demos/basic/alert-basic.module.ts b/demo/src/app/components/alert/demos/basic/alert-basic.module.ts new file mode 100644 index 0000000..77dcecf --- /dev/null +++ b/demo/src/app/components/alert/demos/basic/alert-basic.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdAlertBasic } from './alert-basic'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdAlertBasic], + exports: [NgbdAlertBasic], + bootstrap: [NgbdAlertBasic] +}) +export class NgbdAlertBasicModule {} diff --git a/demo/src/app/components/alert/demos/basic/alert-basic.ts b/demo/src/app/components/alert/demos/basic/alert-basic.ts new file mode 100644 index 0000000..61abb1e --- /dev/null +++ b/demo/src/app/components/alert/demos/basic/alert-basic.ts @@ -0,0 +1,5 @@ +import {Component} from '@angular/core'; + +@Component({selector: 'ngbd-alert-basic', templateUrl: './alert-basic.html'}) +export class NgbdAlertBasic { +} diff --git a/demo/src/app/components/alert/demos/closeable/alert-closeable.html b/demo/src/app/components/alert/demos/closeable/alert-closeable.html new file mode 100644 index 0000000..8c7241b --- /dev/null +++ b/demo/src/app/components/alert/demos/closeable/alert-closeable.html @@ -0,0 +1,6 @@ +

+ {{ alert.message }} +

+

+ +

diff --git a/demo/src/app/components/alert/demos/closeable/alert-closeable.module.ts b/demo/src/app/components/alert/demos/closeable/alert-closeable.module.ts new file mode 100644 index 0000000..49a36b4 --- /dev/null +++ b/demo/src/app/components/alert/demos/closeable/alert-closeable.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdAlertCloseable } from './alert-closeable'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdAlertCloseable], + exports: [NgbdAlertCloseable], + bootstrap: [NgbdAlertCloseable] +}) +export class NgbdAlertCloseableModule {} diff --git a/demo/src/app/components/alert/demos/closeable/alert-closeable.ts b/demo/src/app/components/alert/demos/closeable/alert-closeable.ts new file mode 100644 index 0000000..f76203c --- /dev/null +++ b/demo/src/app/components/alert/demos/closeable/alert-closeable.ts @@ -0,0 +1,54 @@ +import { Input, Component } from '@angular/core'; + +interface Alert { + type: string; + message: string; +} + +const ALERTS: Alert[] = [{ + type: 'success', + message: 'This is an success alert', + }, { + type: 'info', + message: 'This is an info alert', + }, { + type: 'warning', + message: 'This is a warning alert', + }, { + type: 'danger', + message: 'This is a danger alert', + }, { + type: 'primary', + message: 'This is a primary alert', + }, { + type: 'secondary', + message: 'This is a secondary alert', + }, { + type: 'light', + message: 'This is a light alert', + }, { + type: 'dark', + message: 'This is a dark alert', + } +]; + +@Component({ + selector: 'ngbd-alert-closeable', + templateUrl: './alert-closeable.html' +}) +export class NgbdAlertCloseable { + + alerts: Alert[]; + + constructor() { + this.reset(); + } + + close(alert: Alert) { + this.alerts.splice(this.alerts.indexOf(alert), 1); + } + + reset() { + this.alerts = Array.from(ALERTS); + } +} diff --git a/demo/src/app/components/alert/demos/config/alert-config.html b/demo/src/app/components/alert/demos/config/alert-config.html new file mode 100644 index 0000000..d795c20 --- /dev/null +++ b/demo/src/app/components/alert/demos/config/alert-config.html @@ -0,0 +1,5 @@ +

+ + This alert's type is success and it's not dismissible because the config has been customized + +

diff --git a/demo/src/app/components/alert/demos/config/alert-config.module.ts b/demo/src/app/components/alert/demos/config/alert-config.module.ts new file mode 100644 index 0000000..9e5078f --- /dev/null +++ b/demo/src/app/components/alert/demos/config/alert-config.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdAlertConfig } from './alert-config'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdAlertConfig], + exports: [NgbdAlertConfig], + bootstrap: [NgbdAlertConfig] +}) +export class NgbdAlertConfigModule {} diff --git a/demo/src/app/components/alert/demos/config/alert-config.ts b/demo/src/app/components/alert/demos/config/alert-config.ts new file mode 100644 index 0000000..d9f1e9d --- /dev/null +++ b/demo/src/app/components/alert/demos/config/alert-config.ts @@ -0,0 +1,18 @@ +import {Component, Input} from '@angular/core'; +import {NgbAlertConfig} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-alert-config', + templateUrl: './alert-config.html', + // add NgbAlertConfig to the component providers + providers: [NgbAlertConfig] +}) +export class NgbdAlertConfig { + @Input() public alerts: Array = []; + + constructor(alertConfig: NgbAlertConfig) { + // customize default values of alerts used by this component tree + alertConfig.type = 'success'; + alertConfig.dismissible = false; + } +} diff --git a/demo/src/app/components/alert/demos/custom/alert-custom.html b/demo/src/app/components/alert/demos/custom/alert-custom.html new file mode 100644 index 0000000..cd0e4d9 --- /dev/null +++ b/demo/src/app/components/alert/demos/custom/alert-custom.html @@ -0,0 +1,3 @@ +

+ Whoa! This is a custom alert. +

diff --git a/demo/src/app/components/alert/demos/custom/alert-custom.module.ts b/demo/src/app/components/alert/demos/custom/alert-custom.module.ts new file mode 100644 index 0000000..6606b7d --- /dev/null +++ b/demo/src/app/components/alert/demos/custom/alert-custom.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdAlertCustom } from './alert-custom'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdAlertCustom], + exports: [NgbdAlertCustom], + bootstrap: [NgbdAlertCustom] +}) +export class NgbdAlertCustomModule {} diff --git a/demo/src/app/components/alert/demos/custom/alert-custom.ts b/demo/src/app/components/alert/demos/custom/alert-custom.ts new file mode 100644 index 0000000..fc2ee39 --- /dev/null +++ b/demo/src/app/components/alert/demos/custom/alert-custom.ts @@ -0,0 +1,14 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ngbd-alert-custom', + templateUrl: './alert-custom.html', + styles: [` + :host >>> .alert-custom { + color: #99004d; + background-color: #f169b4; + border-color: #800040; + } + `] +}) +export class NgbdAlertCustom {} diff --git a/demo/src/app/components/alert/demos/selfclosing/alert-selfclosing.html b/demo/src/app/components/alert/demos/selfclosing/alert-selfclosing.html new file mode 100644 index 0000000..4d1f1df --- /dev/null +++ b/demo/src/app/components/alert/demos/selfclosing/alert-selfclosing.html @@ -0,0 +1,14 @@ +

+ Static self-closing alert that disappears after 20 seconds (refresh the page if it has already disappeared) +

+Check out our awesome new features! + +
+ +

+ Show a self-closing success message that disappears after 5 seconds. +

+{{ successMessage }} +

+ +

diff --git a/demo/src/app/components/alert/demos/selfclosing/alert-selfclosing.module.ts b/demo/src/app/components/alert/demos/selfclosing/alert-selfclosing.module.ts new file mode 100644 index 0000000..861db10 --- /dev/null +++ b/demo/src/app/components/alert/demos/selfclosing/alert-selfclosing.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdAlertSelfclosing } from './alert-selfclosing'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdAlertSelfclosing], + exports: [NgbdAlertSelfclosing], + bootstrap: [NgbdAlertSelfclosing] +}) +export class NgbdAlertSelfclosingModule {} diff --git a/demo/src/app/components/alert/demos/selfclosing/alert-selfclosing.ts b/demo/src/app/components/alert/demos/selfclosing/alert-selfclosing.ts new file mode 100644 index 0000000..9f7ea19 --- /dev/null +++ b/demo/src/app/components/alert/demos/selfclosing/alert-selfclosing.ts @@ -0,0 +1,27 @@ +import {Component, OnInit} from '@angular/core'; +import {Subject} from 'rxjs'; +import {debounceTime} from 'rxjs/operators'; + +@Component({ + selector: 'ngbd-alert-selfclosing', + templateUrl: './alert-selfclosing.html' +}) +export class NgbdAlertSelfclosing implements OnInit { + private _success = new Subject(); + + staticAlertClosed = false; + successMessage: string; + + ngOnInit(): void { + setTimeout(() => this.staticAlertClosed = true, 20000); + + this._success.subscribe((message) => this.successMessage = message); + this._success.pipe( + debounceTime(5000) + ).subscribe(() => this.successMessage = null); + } + + public changeSuccessMessage() { + this._success.next(`${new Date()} - Message successfully changed.`); + } +} diff --git a/demo/src/app/components/buttons/buttons.module.ts b/demo/src/app/components/buttons/buttons.module.ts new file mode 100644 index 0000000..bda1fce --- /dev/null +++ b/demo/src/app/components/buttons/buttons.module.ts @@ -0,0 +1,70 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component'; +import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared'; +import { NgbdApiPage } from '../shared/api-page/api.component'; +import { NgbdExamplesPage } from '../shared/examples-page/examples.component'; +import { NgbdButtonsCheckbox } from './demos/checkbox/buttons-checkbox'; +import { NgbdButtonsCheckboxModule } from './demos/checkbox/buttons-checkbox.module'; +import { NgbdButtonsCheckboxReactiveModule } from './demos/checkboxreactive/buttons-checkbox-reactive.module'; +import { NgbdButtonsCheckboxreactive } from './demos/checkboxreactive/buttons-checkboxreactive'; +import { NgbdButtonsRadio } from './demos/radio/buttons-radio'; +import { NgbdButtonsRadioModule } from './demos/radio/buttons-radio.module'; +import { NgbdButtonsRadioReactiveModule } from './demos/radioreactive/buttons-radio-reactive.module'; +import { NgbdButtonsRadioreactive } from './demos/radioreactive/buttons-radioreactive'; + +const DEMOS = { + checkbox: { + title: 'Checkbox buttons', + type: NgbdButtonsCheckbox, + code: require('!!raw-loader!./demos/checkbox/buttons-checkbox'), + markup: require('!!raw-loader!./demos/checkbox/buttons-checkbox.html') + }, + checkboxreactive: { + title: 'Checkbox buttons (Reactive Forms)', + type: NgbdButtonsCheckboxreactive, + code: require('!!raw-loader!./demos/checkboxreactive/buttons-checkboxreactive'), + markup: require('!!raw-loader!./demos/checkboxreactive/buttons-checkboxreactive.html') + }, + radio: { + title: 'Radio buttons', + type: NgbdButtonsRadio, + code: require('!!raw-loader!./demos/radio/buttons-radio'), + markup: require('!!raw-loader!./demos/radio/buttons-radio.html') + }, + radioreactive: { + title: 'Radio buttons (Reactive Forms)', + type: NgbdButtonsRadioreactive, + code: require('!!raw-loader!./demos/radioreactive/buttons-radioreactive'), + markup: require('!!raw-loader!./demos/radioreactive/buttons-radioreactive.html') + } +}; + +export const ROUTES = [ + { path: '', pathMatch: 'full', redirectTo: 'examples' }, + { + path: '', + component: ComponentWrapper, + children: [ + { path: 'examples', component: NgbdExamplesPage }, + { path: 'api', component: NgbdApiPage } + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, + NgbdComponentsSharedModule, + NgbdButtonsCheckboxModule, + NgbdButtonsCheckboxReactiveModule, + NgbdButtonsRadioModule, + NgbdButtonsRadioReactiveModule + ] +}) +export class NgbdButtonsModule { + constructor(demoList: NgbdDemoList) { + demoList.register('buttons', DEMOS); + } +} diff --git a/demo/src/app/components/buttons/demos/checkbox/buttons-checkbox.html b/demo/src/app/components/buttons/demos/checkbox/buttons-checkbox.html new file mode 100644 index 0000000..e87a52d --- /dev/null +++ b/demo/src/app/components/buttons/demos/checkbox/buttons-checkbox.html @@ -0,0 +1,13 @@ +
+ + + +
+
+
{{model | json}}
diff --git a/demo/src/app/components/buttons/demos/checkbox/buttons-checkbox.module.ts b/demo/src/app/components/buttons/demos/checkbox/buttons-checkbox.module.ts new file mode 100644 index 0000000..d3fd95d --- /dev/null +++ b/demo/src/app/components/buttons/demos/checkbox/buttons-checkbox.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdButtonsCheckbox } from './buttons-checkbox'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdButtonsCheckbox], + exports: [NgbdButtonsCheckbox], + bootstrap: [NgbdButtonsCheckbox] +}) +export class NgbdButtonsCheckboxModule {} diff --git a/demo/src/app/components/buttons/demos/checkbox/buttons-checkbox.ts b/demo/src/app/components/buttons/demos/checkbox/buttons-checkbox.ts new file mode 100644 index 0000000..c5edd94 --- /dev/null +++ b/demo/src/app/components/buttons/demos/checkbox/buttons-checkbox.ts @@ -0,0 +1,13 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-buttons-checkbox', + templateUrl: './buttons-checkbox.html' +}) +export class NgbdButtonsCheckbox { + model = { + left: true, + middle: false, + right: false + }; +} diff --git a/demo/src/app/components/buttons/demos/checkboxreactive/buttons-checkbox-reactive.module.ts b/demo/src/app/components/buttons/demos/checkboxreactive/buttons-checkbox-reactive.module.ts new file mode 100644 index 0000000..b2c5101 --- /dev/null +++ b/demo/src/app/components/buttons/demos/checkboxreactive/buttons-checkbox-reactive.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdButtonsCheckboxreactive } from './buttons-checkboxreactive'; + +@NgModule({ + imports: [BrowserModule, ReactiveFormsModule, NgbModule], + declarations: [NgbdButtonsCheckboxreactive], + exports: [NgbdButtonsCheckboxreactive], + bootstrap: [NgbdButtonsCheckboxreactive] +}) +export class NgbdButtonsCheckboxReactiveModule {} diff --git a/demo/src/app/components/buttons/demos/checkboxreactive/buttons-checkboxreactive.html b/demo/src/app/components/buttons/demos/checkboxreactive/buttons-checkboxreactive.html new file mode 100644 index 0000000..4fb67e0 --- /dev/null +++ b/demo/src/app/components/buttons/demos/checkboxreactive/buttons-checkboxreactive.html @@ -0,0 +1,15 @@ +
+
+ + + +
+
+
+
{{checkboxGroupForm.value | json}}
diff --git a/demo/src/app/components/buttons/demos/checkboxreactive/buttons-checkboxreactive.ts b/demo/src/app/components/buttons/demos/checkboxreactive/buttons-checkboxreactive.ts new file mode 100644 index 0000000..7c0baf9 --- /dev/null +++ b/demo/src/app/components/buttons/demos/checkboxreactive/buttons-checkboxreactive.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; + +@Component({ + selector: 'ngbd-buttons-checkboxreactive', + templateUrl: './buttons-checkboxreactive.html' +}) +export class NgbdButtonsCheckboxreactive { + public checkboxGroupForm: FormGroup; + + constructor(private formBuilder: FormBuilder) {} + + ngOnInit() { + this.checkboxGroupForm = this.formBuilder.group({ + left: true, + middle: false, + right: false + }); + } +} diff --git a/demo/src/app/components/buttons/demos/radio/buttons-radio.html b/demo/src/app/components/buttons/demos/radio/buttons-radio.html new file mode 100644 index 0000000..944e41b --- /dev/null +++ b/demo/src/app/components/buttons/demos/radio/buttons-radio.html @@ -0,0 +1,13 @@ +
+ + + +
+
+
{{model}}
diff --git a/demo/src/app/components/buttons/demos/radio/buttons-radio.module.ts b/demo/src/app/components/buttons/demos/radio/buttons-radio.module.ts new file mode 100644 index 0000000..b4055d7 --- /dev/null +++ b/demo/src/app/components/buttons/demos/radio/buttons-radio.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdButtonsRadio } from './buttons-radio'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdButtonsRadio], + exports: [NgbdButtonsRadio], + bootstrap: [NgbdButtonsRadio] +}) +export class NgbdButtonsRadioModule {} diff --git a/demo/src/app/components/buttons/demos/radio/buttons-radio.ts b/demo/src/app/components/buttons/demos/radio/buttons-radio.ts new file mode 100644 index 0000000..1125c05 --- /dev/null +++ b/demo/src/app/components/buttons/demos/radio/buttons-radio.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-buttons-radio', + templateUrl: './buttons-radio.html' +}) +export class NgbdButtonsRadio { + model = 1; +} diff --git a/demo/src/app/components/buttons/demos/radioreactive/buttons-radio-reactive.module.ts b/demo/src/app/components/buttons/demos/radioreactive/buttons-radio-reactive.module.ts new file mode 100644 index 0000000..e77c01a --- /dev/null +++ b/demo/src/app/components/buttons/demos/radioreactive/buttons-radio-reactive.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdButtonsRadioreactive } from './buttons-radioreactive'; + +@NgModule({ + imports: [BrowserModule, ReactiveFormsModule, NgbModule], + declarations: [NgbdButtonsRadioreactive], + exports: [NgbdButtonsRadioreactive], + bootstrap: [NgbdButtonsRadioreactive] +}) +export class NgbdButtonsRadioReactiveModule {} diff --git a/demo/src/app/components/buttons/demos/radioreactive/buttons-radioreactive.html b/demo/src/app/components/buttons/demos/radioreactive/buttons-radioreactive.html new file mode 100644 index 0000000..d85a496 --- /dev/null +++ b/demo/src/app/components/buttons/demos/radioreactive/buttons-radioreactive.html @@ -0,0 +1,15 @@ +
+
+ + + +
+
+
+
{{radioGroupForm.value['model']}}
diff --git a/demo/src/app/components/buttons/demos/radioreactive/buttons-radioreactive.ts b/demo/src/app/components/buttons/demos/radioreactive/buttons-radioreactive.ts new file mode 100644 index 0000000..b5803e5 --- /dev/null +++ b/demo/src/app/components/buttons/demos/radioreactive/buttons-radioreactive.ts @@ -0,0 +1,18 @@ +import { Component, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; + +@Component({ + selector: 'ngbd-buttons-radioreactive', + templateUrl: './buttons-radioreactive.html' +}) +export class NgbdButtonsRadioreactive implements OnInit { + public radioGroupForm: FormGroup; + + constructor(private formBuilder: FormBuilder) {} + + ngOnInit() { + this.radioGroupForm = this.formBuilder.group({ + 'model': 1 + }); + } +} diff --git a/demo/src/app/components/carousel/carousel.module.ts b/demo/src/app/components/carousel/carousel.module.ts new file mode 100644 index 0000000..94faa52 --- /dev/null +++ b/demo/src/app/components/carousel/carousel.module.ts @@ -0,0 +1,70 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component'; +import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared'; +import { NgbdApiPage } from '../shared/api-page/api.component'; +import { NgbdExamplesPage } from '../shared/examples-page/examples.component'; +import { NgbdCarouselBasic } from './demos/basic/carousel-basic'; +import { NgbdCarouselBasicModule } from './demos/basic/carousel-basic.module'; +import { NgbdCarouselConfig } from './demos/config/carousel-config'; +import { NgbdCarouselConfigModule } from './demos/config/carousel-config.module'; +import { NgbdCarouselNavigation } from './demos/navigation/carousel-navigation'; +import { NgbdCarouselNavigationModule } from './demos/navigation/carousel-navigation.module'; +import { NgbdCarouselPause } from './demos/pause/carousel-pause'; +import { NgbdCarouselPauseModule } from './demos/pause/carousel-pause.module'; + +const DEMOS = { + basic: { + title: 'Carousel', + type: NgbdCarouselBasic, + code: require('!!raw-loader!./demos/basic/carousel-basic'), + markup: require('!!raw-loader!./demos/basic/carousel-basic.html') + }, + navigation: { + title: 'Navigation arrows and indicators', + type: NgbdCarouselNavigation, + code: require('!!raw-loader!./demos/navigation/carousel-navigation'), + markup: require('!!raw-loader!./demos/navigation/carousel-navigation.html') + }, + pause: { + title: 'Pause/cycle', + type: NgbdCarouselPause, + code: require('!!raw-loader!./demos/pause/carousel-pause'), + markup: require('!!raw-loader!./demos/pause/carousel-pause.html') + }, + config: { + title: 'Global configuration of carousels', + type: NgbdCarouselConfig, + code: require('!!raw-loader!./demos/config/carousel-config'), + markup: require('!!raw-loader!./demos/config/carousel-config.html') + } +}; + +export const ROUTES = [ + { path: '', pathMatch: 'full', redirectTo: 'examples' }, + { + path: '', + component: ComponentWrapper, + children: [ + { path: 'examples', component: NgbdExamplesPage }, + { path: 'api', component: NgbdApiPage } + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, + NgbdComponentsSharedModule, + NgbdCarouselBasicModule, + NgbdCarouselConfigModule, + NgbdCarouselNavigationModule, + NgbdCarouselPauseModule + ] +}) +export class NgbdCarouselModule { + constructor(demoList: NgbdDemoList) { + demoList.register('carousel', DEMOS); + } +} diff --git a/demo/src/app/components/carousel/demos/basic/carousel-basic.html b/demo/src/app/components/carousel/demos/basic/carousel-basic.html new file mode 100644 index 0000000..0bb3474 --- /dev/null +++ b/demo/src/app/components/carousel/demos/basic/carousel-basic.html @@ -0,0 +1,29 @@ + + +
+ Random first slide +
+ +
+ +
+ Random second slide +
+ +
+ +
+ Random third slide +
+ +
+
diff --git a/demo/src/app/components/carousel/demos/basic/carousel-basic.module.ts b/demo/src/app/components/carousel/demos/basic/carousel-basic.module.ts new file mode 100644 index 0000000..4b272b1 --- /dev/null +++ b/demo/src/app/components/carousel/demos/basic/carousel-basic.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdCarouselBasic } from './carousel-basic'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdCarouselBasic], + exports: [NgbdCarouselBasic], + bootstrap: [NgbdCarouselBasic] +}) +export class NgbdCarouselBasicModule {} diff --git a/demo/src/app/components/carousel/demos/basic/carousel-basic.ts b/demo/src/app/components/carousel/demos/basic/carousel-basic.ts new file mode 100644 index 0000000..f0910bb --- /dev/null +++ b/demo/src/app/components/carousel/demos/basic/carousel-basic.ts @@ -0,0 +1,6 @@ +import { Component } from '@angular/core'; + +@Component({selector: 'ngbd-carousel-basic', templateUrl: './carousel-basic.html'}) +export class NgbdCarouselBasic { + images = [1, 2, 3].map(() => `https://picsum.photos/900/500?random&t=${Math.random()}`); +} diff --git a/demo/src/app/components/carousel/demos/config/carousel-config.html b/demo/src/app/components/carousel/demos/config/carousel-config.html new file mode 100644 index 0000000..23439de --- /dev/null +++ b/demo/src/app/components/carousel/demos/config/carousel-config.html @@ -0,0 +1,38 @@ + + +
+ Random first slide +
+ +
+ +
+ Random second slide +
+ +
+ +
+ Random third slide +
+ +
+ +
+ Random fourth slide +
+ +
+
diff --git a/demo/src/app/components/carousel/demos/config/carousel-config.module.ts b/demo/src/app/components/carousel/demos/config/carousel-config.module.ts new file mode 100644 index 0000000..5379286 --- /dev/null +++ b/demo/src/app/components/carousel/demos/config/carousel-config.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdCarouselConfig } from './carousel-config'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdCarouselConfig], + exports: [NgbdCarouselConfig], + bootstrap: [NgbdCarouselConfig] +}) +export class NgbdCarouselConfigModule {} diff --git a/demo/src/app/components/carousel/demos/config/carousel-config.ts b/demo/src/app/components/carousel/demos/config/carousel-config.ts new file mode 100644 index 0000000..3058664 --- /dev/null +++ b/demo/src/app/components/carousel/demos/config/carousel-config.ts @@ -0,0 +1,19 @@ +import { Component } from '@angular/core'; +import { NgbCarouselConfig } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-carousel-config', + templateUrl: './carousel-config.html', + providers: [NgbCarouselConfig] // add NgbCarouselConfig to the component providers +}) +export class NgbdCarouselConfig { + images = [1, 2, 3, 4].map(() => `https://picsum.photos/900/500?random&t=${Math.random()}`); + + constructor(config: NgbCarouselConfig) { + // customize default values of carousels used by this component tree + config.interval = 10000; + config.wrap = false; + config.keyboard = false; + config.pauseOnHover = false; + } +} diff --git a/demo/src/app/components/carousel/demos/navigation/carousel-navigation.html b/demo/src/app/components/carousel/demos/navigation/carousel-navigation.html new file mode 100644 index 0000000..fb3f263 --- /dev/null +++ b/demo/src/app/components/carousel/demos/navigation/carousel-navigation.html @@ -0,0 +1,18 @@ + + +
+ Random slide +
+ +
+
+ +
+ +
+ + +
diff --git a/demo/src/app/components/carousel/demos/navigation/carousel-navigation.module.ts b/demo/src/app/components/carousel/demos/navigation/carousel-navigation.module.ts new file mode 100644 index 0000000..0946890 --- /dev/null +++ b/demo/src/app/components/carousel/demos/navigation/carousel-navigation.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdCarouselNavigation } from './carousel-navigation'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdCarouselNavigation], + exports: [NgbdCarouselNavigation], + bootstrap: [NgbdCarouselNavigation] +}) +export class NgbdCarouselNavigationModule {} diff --git a/demo/src/app/components/carousel/demos/navigation/carousel-navigation.ts b/demo/src/app/components/carousel/demos/navigation/carousel-navigation.ts new file mode 100644 index 0000000..94148b0 --- /dev/null +++ b/demo/src/app/components/carousel/demos/navigation/carousel-navigation.ts @@ -0,0 +1,19 @@ +import {Component} from '@angular/core'; +import {NgbCarouselConfig} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-carousel-navigation', + templateUrl: './carousel-navigation.html', + providers: [NgbCarouselConfig] // add NgbCarouselConfig to the component providers +}) +export class NgbdCarouselNavigation { + showNavigationArrows = false; + showNavigationIndicators = false; + images = [1, 2, 3].map(() => `https://picsum.photos/900/500?random&t=${Math.random()}`); + + constructor(config: NgbCarouselConfig) { + // customize default values of carousels used by this component tree + config.showNavigationArrows = true; + config.showNavigationIndicators = true; + } +} diff --git a/demo/src/app/components/carousel/demos/pause/carousel-pause.html b/demo/src/app/components/carousel/demos/pause/carousel-pause.html new file mode 100644 index 0000000..5f7d063 --- /dev/null +++ b/demo/src/app/components/carousel/demos/pause/carousel-pause.html @@ -0,0 +1,30 @@ + + + + +
+ My image {{i + 1}} description +
+
+
+
+ +
+ +
+ + +
+
+ + +
+
+ + +
+ diff --git a/demo/src/app/components/carousel/demos/pause/carousel-pause.module.ts b/demo/src/app/components/carousel/demos/pause/carousel-pause.module.ts new file mode 100644 index 0000000..ab4ce58 --- /dev/null +++ b/demo/src/app/components/carousel/demos/pause/carousel-pause.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdCarouselPause } from './carousel-pause'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdCarouselPause], + exports: [NgbdCarouselPause], + bootstrap: [NgbdCarouselPause] +}) +export class NgbdCarouselPauseModule {} diff --git a/demo/src/app/components/carousel/demos/pause/carousel-pause.ts b/demo/src/app/components/carousel/demos/pause/carousel-pause.ts new file mode 100644 index 0000000..54355df --- /dev/null +++ b/demo/src/app/components/carousel/demos/pause/carousel-pause.ts @@ -0,0 +1,34 @@ +import { Component, ViewChild } from '@angular/core'; +import { NgbCarousel, NgbSlideEvent, NgbSlideEventSource } from '@ng-bootstrap/ng-bootstrap'; + + +@Component({selector: 'ngbd-carousel-pause', templateUrl: './carousel-pause.html'}) +export class NgbdCarouselPause { + images = [1, 2, 3, 4, 5, 6, 7].map(() => `https://picsum.photos/900/500?random&t=${Math.random()}`); + + paused = false; + unpauseOnArrow = false; + pauseOnIndicator = false; + pauseOnHover = true; + + @ViewChild('carousel', {static : true}) carousel: NgbCarousel; + + togglePaused() { + if (this.paused) { + this.carousel.cycle(); + } else { + this.carousel.pause(); + } + this.paused = !this.paused; + } + + onSlide(slideEvent: NgbSlideEvent) { + if (this.unpauseOnArrow && slideEvent.paused && + (slideEvent.source === NgbSlideEventSource.ARROW_LEFT || slideEvent.source === NgbSlideEventSource.ARROW_RIGHT)) { + this.togglePaused(); + } + if (this.pauseOnIndicator && !slideEvent.paused && slideEvent.source === NgbSlideEventSource.INDICATOR) { + this.togglePaused(); + } + } +} diff --git a/demo/src/app/components/collapse/collapse.module.ts b/demo/src/app/components/collapse/collapse.module.ts new file mode 100644 index 0000000..a610207 --- /dev/null +++ b/demo/src/app/components/collapse/collapse.module.ts @@ -0,0 +1,43 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component'; +import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared'; +import { NgbdApiPage } from '../shared/api-page/api.component'; +import { NgbdExamplesPage } from '../shared/examples-page/examples.component'; +import { NgbdCollapseBasic } from './demos/basic/collapse-basic'; +import { NgbdCollapseBasicModule } from './demos/basic/collapse-basic.module'; + +const DEMOS = { + basic: { + title: 'Collapse', + type: NgbdCollapseBasic, + code: require('!!raw-loader!./demos/basic/collapse-basic'), + markup: require('!!raw-loader!./demos/basic/collapse-basic.html') + } +}; + +export const ROUTES = [ + { path: '', pathMatch: 'full', redirectTo: 'examples' }, + { + path: '', + component: ComponentWrapper, + children: [ + { path: 'examples', component: NgbdExamplesPage }, + { path: 'api', component: NgbdApiPage } + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, + NgbdComponentsSharedModule, + NgbdCollapseBasicModule + ] +}) +export class NgbdCollapseModule { + constructor(demoList: NgbdDemoList) { + demoList.register('collapse', DEMOS); + } +} diff --git a/demo/src/app/components/collapse/demos/basic/collapse-basic.html b/demo/src/app/components/collapse/demos/basic/collapse-basic.html new file mode 100644 index 0000000..3151210 --- /dev/null +++ b/demo/src/app/components/collapse/demos/basic/collapse-basic.html @@ -0,0 +1,13 @@ +

+ +

+
+
+
+ You can collapse this card by clicking Toggle +
+
+
diff --git a/demo/src/app/components/collapse/demos/basic/collapse-basic.module.ts b/demo/src/app/components/collapse/demos/basic/collapse-basic.module.ts new file mode 100644 index 0000000..e6c0838 --- /dev/null +++ b/demo/src/app/components/collapse/demos/basic/collapse-basic.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdCollapseBasic } from './collapse-basic'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdCollapseBasic], + exports: [NgbdCollapseBasic], + bootstrap: [NgbdCollapseBasic] +}) +export class NgbdCollapseBasicModule {} diff --git a/demo/src/app/components/collapse/demos/basic/collapse-basic.ts b/demo/src/app/components/collapse/demos/basic/collapse-basic.ts new file mode 100644 index 0000000..689adcf --- /dev/null +++ b/demo/src/app/components/collapse/demos/basic/collapse-basic.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ngbd-collapse-basic', + templateUrl: './collapse-basic.html' +}) +export class NgbdCollapseBasic { + public isCollapsed = false; +} diff --git a/demo/src/app/components/datepicker/calendars/datepicker-calendars.component.ts b/demo/src/app/components/datepicker/calendars/datepicker-calendars.component.ts new file mode 100644 index 0000000..07dcfb6 --- /dev/null +++ b/demo/src/app/components/datepicker/calendars/datepicker-calendars.component.ts @@ -0,0 +1,113 @@ +import {Component} from '@angular/core'; + +import {Snippet} from '../../../shared/code/snippet'; +import {NgbdExamplesPage} from '../../shared/examples-page/examples.component'; +import {NgbdDatepickerHebrew} from '../demos/hebrew/datepicker-hebrew'; +import {NgbdDatepickerHebrewModule} from '../demos/hebrew/datepicker-hebrew.module'; +import {NgbdDatepickerIslamicCivilModule} from '../demos/islamiccivil/datepicker-islamic-civil.module'; +import {NgbdDatepickerIslamiccivil} from '../demos/islamiccivil/datepicker-islamiccivil'; +import {NgbdDatepickerIslamicUmalquraModule} from '../demos/islamicumalqura/datepicker-islamic-umalqura.module'; +import {NgbdDatepickerIslamicumalqura} from '../demos/islamicumalqura/datepicker-islamicumalqura'; +import {NgbdDatepickerJalali} from '../demos/jalali/datepicker-jalali'; +import {NgbdDatepickerJalaliModule} from '../demos/jalali/datepicker-jalali.module'; + +export const DEMO_CALENDAR_MODULES = [ + NgbdDatepickerHebrewModule, + NgbdDatepickerJalaliModule, + NgbdDatepickerIslamicCivilModule, + NgbdDatepickerIslamicUmalquraModule, +]; + +const DEMOS = [ + { + id: 'hebrew', + title: 'Hebrew', + type: NgbdDatepickerHebrew, + code: require('!!raw-loader!./../demos/hebrew/datepicker-hebrew'), + markup: require('!!raw-loader!./../demos/hebrew/datepicker-hebrew.html') + }, + { + id: 'jalali', + title: 'Jalali', + type: NgbdDatepickerJalali, + code: require('!!raw-loader!./../demos/jalali/datepicker-jalali'), + markup: require('!!raw-loader!./../demos/jalali/datepicker-jalali.html') + }, + { + id: 'islamiccivil', + title: 'Islamic Civil', + type: NgbdDatepickerIslamiccivil, + code: require('!!raw-loader!./../demos/islamiccivil/datepicker-islamiccivil'), + markup: require('!!raw-loader!./../demos/islamiccivil/datepicker-islamiccivil.html') + }, + { + id: 'islamicumalqura', + title: 'Islamic Umm al-Qura', + type: NgbdDatepickerIslamicumalqura, + code: require('!!raw-loader!./../demos/islamicumalqura/datepicker-islamicumalqura'), + markup: require('!!raw-loader!./../demos/islamicumalqura/datepicker-islamicumalqura.html') + } +]; + +@Component({ + selector: 'ngbd-datepicker-calendars', + template: ` +

+ Datepicker relies on NgbCalendar abstract class for calendar-related calculations. + Default implementation is the NgbCalendarGregorian, but can be any + calendar that has notion of days, months and years. +

+ +

For instance, other calendar implementations available are:

+
    +
  • NgbCalendarHebrew + NgbDatepickerI18nHebrew
  • +
  • NgbCalendarPersian
  • +
  • NgbCalendarIslamicCivil
  • +
  • NgbCalendarIslamicUmalqura
  • +
+ + + Please note that calendar support is experimental! + We're not calendar experts and any community help is very much appreciated. + + +

+ To use any of them, simply provide a different calendar implementation. + Some calendars (like Hebrew in the example and demo below) also come with i18n support + to override the way day/week/year numerals and weekday/month names are displayed. +

+ + + +
+ +

Here are some demos of the calendars you can use

+ +
+ + + + + ` +}) +export class NgbdDatepickerCalendarsComponent extends NgbdExamplesPage { + demos = DEMOS; + + snippets = { + calendars: Snippet({ + lang: 'typescript', + code: ` + providers: [ + {provide: NgbCalendar, useClass: NgbCalendarHebrew}, + {provide: NgbDatepickerI18n, useClass: NgbDatepickerI18nHebrew} + ] + `, + }), + }; +} diff --git a/demo/src/app/components/datepicker/datepicker.module.ts b/demo/src/app/components/datepicker/datepicker.module.ts new file mode 100644 index 0000000..d10d637 --- /dev/null +++ b/demo/src/app/components/datepicker/datepicker.module.ts @@ -0,0 +1,162 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component'; +import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared'; +import { NgbdApiPage } from '../shared/api-page/api.component'; +import { NgbdExamplesPage } from '../shared/examples-page/examples.component'; +import { + DEMO_CALENDAR_MODULES, + NgbdDatepickerCalendarsComponent +} from './calendars/datepicker-calendars.component'; +import { NgbdDatepickerAdapter } from './demos/adapter/datepicker-adapter'; +import { NgbdDatepickerAdapterModule } from './demos/adapter/datepicker-adpater.module'; +import { NgbdDatepickerBasic } from './demos/basic/datepicker-basic'; +import { NgbdDatepickerBasicModule } from './demos/basic/datepicker-basic.module'; +import { NgbdDatepickerConfig } from './demos/config/datepicker-config'; +import { NgbdDatepickerConfigModule } from './demos/config/datepicker-config.module'; +import { NgbdDatepickerCustomday } from './demos/customday/datepicker-customday'; +import { NgbdDatepickerCustomdayModule } from './demos/customday/datepicker-customday.module'; +import { NgbdDatepickerDisabled } from './demos/disabled/datepicker-disabled'; +import { NgbdDatepickerDisabledModule } from './demos/disabled/datepicker-disabled.module'; +import { NgbdDatepickerFooterTemplateModule } from './demos/footertemplate/datepicker-footer-template.module'; +import { NgbdDatepickerFootertemplate } from './demos/footertemplate/datepicker-footertemplate'; +import { NgbdDatepickerI18n } from './demos/i18n/datepicker-i18n'; +import { NgbdDatepickerI18nModule } from './demos/i18n/datepicker-i18n.module'; +import { NgbdDatepickerMultiple } from './demos/multiple/datepicker-multiple'; +import { NgbdDatepickerMultipleModule } from './demos/multiple/datepicker-multiple.module'; +import { NgbdDatepickerPopup } from './demos/popup/datepicker-popup'; +import { NgbdDatepickerPopupModule } from './demos/popup/datepicker-popup.module'; +import { NgbdDatepickerRange } from './demos/range/datepicker-range'; +import { NgbdDatepickerRangeModule } from './demos/range/datepicker-range.module'; +import { NgbdDatepickerOverviewComponent } from './overview/datepicker-overview.component'; +import { NgbdDatepickerOverviewDemoComponent } from './overview/demo/datepicker-overview-demo.component'; +import { NgbdDatepickerPositiontargetModule } from './demos/positiontarget/datepicker-position-target.module'; +import { NgbdDatepickerPositiontarget } from './demos/positiontarget/datepicker-positiontarget'; + +const OVERVIEW = { + 'basic-usage': 'Basic Usage', + 'getting-date': 'Getting/setting a date', + 'date-model': 'Date model and format', + navigation: 'Moving around', + 'limiting-dates': 'Disabling and limiting dates', + 'day-template': 'Day display customization', + today: 'Today\'s date', + 'footer-template': 'Custom footer', + range: 'Range selection', + i18n: 'Internationalization', + 'keyboard-shortcuts': 'Keyboard shortcuts' +}; + +const DEMOS = { + basic: { + title: 'Basic datepicker', + type: NgbdDatepickerBasic, + code: require('!!raw-loader!./demos/basic/datepicker-basic'), + markup: require('!!raw-loader!./demos/basic/datepicker-basic.html') + }, + popup: { + title: 'Datepicker in a popup', + type: NgbdDatepickerPopup, + code: require('!!raw-loader!./demos/popup/datepicker-popup'), + markup: require('!!raw-loader!./demos/popup/datepicker-popup.html') + }, + multiple: { + title: 'Multiple months', + type: NgbdDatepickerMultiple, + code: require('!!raw-loader!./demos/multiple/datepicker-multiple'), + markup: require('!!raw-loader!./demos/multiple/datepicker-multiple.html') + }, + range: { + title: 'Range selection', + type: NgbdDatepickerRange, + code: require('!!raw-loader!./demos/range/datepicker-range'), + markup: require('!!raw-loader!./demos/range/datepicker-range.html') + }, + disabled: { + title: 'Disabled datepicker', + type: NgbdDatepickerDisabled, + code: require('!!raw-loader!./demos/disabled/datepicker-disabled'), + markup: require('!!raw-loader!./demos/disabled/datepicker-disabled.html') + }, + adapter: { + title: 'Custom date adapter', + type: NgbdDatepickerAdapter, + code: require('!!raw-loader!./demos/adapter/datepicker-adapter'), + markup: require('!!raw-loader!./demos/adapter/datepicker-adapter.html') + }, + i18n: { + title: 'Internationalization of datepickers', + type: NgbdDatepickerI18n, + code: require('!!raw-loader!./demos/i18n/datepicker-i18n'), + markup: require('!!raw-loader!./demos/i18n/datepicker-i18n.html') + }, + customday: { + title: 'Custom day view', + type: NgbdDatepickerCustomday, + code: require('!!raw-loader!./demos/customday/datepicker-customday'), + markup: require('!!raw-loader!./demos/customday/datepicker-customday.html') + }, + footertemplate: { + title: 'Footer template', + type: NgbdDatepickerFootertemplate, + code: require('!!raw-loader!./demos/footertemplate/datepicker-footertemplate'), + markup: require('!!raw-loader!./demos/footertemplate/datepicker-footertemplate.html') + }, + positiontarget: { + title: 'Position target', + type: NgbdDatepickerPositiontarget, + code: require('!!raw-loader!./demos/positiontarget/datepicker-positiontarget'), + markup: require('!!raw-loader!./demos/positiontarget/datepicker-positiontarget.html') + }, + config: { + title: 'Global configuration of datepickers', + type: NgbdDatepickerConfig, + code: require('!!raw-loader!./demos/config/datepicker-config'), + markup: require('!!raw-loader!./demos/config/datepicker-config.html') + } +}; + +export const ROUTES = [ + { path: '', pathMatch: 'full', redirectTo: 'overview' }, + { + path: '', + component: ComponentWrapper, + data: { OVERVIEW }, + children: [ + { path: 'overview', component: NgbdDatepickerOverviewComponent }, + { path: 'examples', component: NgbdExamplesPage }, + { path: 'calendars', component: NgbdDatepickerCalendarsComponent }, + { path: 'api', component: NgbdApiPage } + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, + NgbdComponentsSharedModule, + NgbdDatepickerBasicModule, + NgbdDatepickerPopupModule, + NgbdDatepickerDisabledModule, + NgbdDatepickerI18nModule, + NgbdDatepickerCustomdayModule, + NgbdDatepickerFooterTemplateModule, + NgbdDatepickerConfigModule, + NgbdDatepickerPositiontargetModule, + NgbdDatepickerMultipleModule, + NgbdDatepickerRangeModule, + NgbdDatepickerAdapterModule, + ...DEMO_CALENDAR_MODULES + ], + declarations: [ + NgbdDatepickerCalendarsComponent, + NgbdDatepickerOverviewComponent, + NgbdDatepickerOverviewDemoComponent + ] +}) +export class NgbdDatepickerModule { + constructor(demoList: NgbdDemoList) { + demoList.register('datepicker', DEMOS, OVERVIEW); + } +} diff --git a/demo/src/app/components/datepicker/demos/adapter/datepicker-adapter.html b/demo/src/app/components/datepicker/demos/adapter/datepicker-adapter.html new file mode 100644 index 0000000..d9d9c90 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/adapter/datepicker-adapter.html @@ -0,0 +1,38 @@ +

These datepickers use custom Date adapter that lets you use your own model implementation. +In this example we are converting from and to a JS native Date object

+ +
+
+ + +
+ +
+ +
Model: {{ model1 | json }}
+
State: {{ c1.status }}
+
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+ + +
Model: {{ model2 | json }}
+
State: {{ c2.status }}
+
+ +
diff --git a/demo/src/app/components/datepicker/demos/adapter/datepicker-adapter.ts b/demo/src/app/components/datepicker/demos/adapter/datepicker-adapter.ts new file mode 100644 index 0000000..700301f --- /dev/null +++ b/demo/src/app/components/datepicker/demos/adapter/datepicker-adapter.ts @@ -0,0 +1,20 @@ +import {Component, Injectable} from '@angular/core'; +import {NgbDateAdapter, NgbDateStruct, NgbDateNativeAdapter} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-datepicker-adapter', + templateUrl: './datepicker-adapter.html', + + // NOTE: For this example we are only providing current component, but probably + // NOTE: you will want to provide your main App Module + providers: [{provide: NgbDateAdapter, useClass: NgbDateNativeAdapter}] +}) +export class NgbdDatepickerAdapter { + + model1: Date; + model2: Date; + + get today() { + return new Date(); + } +} diff --git a/demo/src/app/components/datepicker/demos/adapter/datepicker-adpater.module.ts b/demo/src/app/components/datepicker/demos/adapter/datepicker-adpater.module.ts new file mode 100644 index 0000000..3ff4a4d --- /dev/null +++ b/demo/src/app/components/datepicker/demos/adapter/datepicker-adpater.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDatepickerAdapter } from './datepicker-adapter'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdDatepickerAdapter], + exports: [NgbdDatepickerAdapter], + bootstrap: [NgbdDatepickerAdapter] +}) +export class NgbdDatepickerAdapterModule {} diff --git a/demo/src/app/components/datepicker/demos/basic/datepicker-basic.html b/demo/src/app/components/datepicker/demos/basic/datepicker-basic.html new file mode 100644 index 0000000..32cdd95 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/basic/datepicker-basic.html @@ -0,0 +1,14 @@ +

Simple datepicker

+ + + +
+ + + + + +
+ +
Month: {{ date.month }}.{{ date.year }}
+
Model: {{ model | json }}
diff --git a/demo/src/app/components/datepicker/demos/basic/datepicker-basic.module.ts b/demo/src/app/components/datepicker/demos/basic/datepicker-basic.module.ts new file mode 100644 index 0000000..749f150 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/basic/datepicker-basic.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDatepickerBasic } from './datepicker-basic'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdDatepickerBasic], + exports: [NgbdDatepickerBasic], + bootstrap: [NgbdDatepickerBasic] +}) +export class NgbdDatepickerBasicModule {} diff --git a/demo/src/app/components/datepicker/demos/basic/datepicker-basic.ts b/demo/src/app/components/datepicker/demos/basic/datepicker-basic.ts new file mode 100644 index 0000000..819abd5 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/basic/datepicker-basic.ts @@ -0,0 +1,19 @@ +import {Component} from '@angular/core'; +import {NgbDateStruct, NgbCalendar} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-datepicker-basic', + templateUrl: './datepicker-basic.html' +}) +export class NgbdDatepickerBasic { + + model: NgbDateStruct; + date: {year: number, month: number}; + + constructor(private calendar: NgbCalendar) { + } + + selectToday() { + this.model = this.calendar.getToday(); + } +} diff --git a/demo/src/app/components/datepicker/demos/config/datepicker-config.html b/demo/src/app/components/datepicker/demos/config/datepicker-config.html new file mode 100644 index 0000000..83d2f30 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/config/datepicker-config.html @@ -0,0 +1,3 @@ +

This datepicker uses customized default values.

+ + diff --git a/demo/src/app/components/datepicker/demos/config/datepicker-config.module.ts b/demo/src/app/components/datepicker/demos/config/datepicker-config.module.ts new file mode 100644 index 0000000..c152fb2 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/config/datepicker-config.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDatepickerConfig } from './datepicker-config'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdDatepickerConfig], + exports: [NgbdDatepickerConfig], + bootstrap: [NgbdDatepickerConfig] +}) +export class NgbdDatepickerConfigModule {} diff --git a/demo/src/app/components/datepicker/demos/config/datepicker-config.ts b/demo/src/app/components/datepicker/demos/config/datepicker-config.ts new file mode 100644 index 0000000..d03851b --- /dev/null +++ b/demo/src/app/components/datepicker/demos/config/datepicker-config.ts @@ -0,0 +1,24 @@ +import {Component} from '@angular/core'; +import {NgbDatepickerConfig, NgbCalendar, NgbDate, NgbDateStruct} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-datepicker-config', + templateUrl: './datepicker-config.html', + providers: [NgbDatepickerConfig] // add NgbDatepickerConfig to the component providers +}) +export class NgbdDatepickerConfig { + + model: NgbDateStruct; + + constructor(config: NgbDatepickerConfig, calendar: NgbCalendar) { + // customize default values of datepickers used by this component tree + config.minDate = {year: 1900, month: 1, day: 1}; + config.maxDate = {year: 2099, month: 12, day: 31}; + + // days that don't belong to current month are not visible + config.outsideDays = 'hidden'; + + // weekends are disabled + config.markDisabled = (date: NgbDate) => calendar.getWeekday(date) >= 6; + } +} diff --git a/demo/src/app/components/datepicker/demos/customday/datepicker-customday.html b/demo/src/app/components/datepicker/demos/customday/datepicker-customday.html new file mode 100644 index 0000000..63cc314 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/customday/datepicker-customday.html @@ -0,0 +1,20 @@ +

This datepicker uses a custom template to display days. All week-ends are displayed with an orange background.

+ +
+
+
+ +
+ +
+
+
+
+ + + + {{ date.day }} + + diff --git a/demo/src/app/components/datepicker/demos/customday/datepicker-customday.module.ts b/demo/src/app/components/datepicker/demos/customday/datepicker-customday.module.ts new file mode 100644 index 0000000..4799b6b --- /dev/null +++ b/demo/src/app/components/datepicker/demos/customday/datepicker-customday.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDatepickerCustomday } from './datepicker-customday'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdDatepickerCustomday], + exports: [NgbdDatepickerCustomday], + bootstrap: [NgbdDatepickerCustomday] +}) +export class NgbdDatepickerCustomdayModule {} diff --git a/demo/src/app/components/datepicker/demos/customday/datepicker-customday.ts b/demo/src/app/components/datepicker/demos/customday/datepicker-customday.ts new file mode 100644 index 0000000..eee057e --- /dev/null +++ b/demo/src/app/components/datepicker/demos/customday/datepicker-customday.ts @@ -0,0 +1,36 @@ +import {Component} from '@angular/core'; +import {NgbCalendar, NgbDate, NgbDateStruct} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-datepicker-customday', + templateUrl: './datepicker-customday.html', + styles: [` + .custom-day { + text-align: center; + padding: 0.185rem 0.25rem; + border-radius: 0.25rem; + display: inline-block; + width: 2rem; + } + .custom-day:hover, .custom-day.focused { + background-color: #e6e6e6; + } + .weekend { + background-color: #f0ad4e; + border-radius: 1rem; + color: white; + } + .hidden { + display: none; + } + `] +}) +export class NgbdDatepickerCustomday { + model: NgbDateStruct; + + constructor(private calendar: NgbCalendar) { + } + + isDisabled = (date: NgbDate, current: {month: number}) => date.month !== current.month; + isWeekend = (date: NgbDate) => this.calendar.getWeekday(date) >= 6; +} diff --git a/demo/src/app/components/datepicker/demos/disabled/datepicker-disabled.html b/demo/src/app/components/datepicker/demos/disabled/datepicker-disabled.html new file mode 100644 index 0000000..819e0f9 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/disabled/datepicker-disabled.html @@ -0,0 +1,9 @@ +

Disabled datepicker

+ + + +
+ + diff --git a/demo/src/app/components/datepicker/demos/disabled/datepicker-disabled.module.ts b/demo/src/app/components/datepicker/demos/disabled/datepicker-disabled.module.ts new file mode 100644 index 0000000..4440be0 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/disabled/datepicker-disabled.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDatepickerDisabled } from './datepicker-disabled'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdDatepickerDisabled], + exports: [NgbdDatepickerDisabled], + bootstrap: [NgbdDatepickerDisabled] +}) +export class NgbdDatepickerDisabledModule {} diff --git a/demo/src/app/components/datepicker/demos/disabled/datepicker-disabled.ts b/demo/src/app/components/datepicker/demos/disabled/datepicker-disabled.ts new file mode 100644 index 0000000..e0aecf5 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/disabled/datepicker-disabled.ts @@ -0,0 +1,16 @@ +import {Component} from '@angular/core'; +import {NgbCalendar, NgbDateStruct} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-datepicker-disabled', + templateUrl: './datepicker-disabled.html' +}) +export class NgbdDatepickerDisabled { + + model: NgbDateStruct; + disabled = true; + + constructor(calendar: NgbCalendar) { + this.model = calendar.getToday(); + } +} diff --git a/demo/src/app/components/datepicker/demos/footertemplate/datepicker-footer-template.module.ts b/demo/src/app/components/datepicker/demos/footertemplate/datepicker-footer-template.module.ts new file mode 100644 index 0000000..a39a992 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/footertemplate/datepicker-footer-template.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDatepickerFootertemplate } from './datepicker-footertemplate'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdDatepickerFootertemplate], + exports: [NgbdDatepickerFootertemplate], + bootstrap: [NgbdDatepickerFootertemplate] +}) +export class NgbdDatepickerFooterTemplateModule {} diff --git a/demo/src/app/components/datepicker/demos/footertemplate/datepicker-footertemplate.html b/demo/src/app/components/datepicker/demos/footertemplate/datepicker-footertemplate.html new file mode 100644 index 0000000..d9dd0a5 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/footertemplate/datepicker-footertemplate.html @@ -0,0 +1,19 @@ +

This datepicker uses a footer template which is presented inside datepicker. Today and close buttons used as an example.

+ +
+
+
+ +
+ +
+
+
+
+ + +
+ + +
diff --git a/demo/src/app/components/datepicker/demos/footertemplate/datepicker-footertemplate.ts b/demo/src/app/components/datepicker/demos/footertemplate/datepicker-footertemplate.ts new file mode 100644 index 0000000..c788409 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/footertemplate/datepicker-footertemplate.ts @@ -0,0 +1,13 @@ +import {Component} from '@angular/core'; +import {NgbCalendar, NgbDateStruct} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-datepicker-footertemplate', + templateUrl: './datepicker-footertemplate.html', +}) +export class NgbdDatepickerFootertemplate { + model: NgbDateStruct; + today = this.calendar.getToday(); + + constructor(private calendar: NgbCalendar) {} +} diff --git a/demo/src/app/components/datepicker/demos/hebrew/datepicker-hebrew.html b/demo/src/app/components/datepicker/demos/hebrew/datepicker-hebrew.html new file mode 100644 index 0000000..6e78097 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/hebrew/datepicker-hebrew.html @@ -0,0 +1,24 @@ +

+ The Hebrew or Jewish calendar is a lunisolar calendar. + In Israel it is used for religious purposes and frequently as an official calendar for civil purposes. +

+ + + + + +
+
{{ data.gregorian.day + '/' + (data.gregorian.month) }}
+
{{ i18n.getDayNumerals(date) }}
+
+
+ +
+ + + + +
+ +
Model: {{ model | json }}
diff --git a/demo/src/app/components/datepicker/demos/hebrew/datepicker-hebrew.module.ts b/demo/src/app/components/datepicker/demos/hebrew/datepicker-hebrew.module.ts new file mode 100644 index 0000000..99c41f4 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/hebrew/datepicker-hebrew.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDatepickerHebrew } from './datepicker-hebrew'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdDatepickerHebrew], + exports: [NgbdDatepickerHebrew], + bootstrap: [NgbdDatepickerHebrew] +}) +export class NgbdDatepickerHebrewModule {} diff --git a/demo/src/app/components/datepicker/demos/hebrew/datepicker-hebrew.ts b/demo/src/app/components/datepicker/demos/hebrew/datepicker-hebrew.ts new file mode 100644 index 0000000..be2683e --- /dev/null +++ b/demo/src/app/components/datepicker/demos/hebrew/datepicker-hebrew.ts @@ -0,0 +1,59 @@ +import {Component} from '@angular/core'; +import { + NgbCalendar, + NgbCalendarHebrew, NgbDate, + NgbDatepickerI18n, + NgbDatepickerI18nHebrew, + NgbDateStruct +} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-datepicker-hebrew', + templateUrl: './datepicker-hebrew.html', + styles: [` + .hebrew-day { + text-align: right; + padding: 0.25rem 0.65rem 0.25rem 0.25rem; + border-radius: 0.25rem; + display: inline-block; + height: 2.75rem; + width: 2.75rem; + } + .hebrew-day:hover, .hebrew-day.focused { + background-color: #e6e6e6; + } + .hebrew-day.selected { + background-color: #007bff; + color: white; + } + .outside { + color: lightgray; + } + .gregorian-num { + font-size: 0.5rem; + direction: ltr; + } + `], + providers: [ + {provide: NgbCalendar, useClass: NgbCalendarHebrew}, + {provide: NgbDatepickerI18n, useClass: NgbDatepickerI18nHebrew} + ] +}) +export class NgbdDatepickerHebrew { + + model: NgbDateStruct; + + constructor(private calendar: NgbCalendar, public i18n: NgbDatepickerI18n) { + this.dayTemplateData = this.dayTemplateData.bind(this); + } + + dayTemplateData(date: NgbDate) { + return { + gregorian: (this.calendar as NgbCalendarHebrew).toGregorian(date) + }; + } + + selectToday() { + this.model = this.calendar.getToday(); + } +} diff --git a/demo/src/app/components/datepicker/demos/i18n/datepicker-i18n.html b/demo/src/app/components/datepicker/demos/i18n/datepicker-i18n.html new file mode 100644 index 0000000..0634abf --- /dev/null +++ b/demo/src/app/components/datepicker/demos/i18n/datepicker-i18n.html @@ -0,0 +1,12 @@ + + If you configure the locale and register the locale data as explained in the + i18n guide, the date picker will honor + the locale and use days and months names from the locale data. You can however + provide a custom service, as demonstrated in this example, to customize the + days and months names the way you want to. + + +

Datepicker in French

+ + + diff --git a/demo/src/app/components/datepicker/demos/i18n/datepicker-i18n.module.ts b/demo/src/app/components/datepicker/demos/i18n/datepicker-i18n.module.ts new file mode 100644 index 0000000..a0eedaa --- /dev/null +++ b/demo/src/app/components/datepicker/demos/i18n/datepicker-i18n.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDatepickerI18n } from './datepicker-i18n'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdDatepickerI18n], + exports: [NgbdDatepickerI18n], + bootstrap: [NgbdDatepickerI18n] +}) +export class NgbdDatepickerI18nModule {} diff --git a/demo/src/app/components/datepicker/demos/i18n/datepicker-i18n.ts b/demo/src/app/components/datepicker/demos/i18n/datepicker-i18n.ts new file mode 100644 index 0000000..e7da10c --- /dev/null +++ b/demo/src/app/components/datepicker/demos/i18n/datepicker-i18n.ts @@ -0,0 +1,49 @@ +import {Component, Injectable} from '@angular/core'; +import {NgbDatepickerI18n, NgbDateStruct} from '@ng-bootstrap/ng-bootstrap'; + +const I18N_VALUES = { + 'fr': { + weekdays: ['Lu', 'Ma', 'Me', 'Je', 'Ve', 'Sa', 'Di'], + months: ['Jan', 'Fév', 'Mar', 'Avr', 'Mai', 'Juin', 'Juil', 'Aou', 'Sep', 'Oct', 'Nov', 'Déc'], + } + // other languages you would support +}; + +// Define a service holding the language. You probably already have one if your app is i18ned. Or you could also +// use the Angular LOCALE_ID value +@Injectable() +export class I18n { + language = 'fr'; +} + +// Define custom service providing the months and weekdays translations +@Injectable() +export class CustomDatepickerI18n extends NgbDatepickerI18n { + + constructor(private _i18n: I18n) { + super(); + } + + getWeekdayShortName(weekday: number): string { + return I18N_VALUES[this._i18n.language].weekdays[weekday - 1]; + } + getMonthShortName(month: number): string { + return I18N_VALUES[this._i18n.language].months[month - 1]; + } + getMonthFullName(month: number): string { + return this.getMonthShortName(month); + } + + getDayAriaLabel(date: NgbDateStruct): string { + return `${date.day}-${date.month}-${date.year}`; + } +} + +@Component({ + selector: 'ngbd-datepicker-i18n', + templateUrl: './datepicker-i18n.html', + providers: [I18n, {provide: NgbDatepickerI18n, useClass: CustomDatepickerI18n}] // define custom NgbDatepickerI18n provider +}) +export class NgbdDatepickerI18n { + model: NgbDateStruct; +} diff --git a/demo/src/app/components/datepicker/demos/islamiccivil/datepicker-islamic-civil.module.ts b/demo/src/app/components/datepicker/demos/islamiccivil/datepicker-islamic-civil.module.ts new file mode 100644 index 0000000..0b298bf --- /dev/null +++ b/demo/src/app/components/datepicker/demos/islamiccivil/datepicker-islamic-civil.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDatepickerIslamiccivil } from './datepicker-islamiccivil'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdDatepickerIslamiccivil], + exports: [NgbdDatepickerIslamiccivil], + bootstrap: [NgbdDatepickerIslamiccivil] +}) +export class NgbdDatepickerIslamicCivilModule {} diff --git a/demo/src/app/components/datepicker/demos/islamiccivil/datepicker-islamiccivil.html b/demo/src/app/components/datepicker/demos/islamiccivil/datepicker-islamiccivil.html new file mode 100644 index 0000000..55181f5 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/islamiccivil/datepicker-islamiccivil.html @@ -0,0 +1,17 @@ +

+ The civil calendar is a type of Hijri calendars used in islamic countries. + It uses a fixed cycle of alternating 29- and 30-day months, with a leap day added to the last month of 11 out of every 30 years +

+ + + + +
+ + + + + +
+ +
Model: {{ model | json }}
diff --git a/demo/src/app/components/datepicker/demos/islamiccivil/datepicker-islamiccivil.ts b/demo/src/app/components/datepicker/demos/islamiccivil/datepicker-islamiccivil.ts new file mode 100644 index 0000000..99dc9ed --- /dev/null +++ b/demo/src/app/components/datepicker/demos/islamiccivil/datepicker-islamiccivil.ts @@ -0,0 +1,47 @@ +import {Component, Injectable} from '@angular/core'; +import { + NgbDateStruct, NgbCalendar, NgbCalendarIslamicCivil, NgbDatepickerI18n +} from '@ng-bootstrap/ng-bootstrap'; + +const WEEKDAYS = ['ن', 'ث', 'ر', 'خ', 'ج', 'س', 'ح']; +const MONTHS = ['محرم', 'صفر', 'ربيع الأول', 'ربيع الآخر', 'جمادى الأولى', 'جمادى الآخرة', 'رجب', 'شعبان', 'رمضان', 'شوال', + 'ذو القعدة', 'ذو الحجة']; + +@Injectable() +export class IslamicI18n extends NgbDatepickerI18n { + + getWeekdayShortName(weekday: number) { + return WEEKDAYS[weekday - 1]; + } + + getMonthShortName(month: number) { + return MONTHS[month - 1]; + } + + getMonthFullName(month: number) { + return MONTHS[month - 1]; + } + + getDayAriaLabel(date: NgbDateStruct): string { + return `${date.day}-${date.month}-${date.year}`; + } +} + +@Component({ + selector: 'ngbd-datepicker-islamiccivil', + templateUrl: './datepicker-islamiccivil.html', + providers: [ + {provide: NgbCalendar, useClass: NgbCalendarIslamicCivil}, + {provide: NgbDatepickerI18n, useClass: IslamicI18n} + ] +}) +export class NgbdDatepickerIslamiccivil { + + model: NgbDateStruct; + + constructor(private calendar: NgbCalendar) {} + + selectToday() { + this.model = this.calendar.getToday(); + } +} diff --git a/demo/src/app/components/datepicker/demos/islamicumalqura/datepicker-islamic-umalqura.module.ts b/demo/src/app/components/datepicker/demos/islamicumalqura/datepicker-islamic-umalqura.module.ts new file mode 100644 index 0000000..8576979 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/islamicumalqura/datepicker-islamic-umalqura.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDatepickerIslamicumalqura } from './datepicker-islamicumalqura'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdDatepickerIslamicumalqura], + exports: [NgbdDatepickerIslamicumalqura], + bootstrap: [NgbdDatepickerIslamicumalqura] +}) +export class NgbdDatepickerIslamicUmalquraModule {} diff --git a/demo/src/app/components/datepicker/demos/islamicumalqura/datepicker-islamicumalqura.html b/demo/src/app/components/datepicker/demos/islamicumalqura/datepicker-islamicumalqura.html new file mode 100644 index 0000000..b6b4ee4 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/islamicumalqura/datepicker-islamicumalqura.html @@ -0,0 +1,17 @@ +

+ Umm al-Qura calendar is a type of Hijri calendars based on astronomical calculations and used + in Saudi Arabia for administrative purposes +

+ + + + +
+ + + + + +
+ +
Model: {{ model | json }}
diff --git a/demo/src/app/components/datepicker/demos/islamicumalqura/datepicker-islamicumalqura.ts b/demo/src/app/components/datepicker/demos/islamicumalqura/datepicker-islamicumalqura.ts new file mode 100644 index 0000000..77bde17 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/islamicumalqura/datepicker-islamicumalqura.ts @@ -0,0 +1,47 @@ +import { Component, Injectable } from '@angular/core'; +import { + NgbDateStruct, NgbCalendar, NgbCalendarIslamicUmalqura, NgbDatepickerI18n +} from '@ng-bootstrap/ng-bootstrap'; + +const WEEKDAYS = ['ن', 'ث', 'ر', 'خ', 'ج', 'س', 'ح']; +const MONTHS = ['محرم', 'صفر', 'ربيع الأول', 'ربيع الآخر', 'جمادى الأولى', 'جمادى الآخرة', 'رجب', 'شعبان', 'رمضان', 'شوال', + 'ذو القعدة', 'ذو الحجة']; + +@Injectable() +export class IslamicI18n extends NgbDatepickerI18n { + + getWeekdayShortName(weekday: number) { + return WEEKDAYS[weekday - 1]; + } + + getMonthShortName(month: number) { + return MONTHS[month - 1]; + } + + getMonthFullName(month: number) { + return MONTHS[month - 1]; + } + + getDayAriaLabel(date: NgbDateStruct): string { + return `${date.day}-${date.month}-${date.year}`; + } +} + +@Component({ + selector: 'ngbd-datepicker-islamicumalqura', + templateUrl: './datepicker-islamicumalqura.html', + providers: [ + {provide: NgbCalendar, useClass: NgbCalendarIslamicUmalqura}, + {provide: NgbDatepickerI18n, useClass: IslamicI18n} + ] +}) +export class NgbdDatepickerIslamicumalqura { + + model: NgbDateStruct; + + constructor(private calendar: NgbCalendar) {} + + selectToday() { + this.model = this.calendar.getToday(); + } +} diff --git a/demo/src/app/components/datepicker/demos/jalali/datepicker-jalali.html b/demo/src/app/components/datepicker/demos/jalali/datepicker-jalali.html new file mode 100644 index 0000000..6fbd882 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/jalali/datepicker-jalali.html @@ -0,0 +1,16 @@ +

+ The Jalali calendar is a solar calendar that was used in Persia. + Variants of it are still in use in Iran and Afghanistan +

+ + + +
+ + + + + +
+ +
Model: {{ model | json }}
diff --git a/demo/src/app/components/datepicker/demos/jalali/datepicker-jalali.module.ts b/demo/src/app/components/datepicker/demos/jalali/datepicker-jalali.module.ts new file mode 100644 index 0000000..c55b5a3 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/jalali/datepicker-jalali.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDatepickerJalali } from './datepicker-jalali'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdDatepickerJalali], + exports: [NgbdDatepickerJalali], + bootstrap: [NgbdDatepickerJalali] +}) +export class NgbdDatepickerJalaliModule {} diff --git a/demo/src/app/components/datepicker/demos/jalali/datepicker-jalali.ts b/demo/src/app/components/datepicker/demos/jalali/datepicker-jalali.ts new file mode 100644 index 0000000..4c6bfc3 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/jalali/datepicker-jalali.ts @@ -0,0 +1,34 @@ +import {Component, Injectable} from '@angular/core'; +import {NgbDateStruct, NgbCalendar, NgbDatepickerI18n, NgbCalendarPersian} from '@ng-bootstrap/ng-bootstrap'; + +const WEEKDAYS_SHORT = ['د', 'س', 'چ', 'پ', 'ج', 'ش', 'ی']; +const MONTHS = ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند']; + +@Injectable() +export class NgbDatepickerI18nPersian extends NgbDatepickerI18n { + getWeekdayShortName(weekday: number) { return WEEKDAYS_SHORT[weekday - 1]; } + getMonthShortName(month: number) { return MONTHS[month - 1]; } + getMonthFullName(month: number) { return MONTHS[month - 1]; } + getDayAriaLabel(date: NgbDateStruct): string { return `${date.year}-${this.getMonthFullName(date.month)}-${date.day}`; } +} + +@Component({ + selector: 'ngbd-datepicker-jalali', + templateUrl: './datepicker-jalali.html', + providers: [ + {provide: NgbCalendar, useClass: NgbCalendarPersian}, + {provide: NgbDatepickerI18n, useClass: NgbDatepickerI18nPersian} + ] +}) +export class NgbdDatepickerJalali { + + model: NgbDateStruct; + date: {year: number, month: number}; + + constructor(private calendar: NgbCalendar) { + } + + selectToday() { + this.model = this.calendar.getToday(); + } +} diff --git a/demo/src/app/components/datepicker/demos/multiple/datepicker-multiple.html b/demo/src/app/components/datepicker/demos/multiple/datepicker-multiple.html new file mode 100644 index 0000000..870e375 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/multiple/datepicker-multiple.html @@ -0,0 +1,47 @@ + + + + +
+ +
+
+
+ +
+ +
+
+
+
+ +
+ +
+ + + + + + + +
+ diff --git a/demo/src/app/components/datepicker/demos/multiple/datepicker-multiple.module.ts b/demo/src/app/components/datepicker/demos/multiple/datepicker-multiple.module.ts new file mode 100644 index 0000000..32e93f7 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/multiple/datepicker-multiple.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDatepickerMultiple } from './datepicker-multiple'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdDatepickerMultiple], + exports: [NgbdDatepickerMultiple], + bootstrap: [NgbdDatepickerMultiple] +}) +export class NgbdDatepickerMultipleModule {} diff --git a/demo/src/app/components/datepicker/demos/multiple/datepicker-multiple.ts b/demo/src/app/components/datepicker/demos/multiple/datepicker-multiple.ts new file mode 100644 index 0000000..6458ee8 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/multiple/datepicker-multiple.ts @@ -0,0 +1,19 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-datepicker-multiple', + templateUrl: './datepicker-multiple.html', + styles: [` + select.custom-select { + margin: 0.5rem 0.5rem 0 0; + width: auto; + } + `] +}) +export class NgbdDatepickerMultiple { + + displayMonths = 2; + navigation = 'select'; + showWeekNumbers = false; + outsideDays = 'visible'; +} diff --git a/demo/src/app/components/datepicker/demos/popup/datepicker-popup.html b/demo/src/app/components/datepicker/demos/popup/datepicker-popup.html new file mode 100644 index 0000000..ae965bb --- /dev/null +++ b/demo/src/app/components/datepicker/demos/popup/datepicker-popup.html @@ -0,0 +1,14 @@ +
+
+
+ +
+ +
+
+
+
+ +
+
Model: {{ model | json }}
diff --git a/demo/src/app/components/datepicker/demos/popup/datepicker-popup.module.ts b/demo/src/app/components/datepicker/demos/popup/datepicker-popup.module.ts new file mode 100644 index 0000000..314e75f --- /dev/null +++ b/demo/src/app/components/datepicker/demos/popup/datepicker-popup.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDatepickerPopup } from './datepicker-popup'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdDatepickerPopup], + exports: [NgbdDatepickerPopup], + bootstrap: [NgbdDatepickerPopup] +}) +export class NgbdDatepickerPopupModule {} diff --git a/demo/src/app/components/datepicker/demos/popup/datepicker-popup.ts b/demo/src/app/components/datepicker/demos/popup/datepicker-popup.ts new file mode 100644 index 0000000..a98a470 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/popup/datepicker-popup.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-datepicker-popup', + templateUrl: './datepicker-popup.html' +}) +export class NgbdDatepickerPopup { + model; +} diff --git a/demo/src/app/components/datepicker/demos/positiontarget/datepicker-position-target.module.ts b/demo/src/app/components/datepicker/demos/positiontarget/datepicker-position-target.module.ts new file mode 100644 index 0000000..0b7ebea --- /dev/null +++ b/demo/src/app/components/datepicker/demos/positiontarget/datepicker-position-target.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDatepickerPositiontarget } from './datepicker-positiontarget'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdDatepickerPositiontarget], + exports: [NgbdDatepickerPositiontarget], + bootstrap: [NgbdDatepickerPositiontarget] +}) +export class NgbdDatepickerPositiontargetModule {} diff --git a/demo/src/app/components/datepicker/demos/positiontarget/datepicker-positiontarget.html b/demo/src/app/components/datepicker/demos/positiontarget/datepicker-positiontarget.html new file mode 100644 index 0000000..c205839 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/positiontarget/datepicker-positiontarget.html @@ -0,0 +1,28 @@ +

This datepicker uses a custom position target and placement. Popup is positioned according to the toggle button instead of input which is default.

+ +
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+ + +
+
\ No newline at end of file diff --git a/demo/src/app/components/datepicker/demos/positiontarget/datepicker-positiontarget.ts b/demo/src/app/components/datepicker/demos/positiontarget/datepicker-positiontarget.ts new file mode 100644 index 0000000..502716b --- /dev/null +++ b/demo/src/app/components/datepicker/demos/positiontarget/datepicker-positiontarget.ts @@ -0,0 +1,11 @@ +import {Component} from '@angular/core'; +import {NgbDateStruct} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-datepicker-positiontarget', + templateUrl: './datepicker-positiontarget.html', +}) +export class NgbdDatepickerPositiontarget { + model: NgbDateStruct; + placement = 'bottom'; +} diff --git a/demo/src/app/components/datepicker/demos/range/datepicker-range.html b/demo/src/app/components/datepicker/demos/range/datepicker-range.html new file mode 100644 index 0000000..8705210 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/range/datepicker-range.html @@ -0,0 +1,20 @@ +

Example of the range selection

+ + + + + + + {{ date.day }} + + + +
+ +
From: {{ fromDate | json }} 
+
To: {{ toDate | json }} 
diff --git a/demo/src/app/components/datepicker/demos/range/datepicker-range.module.ts b/demo/src/app/components/datepicker/demos/range/datepicker-range.module.ts new file mode 100644 index 0000000..de1bcd1 --- /dev/null +++ b/demo/src/app/components/datepicker/demos/range/datepicker-range.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDatepickerRange } from './datepicker-range'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdDatepickerRange], + exports: [NgbdDatepickerRange], + bootstrap: [NgbdDatepickerRange] +}) +export class NgbdDatepickerRangeModule {} diff --git a/demo/src/app/components/datepicker/demos/range/datepicker-range.ts b/demo/src/app/components/datepicker/demos/range/datepicker-range.ts new file mode 100644 index 0000000..4c21f0a --- /dev/null +++ b/demo/src/app/components/datepicker/demos/range/datepicker-range.ts @@ -0,0 +1,61 @@ +import {Component} from '@angular/core'; +import {NgbDate, NgbCalendar} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-datepicker-range', + templateUrl: './datepicker-range.html', + styles: [` + .custom-day { + text-align: center; + padding: 0.185rem 0.25rem; + display: inline-block; + height: 2rem; + width: 2rem; + } + .custom-day.focused { + background-color: #e6e6e6; + } + .custom-day.range, .custom-day:hover { + background-color: rgb(2, 117, 216); + color: white; + } + .custom-day.faded { + background-color: rgba(2, 117, 216, 0.5); + } + `] +}) +export class NgbdDatepickerRange { + + hoveredDate: NgbDate; + + fromDate: NgbDate; + toDate: NgbDate; + + constructor(calendar: NgbCalendar) { + this.fromDate = calendar.getToday(); + this.toDate = calendar.getNext(calendar.getToday(), 'd', 10); + } + + onDateSelection(date: NgbDate) { + if (!this.fromDate && !this.toDate) { + this.fromDate = date; + } else if (this.fromDate && !this.toDate && date.after(this.fromDate)) { + this.toDate = date; + } else { + this.toDate = null; + this.fromDate = date; + } + } + + isHovered(date: NgbDate) { + return this.fromDate && !this.toDate && this.hoveredDate && date.after(this.fromDate) && date.before(this.hoveredDate); + } + + isInside(date: NgbDate) { + return date.after(this.fromDate) && date.before(this.toDate); + } + + isRange(date: NgbDate) { + return date.equals(this.fromDate) || date.equals(this.toDate) || this.isInside(date) || this.isHovered(date); + } +} diff --git a/demo/src/app/components/datepicker/overview/datepicker-overview.component.html b/demo/src/app/components/datepicker/overview/datepicker-overview.component.html new file mode 100644 index 0000000..258d66d --- /dev/null +++ b/demo/src/app/components/datepicker/overview/datepicker-overview.component.html @@ -0,0 +1,371 @@ +

+ Datepicker will help you with date selection. + It can be used either inline with NgbDatepicker component or as a + popup on any input element with NgbInputDatepicker directive. + It also comes with the list of services to do date formatting, i18n and + alternative calendars support. +

+

+ We try to keep API of our components simple, but introduce extension points, + so you could enrich and reuse them. + + Here is a short example of the vacation range picker that displays holidays with tooltips + and disables weekends. +

+ + + + + + + +

+ Datepicker can be used either inline or inside of the popup. +

+ + + +

+ In the example above the template variable #d will point + to the instance of the NgbDatepicker component in the first case. + In the second it will point to the instance of the NgbInputDatepicker + directive that handles the popup with inline datepicker component. +

+ +

+ See the NgbDatepicker API + and the NgbInputDatepicker API + for details on available inputs, outputs and methods. + + You can customize the number of displayed months, the way navigation + between months and years looks like, week numbers, etc. +

+ +

+ If you have a very specific use case for the datepicker popup, + you could always create you own one and use the inline datepicker inside. +

+ +

Handling the popup

+ +

+ It's up to you do decide when the datepicker popup should be opened and closed. + The API contains .open(), .close() and .toggle() + methods. +

+ +

+ By default the popup element is attached after the input in the DOM. + You have also the option of attaching it to the document body by setting the + [container] input to 'body' +

+ + + +

+ The popup will be closed with Escape key and when + a date is selected via keyboard or mouse. + It can stay open after date selection if you set [autoClose] input to false +

+
+ + + + + +

+ You have several ways of knowing when user selects a date. The date is selected + either by clicking on it, pressing Space or Enter, + typing text in the input or programmatically. +

+ +

+ Datepicker is integrated with Angular forms and works with both reactive + and template-driven forms. So you could use [(ngModel)], + [formControl], formControlName, etc. Using + ngModel will allow you both to get and set selected value. +

+ +

+ The model, however, is NOT a native javascript date, see the following + Date Model section for more info. +

+ + + +

+ Alternatively you could use the (dateSelect) or (select) outputs. + The difference from ngModel is that outputs will continue emitting the same value, + if user clicks on the same date. NgModel will do it only once. +

+ + +
+ + + + + +

+ Datepicker uses NgbDateStruct + interface as a model and not the native Date object. + It's a simple data structure with 3 fields, but note that months start with 1 (as in ISO 8601). +

+ + + +

+ All datepicker APIs will consume NgbDateStruct, but will produce it's implementation + class NgbDate when returning dates to you. + It offers additional methods for easy date comparison, and using it together with + NgbCalendar will cover most + of the date-related calculations. +

+ + + +

Adapters

+ +

+ You can also tell datepicker to use the native javascript date adapter (bundled with ng-bootstrap) as in the + custom date adapter example. For now + the adapter works only for the form integration, so for instance (ngModelChange) + will return a native date object. All other APIs continue to use NgbDateStruct. +

+ + + +

+ You can also create your own adapters if necessary by extending and implementing the + NgbDateAdapter methods. +

+ + + +

Input date parsing and formatting

+ +

+ In the case of the NgbInputDatepicker you should be able to parse + and format the text entered in the input. This is not as easy task as it seems, + because you have to account for various formats and locales. + For now internally there is a service that does default formatting using ISO 8601 format. +

+ + + +

+ If the entered input value is invalid, the form model will contain the entered text. +

+ +
+ + + + + +

+ Date selection and navigation are two different things. + You might have a date selected in January, but August currently displayed. +

+ +

+ Datepicker fully supports keyboard navigation and screen readers. You can navigate + between controls using Tab (focus will be trapped in the popup), move + date focus with arrow keys, home, page up/down and use Shift modifier + for faster navigation. +

+ +

+ With the API you can tell datepicker to initially open a specific month + via the [startDate] input or go to any month via the .navigateTo() method +

+ + +
+ + + + + +

+ You can limit the dates available for navigation and selection using + [minDate] and [maxDate] inputs. If you don't specify + any of them, you'll have infinite navigation and the year select box + will display [-10, +10] years from currently visible month. +

+ +

+ If you want to disable some dates for selection (ex. weekends), you have to + provide the [markDisabled] function that will mark certain dates + not selectable. It will be called for each newly visible day when you navigate + between months. +

+ + + +
+ + + + + +

+ You can completely replace how each date is rendered by providing a custom template + and rendering anything you want inside. You'll get a date context available inside + the template with info on whether current date is disabled, selected, focused, etc. +

+ +

+ For more info on what is provided in the template context, + see the DayTemplateContext API +

+ + + + + Note that before v3.3.0 there is no $implicit template property and you have to specify + let-date="date" in the template. + See $implicit example in Angular documentation. + + +
+ + + + + +

+ It is often useful to highlight a today's date in the calendar view or add a certain logic to it. Today's date + is the date returned by the NgbCalendar's getToday() + method. +

+ +

+ We add a custom CSS class .ngb-dp-today on a cell that corresponds to the today's date. + We do not add any rules to it at the moment, but you can add your own if necessary. + You would see something like this in the resulting markup +

+ + + +

+ You can also access this information from the DayTemplateContext API + if you're using a custom day template. It contains a today: boolean flag since v4.1.0 +

+ + + +
+ + + + +

+ You can insert anything you want in a datepicker footer by providing a template. +

+ + +
+ + + + + +

+ The datepicker model is a single date, however you still can implement range selection + functionality. With (select) and (dateSelect) outputs you'll know + which dates are being selected and with the [dayTemplate] input + you can customize the way any particular date looks. + If you want to use the NgbDatepickerInput, you can also tell the popup + to stay open by tuning the [autoClose] input. + Check the range selection example + and the initial demo on this page for more details. +

+ +

+ If you can't use the NgbDatepickerInput directive, you should + create your own popup and use NgbDatepicker inside of it. In this case + we'll handle everything related to date selection and navigation for you and you can create + a completely customized popup with any data model you want. +

+
+ + + + + +

+ Since the 2.0.0 release datepicker will use the + application locale + if it is present to get translations of weekdays and month names. The internal service that does + translation is called NgbDatepickerI18n and you could provide your own implementation + if necessary. +

+ + + +

+ The next/previous button labels can be translated using the standard Angular i18n + mechanism. For example, previous month label is extracted under the ngb.datepicker.previous-month + name. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Space / EnterSelects currently focused date if it is not disabled
EscapeCloses the datepicker popup (unless [autoClose] is false)
Arrow(Up|Down|Left|Right)Moves day focus inside the months view
Shift + Arrow(Up|Down|Left|Right)Selects currently focused date (if it is not disabled)
HomeMoves focus to the the first day of currently opened first month
EndMoves focus to the the last day of currently opened last month
Shift + HomeMoves focus to the minDate (if set)
Shift + EndMoves focus to the maxDate (if set)
PageDownMoves focus to the previous month
PageUpMoves focus to the next month
Shift + PageDownMoves focus to the previous year
Shift + PageUpMoves focus to the next year
+ +
diff --git a/demo/src/app/components/datepicker/overview/datepicker-overview.component.ts b/demo/src/app/components/datepicker/overview/datepicker-overview.component.ts new file mode 100644 index 0000000..2d415a4 --- /dev/null +++ b/demo/src/app/components/datepicker/overview/datepicker-overview.component.ts @@ -0,0 +1,186 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +import {Snippet} from '../../../shared/code/snippet'; +import { NgbdDemoList } from '../../shared'; +import { NgbdOverview } from '../../shared/overview'; + +@Component({ + selector: 'ngbd-datepicker-overview', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './datepicker-overview.component.html', + host: { + '[class.overview]': 'true' + } +}) + +export class NgbdDatepickerOverviewComponent { + + snippets = { + basic: Snippet({ + lang: 'html', + code: ` + + + + + + `, + }), + popup: Snippet({ + lang: 'html', + code: ` + + + `, + }), + form: Snippet({ + lang: 'html', + code: ` + + `, + }), + selection: Snippet({ + lang: 'html', + code: ` + + + + + + `, + }), + navigation: Snippet({ + lang: 'html', + code: ` + + + `, + }), + dateStruct: Snippet({ + lang: 'typescript', + code: ` + const date: NgbDateStruct = { year: 1789, month: 7, day: 14 }; // July, 14 1789 + `, + }), + date: Snippet({ + lang: 'typescript', + code: ` + const date: NgbDate = new NgbDate(1789, 7, 14); // July, 14 1789 + + date.before({ year: 1789, month: 7, day: 14 }); // compare to a structure + date.equals(NgbDate.from({ year: 1789, month: 7, day: 14 })); // or to another date object + `, + }), + nativeAdapter: Snippet({ + lang: 'typescript', + code: ` + // native adapter is bundled with library + providers: [{provide: NgbDateAdapter, useClass: NgbDateNativeAdapter}] + + // or another native adapter that works with UTC dates + providers: [{provide: NgbDateAdapter, useClass: NgbDateNativeUTCAdapter}] + `, + }), + adapter: Snippet({ + lang: 'typescript', + code: ` + @Injectable() + export abstract class NgbDateAdapter { + abstract fromModel(value: D): NgbDateStruct; // from your model -> internal model + abstract toModel(date: NgbDateStruct): D; // from internal model -> your mode + } + + // create your own if necessary + providers: [{provide: NgbDateAdapter, useClass: YourOwnDateAdapter}] + `, + }), + formatter: Snippet({ + lang: 'typescript', + code: ` + @Injectable() + export abstract class NgbDateParserFormatter { + abstract parse(value: string): NgbDateStruct; // from input -> internal model + abstract format(date: NgbDateStruct): string; // from internal model -> string + } + + // create your own if necessary + providers: [{provide: NgbDateParserFormatter, useClass: YourOwnParserFormatter}] + `, + }), + dayTemplate: Snippet({ + lang: 'html', + code: ` + + {{ date.day }} + + + + `, + }), + todayHTML: Snippet({ + lang: 'html', + code: ` +
+ +
+ `, + }), + todayTemplate: Snippet({ + lang: 'html', + code: ` + + ... + + + + `, + }), + footerTemplate: Snippet({ + lang: 'html', + code: ` + + + + + + `, + }), + disablingTS: Snippet({ + lang: 'typescript', + code: ` + // disable the 13th of each month + const isDisabled = (date: NgbDate, current: {month: number}) => date.day === 13; + `, + }), + disablingHTML: Snippet({ + lang: 'html', + code: ` + + + `, + }), + i18n: Snippet({ + lang: 'typescript', + code: ` + @Injectable() + export abstract class NgbDatepickerI18n { + abstract getWeekdayShortName(weekday: number): string; + abstract getMonthShortName(month: number): string; + abstract getMonthFullName(month: number): string; + abstract getDayAriaLabel(date: NgbDateStruct): string; + } + + // provide your own if necessary + providers: [{provide: NgbDatepickerI18n, useClass: YourOwnDatepickerI18n}] + `, + }), + }; + + sections: NgbdOverview = {}; + + constructor(demoList: NgbdDemoList) { + this.sections = demoList.getOverviewSections('datepicker'); + } +} diff --git a/demo/src/app/components/datepicker/overview/demo/datepicker-overview-demo.component.ts b/demo/src/app/components/datepicker/overview/demo/datepicker-overview-demo.component.ts new file mode 100644 index 0000000..0ab0f34 --- /dev/null +++ b/demo/src/app/components/datepicker/overview/demo/datepicker-overview-demo.component.ts @@ -0,0 +1,154 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {NgbCalendar, NgbDate, NgbDateNativeAdapter} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-datepicker-demo-overview', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
Vacations
+

+ from + {{ adapter.toModel(fromDate) | date : 'mediumDate' }} + to + {{ adapter.toModel(toDate ? toDate : hoveredDate) | date : 'mediumDate' }} +

+
+ + + + {{ date.day }} + + + + + + `, + styles: [` + .custom-day { + text-align: center; + display: inline-block; + width: 2rem; + height: 2rem; + line-height: 2rem; + } + .custom-day:hover { + background-color: #e6e6e6; + } + .disabled { + color: #bbbbbb; + } + .disabled:hover { + background-color: transparent; + } + .holiday, .holiday.disabled, .holiday:hover { + color: white; + background-color: coral; + } + .range:not(.holiday):not(.disabled), .custom-day:not(.disabled):not(.holiday):hover { + background-color: rgb(2, 117, 216); + color: white; + } + .faded:not(.holiday):not(.disabled) { + background-color: rgba(2, 117, 216, 0.5); + } + `], + providers: [NgbDateNativeAdapter] +}) + +export class NgbdDatepickerOverviewDemoComponent { + + today: NgbDate; + + hoveredDate: NgbDate; + + fromDate: NgbDate; + toDate: NgbDate; + + holidays: {month, day, text}[] = [ + {month: 1, day: 1, text: 'New Years Day'}, + {month: 3, day: 30, text: 'Good Friday (hi, Alsace!)'}, + {month: 5, day: 1, text: 'Labour Day'}, + {month: 5, day: 5, text: 'V-E Day'}, + {month: 7, day: 14, text: 'Bastille Day'}, + {month: 8, day: 15, text: 'Assumption Day'}, + {month: 11, day: 1, text: 'All Saints Day'}, + {month: 11, day: 11, text: 'Armistice Day'}, + {month: 12, day: 25, text: 'Christmas Day'} + ]; + + constructor(private calendar: NgbCalendar, public adapter: NgbDateNativeAdapter) { + this.markDisabled = this.markDisabled.bind(this); + this.today = calendar.getToday(); + this.fromDate = this.getFirstAvailableDate(this.today); + this.toDate = this.getFirstAvailableDate(calendar.getNext(this.today, 'd', 15)); + } + + isHoliday(date: NgbDate): string { + const holiday = this.holidays.find(h => h.day === date.day && h.month === date.month); + return holiday ? holiday.text : ''; + } + + markDisabled(date: NgbDate, current: {month: number}) { + return this.isHoliday(date) || (this.isWeekend(date) && date.month === current.month); + } + + onDateSelection(date: NgbDate) { + if (!this.fromDate && !this.toDate) { + this.fromDate = date; + } else if (this.fromDate && !this.toDate && (date.after(this.fromDate) || date.equals(this.fromDate))) { + this.toDate = date; + } else { + this.toDate = null; + this.fromDate = date; + } + } + + getTooltip(date: NgbDate) { + const holidayTooltip = this.isHoliday(date); + + if (holidayTooltip) { + return holidayTooltip; + } else if (this.isRange(date) && !this.isWeekend(date)) { + return 'Vacations!'; + } else { + return ''; + } + } + + getFirstAvailableDate(date): NgbDate { + while (this.isWeekend(date) || this.isHoliday(date)) { + date = this.calendar.getNext(date, 'd', 1); + } + return date; + } + + isWeekend(date: NgbDate) { + return this.calendar.getWeekday(date) >= 6; + } + + isRange(date: NgbDate) { + return date.equals(this.fromDate) || date.equals(this.toDate) || this.isInside(date) || this.isHovered(date); + } + + isHovered(date: NgbDate) { + return this.fromDate && !this.toDate && this.hoveredDate && date.after(this.fromDate) && date.before(this.hoveredDate); + } + + isInside(date: NgbDate) { + return date.after(this.fromDate) && date.before(this.toDate); + } +} diff --git a/demo/src/app/components/dropdown/demos/basic/dropdown-basic.html b/demo/src/app/components/dropdown/demos/basic/dropdown-basic.html new file mode 100644 index 0000000..eb2040b --- /dev/null +++ b/demo/src/app/components/dropdown/demos/basic/dropdown-basic.html @@ -0,0 +1,23 @@ +
+
+
+ +
+ + + +
+
+
+ +
+
+ +
+ + + +
+
+
+
diff --git a/demo/src/app/components/dropdown/demos/basic/dropdown-basic.module.ts b/demo/src/app/components/dropdown/demos/basic/dropdown-basic.module.ts new file mode 100644 index 0000000..fd2160f --- /dev/null +++ b/demo/src/app/components/dropdown/demos/basic/dropdown-basic.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDropdownBasic } from './dropdown-basic'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdDropdownBasic], + exports: [NgbdDropdownBasic], + bootstrap: [NgbdDropdownBasic] +}) +export class NgbdDropdownBasicModule {} diff --git a/demo/src/app/components/dropdown/demos/basic/dropdown-basic.ts b/demo/src/app/components/dropdown/demos/basic/dropdown-basic.ts new file mode 100644 index 0000000..b3e2fc5 --- /dev/null +++ b/demo/src/app/components/dropdown/demos/basic/dropdown-basic.ts @@ -0,0 +1,8 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-dropdown-basic', + templateUrl: './dropdown-basic.html' +}) +export class NgbdDropdownBasic { +} diff --git a/demo/src/app/components/dropdown/demos/config/dropdown-config.html b/demo/src/app/components/dropdown/demos/config/dropdown-config.html new file mode 100644 index 0000000..a9c30fa --- /dev/null +++ b/demo/src/app/components/dropdown/demos/config/dropdown-config.html @@ -0,0 +1,10 @@ +

This dropdown uses customized default values.

+ +
+ +
+ + + +
+
diff --git a/demo/src/app/components/dropdown/demos/config/dropdown-config.module.ts b/demo/src/app/components/dropdown/demos/config/dropdown-config.module.ts new file mode 100644 index 0000000..b62197d --- /dev/null +++ b/demo/src/app/components/dropdown/demos/config/dropdown-config.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDropdownConfig } from './dropdown-config'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdDropdownConfig], + exports: [NgbdDropdownConfig], + bootstrap: [NgbdDropdownConfig] +}) +export class NgbdDropdownConfigModule {} diff --git a/demo/src/app/components/dropdown/demos/config/dropdown-config.ts b/demo/src/app/components/dropdown/demos/config/dropdown-config.ts new file mode 100644 index 0000000..d1d491b --- /dev/null +++ b/demo/src/app/components/dropdown/demos/config/dropdown-config.ts @@ -0,0 +1,15 @@ +import {Component} from '@angular/core'; +import {NgbDropdownConfig} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-dropdown-config', + templateUrl: './dropdown-config.html', + providers: [NgbDropdownConfig] // add NgbDropdownConfig to the component providers +}) +export class NgbdDropdownConfig { + constructor(config: NgbDropdownConfig) { + // customize default values of dropdowns used by this component tree + config.placement = 'top-left'; + config.autoClose = false; + } +} diff --git a/demo/src/app/components/dropdown/demos/form/dropdown-form.html b/demo/src/app/components/dropdown/demos/form/dropdown-form.html new file mode 100644 index 0000000..43368d4 --- /dev/null +++ b/demo/src/app/components/dropdown/demos/form/dropdown-form.html @@ -0,0 +1,29 @@ +
+
+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+ + + +
+
+
+
diff --git a/demo/src/app/components/dropdown/demos/form/dropdown-form.module.ts b/demo/src/app/components/dropdown/demos/form/dropdown-form.module.ts new file mode 100644 index 0000000..00b9c6d --- /dev/null +++ b/demo/src/app/components/dropdown/demos/form/dropdown-form.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDropdownForm } from './dropdown-form'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdDropdownForm], + exports: [NgbdDropdownForm], + bootstrap: [NgbdDropdownForm] +}) +export class NgbdDropdownFormModule {} diff --git a/demo/src/app/components/dropdown/demos/form/dropdown-form.ts b/demo/src/app/components/dropdown/demos/form/dropdown-form.ts new file mode 100644 index 0000000..6823972 --- /dev/null +++ b/demo/src/app/components/dropdown/demos/form/dropdown-form.ts @@ -0,0 +1,8 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-dropdown-form', + templateUrl: './dropdown-form.html' +}) +export class NgbdDropdownForm { +} diff --git a/demo/src/app/components/dropdown/demos/manual/dropdown-manual.html b/demo/src/app/components/dropdown/demos/manual/dropdown-manual.html new file mode 100644 index 0000000..00f4fe2 --- /dev/null +++ b/demo/src/app/components/dropdown/demos/manual/dropdown-manual.html @@ -0,0 +1,13 @@ +

You can easily control dropdowns programmatically using the exported dropdown instance.

+ +
+ +
+ + + +
+ + + +
diff --git a/demo/src/app/components/dropdown/demos/manual/dropdown-manual.module.ts b/demo/src/app/components/dropdown/demos/manual/dropdown-manual.module.ts new file mode 100644 index 0000000..30c7fff --- /dev/null +++ b/demo/src/app/components/dropdown/demos/manual/dropdown-manual.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDropdownManual } from './dropdown-manual'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdDropdownManual], + exports: [NgbdDropdownManual], + bootstrap: [NgbdDropdownManual] +}) +export class NgbdDropdownManualModule {} diff --git a/demo/src/app/components/dropdown/demos/manual/dropdown-manual.ts b/demo/src/app/components/dropdown/demos/manual/dropdown-manual.ts new file mode 100644 index 0000000..4082ee1 --- /dev/null +++ b/demo/src/app/components/dropdown/demos/manual/dropdown-manual.ts @@ -0,0 +1,8 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-dropdown-manual', + templateUrl: './dropdown-manual.html' +}) +export class NgbdDropdownManual { +} diff --git a/demo/src/app/components/dropdown/demos/navbar/dropdown-navbar.html b/demo/src/app/components/dropdown/demos/navbar/dropdown-navbar.html new file mode 100644 index 0000000..8fb3661 --- /dev/null +++ b/demo/src/app/components/dropdown/demos/navbar/dropdown-navbar.html @@ -0,0 +1,69 @@ +

+ By design, dropdowns are always positioned dynamically via + placement + except when used inside navbar elements. To gracefully display them properly on small screens, they are + rendered in the html as block elements. +

+

+ In order to align a dropdown in a navbar to the right while still keeping correct behavior when the navbar is + collapsed, the CSS class dropdown-menu-right must be added to the dropdown menu. + The second dropdown in this example illustrates it. +

+

+ If completely custom placement of a dropdown in a navbar is needed, then it is only possible if the + display property is explicitlyset to "dynamic". + The third dropdown in this example illustrates this. +

+

+ Beware however that this breaks the positioning of the dropdown when the navbar is + expanded on small displays. You can see the difference between the behavior of the first dropdowns + (with a static display) and the last one (with a dynamic display) if you use this demo on a small resolution. +

+

+ To have dynamic positioning along with correct behavior on smaller displays, the value of the display + property should be dynamically set based on the screen resolution. This is left as an exercise to the reader. +

+ + diff --git a/demo/src/app/components/dropdown/demos/navbar/dropdown-navbar.module.ts b/demo/src/app/components/dropdown/demos/navbar/dropdown-navbar.module.ts new file mode 100644 index 0000000..2800afd --- /dev/null +++ b/demo/src/app/components/dropdown/demos/navbar/dropdown-navbar.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDropdownNavbar } from './dropdown-navbar'; +import { RouterModule } from '@angular/router'; + +@NgModule({ + imports: [BrowserModule, NgbModule, RouterModule], + declarations: [NgbdDropdownNavbar], + exports: [NgbdDropdownNavbar], + bootstrap: [NgbdDropdownNavbar] +}) +export class NgbdDropdownNavbarModule {} diff --git a/demo/src/app/components/dropdown/demos/navbar/dropdown-navbar.ts b/demo/src/app/components/dropdown/demos/navbar/dropdown-navbar.ts new file mode 100644 index 0000000..2f9d355 --- /dev/null +++ b/demo/src/app/components/dropdown/demos/navbar/dropdown-navbar.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-dropdown-navbar', + templateUrl: './dropdown-navbar.html' +}) +export class NgbdDropdownNavbar { + collapsed = true; +} diff --git a/demo/src/app/components/dropdown/demos/split/dropdown-split.html b/demo/src/app/components/dropdown/demos/split/dropdown-split.html new file mode 100644 index 0000000..e30d82f --- /dev/null +++ b/demo/src/app/components/dropdown/demos/split/dropdown-split.html @@ -0,0 +1,44 @@ +

Bootstrap split buttons and dropdowns on button groups are supported with the existing dropdown directives.

+ +
+ +
+ + +
+
+ +
+ +
+ + +
+
+ +
+
+ + +
+
+ + +
+
diff --git a/demo/src/app/components/dropdown/demos/split/dropdown-split.module.ts b/demo/src/app/components/dropdown/demos/split/dropdown-split.module.ts new file mode 100644 index 0000000..5c6910e --- /dev/null +++ b/demo/src/app/components/dropdown/demos/split/dropdown-split.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdDropdownSplit } from './dropdown-split'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdDropdownSplit], + exports: [NgbdDropdownSplit], + bootstrap: [NgbdDropdownSplit] +}) +export class NgbdDropdownSplitModule {} diff --git a/demo/src/app/components/dropdown/demos/split/dropdown-split.ts b/demo/src/app/components/dropdown/demos/split/dropdown-split.ts new file mode 100644 index 0000000..dd9c355 --- /dev/null +++ b/demo/src/app/components/dropdown/demos/split/dropdown-split.ts @@ -0,0 +1,8 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-dropdown-split', + templateUrl: './dropdown-split.html' +}) +export class NgbdDropdownSplit { +} diff --git a/demo/src/app/components/dropdown/dropdown.module.ts b/demo/src/app/components/dropdown/dropdown.module.ts new file mode 100644 index 0000000..71bf363 --- /dev/null +++ b/demo/src/app/components/dropdown/dropdown.module.ts @@ -0,0 +1,88 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component'; +import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared'; +import { NgbdApiPage } from '../shared/api-page/api.component'; +import { NgbdExamplesPage } from '../shared/examples-page/examples.component'; +import { NgbdDropdownBasic } from './demos/basic/dropdown-basic'; +import { NgbdDropdownBasicModule } from './demos/basic/dropdown-basic.module'; +import { NgbdDropdownConfig } from './demos/config/dropdown-config'; +import { NgbdDropdownConfigModule } from './demos/config/dropdown-config.module'; +import { NgbdDropdownForm } from './demos/form/dropdown-form'; +import { NgbdDropdownFormModule } from './demos/form/dropdown-form.module'; +import { NgbdDropdownManual } from './demos/manual/dropdown-manual'; +import { NgbdDropdownManualModule } from './demos/manual/dropdown-manual.module'; +import { NgbdDropdownNavbar } from './demos/navbar/dropdown-navbar'; +import { NgbdDropdownNavbarModule } from './demos/navbar/dropdown-navbar.module'; +import { NgbdDropdownSplit } from './demos/split/dropdown-split'; +import { NgbdDropdownSplitModule } from './demos/split/dropdown-split.module'; + +const DEMOS = { + basic: { + title: 'Dropdown', + type: NgbdDropdownBasic, + code: require('!!raw-loader!./demos/basic/dropdown-basic'), + markup: require('!!raw-loader!./demos/basic/dropdown-basic.html') + }, + manual: { + title: 'Manual and custom triggers', + type: NgbdDropdownManual, + code: require('!!raw-loader!./demos/manual/dropdown-manual'), + markup: require('!!raw-loader!./demos/manual/dropdown-manual.html') + }, + split: { + title: 'Button groups and split buttons', + type: NgbdDropdownSplit, + code: require('!!raw-loader!./demos/split/dropdown-split'), + markup: require('!!raw-loader!./demos/split/dropdown-split.html') + }, + form: { + title: 'Mixed menu items and form', + type: NgbdDropdownForm, + code: require('!!raw-loader!./demos/form/dropdown-form'), + markup: require('!!raw-loader!./demos/form/dropdown-form.html') + }, + navbar: { + title: 'Dynamic positioning in a navbar', + type: NgbdDropdownNavbar, + code: require('!!raw-loader!./demos/navbar/dropdown-navbar'), + markup: require('!!raw-loader!./demos/navbar/dropdown-navbar.html') + }, + config: { + title: 'Global configuration of dropdowns', + type: NgbdDropdownConfig, + code: require('!!raw-loader!./demos/config/dropdown-config'), + markup: require('!!raw-loader!./demos/config/dropdown-config.html') + } +}; + +export const ROUTES = [ + { path: '', pathMatch: 'full', redirectTo: 'examples' }, + { + path: '', + component: ComponentWrapper, + children: [ + { path: 'examples', component: NgbdExamplesPage }, + { path: 'api', component: NgbdApiPage } + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, + NgbdComponentsSharedModule, + NgbdDropdownBasicModule, + NgbdDropdownConfigModule, + NgbdDropdownManualModule, + NgbdDropdownSplitModule, + NgbdDropdownFormModule, + NgbdDropdownNavbarModule + ] +}) +export class NgbdDropdownModule { + constructor(demoList: NgbdDemoList) { + demoList.register('dropdown', DEMOS); + } +} diff --git a/demo/src/app/components/modal/demos/basic/modal-basic.html b/demo/src/app/components/modal/demos/basic/modal-basic.html new file mode 100644 index 0000000..12c681d --- /dev/null +++ b/demo/src/app/components/modal/demos/basic/modal-basic.html @@ -0,0 +1,30 @@ + + + + + + + + +
+ +
{{closeResult}}
diff --git a/demo/src/app/components/modal/demos/basic/modal-basic.module.ts b/demo/src/app/components/modal/demos/basic/modal-basic.module.ts new file mode 100644 index 0000000..351b0d5 --- /dev/null +++ b/demo/src/app/components/modal/demos/basic/modal-basic.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdModalBasic } from './modal-basic'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdModalBasic], + exports: [NgbdModalBasic], + bootstrap: [NgbdModalBasic] +}) +export class NgbdModalBasicModule {} diff --git a/demo/src/app/components/modal/demos/basic/modal-basic.ts b/demo/src/app/components/modal/demos/basic/modal-basic.ts new file mode 100644 index 0000000..cc36b89 --- /dev/null +++ b/demo/src/app/components/modal/demos/basic/modal-basic.ts @@ -0,0 +1,31 @@ +import {Component} from '@angular/core'; + +import {NgbModal, ModalDismissReasons} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-modal-basic', + templateUrl: './modal-basic.html' +}) +export class NgbdModalBasic { + closeResult: string; + + constructor(private modalService: NgbModal) {} + + open(content) { + this.modalService.open(content, {ariaLabelledBy: 'modal-basic-title'}).result.then((result) => { + this.closeResult = `Closed with: ${result}`; + }, (reason) => { + this.closeResult = `Dismissed ${this.getDismissReason(reason)}`; + }); + } + + private getDismissReason(reason: any): string { + if (reason === ModalDismissReasons.ESC) { + return 'by pressing ESC'; + } else if (reason === ModalDismissReasons.BACKDROP_CLICK) { + return 'by clicking on a backdrop'; + } else { + return `with: ${reason}`; + } + } +} diff --git a/demo/src/app/components/modal/demos/component/modal-component.html b/demo/src/app/components/modal/demos/component/modal-component.html new file mode 100644 index 0000000..b2fbbe4 --- /dev/null +++ b/demo/src/app/components/modal/demos/component/modal-component.html @@ -0,0 +1,4 @@ +

You can pass an existing component as content of the modal window. In this case remember to add content component +as an entryComponents section of your NgModule.

+ + diff --git a/demo/src/app/components/modal/demos/component/modal-component.module.ts b/demo/src/app/components/modal/demos/component/modal-component.module.ts new file mode 100644 index 0000000..7fa0fda --- /dev/null +++ b/demo/src/app/components/modal/demos/component/modal-component.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdModalComponent, NgbdModalContent } from './modal-component'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdModalComponent, NgbdModalContent], + exports: [NgbdModalComponent], + bootstrap: [NgbdModalComponent], + entryComponents: [NgbdModalContent] +}) +export class NgbdModalComponentModule {} diff --git a/demo/src/app/components/modal/demos/component/modal-component.ts b/demo/src/app/components/modal/demos/component/modal-component.ts new file mode 100644 index 0000000..7fb6ad5 --- /dev/null +++ b/demo/src/app/components/modal/demos/component/modal-component.ts @@ -0,0 +1,38 @@ +import { Component, Input } from '@angular/core'; +import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-modal-content', + template: ` + + + + ` +}) +export class NgbdModalContent { + @Input() name; + + constructor(public activeModal: NgbActiveModal) {} +} + +@Component({ + selector: 'ngbd-modal-component', + templateUrl: './modal-component.html' +}) +export class NgbdModalComponent { + constructor(private modalService: NgbModal) {} + + open() { + const modalRef = this.modalService.open(NgbdModalContent); + modalRef.componentInstance.name = 'World'; + } +} diff --git a/demo/src/app/components/modal/demos/config/modal-config.html b/demo/src/app/components/modal/demos/config/modal-config.html new file mode 100644 index 0000000..3d79b50 --- /dev/null +++ b/demo/src/app/components/modal/demos/config/modal-config.html @@ -0,0 +1,16 @@ + + + + + + + diff --git a/demo/src/app/components/modal/demos/config/modal-config.module.ts b/demo/src/app/components/modal/demos/config/modal-config.module.ts new file mode 100644 index 0000000..ed20dda --- /dev/null +++ b/demo/src/app/components/modal/demos/config/modal-config.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdModalConfig } from './modal-config'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdModalConfig], + exports: [NgbdModalConfig], + bootstrap: [NgbdModalConfig] +}) +export class NgbdModalConfigModule {} diff --git a/demo/src/app/components/modal/demos/config/modal-config.ts b/demo/src/app/components/modal/demos/config/modal-config.ts new file mode 100644 index 0000000..c326a9a --- /dev/null +++ b/demo/src/app/components/modal/demos/config/modal-config.ts @@ -0,0 +1,21 @@ +import { Component } from '@angular/core'; +import { NgbModalConfig, NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-modal-config', + templateUrl: './modal-config.html', + // add NgbModalConfig and NgbModal to the component providers + providers: [NgbModalConfig, NgbModal] +}) +export class NgbdModalConfig { + constructor(config: NgbModalConfig, private modalService: NgbModal) { + // customize default values of modals used by this component tree + config.backdrop = 'static'; + config.keyboard = false; + } + + open(content) { + this.modalService.open(content); + } +} + diff --git a/demo/src/app/components/modal/demos/focus/modal-focus.html b/demo/src/app/components/modal/demos/focus/modal-focus.html new file mode 100644 index 0000000..5e2173d --- /dev/null +++ b/demo/src/app/components/modal/demos/focus/modal-focus.html @@ -0,0 +1,17 @@ +

First focusable element within the modal window will receive focus upon opening. +This could be configured to focus any other element by adding an ngbAutofocus attribute on it.

+ +
<button type="button" ngbAutofocus class="btn btn-danger"
+      (click)="modal.close('Ok click')">Ok</button>
+ +
+ + + + diff --git a/demo/src/app/components/modal/demos/focus/modal-focus.module.ts b/demo/src/app/components/modal/demos/focus/modal-focus.module.ts new file mode 100644 index 0000000..fea1e60 --- /dev/null +++ b/demo/src/app/components/modal/demos/focus/modal-focus.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { + NgbdModalConfirm, + NgbdModalConfirmAutofocus, + NgbdModalFocus +} from './modal-focus'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdModalFocus, NgbdModalConfirm, NgbdModalConfirmAutofocus], + exports: [NgbdModalFocus], + bootstrap: [NgbdModalFocus], + entryComponents: [NgbdModalConfirm, NgbdModalConfirmAutofocus] +}) +export class NgbdModalFocusModule {} diff --git a/demo/src/app/components/modal/demos/focus/modal-focus.ts b/demo/src/app/components/modal/demos/focus/modal-focus.ts new file mode 100644 index 0000000..6f0ac0c --- /dev/null +++ b/demo/src/app/components/modal/demos/focus/modal-focus.ts @@ -0,0 +1,72 @@ +import { Component } from '@angular/core'; +import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-modal-confirm', + template: ` + + + + ` +}) +export class NgbdModalConfirm { + constructor(public modal: NgbActiveModal) {} +} + +@Component({ + selector: 'ngbd-modal-confirm-autofocus', + template: ` + + + + ` +}) +export class NgbdModalConfirmAutofocus { + constructor(public modal: NgbActiveModal) {} +} + +const MODALS = { + focusFirst: NgbdModalConfirm, + autofocus: NgbdModalConfirmAutofocus +}; + +@Component({ + selector: 'ngbd-modal-focus', + templateUrl: './modal-focus.html' +}) +export class NgbdModalFocus { + withAutofocus = ``; + + constructor(private _modalService: NgbModal) {} + + open(name: string) { + this._modalService.open(MODALS[name]); + } +} diff --git a/demo/src/app/components/modal/demos/options/modal-options.html b/demo/src/app/components/modal/demos/options/modal-options.html new file mode 100644 index 0000000..f3d7c3a --- /dev/null +++ b/demo/src/app/components/modal/demos/options/modal-options.html @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + diff --git a/demo/src/app/components/modal/demos/options/modal-options.module.ts b/demo/src/app/components/modal/demos/options/modal-options.module.ts new file mode 100644 index 0000000..75c9a0d --- /dev/null +++ b/demo/src/app/components/modal/demos/options/modal-options.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdModalOptions } from './modal-options'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdModalOptions], + exports: [NgbdModalOptions], + bootstrap: [NgbdModalOptions] +}) +export class NgbdModalOptionsModule {} diff --git a/demo/src/app/components/modal/demos/options/modal-options.ts b/demo/src/app/components/modal/demos/options/modal-options.ts new file mode 100644 index 0000000..1753062 --- /dev/null +++ b/demo/src/app/components/modal/demos/options/modal-options.ts @@ -0,0 +1,51 @@ +import { Component, ViewEncapsulation } from '@angular/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-modal-options', + templateUrl: './modal-options.html', + encapsulation: ViewEncapsulation.None, + styles: [` + .dark-modal .modal-content { + background-color: #292b2c; + color: white; + } + .dark-modal .close { + color: white; + } + .light-blue-backdrop { + background-color: #5cb3fd; + } + `] +}) +export class NgbdModalOptions { + closeResult: string; + + constructor(private modalService: NgbModal) {} + + openBackDropCustomClass(content) { + this.modalService.open(content, {backdropClass: 'light-blue-backdrop'}); + } + + openWindowCustomClass(content) { + this.modalService.open(content, { windowClass: 'dark-modal' }); + } + + openSm(content) { + this.modalService.open(content, { size: 'sm' }); + } + + openLg(content) { + this.modalService.open(content, { size: 'lg' }); + } + + openXl(content) { this.modalService.open(content, {size: 'xl'}); } + + openVerticallyCentered(content) { + this.modalService.open(content, { centered: true }); + } + + openScrollableContent(longContent) { + this.modalService.open(longContent, { scrollable: true }); + } +} diff --git a/demo/src/app/components/modal/demos/stacked/modal-stacked.html b/demo/src/app/components/modal/demos/stacked/modal-stacked.html new file mode 100644 index 0000000..aab661a --- /dev/null +++ b/demo/src/app/components/modal/demos/stacked/modal-stacked.html @@ -0,0 +1 @@ + diff --git a/demo/src/app/components/modal/demos/stacked/modal-stacked.module.ts b/demo/src/app/components/modal/demos/stacked/modal-stacked.module.ts new file mode 100644 index 0000000..be1fc53 --- /dev/null +++ b/demo/src/app/components/modal/demos/stacked/modal-stacked.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { + NgbdModal1Content, + NgbdModal2Content, + NgbdModalStacked +} from './modal-stacked'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdModalStacked, NgbdModal1Content, NgbdModal2Content], + exports: [NgbdModalStacked], + bootstrap: [NgbdModalStacked], + entryComponents: [NgbdModal1Content, NgbdModal2Content] +}) +export class NgbdModalStackedModule {} diff --git a/demo/src/app/components/modal/demos/stacked/modal-stacked.ts b/demo/src/app/components/modal/demos/stacked/modal-stacked.ts new file mode 100644 index 0000000..c2a0181 --- /dev/null +++ b/demo/src/app/components/modal/demos/stacked/modal-stacked.ts @@ -0,0 +1,61 @@ +import { Component } from '@angular/core'; +import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + template: ` + + + + ` +}) +export class NgbdModal1Content { + constructor(private modalService: NgbModal, public activeModal: NgbActiveModal) {} + + open() { + this.modalService.open(NgbdModal2Content, { + size: 'lg' + }); + } +} + +@Component({ + template: ` + + + + ` +}) +export class NgbdModal2Content { + constructor(public activeModal: NgbActiveModal) {} +} + +@Component({ + selector: 'ngbd-modal-stacked', + templateUrl: './modal-stacked.html' +}) +export class NgbdModalStacked { + constructor(private modalService: NgbModal) {} + + open() { + this.modalService.open(NgbdModal1Content); + } +} diff --git a/demo/src/app/components/modal/modal.module.ts b/demo/src/app/components/modal/modal.module.ts new file mode 100644 index 0000000..9d3b462 --- /dev/null +++ b/demo/src/app/components/modal/modal.module.ts @@ -0,0 +1,88 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component'; +import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared'; +import { NgbdApiPage } from '../shared/api-page/api.component'; +import { NgbdExamplesPage } from '../shared/examples-page/examples.component'; +import { NgbdModalBasic } from './demos/basic/modal-basic'; +import { NgbdModalBasicModule } from './demos/basic/modal-basic.module'; +import { NgbdModalComponent } from './demos/component/modal-component'; +import { NgbdModalComponentModule } from './demos/component/modal-component.module'; +import { NgbdModalConfig } from './demos/config/modal-config'; +import { NgbdModalConfigModule } from './demos/config/modal-config.module'; +import { NgbdModalFocus } from './demos/focus/modal-focus'; +import { NgbdModalFocusModule } from './demos/focus/modal-focus.module'; +import { NgbdModalOptions } from './demos/options/modal-options'; +import { NgbdModalOptionsModule } from './demos/options/modal-options.module'; +import { NgbdModalStacked } from './demos/stacked/modal-stacked'; +import { NgbdModalStackedModule } from './demos/stacked/modal-stacked.module'; + +const DEMOS = { + basic: { + title: 'Modal with default options', + type: NgbdModalBasic, + code: require('!!raw-loader!./demos/basic/modal-basic'), + markup: require('!!raw-loader!./demos/basic/modal-basic.html') + }, + component: { + title: 'Components as content', + type: NgbdModalComponent, + code: require('!!raw-loader!./demos/component/modal-component'), + markup: require('!!raw-loader!./demos/component/modal-component.html') + }, + focus: { + title: 'Focus management', + type: NgbdModalFocus, + code: require('!!raw-loader!./demos/focus/modal-focus'), + markup: require('!!raw-loader!./demos/focus/modal-focus.html') + }, + options: { + title: 'Modal with options', + type: NgbdModalOptions, + code: require('!!raw-loader!./demos/options/modal-options'), + markup: require('!!raw-loader!./demos/options/modal-options.html') + }, + stacked: { + title: 'Stacked modals', + type: NgbdModalStacked, + code: require('!!raw-loader!./demos/stacked/modal-stacked'), + markup: require('!!raw-loader!./demos/stacked/modal-stacked.html') + }, + config: { + title: 'Global configuration of modals', + type: NgbdModalConfig, + code: require('!!raw-loader!./demos/config/modal-config'), + markup: require('!!raw-loader!./demos/config/modal-config.html') + } +}; + +export const ROUTES = [ + { path: '', pathMatch: 'full', redirectTo: 'examples' }, + { + path: '', + component: ComponentWrapper, + children: [ + { path: 'examples', component: NgbdExamplesPage }, + { path: 'api', component: NgbdApiPage } + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, + NgbdComponentsSharedModule, + NgbdModalBasicModule, + NgbdModalComponentModule, + NgbdModalOptionsModule, + NgbdModalStackedModule, + NgbdModalConfigModule, + NgbdModalFocusModule + ] +}) +export class NgbdModalModule { + constructor(demoList: NgbdDemoList) { + demoList.register('modal', DEMOS); + } +} diff --git a/demo/src/app/components/pagination/demos/advanced/pagination-advanced.html b/demo/src/app/components/pagination/demos/advanced/pagination-advanced.html new file mode 100644 index 0000000..5387101 --- /dev/null +++ b/demo/src/app/components/pagination/demos/advanced/pagination-advanced.html @@ -0,0 +1,12 @@ +

Restricted size, no rotation:

+ + +

Restricted size with rotation:

+ + +

Restricted size with rotation and no ellipses:

+ + +
+ +
Current page: {{page}}
diff --git a/demo/src/app/components/pagination/demos/advanced/pagination-advanced.module.ts b/demo/src/app/components/pagination/demos/advanced/pagination-advanced.module.ts new file mode 100644 index 0000000..9fe2655 --- /dev/null +++ b/demo/src/app/components/pagination/demos/advanced/pagination-advanced.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdPaginationAdvanced } from './pagination-advanced'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdPaginationAdvanced], + exports: [NgbdPaginationAdvanced], + bootstrap: [NgbdPaginationAdvanced] +}) +export class NgbdPaginationAdvancedModule {} diff --git a/demo/src/app/components/pagination/demos/advanced/pagination-advanced.ts b/demo/src/app/components/pagination/demos/advanced/pagination-advanced.ts new file mode 100644 index 0000000..fe2de8b --- /dev/null +++ b/demo/src/app/components/pagination/demos/advanced/pagination-advanced.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-pagination-advanced', + templateUrl: './pagination-advanced.html' +}) +export class NgbdPaginationAdvanced { + page = 1; +} diff --git a/demo/src/app/components/pagination/demos/basic/pagination-basic.html b/demo/src/app/components/pagination/demos/basic/pagination-basic.html new file mode 100644 index 0000000..6a6ac7e --- /dev/null +++ b/demo/src/app/components/pagination/demos/basic/pagination-basic.html @@ -0,0 +1,12 @@ +

Default pagination:

+ + +

No direction links:

+ + +

With boundary links:

+ + +
+ +
Current page: {{page}}
diff --git a/demo/src/app/components/pagination/demos/basic/pagination-basic.module.ts b/demo/src/app/components/pagination/demos/basic/pagination-basic.module.ts new file mode 100644 index 0000000..3627b19 --- /dev/null +++ b/demo/src/app/components/pagination/demos/basic/pagination-basic.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdPaginationBasic } from './pagination-basic'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdPaginationBasic], + exports: [NgbdPaginationBasic], + bootstrap: [NgbdPaginationBasic] +}) +export class NgbdPaginationBasicModule {} diff --git a/demo/src/app/components/pagination/demos/basic/pagination-basic.ts b/demo/src/app/components/pagination/demos/basic/pagination-basic.ts new file mode 100644 index 0000000..d0a5a94 --- /dev/null +++ b/demo/src/app/components/pagination/demos/basic/pagination-basic.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-pagination-basic', + templateUrl: './pagination-basic.html' +}) +export class NgbdPaginationBasic { + page = 4; +} diff --git a/demo/src/app/components/pagination/demos/config/pagination-config.html b/demo/src/app/components/pagination/demos/config/pagination-config.html new file mode 100644 index 0000000..d84752f --- /dev/null +++ b/demo/src/app/components/pagination/demos/config/pagination-config.html @@ -0,0 +1,2 @@ +

This pagination uses custom default values

+ diff --git a/demo/src/app/components/pagination/demos/config/pagination-config.module.ts b/demo/src/app/components/pagination/demos/config/pagination-config.module.ts new file mode 100644 index 0000000..3d5ad75 --- /dev/null +++ b/demo/src/app/components/pagination/demos/config/pagination-config.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdPaginationConfig } from './pagination-config'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdPaginationConfig], + exports: [NgbdPaginationConfig], + bootstrap: [NgbdPaginationConfig] +}) +export class NgbdPaginationConfigModule {} diff --git a/demo/src/app/components/pagination/demos/config/pagination-config.ts b/demo/src/app/components/pagination/demos/config/pagination-config.ts new file mode 100644 index 0000000..c331b8d --- /dev/null +++ b/demo/src/app/components/pagination/demos/config/pagination-config.ts @@ -0,0 +1,17 @@ +import {Component} from '@angular/core'; +import {NgbPaginationConfig} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-pagination-config', + templateUrl: './pagination-config.html', + providers: [NgbPaginationConfig] // add NgbPaginationConfig to the component providers +}) +export class NgbdPaginationConfig { + page = 4; + + constructor(config: NgbPaginationConfig) { + // customize default values of paginations used by this component tree + config.size = 'sm'; + config.boundaryLinks = true; + } +} diff --git a/demo/src/app/components/pagination/demos/customization/pagination-customization.html b/demo/src/app/components/pagination/demos/customization/pagination-customization.html new file mode 100644 index 0000000..e379fd5 --- /dev/null +++ b/demo/src/app/components/pagination/demos/customization/pagination-customization.html @@ -0,0 +1,9 @@ +

A pagination with customized links:

+ + Prev + Next + {{ getPageSymbol(p) }} + +
+ +
Current page: {{page}}
diff --git a/demo/src/app/components/pagination/demos/customization/pagination-customization.module.ts b/demo/src/app/components/pagination/demos/customization/pagination-customization.module.ts new file mode 100644 index 0000000..8649597 --- /dev/null +++ b/demo/src/app/components/pagination/demos/customization/pagination-customization.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdPaginationCustomization } from './pagination-customization'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdPaginationCustomization], + exports: [NgbdPaginationCustomization], + bootstrap: [NgbdPaginationCustomization] +}) +export class NgbdPaginationCustomizationModule {} diff --git a/demo/src/app/components/pagination/demos/customization/pagination-customization.ts b/demo/src/app/components/pagination/demos/customization/pagination-customization.ts new file mode 100644 index 0000000..b781bc6 --- /dev/null +++ b/demo/src/app/components/pagination/demos/customization/pagination-customization.ts @@ -0,0 +1,13 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-pagination-customization', + templateUrl: './pagination-customization.html' +}) +export class NgbdPaginationCustomization { + page = 4; + + getPageSymbol(current: number) { + return ['A', 'B', 'C', 'D', 'E', 'F', 'G'][current - 1]; + } +} diff --git a/demo/src/app/components/pagination/demos/disabled/pagination-disabled.html b/demo/src/app/components/pagination/demos/disabled/pagination-disabled.html new file mode 100644 index 0000000..da1a8f0 --- /dev/null +++ b/demo/src/app/components/pagination/demos/disabled/pagination-disabled.html @@ -0,0 +1,6 @@ +

Pagination control can be disabled:

+ +
+ diff --git a/demo/src/app/components/pagination/demos/disabled/pagination-disabled.module.ts b/demo/src/app/components/pagination/demos/disabled/pagination-disabled.module.ts new file mode 100644 index 0000000..eaa0d90 --- /dev/null +++ b/demo/src/app/components/pagination/demos/disabled/pagination-disabled.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdPaginationDisabled } from './pagination-disabled'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdPaginationDisabled], + exports: [NgbdPaginationDisabled], + bootstrap: [NgbdPaginationDisabled] +}) +export class NgbdPaginationDisabledModule {} diff --git a/demo/src/app/components/pagination/demos/disabled/pagination-disabled.ts b/demo/src/app/components/pagination/demos/disabled/pagination-disabled.ts new file mode 100644 index 0000000..f8cade2 --- /dev/null +++ b/demo/src/app/components/pagination/demos/disabled/pagination-disabled.ts @@ -0,0 +1,15 @@ +import {Component} from '@angular/core'; +import {NgbPaginationConfig} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-pagination-disabled', + templateUrl: './pagination-disabled.html' +}) +export class NgbdPaginationDisabled { + page = 3; + isDisabled = true; + + toggleDisabled() { + this.isDisabled = !this.isDisabled; + } +} diff --git a/demo/src/app/components/pagination/demos/justify/pagination-justify.html b/demo/src/app/components/pagination/demos/justify/pagination-justify.html new file mode 100755 index 0000000..8de0145 --- /dev/null +++ b/demo/src/app/components/pagination/demos/justify/pagination-justify.html @@ -0,0 +1,6 @@ + +

Change the alignment of pagination components with flexbox utilities.

+ + + + diff --git a/demo/src/app/components/pagination/demos/justify/pagination-justify.module.ts b/demo/src/app/components/pagination/demos/justify/pagination-justify.module.ts new file mode 100644 index 0000000..71fc508 --- /dev/null +++ b/demo/src/app/components/pagination/demos/justify/pagination-justify.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdPaginationJustify } from './pagination-justify'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdPaginationJustify], + exports: [NgbdPaginationJustify], + bootstrap: [NgbdPaginationJustify] +}) +export class NgbdPaginationJustifyModule {} diff --git a/demo/src/app/components/pagination/demos/justify/pagination-justify.ts b/demo/src/app/components/pagination/demos/justify/pagination-justify.ts new file mode 100755 index 0000000..7d85179 --- /dev/null +++ b/demo/src/app/components/pagination/demos/justify/pagination-justify.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-pagination-justify', + templateUrl: './pagination-justify.html' +}) +export class NgbdPaginationJustify { + page = 4; +} diff --git a/demo/src/app/components/pagination/demos/size/pagination-size.html b/demo/src/app/components/pagination/demos/size/pagination-size.html new file mode 100644 index 0000000..6a4b0c4 --- /dev/null +++ b/demo/src/app/components/pagination/demos/size/pagination-size.html @@ -0,0 +1,3 @@ + + + diff --git a/demo/src/app/components/pagination/demos/size/pagination-size.module.ts b/demo/src/app/components/pagination/demos/size/pagination-size.module.ts new file mode 100644 index 0000000..f47e27f --- /dev/null +++ b/demo/src/app/components/pagination/demos/size/pagination-size.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdPaginationSize } from './pagination-size'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdPaginationSize], + exports: [NgbdPaginationSize], + bootstrap: [NgbdPaginationSize] +}) +export class NgbdPaginationSizeModule {} diff --git a/demo/src/app/components/pagination/demos/size/pagination-size.ts b/demo/src/app/components/pagination/demos/size/pagination-size.ts new file mode 100644 index 0000000..1ae8b2e --- /dev/null +++ b/demo/src/app/components/pagination/demos/size/pagination-size.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-pagination-size', + templateUrl: './pagination-size.html' +}) +export class NgbdPaginationSize { + currentPage = 3; +} diff --git a/demo/src/app/components/pagination/overview/pagination-overview.component.html b/demo/src/app/components/pagination/overview/pagination-overview.component.html new file mode 100644 index 0000000..a5aa1b1 --- /dev/null +++ b/demo/src/app/components/pagination/overview/pagination-overview.component.html @@ -0,0 +1,86 @@ +

+ Pagination is a component that only displays page numbers. It will not manipulate your data collection. You will have + to split your data collection into pages yourself. +

+ + + +

+ In order to properly operate a pagination behaviour, there are 3 important notions you have to be familiar with, + which are actually available as @Input() on the widget: +

+
    +
  1. collectionSize - Number of elements/items in the collection. + i.e. the total number of items the + pagination should handle.
  2. +
  3. pageSize - Number of elements/items per page.
  4. +
  5. page - The current page.
  6. +
+ +

To split the data collection yourself, use *ngFor associated with the slice pipe to extract (or slice) a sub-part of it.

+ +

Corresponding code could be something like:

+ +

and the associated <ngb-pagination> would be like:

+ + +
+ + Be aware that both page and pageSize have default values, + which are respectively 1 and 10. + + +

Filtering and sorting

+

+ To add filtering or sorting on top of your pagination, you will have to update the way you split your + data collection. As mentionned in Angular documentation, you don't need to reimplement dedicated pipes for + that purpose. + Recommendation is to move filtering and sorting logic into the component itself where some property getters could + be exposed. +

+
+ + +

+ It is possible to customize what exactly is displayed in each pagination link and there are several ways of doing it. +

+ +

+ You could use the Angular i18n API as all labels are translated. For instance you could replace the default + '«' (previous arrow) with the 'Prev' text by providing a different translation for the + ngb.pagination.previous key in your i18n file and ngb.pagination.previous-aria for the + corresponding aria-label attribute. +

+ +

+ You could also override the CSS to hide the default span and provide an alternative content. + For example for the previous arrow: +

+ + +

+ Using templates +

+ + +

+ Sometimes you would want to display an icon, an image or any arbitrary markup instead of the page number. + In this case since you could use the template-based API to override any pagination link: +

+ + +

+ In this case we customize all pagination links, but you can pick only the ones you need of course. + The template NgbPaginationLinkContext + is available for all templates and for page numbers there is a + NgbPaginationNumberContext + that adds displayed number on top. +

+ +

+ Also see the Customization example for a + live version. +

+
diff --git a/demo/src/app/components/pagination/overview/pagination-overview.component.ts b/demo/src/app/components/pagination/overview/pagination-overview.component.ts new file mode 100644 index 0000000..232ba8c --- /dev/null +++ b/demo/src/app/components/pagination/overview/pagination-overview.component.ts @@ -0,0 +1,70 @@ +import {Component} from '@angular/core'; + +import {Snippet} from '../../../shared/code/snippet'; +import {NgbdDemoList} from '../../shared'; +import {NgbdOverview} from '../../shared/overview'; + + +@Component({ + selector: 'ngbd-pagination-overview', + templateUrl: './pagination-overview.component.html', + host: {'[class.overview]': 'true'} +}) +export class NgbdPaginationOverviewComponent { + NGFOR = Snippet({ + lang: 'html', + code: ` + + + + +
+ `, + }); + + NGB_PAGINATION = Snippet({ + lang: 'html', + code: ` + + `, + }); + + CUSTOM_CSS = Snippet({ + lang: 'css', + code: ` + ngb-pagination li { + &:first-child a { + span { + display: none; + } + &:before { + /* provide your content here */ + } + } + } + `, + }); + + CUSTOM_TPL = Snippet({ + lang: 'html', + code: ` + + First + Last + Prev + Next + ... + {{ page }} + + `, + }); + + sections: NgbdOverview = {}; + + constructor(demoList: NgbdDemoList) { + this.sections = demoList.getOverviewSections('pagination'); + } +} diff --git a/demo/src/app/components/pagination/pagination.module.ts b/demo/src/app/components/pagination/pagination.module.ts new file mode 100644 index 0000000..104157f --- /dev/null +++ b/demo/src/app/components/pagination/pagination.module.ts @@ -0,0 +1,106 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component'; +import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared'; +import { NgbdApiPage } from '../shared/api-page/api.component'; +import { NgbdExamplesPage } from '../shared/examples-page/examples.component'; +import { NgbdPaginationAdvanced } from './demos/advanced/pagination-advanced'; +import { NgbdPaginationAdvancedModule } from './demos/advanced/pagination-advanced.module'; +import { NgbdPaginationBasic } from './demos/basic/pagination-basic'; +import { NgbdPaginationBasicModule } from './demos/basic/pagination-basic.module'; +import { NgbdPaginationConfig } from './demos/config/pagination-config'; +import { NgbdPaginationConfigModule } from './demos/config/pagination-config.module'; +import { NgbdPaginationCustomization } from './demos/customization/pagination-customization'; +import { NgbdPaginationCustomizationModule } from './demos/customization/pagination-customization.module'; +import { NgbdPaginationDisabled } from './demos/disabled/pagination-disabled'; +import { NgbdPaginationDisabledModule } from './demos/disabled/pagination-disabled.module'; +import { NgbdPaginationJustify } from './demos/justify/pagination-justify'; +import { NgbdPaginationJustifyModule } from './demos/justify/pagination-justify.module'; +import { NgbdPaginationSize } from './demos/size/pagination-size'; +import { NgbdPaginationSizeModule } from './demos/size/pagination-size.module'; +import { NgbdPaginationOverviewComponent } from './overview/pagination-overview.component'; + +const OVERVIEW = { + 'basic-usage': 'Basic Usage', + customization: 'Customization' +}; + +const DEMOS = { + basic: { + title: 'Basic pagination', + type: NgbdPaginationBasic, + code: require('!!raw-loader!./demos/basic/pagination-basic'), + markup: require('!!raw-loader!./demos/basic/pagination-basic.html') + }, + advanced: { + title: 'Advanced pagination', + type: NgbdPaginationAdvanced, + code: require('!!raw-loader!./demos/advanced/pagination-advanced'), + markup: require('!!raw-loader!./demos/advanced/pagination-advanced.html') + }, + customization: { + title: 'Custom links', + type: NgbdPaginationCustomization, + code: require('!!raw-loader!./demos/customization/pagination-customization'), + markup: require('!!raw-loader!./demos/customization/pagination-customization.html') + }, + size: { + title: 'Pagination size', + type: NgbdPaginationSize, + code: require('!!raw-loader!./demos/size/pagination-size'), + markup: require('!!raw-loader!./demos/size/pagination-size.html') + }, + justify: { + title: 'Pagination alignment', + type: NgbdPaginationJustify, + code: require('!!raw-loader!./demos/justify/pagination-justify'), + markup: require('!!raw-loader!./demos/justify/pagination-justify.html') + }, + disabled: { + title: 'Disabled pagination', + type: NgbdPaginationDisabled, + code: require('!!raw-loader!./demos/disabled/pagination-disabled'), + markup: require('!!raw-loader!./demos/disabled/pagination-disabled.html') + }, + config: { + title: 'Global configuration', + type: NgbdPaginationConfig, + code: require('!!raw-loader!./demos/config/pagination-config'), + markup: require('!!raw-loader!./demos/config/pagination-config.html') + } +}; + +export const ROUTES = [ + { path: '', pathMatch: 'full', redirectTo: 'overview' }, + { + path: '', + component: ComponentWrapper, + data: { OVERVIEW }, + children: [ + { path: 'overview', component: NgbdPaginationOverviewComponent }, + { path: 'examples', component: NgbdExamplesPage }, + { path: 'api', component: NgbdApiPage } + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, + NgbdComponentsSharedModule, + NgbdPaginationAdvancedModule, + NgbdPaginationBasicModule, + NgbdPaginationSizeModule, + NgbdPaginationConfigModule, + NgbdPaginationCustomizationModule, + NgbdPaginationDisabledModule, + NgbdPaginationJustifyModule + ], + declarations: [NgbdPaginationOverviewComponent] +}) +export class NgbdPaginationModule { + constructor(demoList: NgbdDemoList) { + demoList.register('pagination', DEMOS, OVERVIEW); + } +} diff --git a/demo/src/app/components/popover/demos/autoclose/popover-autoclose.html b/demo/src/app/components/popover/demos/autoclose/popover-autoclose.html new file mode 100644 index 0000000..98a0454 --- /dev/null +++ b/demo/src/app/components/popover/demos/autoclose/popover-autoclose.html @@ -0,0 +1,39 @@ +

As for some other popup-based widgets, you can set the popover to close automatically upon some events.

+

In the following examples, they will all close on Escape as well as:

+ +
    +
  • + click inside: + +
  • + +
  • + click outside: + +
  • + +
  • + all clicks: + +   + +
  • +
diff --git a/demo/src/app/components/popover/demos/autoclose/popover-autoclose.module.ts b/demo/src/app/components/popover/demos/autoclose/popover-autoclose.module.ts new file mode 100644 index 0000000..114818b --- /dev/null +++ b/demo/src/app/components/popover/demos/autoclose/popover-autoclose.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdPopoverAutoclose } from './popover-autoclose'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdPopoverAutoclose], + exports: [NgbdPopoverAutoclose], + bootstrap: [NgbdPopoverAutoclose] +}) +export class NgbdPopoverAutocloseModule {} diff --git a/demo/src/app/components/popover/demos/autoclose/popover-autoclose.ts b/demo/src/app/components/popover/demos/autoclose/popover-autoclose.ts new file mode 100644 index 0000000..cde20c3 --- /dev/null +++ b/demo/src/app/components/popover/demos/autoclose/popover-autoclose.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + + + +@Component({ + selector: 'ngbd-popover-autoclose', + templateUrl: './popover-autoclose.html' +}) +export class NgbdPopoverAutoclose {} diff --git a/demo/src/app/components/popover/demos/basic/popover-basic.html b/demo/src/app/components/popover/demos/basic/popover-basic.html new file mode 100644 index 0000000..ab9e3b4 --- /dev/null +++ b/demo/src/app/components/popover/demos/basic/popover-basic.html @@ -0,0 +1,19 @@ + + + + + + + diff --git a/demo/src/app/components/popover/demos/basic/popover-basic.module.ts b/demo/src/app/components/popover/demos/basic/popover-basic.module.ts new file mode 100644 index 0000000..8d84297 --- /dev/null +++ b/demo/src/app/components/popover/demos/basic/popover-basic.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdPopoverBasic } from './popover-basic'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdPopoverBasic], + exports: [NgbdPopoverBasic], + bootstrap: [NgbdPopoverBasic] +}) +export class NgbdPopoverBasicModule {} diff --git a/demo/src/app/components/popover/demos/basic/popover-basic.ts b/demo/src/app/components/popover/demos/basic/popover-basic.ts new file mode 100644 index 0000000..9de05c0 --- /dev/null +++ b/demo/src/app/components/popover/demos/basic/popover-basic.ts @@ -0,0 +1,8 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-popover-basic', + templateUrl: './popover-basic.html' +}) +export class NgbdPopoverBasic { +} diff --git a/demo/src/app/components/popover/demos/config/popover-config.html b/demo/src/app/components/popover/demos/config/popover-config.html new file mode 100644 index 0000000..831b599 --- /dev/null +++ b/demo/src/app/components/popover/demos/config/popover-config.html @@ -0,0 +1,4 @@ + diff --git a/demo/src/app/components/popover/demos/config/popover-config.module.ts b/demo/src/app/components/popover/demos/config/popover-config.module.ts new file mode 100644 index 0000000..3158c22 --- /dev/null +++ b/demo/src/app/components/popover/demos/config/popover-config.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdPopoverConfig } from './popover-config'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdPopoverConfig], + exports: [NgbdPopoverConfig], + bootstrap: [NgbdPopoverConfig] +}) +export class NgbdPopoverConfigModule {} diff --git a/demo/src/app/components/popover/demos/config/popover-config.ts b/demo/src/app/components/popover/demos/config/popover-config.ts new file mode 100644 index 0000000..482818f --- /dev/null +++ b/demo/src/app/components/popover/demos/config/popover-config.ts @@ -0,0 +1,15 @@ +import {Component} from '@angular/core'; +import {NgbPopoverConfig} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-popover-config', + templateUrl: './popover-config.html', + providers: [NgbPopoverConfig] // add NgbPopoverConfig to the component providers +}) +export class NgbdPopoverConfig { + constructor(config: NgbPopoverConfig) { + // customize default values of popovers used by this component tree + config.placement = 'right'; + config.triggers = 'hover'; + } +} diff --git a/demo/src/app/components/popover/demos/container/popover-container.html b/demo/src/app/components/popover/demos/container/popover-container.html new file mode 100644 index 0000000..3d51559 --- /dev/null +++ b/demo/src/app/components/popover/demos/container/popover-container.html @@ -0,0 +1,16 @@ +

+ Set the container property to "body" to have the popover be appended to the body instead of the triggering element's parent. This option is useful if the element triggering the popover is inside an element that clips its contents (i.e. overflow: hidden). +

+ +
+
+ + +
+
diff --git a/demo/src/app/components/popover/demos/container/popover-container.module.ts b/demo/src/app/components/popover/demos/container/popover-container.module.ts new file mode 100644 index 0000000..4820deb --- /dev/null +++ b/demo/src/app/components/popover/demos/container/popover-container.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdPopoverContainer } from './popover-container'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdPopoverContainer], + exports: [NgbdPopoverContainer], + bootstrap: [NgbdPopoverContainer] +}) +export class NgbdPopoverContainerModule {} diff --git a/demo/src/app/components/popover/demos/container/popover-container.ts b/demo/src/app/components/popover/demos/container/popover-container.ts new file mode 100644 index 0000000..cfb5fc0 --- /dev/null +++ b/demo/src/app/components/popover/demos/container/popover-container.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-popover-container', + templateUrl: './popover-container.html', + styles: ['.card { overflow: hidden }'] +}) +export class NgbdPopoverContainer { +} diff --git a/demo/src/app/components/popover/demos/customclass/popover-custom-class.module.ts b/demo/src/app/components/popover/demos/customclass/popover-custom-class.module.ts new file mode 100644 index 0000000..ecd15d7 --- /dev/null +++ b/demo/src/app/components/popover/demos/customclass/popover-custom-class.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdPopoverCustomclass } from './popover-customclass'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdPopoverCustomclass], + exports: [NgbdPopoverCustomclass], + bootstrap: [NgbdPopoverCustomclass] +}) +export class NgbdPopoverCustomClassModule {} diff --git a/demo/src/app/components/popover/demos/customclass/popover-customclass.html b/demo/src/app/components/popover/demos/customclass/popover-customclass.html new file mode 100644 index 0000000..210bff9 --- /dev/null +++ b/demo/src/app/components/popover/demos/customclass/popover-customclass.html @@ -0,0 +1,8 @@ +

+ You can optionally pass in a custom class via popoverClass +

+ + diff --git a/demo/src/app/components/popover/demos/customclass/popover-customclass.ts b/demo/src/app/components/popover/demos/customclass/popover-customclass.ts new file mode 100644 index 0000000..9e0484c --- /dev/null +++ b/demo/src/app/components/popover/demos/customclass/popover-customclass.ts @@ -0,0 +1,18 @@ +import {Component, ViewEncapsulation} from '@angular/core'; + +@Component({ + selector: 'ngbd-popover-customclass', + templateUrl: './popover-customclass.html', + encapsulation: ViewEncapsulation.None, + styles: [` + .my-custom-class { + background: aliceblue; + font-size: 125%; + } + .my-custom-class .arrow::after { + border-top-color: aliceblue; + } + `] +}) +export class NgbdPopoverCustomclass { +} diff --git a/demo/src/app/components/popover/demos/delay/popover-delay.html b/demo/src/app/components/popover/demos/delay/popover-delay.html new file mode 100644 index 0000000..2fcd355 --- /dev/null +++ b/demo/src/app/components/popover/demos/delay/popover-delay.html @@ -0,0 +1,16 @@ +

+ When using non-manual triggers, you can control the delay to open and close the popover through the openDelay and + closeDelay properties. Note that the autoClose feature does not use the close delay, it closes the popover immediately. +

+ + + + diff --git a/demo/src/app/components/popover/demos/delay/popover-delay.module.ts b/demo/src/app/components/popover/demos/delay/popover-delay.module.ts new file mode 100644 index 0000000..dc4978e --- /dev/null +++ b/demo/src/app/components/popover/demos/delay/popover-delay.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdPopoverDelay } from './popover-delay'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdPopoverDelay], + exports: [NgbdPopoverDelay], + bootstrap: [NgbdPopoverDelay] +}) +export class NgbdPopoverDelayModule {} diff --git a/demo/src/app/components/popover/demos/delay/popover-delay.ts b/demo/src/app/components/popover/demos/delay/popover-delay.ts new file mode 100644 index 0000000..d11ab36 --- /dev/null +++ b/demo/src/app/components/popover/demos/delay/popover-delay.ts @@ -0,0 +1,8 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-popover-delay', + templateUrl: './popover-delay.html' +}) +export class NgbdPopoverDelay { +} diff --git a/demo/src/app/components/popover/demos/tplcontent/popover-tpl-content.module.ts b/demo/src/app/components/popover/demos/tplcontent/popover-tpl-content.module.ts new file mode 100644 index 0000000..1af0a58 --- /dev/null +++ b/demo/src/app/components/popover/demos/tplcontent/popover-tpl-content.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdPopoverTplcontent } from './popover-tplcontent'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdPopoverTplcontent], + exports: [NgbdPopoverTplcontent], + bootstrap: [NgbdPopoverTplcontent] +}) +export class NgbdPopoverTplContentModule {} diff --git a/demo/src/app/components/popover/demos/tplcontent/popover-tplcontent.html b/demo/src/app/components/popover/demos/tplcontent/popover-tplcontent.html new file mode 100644 index 0000000..9de0870 --- /dev/null +++ b/demo/src/app/components/popover/demos/tplcontent/popover-tplcontent.html @@ -0,0 +1,10 @@ +

+ Popovers can contain any arbitrary HTML, Angular bindings and even directives! + Simply enclose desired content or title in a <ng-template> element. +

+ +Hello, {{name}}! +Fancy content!! + diff --git a/demo/src/app/components/popover/demos/tplcontent/popover-tplcontent.ts b/demo/src/app/components/popover/demos/tplcontent/popover-tplcontent.ts new file mode 100644 index 0000000..da07919 --- /dev/null +++ b/demo/src/app/components/popover/demos/tplcontent/popover-tplcontent.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-popover-tplcontent', + templateUrl: './popover-tplcontent.html' +}) +export class NgbdPopoverTplcontent { + name = 'World'; +} diff --git a/demo/src/app/components/popover/demos/tplwithcontext/popover-tpl-with-context.module.ts b/demo/src/app/components/popover/demos/tplwithcontext/popover-tpl-with-context.module.ts new file mode 100644 index 0000000..169507d --- /dev/null +++ b/demo/src/app/components/popover/demos/tplwithcontext/popover-tpl-with-context.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdPopoverTplwithcontext } from './popover-tplwithcontext'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdPopoverTplwithcontext], + exports: [NgbdPopoverTplwithcontext], + bootstrap: [NgbdPopoverTplwithcontext] +}) +export class NgbdPopoverTplWithContextModule {} diff --git a/demo/src/app/components/popover/demos/tplwithcontext/popover-tplwithcontext.html b/demo/src/app/components/popover/demos/tplwithcontext/popover-tplwithcontext.html new file mode 100644 index 0000000..5376297 --- /dev/null +++ b/demo/src/app/components/popover/demos/tplwithcontext/popover-tplwithcontext.html @@ -0,0 +1,27 @@ +

+ You can optionally pass in a context when manually triggering a popover. +

+ +{{greeting}}, {{name}}! +Greeting in {{language}} + + + diff --git a/demo/src/app/components/popover/demos/tplwithcontext/popover-tplwithcontext.ts b/demo/src/app/components/popover/demos/tplwithcontext/popover-tplwithcontext.ts new file mode 100644 index 0000000..54728e5 --- /dev/null +++ b/demo/src/app/components/popover/demos/tplwithcontext/popover-tplwithcontext.ts @@ -0,0 +1,17 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-popover-tplwithcontext', + templateUrl: './popover-tplwithcontext.html' +}) +export class NgbdPopoverTplwithcontext { + name = 'World'; + + toggleWithGreeting(popover, greeting: string, language: string) { + if (popover.isOpen()) { + popover.close(); + } else { + popover.open({greeting, language}); + } + } +} diff --git a/demo/src/app/components/popover/demos/triggers/popover-triggers.html b/demo/src/app/components/popover/demos/triggers/popover-triggers.html new file mode 100644 index 0000000..4db24d3 --- /dev/null +++ b/demo/src/app/components/popover/demos/triggers/popover-triggers.html @@ -0,0 +1,19 @@ +

+ You can easily override open and close triggers by specifying event names (separated by :) in the triggers property. +

+ + + +
+

+ Alternatively you can take full manual control over popover opening / closing events. +

+ + + diff --git a/demo/src/app/components/popover/demos/triggers/popover-triggers.module.ts b/demo/src/app/components/popover/demos/triggers/popover-triggers.module.ts new file mode 100644 index 0000000..e7dea15 --- /dev/null +++ b/demo/src/app/components/popover/demos/triggers/popover-triggers.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdPopoverTriggers } from './popover-triggers'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdPopoverTriggers], + exports: [NgbdPopoverTriggers], + bootstrap: [NgbdPopoverTriggers] +}) +export class NgbdPopoverTriggersModule {} diff --git a/demo/src/app/components/popover/demos/triggers/popover-triggers.ts b/demo/src/app/components/popover/demos/triggers/popover-triggers.ts new file mode 100644 index 0000000..213a8e8 --- /dev/null +++ b/demo/src/app/components/popover/demos/triggers/popover-triggers.ts @@ -0,0 +1,8 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-popover-triggers', + templateUrl: './popover-triggers.html' +}) +export class NgbdPopoverTriggers { +} diff --git a/demo/src/app/components/popover/demos/visibility/popover-visibility.html b/demo/src/app/components/popover/demos/visibility/popover-visibility.html new file mode 100644 index 0000000..0c52854 --- /dev/null +++ b/demo/src/app/components/popover/demos/visibility/popover-visibility.html @@ -0,0 +1,13 @@ + + +
+ +
    +
  • Popover is currently: {{ popover.isOpen() ? 'open' : 'closed' }}
  • +
  • Last shown at: {{lastShown | date:'h:mm:ss'}}
  • +
  • Last hidden at: {{lastHidden | date:'h:mm:ss'}}
  • +
diff --git a/demo/src/app/components/popover/demos/visibility/popover-visibility.module.ts b/demo/src/app/components/popover/demos/visibility/popover-visibility.module.ts new file mode 100644 index 0000000..fe0da14 --- /dev/null +++ b/demo/src/app/components/popover/demos/visibility/popover-visibility.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdPopoverVisibility } from './popover-visibility'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdPopoverVisibility], + exports: [NgbdPopoverVisibility], + bootstrap: [NgbdPopoverVisibility] +}) +export class NgbdPopoverVisibilityModule {} diff --git a/demo/src/app/components/popover/demos/visibility/popover-visibility.ts b/demo/src/app/components/popover/demos/visibility/popover-visibility.ts new file mode 100644 index 0000000..8c78e0f --- /dev/null +++ b/demo/src/app/components/popover/demos/visibility/popover-visibility.ts @@ -0,0 +1,18 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-popover-visibility', + templateUrl: './popover-visibility.html' +}) +export class NgbdPopoverVisibility { + lastShown: Date; + lastHidden: Date; + + recordShown() { + this.lastShown = new Date(); + } + + recordHidden() { + this.lastHidden = new Date(); + } +} diff --git a/demo/src/app/components/popover/popover.module.ts b/demo/src/app/components/popover/popover.module.ts new file mode 100644 index 0000000..c7ad32e --- /dev/null +++ b/demo/src/app/components/popover/popover.module.ts @@ -0,0 +1,124 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component'; +import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared'; +import { NgbdApiPage } from '../shared/api-page/api.component'; +import { NgbdExamplesPage } from '../shared/examples-page/examples.component'; +import { NgbdPopoverAutoclose } from './demos/autoclose/popover-autoclose'; +import { NgbdPopoverAutocloseModule } from './demos/autoclose/popover-autoclose.module'; +import { NgbdPopoverBasic } from './demos/basic/popover-basic'; +import { NgbdPopoverBasicModule } from './demos/basic/popover-basic.module'; +import { NgbdPopoverConfig } from './demos/config/popover-config'; +import { NgbdPopoverConfigModule } from './demos/config/popover-config.module'; +import { NgbdPopoverContainer } from './demos/container/popover-container'; +import { NgbdPopoverContainerModule } from './demos/container/popover-container.module'; +import { NgbdPopoverCustomClassModule } from './demos/customclass/popover-custom-class.module'; +import { NgbdPopoverCustomclass } from './demos/customclass/popover-customclass'; +import { NgbdPopoverDelay } from './demos/delay/popover-delay'; +import { NgbdPopoverDelayModule } from './demos/delay/popover-delay.module'; +import { NgbdPopoverTplContentModule } from './demos/tplcontent/popover-tpl-content.module'; +import { NgbdPopoverTplcontent } from './demos/tplcontent/popover-tplcontent'; +import { NgbdPopoverTplWithContextModule } from './demos/tplwithcontext/popover-tpl-with-context.module'; +import { NgbdPopoverTplwithcontext } from './demos/tplwithcontext/popover-tplwithcontext'; +import { NgbdPopoverTriggers } from './demos/triggers/popover-triggers'; +import { NgbdPopoverTriggersModule } from './demos/triggers/popover-triggers.module'; +import { NgbdPopoverVisibility } from './demos/visibility/popover-visibility'; +import { NgbdPopoverVisibilityModule } from './demos/visibility/popover-visibility.module'; + +const DEMOS = { + basic: { + title: 'Quick and easy popovers', + type: NgbdPopoverBasic, + code: require('!!raw-loader!./demos/basic/popover-basic'), + markup: require('!!raw-loader!./demos/basic/popover-basic.html') + }, + tplcontent: { + title: 'HTML and bindings in popovers', + type: NgbdPopoverTplcontent, + code: require('!!raw-loader!./demos/tplcontent/popover-tplcontent'), + markup: require('!!raw-loader!./demos/tplcontent/popover-tplcontent.html') + }, + triggers: { + title: 'Custom and manual triggers', + type: NgbdPopoverTriggers, + code: require('!!raw-loader!./demos/triggers/popover-triggers'), + markup: require('!!raw-loader!./demos/triggers/popover-triggers.html') + }, + autoclose: { + title: 'Automatic closing with keyboard and mouse', + type: NgbdPopoverAutoclose, + code: require('!!raw-loader!./demos/autoclose/popover-autoclose'), + markup: require('!!raw-loader!./demos/autoclose/popover-autoclose.html') + }, + tplwithcontext: { + title: 'Context and manual triggers', + type: NgbdPopoverTplwithcontext, + code: require('!!raw-loader!./demos/tplwithcontext/popover-tplwithcontext'), + markup: require('!!raw-loader!./demos/tplwithcontext/popover-tplwithcontext.html') + }, + delay: { + title: 'Open and close delays', + type: NgbdPopoverDelay, + code: require('!!raw-loader!./demos/delay/popover-delay'), + markup: require('!!raw-loader!./demos/delay/popover-delay.html') + }, + visibility: { + title: 'Popover visibility events', + type: NgbdPopoverVisibility, + code: require('!!raw-loader!./demos/visibility/popover-visibility'), + markup: require('!!raw-loader!./demos/visibility/popover-visibility.html') + }, + container: { + title: 'Append popover in the body', + type: NgbdPopoverContainer, + code: require('!!raw-loader!./demos/container/popover-container'), + markup: require('!!raw-loader!./demos/container/popover-container.html') + }, + customclass: { + title: 'Popover with custom class', + type: NgbdPopoverCustomclass, + code: require('!!raw-loader!./demos/customclass/popover-customclass'), + markup: require('!!raw-loader!./demos/customclass/popover-customclass.html') + }, + config: { + title: 'Global configuration of popovers', + type: NgbdPopoverConfig, + code: require('!!raw-loader!./demos/config/popover-config'), + markup: require('!!raw-loader!./demos/config/popover-config.html') + } +}; + +export const ROUTES = [ + { path: '', pathMatch: 'full', redirectTo: 'examples' }, + { + path: '', + component: ComponentWrapper, + children: [ + { path: 'examples', component: NgbdExamplesPage }, + { path: 'api', component: NgbdApiPage } + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, + NgbdComponentsSharedModule, + NgbdPopoverBasicModule, + NgbdPopoverTplContentModule, + NgbdPopoverTplWithContextModule, + NgbdPopoverTriggersModule, + NgbdPopoverAutocloseModule, + NgbdPopoverVisibilityModule, + NgbdPopoverContainerModule, + NgbdPopoverCustomClassModule, + NgbdPopoverDelayModule, + NgbdPopoverConfigModule + ] +}) +export class NgbdPopoverModule { + constructor(demoList: NgbdDemoList) { + demoList.register('popover', DEMOS); + } +} diff --git a/demo/src/app/components/progressbar/demos/basic/progressbar-basic.html b/demo/src/app/components/progressbar/demos/basic/progressbar-basic.html new file mode 100644 index 0000000..2770946 --- /dev/null +++ b/demo/src/app/components/progressbar/demos/basic/progressbar-basic.html @@ -0,0 +1,4 @@ +

+

+

+

diff --git a/demo/src/app/components/progressbar/demos/basic/progressbar-basic.module.ts b/demo/src/app/components/progressbar/demos/basic/progressbar-basic.module.ts new file mode 100644 index 0000000..c83922e --- /dev/null +++ b/demo/src/app/components/progressbar/demos/basic/progressbar-basic.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdProgressbarBasic } from './progressbar-basic'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdProgressbarBasic], + exports: [NgbdProgressbarBasic], + bootstrap: [NgbdProgressbarBasic] +}) +export class NgbdProgressbarBasicModule {} diff --git a/demo/src/app/components/progressbar/demos/basic/progressbar-basic.ts b/demo/src/app/components/progressbar/demos/basic/progressbar-basic.ts new file mode 100644 index 0000000..73d6aec --- /dev/null +++ b/demo/src/app/components/progressbar/demos/basic/progressbar-basic.ts @@ -0,0 +1,13 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-progressbar-basic', + templateUrl: './progressbar-basic.html', + styles: [` + ngb-progressbar { + margin-top: 5rem; + } + `] +}) +export class NgbdProgressbarBasic { +} diff --git a/demo/src/app/components/progressbar/demos/config/progressbar-config.html b/demo/src/app/components/progressbar/demos/config/progressbar-config.html new file mode 100644 index 0000000..44569a7 --- /dev/null +++ b/demo/src/app/components/progressbar/demos/config/progressbar-config.html @@ -0,0 +1,5 @@ +

This progress bar uses the customized default values.

+

+ +

This progress bar uses the customized default values, but changes the type using an input.

+

diff --git a/demo/src/app/components/progressbar/demos/config/progressbar-config.module.ts b/demo/src/app/components/progressbar/demos/config/progressbar-config.module.ts new file mode 100644 index 0000000..3140ef3 --- /dev/null +++ b/demo/src/app/components/progressbar/demos/config/progressbar-config.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdProgressbarConfig } from './progressbar-config'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdProgressbarConfig], + exports: [NgbdProgressbarConfig], + bootstrap: [NgbdProgressbarConfig] +}) +export class NgbdProgressbarConfigModule {} diff --git a/demo/src/app/components/progressbar/demos/config/progressbar-config.ts b/demo/src/app/components/progressbar/demos/config/progressbar-config.ts new file mode 100644 index 0000000..db51347 --- /dev/null +++ b/demo/src/app/components/progressbar/demos/config/progressbar-config.ts @@ -0,0 +1,18 @@ +import {Component} from '@angular/core'; +import {NgbProgressbarConfig} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-progressbar-config', + templateUrl: './progressbar-config.html', + providers: [NgbProgressbarConfig] // add the NgbProgressbarConfig to the component providers +}) +export class NgbdProgressbarConfig { + constructor(config: NgbProgressbarConfig) { + // customize default values of progress bars used by this component tree + config.max = 1000; + config.striped = true; + config.animated = true; + config.type = 'success'; + config.height = '20px'; + } +} diff --git a/demo/src/app/components/progressbar/demos/height/progressbar-height.html b/demo/src/app/components/progressbar/demos/height/progressbar-height.html new file mode 100644 index 0000000..1858258 --- /dev/null +++ b/demo/src/app/components/progressbar/demos/height/progressbar-height.html @@ -0,0 +1,4 @@ +

default

+

10px

+

.5rem

+

{{height}}

\ No newline at end of file diff --git a/demo/src/app/components/progressbar/demos/height/progressbar-height.module.ts b/demo/src/app/components/progressbar/demos/height/progressbar-height.module.ts new file mode 100644 index 0000000..7c74f97 --- /dev/null +++ b/demo/src/app/components/progressbar/demos/height/progressbar-height.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdProgressbarHeight } from './progressbar-height'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdProgressbarHeight], + exports: [NgbdProgressbarHeight], + bootstrap: [NgbdProgressbarHeight] +}) +export class NgbdProgressbarHeightModule {} diff --git a/demo/src/app/components/progressbar/demos/height/progressbar-height.ts b/demo/src/app/components/progressbar/demos/height/progressbar-height.ts new file mode 100644 index 0000000..77f7ac4 --- /dev/null +++ b/demo/src/app/components/progressbar/demos/height/progressbar-height.ts @@ -0,0 +1,14 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-progressbar-height', + templateUrl: './progressbar-height.html', + styles: [` + ngb-progressbar { + margin-top: 5rem; + } + `] +}) +export class NgbdProgressbarHeight { + height = '20px'; +} diff --git a/demo/src/app/components/progressbar/demos/labels/progressbar-labels.html b/demo/src/app/components/progressbar/demos/labels/progressbar-labels.html new file mode 100644 index 0000000..0934423 --- /dev/null +++ b/demo/src/app/components/progressbar/demos/labels/progressbar-labels.html @@ -0,0 +1,4 @@ +

25

+

Copying file 2 of 4...

+

50%

+

Completed!

diff --git a/demo/src/app/components/progressbar/demos/labels/progressbar-labels.module.ts b/demo/src/app/components/progressbar/demos/labels/progressbar-labels.module.ts new file mode 100644 index 0000000..f0a9373 --- /dev/null +++ b/demo/src/app/components/progressbar/demos/labels/progressbar-labels.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdProgressbarLabels } from './progressbar-labels'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdProgressbarLabels], + exports: [NgbdProgressbarLabels], + bootstrap: [NgbdProgressbarLabels] +}) +export class NgbdProgressbarLabelsModule {} diff --git a/demo/src/app/components/progressbar/demos/labels/progressbar-labels.ts b/demo/src/app/components/progressbar/demos/labels/progressbar-labels.ts new file mode 100644 index 0000000..76ff540 --- /dev/null +++ b/demo/src/app/components/progressbar/demos/labels/progressbar-labels.ts @@ -0,0 +1,13 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-progressbar-labels', + templateUrl: './progressbar-labels.html', + styles: [` + ngb-progressbar { + margin-top: 5rem; + } + `] +}) +export class NgbdProgressbarLabels { +} diff --git a/demo/src/app/components/progressbar/demos/showvalue/progressbar-show-value.module.ts b/demo/src/app/components/progressbar/demos/showvalue/progressbar-show-value.module.ts new file mode 100644 index 0000000..579cb5c --- /dev/null +++ b/demo/src/app/components/progressbar/demos/showvalue/progressbar-show-value.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdProgressbarShowvalue } from './progressbar-showvalue'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdProgressbarShowvalue], + exports: [NgbdProgressbarShowvalue], + bootstrap: [NgbdProgressbarShowvalue] +}) +export class NgbdProgressbarShowValueModule {} diff --git a/demo/src/app/components/progressbar/demos/showvalue/progressbar-showvalue.html b/demo/src/app/components/progressbar/demos/showvalue/progressbar-showvalue.html new file mode 100644 index 0000000..4dca48c --- /dev/null +++ b/demo/src/app/components/progressbar/demos/showvalue/progressbar-showvalue.html @@ -0,0 +1,4 @@ +

+

+

+

diff --git a/demo/src/app/components/progressbar/demos/showvalue/progressbar-showvalue.ts b/demo/src/app/components/progressbar/demos/showvalue/progressbar-showvalue.ts new file mode 100644 index 0000000..0f9a0ec --- /dev/null +++ b/demo/src/app/components/progressbar/demos/showvalue/progressbar-showvalue.ts @@ -0,0 +1,13 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-progressbar-showvalue', + templateUrl: './progressbar-showvalue.html', + styles: [` + ngb-progressbar { + margin-top: 5rem; + } + `] +}) +export class NgbdProgressbarShowvalue { +} diff --git a/demo/src/app/components/progressbar/demos/striped/progressbar-striped.html b/demo/src/app/components/progressbar/demos/striped/progressbar-striped.html new file mode 100644 index 0000000..f38a0bc --- /dev/null +++ b/demo/src/app/components/progressbar/demos/striped/progressbar-striped.html @@ -0,0 +1,4 @@ +

+

+

+

diff --git a/demo/src/app/components/progressbar/demos/striped/progressbar-striped.module.ts b/demo/src/app/components/progressbar/demos/striped/progressbar-striped.module.ts new file mode 100644 index 0000000..fad90ad --- /dev/null +++ b/demo/src/app/components/progressbar/demos/striped/progressbar-striped.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdProgressbarStriped } from './progressbar-striped'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdProgressbarStriped], + exports: [NgbdProgressbarStriped], + bootstrap: [NgbdProgressbarStriped] +}) +export class NgbdProgressbarStripedModule {} diff --git a/demo/src/app/components/progressbar/demos/striped/progressbar-striped.ts b/demo/src/app/components/progressbar/demos/striped/progressbar-striped.ts new file mode 100644 index 0000000..ded36a0 --- /dev/null +++ b/demo/src/app/components/progressbar/demos/striped/progressbar-striped.ts @@ -0,0 +1,8 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-progressbar-striped', + templateUrl: './progressbar-striped.html' +}) +export class NgbdProgressbarStriped { +} diff --git a/demo/src/app/components/progressbar/progressbar.module.ts b/demo/src/app/components/progressbar/progressbar.module.ts new file mode 100644 index 0000000..8f3babb --- /dev/null +++ b/demo/src/app/components/progressbar/progressbar.module.ts @@ -0,0 +1,88 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component'; +import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared'; +import { NgbdApiPage } from '../shared/api-page/api.component'; +import { NgbdExamplesPage } from '../shared/examples-page/examples.component'; +import { NgbdProgressbarBasic } from './demos/basic/progressbar-basic'; +import { NgbdProgressbarBasicModule } from './demos/basic/progressbar-basic.module'; +import { NgbdProgressbarConfig } from './demos/config/progressbar-config'; +import { NgbdProgressbarConfigModule } from './demos/config/progressbar-config.module'; +import { NgbdProgressbarHeight } from './demos/height/progressbar-height'; +import { NgbdProgressbarHeightModule } from './demos/height/progressbar-height.module'; +import { NgbdProgressbarLabels } from './demos/labels/progressbar-labels'; +import { NgbdProgressbarLabelsModule } from './demos/labels/progressbar-labels.module'; +import { NgbdProgressbarShowValueModule } from './demos/showvalue/progressbar-show-value.module'; +import { NgbdProgressbarShowvalue } from './demos/showvalue/progressbar-showvalue'; +import { NgbdProgressbarStriped } from './demos/striped/progressbar-striped'; +import { NgbdProgressbarStripedModule } from './demos/striped/progressbar-striped.module'; + +const DEMOS = { + basic: { + title: 'Contextual progress bars', + type: NgbdProgressbarBasic, + code: require('!!raw-loader!./demos/basic/progressbar-basic'), + markup: require('!!raw-loader!./demos/basic/progressbar-basic.html') + }, + showvalue: { + title: 'Progress bars with current value labels', + type: NgbdProgressbarShowvalue, + code: require('!!raw-loader!./demos/showvalue/progressbar-showvalue'), + markup: require('!!raw-loader!./demos/showvalue/progressbar-showvalue.html') + }, + striped: { + title: 'Striped progress bars', + type: NgbdProgressbarStriped, + code: require('!!raw-loader!./demos/striped/progressbar-striped'), + markup: require('!!raw-loader!./demos/striped/progressbar-striped.html') + }, + labels: { + title: 'Progress bars with custom labels', + type: NgbdProgressbarLabels, + code: require('!!raw-loader!./demos/labels/progressbar-labels'), + markup: require('!!raw-loader!./demos/labels/progressbar-labels.html') + }, + height: { + title: 'Progress bars with height', + type: NgbdProgressbarHeight, + code: require('!!raw-loader!./demos/height/progressbar-height'), + markup: require('!!raw-loader!./demos/height/progressbar-height.html') + }, + config: { + title: 'Global configuration of progress bars', + type: NgbdProgressbarConfig, + code: require('!!raw-loader!./demos/config/progressbar-config'), + markup: require('!!raw-loader!./demos/config/progressbar-config.html') + } +}; + +export const ROUTES = [ + { path: '', pathMatch: 'full', redirectTo: 'examples' }, + { + path: '', + component: ComponentWrapper, + children: [ + { path: 'examples', component: NgbdExamplesPage }, + { path: 'api', component: NgbdApiPage } + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, + NgbdComponentsSharedModule, + NgbdProgressbarBasicModule, + NgbdProgressbarShowValueModule, + NgbdProgressbarStripedModule, + NgbdProgressbarConfigModule, + NgbdProgressbarLabelsModule, + NgbdProgressbarHeightModule + ] +}) +export class NgbdProgressbarModule { + constructor(demoList: NgbdDemoList) { + demoList.register('progressbar', DEMOS); + } +} diff --git a/demo/src/app/components/rating/demos/basic/rating-basic.html b/demo/src/app/components/rating/demos/basic/rating-basic.html new file mode 100644 index 0000000..79d4881 --- /dev/null +++ b/demo/src/app/components/rating/demos/basic/rating-basic.html @@ -0,0 +1,3 @@ + +
+
Rate: {{currentRate}}
diff --git a/demo/src/app/components/rating/demos/basic/rating-basic.module.ts b/demo/src/app/components/rating/demos/basic/rating-basic.module.ts new file mode 100644 index 0000000..e1b8237 --- /dev/null +++ b/demo/src/app/components/rating/demos/basic/rating-basic.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdRatingBasic } from './rating-basic'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdRatingBasic], + exports: [NgbdRatingBasic], + bootstrap: [NgbdRatingBasic] +}) +export class NgbdRatingBasicModule {} diff --git a/demo/src/app/components/rating/demos/basic/rating-basic.ts b/demo/src/app/components/rating/demos/basic/rating-basic.ts new file mode 100644 index 0000000..f94da7a --- /dev/null +++ b/demo/src/app/components/rating/demos/basic/rating-basic.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-rating-basic', + templateUrl: './rating-basic.html' +}) +export class NgbdRatingBasic { + currentRate = 8; +} diff --git a/demo/src/app/components/rating/demos/config/rating-config.html b/demo/src/app/components/rating/demos/config/rating-config.html new file mode 100644 index 0000000..b89df28 --- /dev/null +++ b/demo/src/app/components/rating/demos/config/rating-config.html @@ -0,0 +1,3 @@ +

This rating uses customized default values.

+ + diff --git a/demo/src/app/components/rating/demos/config/rating-config.module.ts b/demo/src/app/components/rating/demos/config/rating-config.module.ts new file mode 100644 index 0000000..7bce6ee --- /dev/null +++ b/demo/src/app/components/rating/demos/config/rating-config.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdRatingConfig } from './rating-config'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdRatingConfig], + exports: [NgbdRatingConfig], + bootstrap: [NgbdRatingConfig] +}) +export class NgbdRatingConfigModule {} diff --git a/demo/src/app/components/rating/demos/config/rating-config.ts b/demo/src/app/components/rating/demos/config/rating-config.ts new file mode 100644 index 0000000..281a3f7 --- /dev/null +++ b/demo/src/app/components/rating/demos/config/rating-config.ts @@ -0,0 +1,15 @@ +import {Component} from '@angular/core'; +import {NgbRatingConfig} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-rating-config', + templateUrl: './rating-config.html', + providers: [NgbRatingConfig] // add NgbRatingConfig to the component providers +}) +export class NgbdRatingConfig { + constructor(config: NgbRatingConfig) { + // customize default values of ratings used by this component tree + config.max = 5; + config.readonly = true; + } +} diff --git a/demo/src/app/components/rating/demos/decimal/rating-decimal.html b/demo/src/app/components/rating/demos/decimal/rating-decimal.html new file mode 100644 index 0000000..83a7a99 --- /dev/null +++ b/demo/src/app/components/rating/demos/decimal/rating-decimal.html @@ -0,0 +1,14 @@ +

Custom rating template provided via a variable. Shows fine-grained rating display

+ + + + ♥ + + + + + +
+
Rate: {{currentRate}}
+ + diff --git a/demo/src/app/components/rating/demos/decimal/rating-decimal.module.ts b/demo/src/app/components/rating/demos/decimal/rating-decimal.module.ts new file mode 100644 index 0000000..0059994 --- /dev/null +++ b/demo/src/app/components/rating/demos/decimal/rating-decimal.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdRatingDecimal } from './rating-decimal'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdRatingDecimal], + exports: [NgbdRatingDecimal], + bootstrap: [NgbdRatingDecimal] +}) +export class NgbdRatingDecimalModule {} diff --git a/demo/src/app/components/rating/demos/decimal/rating-decimal.ts b/demo/src/app/components/rating/demos/decimal/rating-decimal.ts new file mode 100644 index 0000000..798d6d9 --- /dev/null +++ b/demo/src/app/components/rating/demos/decimal/rating-decimal.ts @@ -0,0 +1,26 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-rating-decimal', + templateUrl: './rating-decimal.html', + styles: [` + .star { + position: relative; + display: inline-block; + font-size: 3rem; + color: #d3d3d3; + } + .full { + color: red; + } + .half { + position: absolute; + display: inline-block; + overflow: hidden; + color: red; + } + `] +}) +export class NgbdRatingDecimal { + currentRate = 3.14; +} diff --git a/demo/src/app/components/rating/demos/events/rating-events.html b/demo/src/app/components/rating/demos/events/rating-events.html new file mode 100644 index 0000000..7ec2131 --- /dev/null +++ b/demo/src/app/components/rating/demos/events/rating-events.html @@ -0,0 +1,9 @@ + +
+
+Selected: {{selected}}
+Hovered: {{hovered}}
+
+ diff --git a/demo/src/app/components/rating/demos/events/rating-events.module.ts b/demo/src/app/components/rating/demos/events/rating-events.module.ts new file mode 100644 index 0000000..eb5d95e --- /dev/null +++ b/demo/src/app/components/rating/demos/events/rating-events.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdRatingEvents } from './rating-events'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdRatingEvents], + exports: [NgbdRatingEvents], + bootstrap: [NgbdRatingEvents] +}) +export class NgbdRatingEventsModule {} diff --git a/demo/src/app/components/rating/demos/events/rating-events.ts b/demo/src/app/components/rating/demos/events/rating-events.ts new file mode 100644 index 0000000..af9ec63 --- /dev/null +++ b/demo/src/app/components/rating/demos/events/rating-events.ts @@ -0,0 +1,11 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-rating-events', + templateUrl: './rating-events.html' +}) +export class NgbdRatingEvents { + selected = 0; + hovered = 0; + readonly = false; +} diff --git a/demo/src/app/components/rating/demos/form/rating-form.html b/demo/src/app/components/rating/demos/form/rating-form.html new file mode 100644 index 0000000..35000ab --- /dev/null +++ b/demo/src/app/components/rating/demos/form/rating-form.html @@ -0,0 +1,16 @@ +

NgModel and reactive forms can be used without the 'rate' binding

+ +
+ +
+
Thanks!
+
Please rate us
+
+
+ +
+
Model: {{ ctrl.value }}
+ + diff --git a/demo/src/app/components/rating/demos/form/rating-form.module.ts b/demo/src/app/components/rating/demos/form/rating-form.module.ts new file mode 100644 index 0000000..794c576 --- /dev/null +++ b/demo/src/app/components/rating/demos/form/rating-form.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdRatingForm } from './rating-form'; + +@NgModule({ + imports: [BrowserModule, ReactiveFormsModule, NgbModule], + declarations: [NgbdRatingForm], + exports: [NgbdRatingForm], + bootstrap: [NgbdRatingForm] +}) +export class NgbdRatingFormModule {} diff --git a/demo/src/app/components/rating/demos/form/rating-form.ts b/demo/src/app/components/rating/demos/form/rating-form.ts new file mode 100644 index 0000000..6c95edc --- /dev/null +++ b/demo/src/app/components/rating/demos/form/rating-form.ts @@ -0,0 +1,18 @@ +import {Component} from '@angular/core'; +import {FormControl, Validators} from '@angular/forms'; + +@Component({ + selector: 'ngbd-rating-form', + templateUrl: './rating-form.html' +}) +export class NgbdRatingForm { + ctrl = new FormControl(null, Validators.required); + + toggle() { + if (this.ctrl.disabled) { + this.ctrl.enable(); + } else { + this.ctrl.disable(); + } + } +} diff --git a/demo/src/app/components/rating/demos/template/rating-template.html b/demo/src/app/components/rating/demos/template/rating-template.html new file mode 100644 index 0000000..f73b55f --- /dev/null +++ b/demo/src/app/components/rating/demos/template/rating-template.html @@ -0,0 +1,9 @@ +

Custom rating template provided as child element

+ + + + + + +
+
Rate: {{currentRate}}
diff --git a/demo/src/app/components/rating/demos/template/rating-template.module.ts b/demo/src/app/components/rating/demos/template/rating-template.module.ts new file mode 100644 index 0000000..433c0a0 --- /dev/null +++ b/demo/src/app/components/rating/demos/template/rating-template.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdRatingTemplate } from './rating-template'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdRatingTemplate], + exports: [NgbdRatingTemplate], + bootstrap: [NgbdRatingTemplate] +}) +export class NgbdRatingTemplateModule {} diff --git a/demo/src/app/components/rating/demos/template/rating-template.ts b/demo/src/app/components/rating/demos/template/rating-template.ts new file mode 100644 index 0000000..9f26183 --- /dev/null +++ b/demo/src/app/components/rating/demos/template/rating-template.ts @@ -0,0 +1,24 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-rating-template', + templateUrl: './rating-template.html', + styles: [` + .star { + font-size: 1.5rem; + color: #b0c4de; + } + .filled { + color: #1e90ff; + } + .bad { + color: #deb0b0; + } + .filled.bad { + color: #ff1e1e; + } + `] +}) +export class NgbdRatingTemplate { + currentRate = 6; +} diff --git a/demo/src/app/components/rating/rating.module.ts b/demo/src/app/components/rating/rating.module.ts new file mode 100644 index 0000000..830a854 --- /dev/null +++ b/demo/src/app/components/rating/rating.module.ts @@ -0,0 +1,88 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component'; +import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared'; +import { NgbdApiPage } from '../shared/api-page/api.component'; +import { NgbdExamplesPage } from '../shared/examples-page/examples.component'; +import { NgbdRatingBasic } from './demos/basic/rating-basic'; +import { NgbdRatingBasicModule } from './demos/basic/rating-basic.module'; +import { NgbdRatingConfig } from './demos/config/rating-config'; +import { NgbdRatingConfigModule } from './demos/config/rating-config.module'; +import { NgbdRatingDecimal } from './demos/decimal/rating-decimal'; +import { NgbdRatingDecimalModule } from './demos/decimal/rating-decimal.module'; +import { NgbdRatingEvents } from './demos/events/rating-events'; +import { NgbdRatingEventsModule } from './demos/events/rating-events.module'; +import { NgbdRatingForm } from './demos/form/rating-form'; +import { NgbdRatingFormModule } from './demos/form/rating-form.module'; +import { NgbdRatingTemplate } from './demos/template/rating-template'; +import { NgbdRatingTemplateModule } from './demos/template/rating-template.module'; + +const DEMOS = { + basic: { + title: 'Basic demo', + type: NgbdRatingBasic, + code: require('!!raw-loader!./demos/basic/rating-basic'), + markup: require('!!raw-loader!./demos/basic/rating-basic.html') + }, + events: { + title: 'Events and readonly ratings', + type: NgbdRatingEvents, + code: require('!!raw-loader!./demos/events/rating-events'), + markup: require('!!raw-loader!./demos/events/rating-events.html') + }, + template: { + title: 'Custom star template', + type: NgbdRatingTemplate, + code: require('!!raw-loader!./demos/template/rating-template'), + markup: require('!!raw-loader!./demos/template/rating-template.html') + }, + decimal: { + title: 'Custom decimal rating', + type: NgbdRatingDecimal, + code: require('!!raw-loader!./demos/decimal/rating-decimal'), + markup: require('!!raw-loader!./demos/decimal/rating-decimal.html') + }, + form: { + title: 'Form integration', + type: NgbdRatingForm, + code: require('!!raw-loader!./demos/form/rating-form'), + markup: require('!!raw-loader!./demos/form/rating-form.html') + }, + config: { + title: 'Global configuration of ratings', + type: NgbdRatingConfig, + code: require('!!raw-loader!./demos/config/rating-config'), + markup: require('!!raw-loader!./demos/config/rating-config.html') + } +}; + +export const ROUTES = [ + { path: '', pathMatch: 'full', redirectTo: 'examples' }, + { + path: '', + component: ComponentWrapper, + children: [ + { path: 'examples', component: NgbdExamplesPage }, + { path: 'api', component: NgbdApiPage } + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, + NgbdComponentsSharedModule, + NgbdRatingBasicModule, + NgbdRatingConfigModule, + NgbdRatingTemplateModule, + NgbdRatingEventsModule, + NgbdRatingDecimalModule, + NgbdRatingFormModule + ] +}) +export class NgbdRatingModule { + constructor(demoList: NgbdDemoList) { + demoList.register('rating', DEMOS); + } +} diff --git a/demo/src/app/components/shared/api-docs/api-docs-badge.component.ts b/demo/src/app/components/shared/api-docs/api-docs-badge.component.ts new file mode 100644 index 0000000..93c9fbe --- /dev/null +++ b/demo/src/app/components/shared/api-docs/api-docs-badge.component.ts @@ -0,0 +1,37 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; + +const BADGES = { + 'Directive': 'success', + 'Component': 'success', + 'Service': 'primary', + 'Configuration': 'primary', + 'Class': 'danger', + 'Interface': 'danger' +}; + +@Component({ + selector: 'ngbd-api-docs-badge', + template: ` +
+ Deprecated {{ deprecated.version }}&ngsp; + Since {{ since.version }}&ngsp; + {{text}} +
+ `, + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class NgbdApiDocsBadge { + + badgeClass; + text; + + @Input() deprecated: {version: string}; + + @Input() since: {version: string}; + + @Input() + set type(type: string) { + this.text = type; + this.badgeClass = `badge-${BADGES[type] || 'secondary'}`; + } +} diff --git a/demo/src/app/components/shared/api-docs/api-docs-class.component.html b/demo/src/app/components/shared/api-docs/api-docs-class.component.html new file mode 100644 index 0000000..717bbe7 --- /dev/null +++ b/demo/src/app/components/shared/api-docs/api-docs-class.component.html @@ -0,0 +1,82 @@ +
+

+ + Anchor link to: {{apiDocs.className}} + + + {{apiDocs.className}}<{{apiDocs.typeParameter}}> + + + Link to Github {{apiDocs.className}} + +

+ +

{{ apiDocs.deprecated.description }}

+

+ + +
+

Properties

+ + + + + + + +
+ {{prop.name}}
+ since {{ prop.since.version }}&ngsp; + deprecated {{ prop.deprecated.version }} +
+

{{ prop.deprecated.description }}

+

+
+
+ Type: {{ prop.type }} +
+
+ Default value: {{prop.defaultValue || '-'}} +
+
+
+
+
+ + +
+

Methods

+ + + + + + + +
+ {{method.name}}
+ since {{ method.since.version }}&ngsp; + deprecated {{ method.deprecated.version }} +
+

+ {{methodSignature(method)}}&ngsp; + => {{ method.returnType }} +

+

{{ method.deprecated.description }}

+

+
+
+
+
+ diff --git a/demo/src/app/components/shared/api-docs/api-docs-class.component.ts b/demo/src/app/components/shared/api-docs/api-docs-class.component.ts new file mode 100644 index 0000000..de6b26a --- /dev/null +++ b/demo/src/app/components/shared/api-docs/api-docs-class.component.ts @@ -0,0 +1,39 @@ +import {Component, ChangeDetectionStrategy, Input} from '@angular/core'; +import docs from '../../../../api-docs'; +import {ClassDesc, MethodDesc, signature} from './api-docs.model'; +import {Analytics} from '../../../shared/analytics/analytics'; + +/** + * Displays the API docs of a class, which is not a directive. + * + * For Config services, use NgbdApiDocsConfig instead. + */ +@Component({ + selector: 'ngbd-api-docs-class', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './api-docs-class.component.html', + styles: [` + .label-cell { + width: 25%; + } + .content-cell { + width: 75%; + } + ` + ] +}) +export class NgbdApiDocsClass { + apiDocs: ClassDesc; + + constructor(private _analytics: Analytics) {} + + @Input() set type(typeName: string) { + this.apiDocs = docs[typeName]; + } + + methodSignature(method: MethodDesc): string { return signature(method); } + + trackSourceClick() { + this._analytics.trackEvent('Source File View', this.apiDocs.className); + } +} diff --git a/demo/src/app/components/shared/api-docs/api-docs-config.component.html b/demo/src/app/components/shared/api-docs/api-docs-config.component.html new file mode 100644 index 0000000..51888db --- /dev/null +++ b/demo/src/app/components/shared/api-docs/api-docs-config.component.html @@ -0,0 +1,40 @@ +
+

+ + Anchor link to: {{apiDocs.className}} + + + {{apiDocs.className}}<{{apiDocs.typeParameter}}> + + + Link to Github {{apiDocs.className}} + +

+ +

{{ apiDocs.deprecated.description }}

+

+ + +
+

Properties

+

+ + {{ property.name }}&ngsp; + +

+

Documentation available in {{ directiveName }}

+
+
+
+ diff --git a/demo/src/app/components/shared/api-docs/api-docs-config.component.ts b/demo/src/app/components/shared/api-docs/api-docs-config.component.ts new file mode 100644 index 0000000..a3b5296 --- /dev/null +++ b/demo/src/app/components/shared/api-docs/api-docs-config.component.ts @@ -0,0 +1,34 @@ +import {Component, ChangeDetectionStrategy, Input} from '@angular/core'; +import docs from '../../../../api-docs'; +import {ClassDesc} from './api-docs.model'; +import {Analytics} from '../../../shared/analytics/analytics'; + +const CONFIG_SUFFIX_LENGTH = 'Config'.length; + +/** + * Displays the API docs of a Config service. A Config service for a component Foo is named, by convention, + * FooConfig, and only has properties, whose name matches with an input of the directive. + * In order to avoid cluttering the demo pages, the only things displayed by this component + * is the description of the Config service and the list of its properties, whose documentation and + * default value is documented in the directive itself. + */ +@Component({ + selector: 'ngbd-api-docs-config', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './api-docs-config.component.html' +}) +export class NgbdApiDocsConfig { + apiDocs: ClassDesc; + directiveName: string; + + constructor(private _analytics: Analytics) {} + + @Input() set type(typeName: string) { + this.apiDocs = docs[typeName]; + this.directiveName = typeName.slice(0, -CONFIG_SUFFIX_LENGTH); + } + + trackSourceClick() { + this._analytics.trackEvent('Source File View', this.apiDocs.className); + } +} diff --git a/demo/src/app/components/shared/api-docs/api-docs.component.html b/demo/src/app/components/shared/api-docs/api-docs.component.html new file mode 100644 index 0000000..fec023d --- /dev/null +++ b/demo/src/app/components/shared/api-docs/api-docs.component.html @@ -0,0 +1,108 @@ +
+

+ + Anchor link to: {{apiDocs.className}} + + + {{apiDocs.className}}<{{apiDocs.typeParameter}}> + + + Link to Github {{apiDocs.className}} + +

+ +

{{ apiDocs.deprecated.description }}

+

+

+ +
+

Selector {{apiDocs.selector}}

+
Exported as {{apiDocs.exportAs}}
+
+ + +
+

Inputs

+ + + + + + + +
+ {{input.name}}
+ since {{ input.since.version }}&ngsp; + deprecated {{ input.deprecated.version }} +
+

{{ input.deprecated.description }}

+

+
+
+ Type: {{ input.type }} +
+
+ Default value: {{ defaultInputValue(input) || '-' }}&ngsp; + — initialized from {{ configServiceName }} service +
+
+
+
+
+ + +
+

Outputs

+ + + + + + + +
+ {{output.name}}
+ since {{ output.since.version }}&ngsp; + deprecated {{ output.deprecated.version }} +
+

{{ output.deprecated.description }}

+

+
+
+
+ + +
+

Methods

+ + + + + + + +
+ {{method.name}}
+ since {{ method.since.version }}&ngsp; + deprecated {{ method.deprecated.version }} +
+

+ {{methodSignature(method)}}&ngsp; + => {{ method.returnType }} +

+

{{ method.deprecated.description }}

+

+
+
+
+
diff --git a/demo/src/app/components/shared/api-docs/api-docs.component.ts b/demo/src/app/components/shared/api-docs/api-docs.component.ts new file mode 100644 index 0000000..6eda10a --- /dev/null +++ b/demo/src/app/components/shared/api-docs/api-docs.component.ts @@ -0,0 +1,77 @@ +import {Component, ChangeDetectionStrategy, Input} from '@angular/core'; +import docs from '../../../../api-docs'; +import {PropertyDesc, DirectiveDesc, InputDesc, MethodDesc, ClassDesc, signature} from './api-docs.model'; +import {Analytics} from '../../../shared/analytics/analytics'; + +/** + * Displays the API docs of a directive. + * + * The default values of its inputs are looked for in the directive api doc itself, or in the matching property + * of associated Config service, if any. + * + * The config service of a directive NgbFoo is, by convention, named NgbFooConfig. + */ +@Component({ + selector: 'ngbd-api-docs', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './api-docs.component.html', + styles: [` + .label-cell { + width: 25%; + } + .content-cell { + width: 75%; + } + ` + ] +}) +export class NgbdApiDocs { + + /** + * Object which contains, for each input name of the directive, the corresponding property of the associated config + * service (if any) + */ + private _configProperties: {[propertyName: string]: PropertyDesc}; + + apiDocs: DirectiveDesc; + configServiceName: string; + + constructor(private _analytics: Analytics) {} + + @Input() set directive(directiveName: string) { + this.apiDocs = docs[directiveName]; + this.configServiceName = `${directiveName}Config`; + const configApiDocs = docs[this.configServiceName]; + this._configProperties = {}; + if (configApiDocs) { + this.apiDocs.inputs.forEach( + input => this._configProperties[input.name] = this._findInputConfigProperty(configApiDocs, input)); + } + } + + /** + * Returns the default value of the given directive input by first looking for it in the matching config service + * property. If there is no matching config property, it reads it from the input. + */ + defaultInputValue(input: InputDesc): string { + const configProperty = this._configProperties[input.name]; + return configProperty ? configProperty.defaultValue : input.defaultValue; + } + + /** + * Returns true if there is a config service property matching with the given directive input + */ + hasConfigProperty(input: InputDesc): boolean { + return !!this._configProperties[input.name]; + } + + methodSignature(method: MethodDesc): string { return signature(method); } + + trackSourceClick() { + this._analytics.trackEvent('Source File View', this.apiDocs.className); + } + + private _findInputConfigProperty(configApiDocs: ClassDesc, input: InputDesc): PropertyDesc { + return configApiDocs.properties.filter(prop => prop.name === input.name)[0]; + } +} diff --git a/demo/src/app/components/shared/api-docs/api-docs.model.ts b/demo/src/app/components/shared/api-docs/api-docs.model.ts new file mode 100644 index 0000000..e1ed598 --- /dev/null +++ b/demo/src/app/components/shared/api-docs/api-docs.model.ts @@ -0,0 +1,55 @@ +export interface ClassDesc { + type: string; + typeParameter: string; + fileName: string; + className: string; + description: string; + deprecated?: VersionDesc; + since?: VersionDesc; + properties: PropertyDesc[]; + methods: MethodDesc[]; +} + +export interface DirectiveDesc extends ClassDesc { + selector: string; + exportAs?: string; + inputs: InputDesc[]; + outputs: OutputDesc[]; +} + +export interface PropertyDesc { + name: string; + type: string; + description: string; + deprecated?: VersionDesc; + since?: VersionDesc; + defaultValue?: string; +} + +export interface MethodDesc { + name: string; + description: string; + deprecated?: VersionDesc; + since?: VersionDesc; + args: ArgumentDesc[]; + returnType: string; +} + +export interface VersionDesc { + version: string; + description: string; +} + +export interface ArgumentDesc { + name: string; + type: string; +} + +export interface InputDesc extends PropertyDesc {} + +export interface OutputDesc extends PropertyDesc {} + +export function signature(method: MethodDesc): string { + const args = method['args'].map(arg => `${arg.name}: ${arg.type}`).join(', '); + return `${method.name}(${args})`; +} diff --git a/demo/src/app/components/shared/api-docs/index.ts b/demo/src/app/components/shared/api-docs/index.ts new file mode 100644 index 0000000..c3f4dfb --- /dev/null +++ b/demo/src/app/components/shared/api-docs/index.ts @@ -0,0 +1,4 @@ +export * from './api-docs.component'; +export * from './api-docs-badge.component'; +export * from './api-docs-class.component'; +export * from './api-docs-config.component'; diff --git a/demo/src/app/components/shared/api-page/api.component.ts b/demo/src/app/components/shared/api-page/api.component.ts new file mode 100644 index 0000000..0345e63 --- /dev/null +++ b/demo/src/app/components/shared/api-page/api.component.ts @@ -0,0 +1,54 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; + +import apiDocs from '../../../../api-docs'; + +export function getApis(component) { + const components = []; + const classes = []; + const configs = []; + Object.values(apiDocs) + .filter(entity => entity.fileName.startsWith(`src/${component}`)) + .forEach(entity => { + switch (entity.type) { + case 'Directive': + case 'Component': + components.push(entity.className); + break; + + case 'Service': + if (entity.className.endsWith('Config')) { + configs.push(entity.className); + } else { + classes.push(entity.className); + } + break; + default: + classes.push(entity.className); + break; + } + }); + return { components, classes, configs }; +} + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + + ` +}) +export class NgbdApiPage { + classes: string[]; + components: string[]; + configs: string[]; + + constructor(route: ActivatedRoute) { + const component = route.parent.parent.snapshot.url[1].path; + const apis = getApis(component); + this.components = apis.components.sort(); + this.classes = apis.classes.sort(); + this.configs = apis.configs.sort(); + } +} diff --git a/demo/src/app/components/shared/demo-list.ts b/demo/src/app/components/shared/demo-list.ts new file mode 100644 index 0000000..0cb88d6 --- /dev/null +++ b/demo/src/app/components/shared/demo-list.ts @@ -0,0 +1,40 @@ +import {Injectable} from '@angular/core'; + +export interface NgbdDemoConfig { + title: string; + code?: string; + markup?: string; + type: any; + files?: Array<{[name: string]: string}>; + showCode?: boolean; +} + +export interface NgbdDemoListConfig { [demo: string]: NgbdDemoConfig; } + +export interface NgbdDemoOverviewConfig { [anchor: string]: string; } + +@Injectable({providedIn: 'root'}) +export class NgbdDemoList { + private _demos: {[widget: string]: NgbdDemoListConfig} = {}; + + private _overviews: {[widget: string]: NgbdDemoOverviewConfig} = {}; + + + register(widget: string, list: NgbdDemoListConfig, overview?: NgbdDemoOverviewConfig) { + this._demos[widget] = list; + if (overview) { + this._overviews[widget] = overview; + } + } + + getDemos(widget: string) { return this._demos[widget]; } + + getOverviewSections(widget: string) { + const overview = this._overviews[widget]; + const sections = {}; + if (overview) { + Object.keys(overview).forEach(fragment => { sections[fragment] = {fragment, title: overview[fragment]}; }); + } + return sections; + } +} diff --git a/demo/src/app/components/shared/examples-page/demo.component.html b/demo/src/app/components/shared/examples-page/demo.component.html new file mode 100644 index 0000000..d3488c4 --- /dev/null +++ b/demo/src/app/components/shared/examples-page/demo.component.html @@ -0,0 +1,62 @@ +
+ +

+ + Anchor link to: {{id}} + + {{ demoTitle }} + + + StackBlitz icon + StackBlitz + +

+
+
+ + + + + {{file.name}} + + + + + + + + + + + {{component}}-{{id}}.html + + + + + + + + {{component}}-{{id}}.ts + + + + + + + +
+
+ +
+ +
+
diff --git a/demo/src/app/components/shared/examples-page/demo.component.ts b/demo/src/app/components/shared/examples-page/demo.component.ts new file mode 100644 index 0000000..d1b47b8 --- /dev/null +++ b/demo/src/app/components/shared/examples-page/demo.component.ts @@ -0,0 +1,59 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; + +import {Analytics} from '../../../shared/analytics/analytics'; +import {Snippet} from '../../../shared/code/snippet'; + +@Component({ + selector: 'ngbd-widget-demo', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './demo.component.html' +}) +export class NgbdWidgetDemoComponent { + @Input() demoTitle: string; + @Input() component: string; + @Input() id: string; + @Input() code: string; + @Input() markup: string; + @Input() files: { name: string; source: string }[]; + @Input() showCode = false; + + get markupSnippet() { return Snippet({lang: 'html', code: this.markup}); } + get codeSnippet() { return Snippet({lang: 'typescript', code: this.code}); } + + getFileSnippet({name, source}) { + return Snippet({code: source, lang: name.split('.').pop()}); + } + + get hasManyFiles() { + return this.files && this.files.length > 5; + } + + constructor(private _analytics: Analytics) {} + + tabType(name: string) { + const ext = name.split('.').pop(); + return ( + { + html: 'HTML', + scss: 'Style (SCSS)', + css: 'Style (CSS)', + ts: 'Typescript' + }[ext] || 'Code' + ); + } + + trackStackBlitzClick() { + this._analytics.trackEvent( + 'StackBlitz View', + this.component + ' ' + this.id + ); + } + trackShowCodeClick() { + if (this.showCode) { + this._analytics.trackEvent( + 'Show Code View', + this.component + ' ' + this.id + ); + } + } +} diff --git a/demo/src/app/components/shared/examples-page/examples.component.ts b/demo/src/app/components/shared/examples-page/examples.component.ts new file mode 100644 index 0000000..d2b52cf --- /dev/null +++ b/demo/src/app/components/shared/examples-page/examples.component.ts @@ -0,0 +1,40 @@ +import {Component} from '@angular/core'; +import {ActivatedRoute} from '@angular/router'; + +import {NgbdDemoList} from '../demo-list'; + +@Component({ + template: ` + + + + ` +}) +export class NgbdExamplesPage { + component: string; + demos = []; + + constructor(route: ActivatedRoute, demoList: NgbdDemoList) { + // We go up to parent route defining /components/:widget to read the widget name + // This route is declared in root app.routing.ts. + const componentName = (this.component = + route.parent.parent.snapshot.url[1].path); + if (componentName) { + const demos = demoList.getDemos(componentName); + if (demos) { + this.demos = Object.keys(demos).map(id => { + return { id, ...demos[id] }; + }); + } + } + } +} diff --git a/demo/src/app/components/shared/index.ts b/demo/src/app/components/shared/index.ts new file mode 100644 index 0000000..82e5348 --- /dev/null +++ b/demo/src/app/components/shared/index.ts @@ -0,0 +1,37 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { NgbdApiDocs, NgbdApiDocsBadge, NgbdApiDocsClass, NgbdApiDocsConfig } from './api-docs'; +import { NgbdApiPage } from './api-page/api.component'; +import { NgbdWidgetDemoComponent } from './examples-page/demo.component'; +import { NgbdExamplesPage } from './examples-page/examples.component'; +import { NgbdOverviewDirective, NgbdOverviewSectionComponent } from './overview'; + +export * from './demo-list'; + +@NgModule({ + imports: [NgbdSharedModule], + declarations: [ + NgbdApiDocsBadge, + NgbdApiDocs, + NgbdApiDocsClass, + NgbdApiDocsConfig, + NgbdOverviewDirective, + NgbdOverviewSectionComponent, + NgbdExamplesPage, + NgbdApiPage, + NgbdWidgetDemoComponent + ], + exports: [ + NgbdApiDocsBadge, + NgbdApiDocs, + NgbdApiDocsClass, + NgbdApiDocsConfig, + NgbdOverviewDirective, + NgbdOverviewSectionComponent, + NgbdExamplesPage, + NgbdApiPage, + NgbdWidgetDemoComponent + ] +}) +export class NgbdComponentsSharedModule {} diff --git a/demo/src/app/components/shared/overview/index.ts b/demo/src/app/components/shared/overview/index.ts new file mode 100644 index 0000000..069033e --- /dev/null +++ b/demo/src/app/components/shared/overview/index.ts @@ -0,0 +1,3 @@ +export * from './overview'; +export * from './overview.directive'; +export * from './overview-section.component'; diff --git a/demo/src/app/components/shared/overview/overview-section.component.ts b/demo/src/app/components/shared/overview/overview-section.component.ts new file mode 100644 index 0000000..92adcd4 --- /dev/null +++ b/demo/src/app/components/shared/overview/overview-section.component.ts @@ -0,0 +1,24 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; + +import { NgbdOverviewSection } from './overview'; + +@Component({ + selector: 'ngbd-overview-section', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + 'class': 'd-block' + }, + template: ` +

+ + + + {{ section.title }} +

+ + + ` +}) +export class NgbdOverviewSectionComponent { + @Input() section: NgbdOverviewSection; +} diff --git a/demo/src/app/components/shared/overview/overview.directive.ts b/demo/src/app/components/shared/overview/overview.directive.ts new file mode 100644 index 0000000..31478cc --- /dev/null +++ b/demo/src/app/components/shared/overview/overview.directive.ts @@ -0,0 +1,7 @@ +import {Directive} from '@angular/core'; + +@Directive({ + selector: '[ngbdOverview]' +}) +export class NgbdOverviewDirective { +} diff --git a/demo/src/app/components/shared/overview/overview.ts b/demo/src/app/components/shared/overview/overview.ts new file mode 100644 index 0000000..b6e7a8f --- /dev/null +++ b/demo/src/app/components/shared/overview/overview.ts @@ -0,0 +1,8 @@ +export interface NgbdOverviewSection { + title: string | false; + fragment?: string; +} + +export interface NgbdOverview { + [fragment: string]: NgbdOverviewSection; +} diff --git a/demo/src/app/components/table/demos/basic/table-basic.html b/demo/src/app/components/table/demos/basic/table-basic.html new file mode 100644 index 0000000..450d906 --- /dev/null +++ b/demo/src/app/components/table/demos/basic/table-basic.html @@ -0,0 +1,23 @@ +

Table is just a mapping of objects to table rows with ngFor

+ + + + + + + + + + + + + + + + + + +
#CountryAreaPopulation
{{ i + 1 }} + + {{ country.name }} + {{ country.area | number }}{{ country.population | number }}
diff --git a/demo/src/app/components/table/demos/basic/table-basic.module.ts b/demo/src/app/components/table/demos/basic/table-basic.module.ts new file mode 100644 index 0000000..f088e73 --- /dev/null +++ b/demo/src/app/components/table/demos/basic/table-basic.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTableBasic } from './table-basic'; + +@NgModule({ + imports: [BrowserModule, CommonModule, NgbModule], + declarations: [NgbdTableBasic], + exports: [NgbdTableBasic], + bootstrap: [NgbdTableBasic] +}) +export class NgbdTableBasicModule {} diff --git a/demo/src/app/components/table/demos/basic/table-basic.ts b/demo/src/app/components/table/demos/basic/table-basic.ts new file mode 100644 index 0000000..bef9a4c --- /dev/null +++ b/demo/src/app/components/table/demos/basic/table-basic.ts @@ -0,0 +1,44 @@ +import { Component } from '@angular/core'; + +interface Country { + name: string; + flag: string; + area: number; + population: number; +} + +const COUNTRIES: Country[] = [ + { + name: 'Russia', + flag: 'f/f3/Flag_of_Russia.svg', + area: 17075200, + population: 146989754 + }, + { + name: 'Canada', + flag: 'c/cf/Flag_of_Canada.svg', + area: 9976140, + population: 36624199 + }, + { + name: 'United States', + flag: 'a/a4/Flag_of_the_United_States.svg', + area: 9629091, + population: 324459463 + }, + { + name: 'China', + flag: 'f/fa/Flag_of_the_People%27s_Republic_of_China.svg', + area: 9596960, + population: 1409517397 + } +]; + +@Component({ + selector: 'ngbd-table-basic', + templateUrl: './table-basic.html' +}) +export class NgbdTableBasic { + + countries = COUNTRIES; +} diff --git a/demo/src/app/components/table/demos/complete/countries.ts b/demo/src/app/components/table/demos/complete/countries.ts new file mode 100644 index 0000000..b7fd4be --- /dev/null +++ b/demo/src/app/components/table/demos/complete/countries.ts @@ -0,0 +1,95 @@ +import {Country} from './country'; + +export const COUNTRIES: Country[] = [ + { + id: 1, + name: 'Russia', + flag: 'f/f3/Flag_of_Russia.svg', + area: 17075200, + population: 146989754 + }, + { + id: 2, + name: 'France', + flag: 'c/c3/Flag_of_France.svg', + area: 640679, + population: 64979548 + }, + { + id: 3, + name: 'Germany', + flag: 'b/ba/Flag_of_Germany.svg', + area: 357114, + population: 82114224 + }, + { + id: 4, + name: 'Portugal', + flag: '5/5c/Flag_of_Portugal.svg', + area: 92090, + population: 10329506 + }, + { + id: 5, + name: 'Canada', + flag: 'c/cf/Flag_of_Canada.svg', + area: 9976140, + population: 36624199 + }, + { + id: 6, + name: 'Vietnam', + flag: '2/21/Flag_of_Vietnam.svg', + area: 331212, + population: 95540800 + }, + { + id: 7, + name: 'Brazil', + flag: '0/05/Flag_of_Brazil.svg', + area: 8515767, + population: 209288278 + }, + { + id: 8, + name: 'Mexico', + flag: 'f/fc/Flag_of_Mexico.svg', + area: 1964375, + population: 129163276 + }, + { + id: 9, + name: 'United States', + flag: 'a/a4/Flag_of_the_United_States.svg', + area: 9629091, + population: 324459463 + }, + { + id: 10, + name: 'India', + flag: '4/41/Flag_of_India.svg', + area: 3287263, + population: 1324171354 + }, + { + id: 11, + name: 'Indonesia', + flag: '9/9f/Flag_of_Indonesia.svg', + area: 1910931, + population: 263991379 + }, + { + id: 12, + name: 'Tuvalu', + flag: '3/38/Flag_of_Tuvalu.svg', + area: 26, + population: 11097 + }, + { + id: 13, + name: 'China', + flag: 'f/fa/Flag_of_the_People%27s_Republic_of_China.svg', + area: 9596960, + population: 1409517397 + } +]; diff --git a/demo/src/app/components/table/demos/complete/country.service.ts b/demo/src/app/components/table/demos/complete/country.service.ts new file mode 100644 index 0000000..5b31652 --- /dev/null +++ b/demo/src/app/components/table/demos/complete/country.service.ts @@ -0,0 +1,107 @@ +import {Injectable, PipeTransform} from '@angular/core'; + +import {BehaviorSubject, Observable, of, Subject} from 'rxjs'; + +import {Country} from './country'; +import {COUNTRIES} from './countries'; +import {DecimalPipe} from '@angular/common'; +import {debounceTime, delay, switchMap, tap} from 'rxjs/operators'; +import {SortDirection} from './sortable.directive'; + +interface SearchResult { + countries: Country[]; + total: number; +} + +interface State { + page: number; + pageSize: number; + searchTerm: string; + sortColumn: string; + sortDirection: SortDirection; +} + +function compare(v1, v2) { + return v1 < v2 ? -1 : v1 > v2 ? 1 : 0; +} + +function sort(countries: Country[], column: string, direction: string): Country[] { + if (direction === '') { + return countries; + } else { + return [...countries].sort((a, b) => { + const res = compare(a[column], b[column]); + return direction === 'asc' ? res : -res; + }); + } +} + +function matches(country: Country, term: string, pipe: PipeTransform) { + return country.name.toLowerCase().includes(term.toLowerCase()) + || pipe.transform(country.area).includes(term) + || pipe.transform(country.population).includes(term); +} + +@Injectable({providedIn: 'root'}) +export class CountryService { + private _loading$ = new BehaviorSubject(true); + private _search$ = new Subject(); + private _countries$ = new BehaviorSubject([]); + private _total$ = new BehaviorSubject(0); + + private _state: State = { + page: 1, + pageSize: 4, + searchTerm: '', + sortColumn: '', + sortDirection: '' + }; + + constructor(private pipe: DecimalPipe) { + this._search$.pipe( + tap(() => this._loading$.next(true)), + debounceTime(200), + switchMap(() => this._search()), + delay(200), + tap(() => this._loading$.next(false)) + ).subscribe(result => { + this._countries$.next(result.countries); + this._total$.next(result.total); + }); + + this._search$.next(); + } + + get countries$() { return this._countries$.asObservable(); } + get total$() { return this._total$.asObservable(); } + get loading$() { return this._loading$.asObservable(); } + get page() { return this._state.page; } + get pageSize() { return this._state.pageSize; } + get searchTerm() { return this._state.searchTerm; } + + set page(page: number) { this._set({page}); } + set pageSize(pageSize: number) { this._set({pageSize}); } + set searchTerm(searchTerm: string) { this._set({searchTerm}); } + set sortColumn(sortColumn: string) { this._set({sortColumn}); } + set sortDirection(sortDirection: SortDirection) { this._set({sortDirection}); } + + private _set(patch: Partial) { + Object.assign(this._state, patch); + this._search$.next(); + } + + private _search(): Observable { + const {sortColumn, sortDirection, pageSize, page, searchTerm} = this._state; + + // 1. sort + let countries = sort(COUNTRIES, sortColumn, sortDirection); + + // 2. filter + countries = countries.filter(country => matches(country, searchTerm, this.pipe)); + const total = countries.length; + + // 3. paginate + countries = countries.slice((page - 1) * pageSize, (page - 1) * pageSize + pageSize); + return of({countries, total}); + } +} diff --git a/demo/src/app/components/table/demos/complete/country.ts b/demo/src/app/components/table/demos/complete/country.ts new file mode 100644 index 0000000..da1820b --- /dev/null +++ b/demo/src/app/components/table/demos/complete/country.ts @@ -0,0 +1,7 @@ +export interface Country { + id: number; + name: string; + flag: string; + area: number; + population: number; +} diff --git a/demo/src/app/components/table/demos/complete/sortable.directive.ts b/demo/src/app/components/table/demos/complete/sortable.directive.ts new file mode 100644 index 0000000..d8f11d1 --- /dev/null +++ b/demo/src/app/components/table/demos/complete/sortable.directive.ts @@ -0,0 +1,29 @@ +import {Directive, EventEmitter, Input, Output} from '@angular/core'; + +export type SortDirection = 'asc' | 'desc' | ''; +const rotate: {[key: string]: SortDirection} = { 'asc': 'desc', 'desc': '', '': 'asc' }; + +export interface SortEvent { + column: string; + direction: SortDirection; +} + +@Directive({ + selector: 'th[sortable]', + host: { + '[class.asc]': 'direction === "asc"', + '[class.desc]': 'direction === "desc"', + '(click)': 'rotate()' + } +}) +export class NgbdSortableHeader { + + @Input() sortable: string; + @Input() direction: SortDirection = ''; + @Output() sort = new EventEmitter(); + + rotate() { + this.direction = rotate[this.direction]; + this.sort.emit({column: this.sortable, direction: this.direction}); + } +} diff --git a/demo/src/app/components/table/demos/complete/table-complete.html b/demo/src/app/components/table/demos/complete/table-complete.html new file mode 100644 index 0000000..9413bb1 --- /dev/null +++ b/demo/src/app/components/table/demos/complete/table-complete.html @@ -0,0 +1,50 @@ +

This is a more complete example with a service that simulates server calling:

+ +
    +
  • an observable async service to fetch a list of countries
  • +
  • sorting, filtering and pagination
  • +
  • simulated delay and loading indicator
  • +
  • debouncing of search requests
  • +
+ +
+
+ Full text search: + Loading... +
+ + + + + + + + + + + + + + + + + + +
#CountryAreaPopulation
{{ country.id }} + + +
+ +
+ + + + +
+ +
diff --git a/demo/src/app/components/table/demos/complete/table-complete.module.ts b/demo/src/app/components/table/demos/complete/table-complete.module.ts new file mode 100644 index 0000000..033d34c --- /dev/null +++ b/demo/src/app/components/table/demos/complete/table-complete.module.ts @@ -0,0 +1,22 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdSortableHeader } from './sortable.directive'; +import { NgbdTableComplete } from './table-complete'; + +@NgModule({ + imports: [ + BrowserModule, + CommonModule, + FormsModule, + ReactiveFormsModule, + NgbModule + ], + declarations: [NgbdTableComplete, NgbdSortableHeader], + exports: [NgbdTableComplete], + bootstrap: [NgbdTableComplete] +}) +export class NgbdTableCompleteModule {} diff --git a/demo/src/app/components/table/demos/complete/table-complete.ts b/demo/src/app/components/table/demos/complete/table-complete.ts new file mode 100644 index 0000000..9f5cc55 --- /dev/null +++ b/demo/src/app/components/table/demos/complete/table-complete.ts @@ -0,0 +1,34 @@ +import {DecimalPipe} from '@angular/common'; +import {Component, QueryList, ViewChildren} from '@angular/core'; +import {Observable} from 'rxjs'; + +import {Country} from './country'; +import {CountryService} from './country.service'; +import {NgbdSortableHeader, SortEvent} from './sortable.directive'; + + +@Component( + {selector: 'ngbd-table-complete', templateUrl: './table-complete.html', providers: [CountryService, DecimalPipe]}) +export class NgbdTableComplete { + countries$: Observable; + total$: Observable; + + @ViewChildren(NgbdSortableHeader) headers: QueryList; + + constructor(public service: CountryService) { + this.countries$ = service.countries$; + this.total$ = service.total$; + } + + onSort({column, direction}: SortEvent) { + // resetting other headers + this.headers.forEach(header => { + if (header.sortable !== column) { + header.direction = ''; + } + }); + + this.service.sortColumn = column; + this.service.sortDirection = direction; + } +} diff --git a/demo/src/app/components/table/demos/filtering/table-filtering.html b/demo/src/app/components/table/demos/filtering/table-filtering.html new file mode 100644 index 0000000..2d8ed2d --- /dev/null +++ b/demo/src/app/components/table/demos/filtering/table-filtering.html @@ -0,0 +1,29 @@ +

You can do filter table data, in this case with observables and our NgbHighlight component used in Typeahead

+ +
+
+ Full text search: +
+
+ + + + + + + + + + + + + + + + + + +
#CountryAreaPopulation
{{ i + 1 }} + + +
diff --git a/demo/src/app/components/table/demos/filtering/table-filtering.module.ts b/demo/src/app/components/table/demos/filtering/table-filtering.module.ts new file mode 100644 index 0000000..d31acad --- /dev/null +++ b/demo/src/app/components/table/demos/filtering/table-filtering.module.ts @@ -0,0 +1,21 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTableFiltering } from './table-filtering'; + +@NgModule({ + imports: [ + BrowserModule, + CommonModule, + FormsModule, + ReactiveFormsModule, + NgbModule + ], + declarations: [NgbdTableFiltering], + exports: [NgbdTableFiltering], + bootstrap: [NgbdTableFiltering] +}) +export class NgbdTableFilteringModule {} diff --git a/demo/src/app/components/table/demos/filtering/table-filtering.ts b/demo/src/app/components/table/demos/filtering/table-filtering.ts new file mode 100644 index 0000000..6bb32e5 --- /dev/null +++ b/demo/src/app/components/table/demos/filtering/table-filtering.ts @@ -0,0 +1,67 @@ +import { Component, PipeTransform } from '@angular/core'; +import { DecimalPipe } from '@angular/common'; +import { FormControl } from '@angular/forms'; + +import { Observable } from 'rxjs'; +import { map, startWith } from 'rxjs/operators'; + +interface Country { + name: string; + flag: string; + area: number; + population: number; +} + +const COUNTRIES: Country[] = [ + { + name: 'Russia', + flag: 'f/f3/Flag_of_Russia.svg', + area: 17075200, + population: 146989754 + }, + { + name: 'Canada', + flag: 'c/cf/Flag_of_Canada.svg', + area: 9976140, + population: 36624199 + }, + { + name: 'United States', + flag: 'a/a4/Flag_of_the_United_States.svg', + area: 9629091, + population: 324459463 + }, + { + name: 'China', + flag: 'f/fa/Flag_of_the_People%27s_Republic_of_China.svg', + area: 9596960, + population: 1409517397 + } +]; + +function search(text: string, pipe: PipeTransform): Country[] { + return COUNTRIES.filter(country => { + const term = text.toLowerCase(); + return country.name.toLowerCase().includes(term) + || pipe.transform(country.area).includes(term) + || pipe.transform(country.population).includes(term); + }); +} + +@Component({ + selector: 'ngbd-table-filtering', + templateUrl: './table-filtering.html', + providers: [DecimalPipe] +}) +export class NgbdTableFiltering { + + countries$: Observable; + filter = new FormControl(''); + + constructor(pipe: DecimalPipe) { + this.countries$ = this.filter.valueChanges.pipe( + startWith(''), + map(text => search(text, pipe)) + ); + } +} diff --git a/demo/src/app/components/table/demos/pagination/table-pagination.html b/demo/src/app/components/table/demos/pagination/table-pagination.html new file mode 100644 index 0000000..7423244 --- /dev/null +++ b/demo/src/app/components/table/demos/pagination/table-pagination.html @@ -0,0 +1,34 @@ +

You can bind our NgbPagination component with slicing the data list

+ + + + + + + + + + + + + + + + + + +
#CountryAreaPopulation
{{ country.id }} + + {{ country.name }} + {{ country.area | number}}{{ country.population | number }}
+ +
+ + + + +
diff --git a/demo/src/app/components/table/demos/pagination/table-pagination.module.ts b/demo/src/app/components/table/demos/pagination/table-pagination.module.ts new file mode 100644 index 0000000..5a326d0 --- /dev/null +++ b/demo/src/app/components/table/demos/pagination/table-pagination.module.ts @@ -0,0 +1,15 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTablePagination } from './table-pagination'; + +@NgModule({ + imports: [BrowserModule, CommonModule, FormsModule, NgbModule], + declarations: [NgbdTablePagination], + exports: [NgbdTablePagination], + bootstrap: [NgbdTablePagination] +}) +export class NgbdTablePaginationModule {} diff --git a/demo/src/app/components/table/demos/pagination/table-pagination.ts b/demo/src/app/components/table/demos/pagination/table-pagination.ts new file mode 100644 index 0000000..1f553cf --- /dev/null +++ b/demo/src/app/components/table/demos/pagination/table-pagination.ts @@ -0,0 +1,107 @@ +import { Component } from '@angular/core'; + +interface Country { + id?: number; + name: string; + flag: string; + area: number; + population: number; +} + +const COUNTRIES: Country[] = [ + { + name: 'Russia', + flag: 'f/f3/Flag_of_Russia.svg', + area: 17075200, + population: 146989754 + }, + { + name: 'France', + flag: 'c/c3/Flag_of_France.svg', + area: 640679, + population: 64979548 + }, + { + name: 'Germany', + flag: 'b/ba/Flag_of_Germany.svg', + area: 357114, + population: 82114224 + }, + { + name: 'Portugal', + flag: '5/5c/Flag_of_Portugal.svg', + area: 92090, + population: 10329506 + }, + { + name: 'Canada', + flag: 'c/cf/Flag_of_Canada.svg', + area: 9976140, + population: 36624199 + }, + { + name: 'Vietnam', + flag: '2/21/Flag_of_Vietnam.svg', + area: 331212, + population: 95540800 + }, + { + name: 'Brazil', + flag: '0/05/Flag_of_Brazil.svg', + area: 8515767, + population: 209288278 + }, + { + name: 'Mexico', + flag: 'f/fc/Flag_of_Mexico.svg', + area: 1964375, + population: 129163276 + }, + { + name: 'United States', + flag: 'a/a4/Flag_of_the_United_States.svg', + area: 9629091, + population: 324459463 + }, + { + name: 'India', + flag: '4/41/Flag_of_India.svg', + area: 3287263, + population: 1324171354 + }, + { + name: 'Indonesia', + flag: '9/9f/Flag_of_Indonesia.svg', + area: 1910931, + population: 263991379 + }, + { + name: 'Tuvalu', + flag: '3/38/Flag_of_Tuvalu.svg', + area: 26, + population: 11097 + }, + { + name: 'China', + flag: 'f/fa/Flag_of_the_People%27s_Republic_of_China.svg', + area: 9596960, + population: 1409517397 + } +]; + +@Component({ + selector: 'ngbd-table-pagination', + templateUrl: './table-pagination.html' +}) +export class NgbdTablePagination { + + page = 1; + pageSize = 4; + collectionSize = COUNTRIES.length; + + get countries(): Country[] { + return COUNTRIES + .map((country, i) => ({id: i + 1, ...country})) + .slice((this.page - 1) * this.pageSize, (this.page - 1) * this.pageSize + this.pageSize); + } +} diff --git a/demo/src/app/components/table/demos/sortable/table-sortable.html b/demo/src/app/components/table/demos/sortable/table-sortable.html new file mode 100644 index 0000000..18acb18 --- /dev/null +++ b/demo/src/app/components/table/demos/sortable/table-sortable.html @@ -0,0 +1,23 @@ +

You can introduce custom directives for table headers to sort columns

+ + + + + + + + + + + + + + + + + + +
#CountryAreaPopulation
{{ country.id }} + + {{ country.name }} + {{ country.area | number }}{{ country.population | number }}
diff --git a/demo/src/app/components/table/demos/sortable/table-sortable.module.ts b/demo/src/app/components/table/demos/sortable/table-sortable.module.ts new file mode 100644 index 0000000..7590bbc --- /dev/null +++ b/demo/src/app/components/table/demos/sortable/table-sortable.module.ts @@ -0,0 +1,14 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdSortableHeader, NgbdTableSortable } from './table-sortable'; + +@NgModule({ + imports: [BrowserModule, CommonModule, NgbModule], + declarations: [NgbdTableSortable, NgbdSortableHeader], + exports: [NgbdTableSortable], + bootstrap: [NgbdTableSortable] +}) +export class NgbdTableSortableModule {} diff --git a/demo/src/app/components/table/demos/sortable/table-sortable.ts b/demo/src/app/components/table/demos/sortable/table-sortable.ts new file mode 100644 index 0000000..1d232f6 --- /dev/null +++ b/demo/src/app/components/table/demos/sortable/table-sortable.ts @@ -0,0 +1,101 @@ +import { Component, Directive, EventEmitter, Input, Output, QueryList, ViewChildren } from '@angular/core'; + +interface Country { + id: number; + name: string; + flag: string; + area: number; + population: number; +} + +const COUNTRIES: Country[] = [ + { + id: 1, + name: 'Russia', + flag: 'f/f3/Flag_of_Russia.svg', + area: 17075200, + population: 146989754 + }, + { + id: 2, + name: 'Canada', + flag: 'c/cf/Flag_of_Canada.svg', + area: 9976140, + population: 36624199 + }, + { + id: 3, + name: 'United States', + flag: 'a/a4/Flag_of_the_United_States.svg', + area: 9629091, + population: 324459463 + }, + { + id: 4, + name: 'China', + flag: 'f/fa/Flag_of_the_People%27s_Republic_of_China.svg', + area: 9596960, + population: 1409517397 + } +]; + +export type SortDirection = 'asc' | 'desc' | ''; +const rotate: {[key: string]: SortDirection} = { 'asc': 'desc', 'desc': '', '': 'asc' }; +export const compare = (v1, v2) => v1 < v2 ? -1 : v1 > v2 ? 1 : 0; + +export interface SortEvent { + column: string; + direction: SortDirection; +} + +@Directive({ + selector: 'th[sortable]', + host: { + '[class.asc]': 'direction === "asc"', + '[class.desc]': 'direction === "desc"', + '(click)': 'rotate()' + } +}) +export class NgbdSortableHeader { + + @Input() sortable: string; + @Input() direction: SortDirection = ''; + @Output() sort = new EventEmitter(); + + rotate() { + this.direction = rotate[this.direction]; + this.sort.emit({column: this.sortable, direction: this.direction}); + } +} + +@Component({ + selector: 'ngbd-table-sortable', + templateUrl: './table-sortable.html' +}) +export class NgbdTableSortable { + + countries = COUNTRIES; + + @ViewChildren(NgbdSortableHeader) headers: QueryList; + + onSort({column, direction}: SortEvent) { + + // resetting other headers + this.headers.forEach(header => { + if (header.sortable !== column) { + header.direction = ''; + } + }); + + // sorting countries + if (direction === '') { + this.countries = COUNTRIES; + } else { + this.countries = [...COUNTRIES].sort((a, b) => { + const res = compare(a[column], b[column]); + return direction === 'asc' ? res : -res; + }); + } + } + +} diff --git a/demo/src/app/components/table/overview/demo/table-overview-demo.component.ts b/demo/src/app/components/table/overview/demo/table-overview-demo.component.ts new file mode 100644 index 0000000..8746d24 --- /dev/null +++ b/demo/src/app/components/table/overview/demo/table-overview-demo.component.ts @@ -0,0 +1,58 @@ +import { Component } from '@angular/core'; + +interface Country { + name: string; + flag: string; + area: number; + population: number; +} + +@Component({ + selector: 'ngbd-table-overview-demo', + template: ` + + + + + + + + + + + + + + + + + +
#CountryAreaPopulation
{{ i + 1 }} + + {{ country.name }} + {{ country.area | number }}{{ country.population | number }}
+ ` +}) +export class NgbdTableOverviewDemo { + + countries: Country[] = [ + { + name: 'Russia', + flag: 'f/f3/Flag_of_Russia.svg', + area: 17075200, + population: 146989754 + }, + { + name: 'Canada', + flag: 'c/cf/Flag_of_Canada.svg', + area: 9976140, + population: 36624199 + }, + { + name: 'United States', + flag: 'a/a4/Flag_of_the_United_States.svg', + area: 9629091, + population: 324459463 + } + ]; +} diff --git a/demo/src/app/components/table/overview/table-overview.component.html b/demo/src/app/components/table/overview/table-overview.component.html new file mode 100644 index 0000000..97a46a8 --- /dev/null +++ b/demo/src/app/components/table/overview/table-overview.component.html @@ -0,0 +1,67 @@ +

+ Bootstrap provides the some basic styling for the tables + including CSS classes for responsiveness, striping odd/even rows, changing borders and captions, hovering rows, etc. + These styles are opt-in and can be used with pure Angular to produce something like this: +

+ + + +
+ + + At the moment we do not have plans to provide a dedicated component like NgbTable or NgbGrid + as a part of ng-bootstrap project. As usual we're open to the productive discussion on GitHub. + + + +

+ Most importantly, there are way too many different use cases and options for such a complex component. + Instead of building a monster-of-a-widget with hundreds of options and customizations, we would + encourage you to use composition and pure Angular. Most tables don't need all the features + and if you want a spreadsheet-like functionality there are dedicated libraries available. +

+ +

+ Think about implementing the features you need and wrapping them into a component for your application. + It might be simpler than it seems. +

+ +

+ If you decide to choose a library for tables, make sure that it plays nicely with Angular: +

+ +
    +
  • doesn't trigger change detection excessively
  • +
  • doesn't generate thousands of DOM nodes
  • +
  • doesn't bloat your resulting bundle size by bringing 3rd party dependencies
  • +
  • doesn't pretend to be an Angular library by wrapping something else
  • +
+
+ + +

+ Having said that, we decided to give you some simple examples of common table features. + Take a look at them for the inspiration and maybe even use them as a starting point. +

+ +
    +
  • + Sorting - + shows a sample NgbdSortableHeader directive that you can stick on a <th> + element to handle sorting +
  • +
  • + Pagination - + shows how to use a NgbPagination component together with the table +
  • +
  • + Search / filtering - + full text search example over the table data +
  • +
  • + Service example - + a service that will handle sorting, pagination and filtering in an asynchronous manner. + It is based on observables and simulates debouncing and a custom delay for the data fetch. +
  • +
+
diff --git a/demo/src/app/components/table/overview/table-overview.component.ts b/demo/src/app/components/table/overview/table-overview.component.ts new file mode 100644 index 0000000..ed22f32 --- /dev/null +++ b/demo/src/app/components/table/overview/table-overview.component.ts @@ -0,0 +1,25 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +import { environment } from '../../../../environments/environment'; + +import { NgbdDemoList } from '../../shared'; +import { NgbdOverview } from '../../shared/overview'; + +@Component({ + selector: 'ngbd-table-overview', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './table-overview.component.html', + host: { + '[class.overview]': 'true' + } +}) +export class NgbdTableOverviewComponent { + + bootstrapVersion = environment.bootstrap; + + sections: NgbdOverview = {}; + + constructor(demoList: NgbdDemoList) { + this.sections = demoList.getOverviewSections('table'); + } +} diff --git a/demo/src/app/components/table/table.module.ts b/demo/src/app/components/table/table.module.ts new file mode 100644 index 0000000..2eabf9d --- /dev/null +++ b/demo/src/app/components/table/table.module.ts @@ -0,0 +1,143 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component'; +import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared'; +import { NgbdExamplesPage } from '../shared/examples-page/examples.component'; +import { NgbdTableBasic } from './demos/basic/table-basic'; +import { NgbdTableBasicModule } from './demos/basic/table-basic.module'; +import { NgbdTableComplete } from './demos/complete/table-complete'; +import { NgbdTableCompleteModule } from './demos/complete/table-complete.module'; +import { NgbdTableFiltering } from './demos/filtering/table-filtering'; +import { NgbdTableFilteringModule } from './demos/filtering/table-filtering.module'; +import { NgbdTablePagination } from './demos/pagination/table-pagination'; +import { NgbdTablePaginationModule } from './demos/pagination/table-pagination.module'; +import { NgbdTableSortable } from './demos/sortable/table-sortable'; +import { NgbdTableSortableModule } from './demos/sortable/table-sortable.module'; +import { NgbdTableOverviewDemo } from './overview/demo/table-overview-demo.component'; +import { NgbdTableOverviewComponent } from './overview/table-overview.component'; + +const OVERVIEW = { + 'why-not': 'Why not?', + examples: 'Code examples' +}; + +const DEMOS = { + basic: { + title: 'Basic table', + type: NgbdTableBasic, + files: [ + { + name: 'table-basic.html', + source: require('!!raw-loader!./demos/basic/table-basic.html') + }, + { + name: 'table-basic.ts', + source: require('!!raw-loader!./demos/basic/table-basic') + } + ] + }, + sortable: { + title: 'Sortable table', + type: NgbdTableSortable, + files: [ + { + name: 'table-sortable.html', + source: require('!!raw-loader!./demos/sortable/table-sortable.html') + }, + { + name: 'table-sortable.ts', + source: require('!!raw-loader!./demos/sortable/table-sortable') + } + ] + }, + filtering: { + title: 'Search and filtering', + type: NgbdTableFiltering, + files: [ + { + name: 'table-filtering.html', + source: require('!!raw-loader!./demos/filtering/table-filtering.html') + }, + { + name: 'table-filtering.ts', + source: require('!!raw-loader!./demos/filtering/table-filtering') + } + ] + }, + pagination: { + title: 'Pagination', + type: NgbdTablePagination, + files: [ + { + name: 'table-pagination.html', + source: require('!!raw-loader!./demos/pagination/table-pagination.html') + }, + { + name: 'table-pagination.ts', + source: require('!!raw-loader!./demos/pagination/table-pagination') + } + ] + }, + complete: { + title: 'Complete example', + type: NgbdTableComplete, + files: [ + { + name: 'countries.ts', + source: require('!!raw-loader!./demos/complete/countries') + }, + { + name: 'country.service.ts', + source: require('!!raw-loader!./demos/complete/country.service') + }, + { + name: 'country.ts', + source: require('!!raw-loader!./demos/complete/country') + }, + { + name: 'table-complete.html', + source: require('!!raw-loader!./demos/complete/table-complete.html') + }, + { + name: 'table-complete.ts', + source: require('!!raw-loader!./demos/complete/table-complete') + }, + { + name: 'sortable.directive.ts', + source: require('!!raw-loader!./demos/complete/sortable.directive') + } + ] + } +}; + +export const ROUTES = [ + { path: '', pathMatch: 'full', redirectTo: 'overview' }, + { + path: '', + component: ComponentWrapper, + data: { OVERVIEW }, + children: [ + { path: 'overview', component: NgbdTableOverviewComponent }, + { path: 'examples', component: NgbdExamplesPage } + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, + NgbdComponentsSharedModule, + NgbdTableBasicModule, + NgbdTableSortableModule, + NgbdTableFilteringModule, + NgbdTablePaginationModule, + NgbdTableCompleteModule + ], + declarations: [NgbdTableOverviewComponent, NgbdTableOverviewDemo] +}) +export class NgbdTableModule { + constructor(demoList: NgbdDemoList) { + demoList.register('table', DEMOS, OVERVIEW); + } +} diff --git a/demo/src/app/components/tabset/demos/basic/tabset-basic.html b/demo/src/app/components/tabset/demos/basic/tabset-basic.html new file mode 100644 index 0000000..033545d --- /dev/null +++ b/demo/src/app/components/tabset/demos/basic/tabset-basic.html @@ -0,0 +1,26 @@ + + + +

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth + master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh + dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum + iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

+
+
+ + Fancy title + Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. +

Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table + craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl + cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia + yr, vero magna velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean + shorts beard ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero + sint qui sapiente accusamus tattooed echo park.

+
+
+ + +

Sed commodo, leo at suscipit dictum, quam est porttitor sapien, eget sodales nibh elit id diam. Nulla facilisi. Donec egestas ligula vitae odio interdum aliquet. Duis lectus turpis, luctus eget tincidunt eu, congue et odio. Duis pharetra et nisl at faucibus. Quisque luctus pulvinar arcu, et molestie lectus ultrices et. Sed diam urna, egestas ut ipsum vel, volutpat volutpat neque. Praesent fringilla tortor arcu. Vivamus faucibus nisl enim, nec tristique ipsum euismod facilisis. Morbi ut bibendum est, eu tincidunt odio. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris aliquet odio ac lorem aliquet ultricies in eget neque. Phasellus nec tortor vel tellus pulvinar feugiat.

+
+
+
diff --git a/demo/src/app/components/tabset/demos/basic/tabset-basic.module.ts b/demo/src/app/components/tabset/demos/basic/tabset-basic.module.ts new file mode 100644 index 0000000..c7d5dcb --- /dev/null +++ b/demo/src/app/components/tabset/demos/basic/tabset-basic.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTabsetBasic } from './tabset-basic'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdTabsetBasic], + exports: [NgbdTabsetBasic], + bootstrap: [NgbdTabsetBasic] +}) +export class NgbdTabsetBasicModule {} diff --git a/demo/src/app/components/tabset/demos/basic/tabset-basic.ts b/demo/src/app/components/tabset/demos/basic/tabset-basic.ts new file mode 100644 index 0000000..e76b2ff --- /dev/null +++ b/demo/src/app/components/tabset/demos/basic/tabset-basic.ts @@ -0,0 +1,7 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-tabset-basic', + templateUrl: './tabset-basic.html' +}) +export class NgbdTabsetBasic { } diff --git a/demo/src/app/components/tabset/demos/config/tabset-config.html b/demo/src/app/components/tabset/demos/config/tabset-config.html new file mode 100644 index 0000000..e9c53ce --- /dev/null +++ b/demo/src/app/components/tabset/demos/config/tabset-config.html @@ -0,0 +1,12 @@ + + + +

These tabs are displayed as pills...

+
+
+ + +

Because default values have been customized.

+
+
+
diff --git a/demo/src/app/components/tabset/demos/config/tabset-config.module.ts b/demo/src/app/components/tabset/demos/config/tabset-config.module.ts new file mode 100644 index 0000000..7f9f8b7 --- /dev/null +++ b/demo/src/app/components/tabset/demos/config/tabset-config.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTabsetConfig } from './tabset-config'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdTabsetConfig], + exports: [NgbdTabsetConfig], + bootstrap: [NgbdTabsetConfig] +}) +export class NgbdTabsetConfigModule {} diff --git a/demo/src/app/components/tabset/demos/config/tabset-config.ts b/demo/src/app/components/tabset/demos/config/tabset-config.ts new file mode 100644 index 0000000..7820fb5 --- /dev/null +++ b/demo/src/app/components/tabset/demos/config/tabset-config.ts @@ -0,0 +1,15 @@ +import {Component} from '@angular/core'; +import {NgbTabsetConfig} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-tabset-config', + templateUrl: './tabset-config.html', + providers: [NgbTabsetConfig] // add NgbTabsetConfig to the component providers +}) +export class NgbdTabsetConfig { + constructor(config: NgbTabsetConfig) { + // customize default values of tabsets used by this component tree + config.justify = 'center'; + config.type = 'pills'; + } +} diff --git a/demo/src/app/components/tabset/demos/justify/tabset-justify.html b/demo/src/app/components/tabset/demos/justify/tabset-justify.html new file mode 100644 index 0000000..782833e --- /dev/null +++ b/demo/src/app/components/tabset/demos/justify/tabset-justify.html @@ -0,0 +1,44 @@ + + + +

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth + master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh + dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum + iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

+
+
+ + Fancy title + Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. +

Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table + craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl + cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia + yr, vero magna velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean + shorts beard ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero + sint qui sapiente accusamus tattooed echo park.

+
+
+ + +

Sed commodo, leo at suscipit dictum, quam est porttitor sapien, eget sodales nibh elit id diam. Nulla facilisi. Donec egestas ligula vitae odio interdum aliquet. Duis lectus turpis, luctus eget tincidunt eu, congue et odio. Duis pharetra et nisl at faucibus. Quisque luctus pulvinar arcu, et molestie lectus ultrices et. Sed diam urna, egestas ut ipsum vel, volutpat volutpat neque. Praesent fringilla tortor arcu. Vivamus faucibus nisl enim, nec tristique ipsum euismod facilisis. Morbi ut bibendum est, eu tincidunt odio. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris aliquet odio ac lorem aliquet ultricies in eget neque. Phasellus nec tortor vel tellus pulvinar feugiat.

+
+
+
+ +
+ + + + + +
diff --git a/demo/src/app/components/tabset/demos/justify/tabset-justify.module.ts b/demo/src/app/components/tabset/demos/justify/tabset-justify.module.ts new file mode 100644 index 0000000..426e05e --- /dev/null +++ b/demo/src/app/components/tabset/demos/justify/tabset-justify.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTabsetJustify } from './tabset-justify'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTabsetJustify], + exports: [NgbdTabsetJustify], + bootstrap: [NgbdTabsetJustify] +}) +export class NgbdTabsetJustifyModule {} diff --git a/demo/src/app/components/tabset/demos/justify/tabset-justify.ts b/demo/src/app/components/tabset/demos/justify/tabset-justify.ts new file mode 100644 index 0000000..11150e5 --- /dev/null +++ b/demo/src/app/components/tabset/demos/justify/tabset-justify.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-tabset-justify', + templateUrl: './tabset-justify.html' +}) +export class NgbdTabsetJustify { + currentJustify = 'start'; + } diff --git a/demo/src/app/components/tabset/demos/orientation/tabset-orientation.html b/demo/src/app/components/tabset/demos/orientation/tabset-orientation.html new file mode 100644 index 0000000..3a87eb2 --- /dev/null +++ b/demo/src/app/components/tabset/demos/orientation/tabset-orientation.html @@ -0,0 +1,35 @@ + + + +

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth + master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh + dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum + iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

+
+
+ + Fancy title + Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. +

Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table + craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl + cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia + yr, vero magna velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean + shorts beard ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero + sint qui sapiente accusamus tattooed echo park.

+
+
+ + +

Sed commodo, leo at suscipit dictum, quam est porttitor sapien, eget sodales nibh elit id diam. Nulla facilisi. Donec egestas ligula vitae odio interdum aliquet. Duis lectus turpis, luctus eget tincidunt eu, congue et odio. Duis pharetra et nisl at faucibus. Quisque luctus pulvinar arcu, et molestie lectus ultrices et. Sed diam urna, egestas ut ipsum vel, volutpat volutpat neque. Praesent fringilla tortor arcu. Vivamus faucibus nisl enim, nec tristique ipsum euismod facilisis. Morbi ut bibendum est, eu tincidunt odio. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris aliquet odio ac lorem aliquet ultricies in eget neque. Phasellus nec tortor vel tellus pulvinar feugiat.

+
+
+
+ +
+ + +
diff --git a/demo/src/app/components/tabset/demos/orientation/tabset-orientation.module.ts b/demo/src/app/components/tabset/demos/orientation/tabset-orientation.module.ts new file mode 100644 index 0000000..5b078dd --- /dev/null +++ b/demo/src/app/components/tabset/demos/orientation/tabset-orientation.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTabsetOrientation } from './tabset-orientation'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTabsetOrientation], + exports: [NgbdTabsetOrientation], + bootstrap: [NgbdTabsetOrientation] +}) +export class NgbdTabsetOrientationModule {} diff --git a/demo/src/app/components/tabset/demos/orientation/tabset-orientation.ts b/demo/src/app/components/tabset/demos/orientation/tabset-orientation.ts new file mode 100644 index 0000000..37f131b --- /dev/null +++ b/demo/src/app/components/tabset/demos/orientation/tabset-orientation.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-tabset-orientation', + templateUrl: './tabset-orientation.html' +}) +export class NgbdTabsetOrientation { + currentOrientation = 'horizontal'; +} diff --git a/demo/src/app/components/tabset/demos/pills/tabset-pills.html b/demo/src/app/components/tabset/demos/pills/tabset-pills.html new file mode 100644 index 0000000..44537f1 --- /dev/null +++ b/demo/src/app/components/tabset/demos/pills/tabset-pills.html @@ -0,0 +1,26 @@ + + + +

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth + master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh + dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum + iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

+
+
+ + Fancy title + Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. +

Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table + craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl + cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia + yr, vero magna velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean + shorts beard ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero + sint qui sapiente accusamus tattooed echo park.

+
+
+ + +

Sed commodo, leo at suscipit dictum, quam est porttitor sapien, eget sodales nibh elit id diam. Nulla facilisi. Donec egestas ligula vitae odio interdum aliquet. Duis lectus turpis, luctus eget tincidunt eu, congue et odio. Duis pharetra et nisl at faucibus. Quisque luctus pulvinar arcu, et molestie lectus ultrices et. Sed diam urna, egestas ut ipsum vel, volutpat volutpat neque. Praesent fringilla tortor arcu. Vivamus faucibus nisl enim, nec tristique ipsum euismod facilisis. Morbi ut bibendum est, eu tincidunt odio. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris aliquet odio ac lorem aliquet ultricies in eget neque. Phasellus nec tortor vel tellus pulvinar feugiat.

+
+
+
diff --git a/demo/src/app/components/tabset/demos/pills/tabset-pills.module.ts b/demo/src/app/components/tabset/demos/pills/tabset-pills.module.ts new file mode 100644 index 0000000..dcc3ae8 --- /dev/null +++ b/demo/src/app/components/tabset/demos/pills/tabset-pills.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTabsetPills } from './tabset-pills'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdTabsetPills], + exports: [NgbdTabsetPills], + bootstrap: [NgbdTabsetPills] +}) +export class NgbdTabsetPillsModule {} diff --git a/demo/src/app/components/tabset/demos/pills/tabset-pills.ts b/demo/src/app/components/tabset/demos/pills/tabset-pills.ts new file mode 100644 index 0000000..8ea4b03 --- /dev/null +++ b/demo/src/app/components/tabset/demos/pills/tabset-pills.ts @@ -0,0 +1,7 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-tabset-pills', + templateUrl: './tabset-pills.html' +}) +export class NgbdTabsetPills { } diff --git a/demo/src/app/components/tabset/demos/preventchange/tabset-prevent-change.module.ts b/demo/src/app/components/tabset/demos/preventchange/tabset-prevent-change.module.ts new file mode 100644 index 0000000..5be9607 --- /dev/null +++ b/demo/src/app/components/tabset/demos/preventchange/tabset-prevent-change.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTabsetPreventchange } from './tabset-preventchange'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdTabsetPreventchange], + exports: [NgbdTabsetPreventchange], + bootstrap: [NgbdTabsetPreventchange] +}) +export class NgbdTabsetPreventChangeModule {} diff --git a/demo/src/app/components/tabset/demos/preventchange/tabset-preventchange.html b/demo/src/app/components/tabset/demos/preventchange/tabset-preventchange.html new file mode 100644 index 0000000..b20e28c --- /dev/null +++ b/demo/src/app/components/tabset/demos/preventchange/tabset-preventchange.html @@ -0,0 +1,25 @@ + + + +

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth + master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh + dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum + iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

+
+
+ + Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. +

Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table + craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl + cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia + yr, vero magna velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean + shorts beard ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero + sint qui sapiente accusamus tattooed echo park.

+
+
+ + +

Sed commodo, leo at suscipit dictum, quam est porttitor sapien, eget sodales nibh elit id diam. Nulla facilisi. Donec egestas ligula vitae odio interdum aliquet. Duis lectus turpis, luctus eget tincidunt eu, congue et odio. Duis pharetra et nisl at faucibus. Quisque luctus pulvinar arcu, et molestie lectus ultrices et. Sed diam urna, egestas ut ipsum vel, volutpat volutpat neque. Praesent fringilla tortor arcu. Vivamus faucibus nisl enim, nec tristique ipsum euismod facilisis. Morbi ut bibendum est, eu tincidunt odio. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris aliquet odio ac lorem aliquet ultricies in eget neque. Phasellus nec tortor vel tellus pulvinar feugiat.

+
+
+
diff --git a/demo/src/app/components/tabset/demos/preventchange/tabset-preventchange.ts b/demo/src/app/components/tabset/demos/preventchange/tabset-preventchange.ts new file mode 100644 index 0000000..f700599 --- /dev/null +++ b/demo/src/app/components/tabset/demos/preventchange/tabset-preventchange.ts @@ -0,0 +1,14 @@ +import {Component} from '@angular/core'; +import {NgbTabChangeEvent} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-tabset-preventchange', + templateUrl: './tabset-preventchange.html' +}) +export class NgbdTabsetPreventchange { + public beforeChange($event: NgbTabChangeEvent) { + if ($event.nextId === 'tab-preventchange2') { + $event.preventDefault(); + } + } +} diff --git a/demo/src/app/components/tabset/demos/selectbyid/tabset-selectbyid.html b/demo/src/app/components/tabset/demos/selectbyid/tabset-selectbyid.html new file mode 100644 index 0000000..5b37821 --- /dev/null +++ b/demo/src/app/components/tabset/demos/selectbyid/tabset-selectbyid.html @@ -0,0 +1,25 @@ + + + +

Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth + master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro keffiyeh + dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat salvia cillum + iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.

+
+
+ + Fancy title + Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. +

Exercitation +1 labore velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table + craft beer twee. Qui photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl + cillum PBR. Homo nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia + yr, vero magna velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean + shorts beard ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero + sint qui sapiente accusamus tattooed echo park.

+
+
+
+ +

+ +

diff --git a/demo/src/app/components/tabset/demos/selectbyid/tabset-selectbyid.module.ts b/demo/src/app/components/tabset/demos/selectbyid/tabset-selectbyid.module.ts new file mode 100644 index 0000000..0ec4622 --- /dev/null +++ b/demo/src/app/components/tabset/demos/selectbyid/tabset-selectbyid.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTabsetSelectbyid } from './tabset-selectbyid'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdTabsetSelectbyid], + exports: [NgbdTabsetSelectbyid], + bootstrap: [NgbdTabsetSelectbyid] +}) +export class NgbdTabsetSelectbyidModule {} diff --git a/demo/src/app/components/tabset/demos/selectbyid/tabset-selectbyid.ts b/demo/src/app/components/tabset/demos/selectbyid/tabset-selectbyid.ts new file mode 100644 index 0000000..0b1f1fa --- /dev/null +++ b/demo/src/app/components/tabset/demos/selectbyid/tabset-selectbyid.ts @@ -0,0 +1,8 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-tabset-selectbyid', + templateUrl: './tabset-selectbyid.html' +}) +export class NgbdTabsetSelectbyid { +} diff --git a/demo/src/app/components/tabset/tabset.module.ts b/demo/src/app/components/tabset/tabset.module.ts new file mode 100644 index 0000000..4453f77 --- /dev/null +++ b/demo/src/app/components/tabset/tabset.module.ts @@ -0,0 +1,97 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component'; +import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared'; +import { NgbdApiPage } from '../shared/api-page/api.component'; +import { NgbdExamplesPage } from '../shared/examples-page/examples.component'; +import { NgbdTabsetBasic } from './demos/basic/tabset-basic'; +import { NgbdTabsetBasicModule } from './demos/basic/tabset-basic.module'; +import { NgbdTabsetConfig } from './demos/config/tabset-config'; +import { NgbdTabsetConfigModule } from './demos/config/tabset-config.module'; +import { NgbdTabsetJustify } from './demos/justify/tabset-justify'; +import { NgbdTabsetJustifyModule } from './demos/justify/tabset-justify.module'; +import { NgbdTabsetOrientation } from './demos/orientation/tabset-orientation'; +import { NgbdTabsetOrientationModule } from './demos/orientation/tabset-orientation.module'; +import { NgbdTabsetPills } from './demos/pills/tabset-pills'; +import { NgbdTabsetPillsModule } from './demos/pills/tabset-pills.module'; +import { NgbdTabsetPreventChangeModule } from './demos/preventchange/tabset-prevent-change.module'; +import { NgbdTabsetPreventchange } from './demos/preventchange/tabset-preventchange'; +import { NgbdTabsetSelectbyid } from './demos/selectbyid/tabset-selectbyid'; +import { NgbdTabsetSelectbyidModule } from './demos/selectbyid/tabset-selectbyid.module'; + +const DEMOS = { + basic: { + title: 'Tabset', + type: NgbdTabsetBasic, + code: require('!!raw-loader!./demos/basic/tabset-basic'), + markup: require('!!raw-loader!./demos/basic/tabset-basic.html') + }, + pills: { + title: 'Pills', + type: NgbdTabsetPills, + code: require('!!raw-loader!./demos/pills/tabset-pills'), + markup: require('!!raw-loader!./demos/pills/tabset-pills.html') + }, + selectbyid: { + title: 'Select an active tab by id', + type: NgbdTabsetSelectbyid, + code: require('!!raw-loader!./demos/selectbyid/tabset-selectbyid'), + markup: require('!!raw-loader!./demos/selectbyid/tabset-selectbyid.html') + }, + preventchange: { + title: 'Prevent tab change', + type: NgbdTabsetPreventchange, + code: require('!!raw-loader!./demos/preventchange/tabset-preventchange'), + markup: require('!!raw-loader!./demos/preventchange/tabset-preventchange.html') + }, + justify: { + title: 'Nav justification', + type: NgbdTabsetJustify, + code: require('!!raw-loader!./demos/justify/tabset-justify'), + markup: require('!!raw-loader!./demos/justify/tabset-justify.html') + }, + orientation: { + title: 'Nav orientation', + type: NgbdTabsetOrientation, + code: require('!!raw-loader!./demos/orientation/tabset-orientation'), + markup: require('!!raw-loader!./demos/orientation/tabset-orientation.html') + }, + config: { + title: 'Global configuration of tabs', + type: NgbdTabsetConfig, + code: require('!!raw-loader!./demos/config/tabset-config'), + markup: require('!!raw-loader!./demos/config/tabset-config.html') + } +}; + +export const ROUTES = [ + { path: '', pathMatch: 'full', redirectTo: 'examples' }, + { + path: '', + component: ComponentWrapper, + children: [ + { path: 'examples', component: NgbdExamplesPage }, + { path: 'api', component: NgbdApiPage } + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, + NgbdComponentsSharedModule, + NgbdTabsetBasicModule, + NgbdTabsetPillsModule, + NgbdTabsetPreventChangeModule, + NgbdTabsetSelectbyidModule, + NgbdTabsetConfigModule, + NgbdTabsetJustifyModule, + NgbdTabsetOrientationModule + ] +}) +export class NgbdTabsetModule { + constructor(demoList: NgbdDemoList) { + demoList.register('tabset', DEMOS); + } +} diff --git a/demo/src/app/components/timepicker/demos/adapter/timepicker-adapter.html b/demo/src/app/components/timepicker/demos/adapter/timepicker-adapter.html new file mode 100644 index 0000000..894ce19 --- /dev/null +++ b/demo/src/app/components/timepicker/demos/adapter/timepicker-adapter.html @@ -0,0 +1,6 @@ +

This timepicker uses a custom Time adapter that lets you use your own model implementation. + In this example we are converting from and to an ISO string (with the format HH:mm:ss)

+ + +
+
Selected time: {{ time }}
diff --git a/demo/src/app/components/timepicker/demos/adapter/timepicker-adapter.module.ts b/demo/src/app/components/timepicker/demos/adapter/timepicker-adapter.module.ts new file mode 100644 index 0000000..9e17a25 --- /dev/null +++ b/demo/src/app/components/timepicker/demos/adapter/timepicker-adapter.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTimepickerAdapter } from './timepicker-adapter'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTimepickerAdapter], + exports: [NgbdTimepickerAdapter], + bootstrap: [NgbdTimepickerAdapter] +}) +export class NgbdTimepickerAdapterModule {} diff --git a/demo/src/app/components/timepicker/demos/adapter/timepicker-adapter.ts b/demo/src/app/components/timepicker/demos/adapter/timepicker-adapter.ts new file mode 100644 index 0000000..6455873 --- /dev/null +++ b/demo/src/app/components/timepicker/demos/adapter/timepicker-adapter.ts @@ -0,0 +1,43 @@ +import {Component, Injectable} from '@angular/core'; +import {NgbTimeStruct, NgbTimeAdapter} from '@ng-bootstrap/ng-bootstrap'; + +/** + * Example of a String Time adapter + */ +@Injectable() +export class NgbTimeStringAdapter extends NgbTimeAdapter { + + fromModel(value: string): NgbTimeStruct { + if (!value) { + return null; + } + const split = value.split(':'); + return { + hour: parseInt(split[0], 10), + minute: parseInt(split[1], 10), + second: parseInt(split[2], 10) + }; + } + + toModel(time: NgbTimeStruct): string { + if (!time) { + return null; + } + return `${this.pad(time.hour)}:${this.pad(time.minute)}:${this.pad(time.second)}`; + } + + private pad(i: number): string { + return i < 10 ? `0${i}` : `${i}`; + } +} + +@Component({ + selector: 'ngbd-timepicker-adapter', + templateUrl: './timepicker-adapter.html', + // NOTE: For this example we are only providing current component, but probably + // NOTE: you will want to provide your main App Module + providers: [{provide: NgbTimeAdapter, useClass: NgbTimeStringAdapter}] +}) +export class NgbdTimepickerAdapter { + time: '13:30:00'; +} diff --git a/demo/src/app/components/timepicker/demos/basic/timepicker-basic.html b/demo/src/app/components/timepicker/demos/basic/timepicker-basic.html new file mode 100644 index 0000000..a84d6cd --- /dev/null +++ b/demo/src/app/components/timepicker/demos/basic/timepicker-basic.html @@ -0,0 +1,3 @@ + +
+
Selected time: {{time | json}}
diff --git a/demo/src/app/components/timepicker/demos/basic/timepicker-basic.module.ts b/demo/src/app/components/timepicker/demos/basic/timepicker-basic.module.ts new file mode 100644 index 0000000..38eadea --- /dev/null +++ b/demo/src/app/components/timepicker/demos/basic/timepicker-basic.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTimepickerBasic } from './timepicker-basic'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTimepickerBasic], + exports: [NgbdTimepickerBasic], + bootstrap: [NgbdTimepickerBasic] +}) +export class NgbdTimepickerBasicModule {} diff --git a/demo/src/app/components/timepicker/demos/basic/timepicker-basic.ts b/demo/src/app/components/timepicker/demos/basic/timepicker-basic.ts new file mode 100644 index 0000000..ae5ab41 --- /dev/null +++ b/demo/src/app/components/timepicker/demos/basic/timepicker-basic.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-timepicker-basic', + templateUrl: './timepicker-basic.html' +}) +export class NgbdTimepickerBasic { + time = {hour: 13, minute: 30}; +} diff --git a/demo/src/app/components/timepicker/demos/config/timepicker-config.html b/demo/src/app/components/timepicker/demos/config/timepicker-config.html new file mode 100644 index 0000000..120036a --- /dev/null +++ b/demo/src/app/components/timepicker/demos/config/timepicker-config.html @@ -0,0 +1,3 @@ +

This timepicker uses customized default values.

+ + diff --git a/demo/src/app/components/timepicker/demos/config/timepicker-config.module.ts b/demo/src/app/components/timepicker/demos/config/timepicker-config.module.ts new file mode 100644 index 0000000..f1a8539 --- /dev/null +++ b/demo/src/app/components/timepicker/demos/config/timepicker-config.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTimepickerConfig } from './timepicker-config'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTimepickerConfig], + exports: [NgbdTimepickerConfig], + bootstrap: [NgbdTimepickerConfig] +}) +export class NgbdTimepickerConfigModule {} diff --git a/demo/src/app/components/timepicker/demos/config/timepicker-config.ts b/demo/src/app/components/timepicker/demos/config/timepicker-config.ts new file mode 100644 index 0000000..65d8fbd --- /dev/null +++ b/demo/src/app/components/timepicker/demos/config/timepicker-config.ts @@ -0,0 +1,18 @@ +import {Component} from '@angular/core'; +import {NgbTimepickerConfig} from '@ng-bootstrap/ng-bootstrap'; +import {NgbTimeStruct} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-timepicker-config', + templateUrl: './timepicker-config.html', + providers: [NgbTimepickerConfig] // add NgbTimepickerConfig to the component providers +}) +export class NgbdTimepickerConfig { + time: NgbTimeStruct = {hour: 13, minute: 30, second: 0}; + + constructor(config: NgbTimepickerConfig) { + // customize default values of ratings used by this component tree + config.seconds = true; + config.spinners = false; + } +} diff --git a/demo/src/app/components/timepicker/demos/i18n/timepicker-i18n.html b/demo/src/app/components/timepicker/demos/i18n/timepicker-i18n.html new file mode 100644 index 0000000..e261d0e --- /dev/null +++ b/demo/src/app/components/timepicker/demos/i18n/timepicker-i18n.html @@ -0,0 +1,11 @@ + + If you configure the locale and register the locale data as explained in the + i18n guide, the time picker will honor + the locale and use the day periods ("AM" and "PM") from the locale data. You can however + provide a custom service, as demonstrated in this example, to customize the + days and months names the way you want to. + + +

Timepicker in Greek

+ + diff --git a/demo/src/app/components/timepicker/demos/i18n/timepicker-i18n.module.ts b/demo/src/app/components/timepicker/demos/i18n/timepicker-i18n.module.ts new file mode 100644 index 0000000..2557895 --- /dev/null +++ b/demo/src/app/components/timepicker/demos/i18n/timepicker-i18n.module.ts @@ -0,0 +1,15 @@ +import {NgModule} from '@angular/core'; +import {FormsModule} from '@angular/forms'; +import {BrowserModule} from '@angular/platform-browser'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; + +import {NgbdTimepickerI18n} from './timepicker-i18n'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTimepickerI18n], + exports: [NgbdTimepickerI18n], + bootstrap: [NgbdTimepickerI18n] +}) +export class NgbdTimepickerI18nModule { +} diff --git a/demo/src/app/components/timepicker/demos/i18n/timepicker-i18n.ts b/demo/src/app/components/timepicker/demos/i18n/timepicker-i18n.ts new file mode 100644 index 0000000..fa6d5aa --- /dev/null +++ b/demo/src/app/components/timepicker/demos/i18n/timepicker-i18n.ts @@ -0,0 +1,34 @@ +import {Component, Injectable} from '@angular/core'; +import {NgbTimepickerI18n} from '@ng-bootstrap/ng-bootstrap'; + +const I18N_VALUES = { + 'el': {periods: ['πμ', 'μμ']} + // other languages you would support +}; + +// Define a service holding the language. You probably already have one if your app is i18ned. Or you could also +// use the Angular LOCALE_ID value +@Injectable() +export class I18n { + language = 'el'; +} + +// Define custom service providing the "AM" and "PM" translations. +@Injectable() +export class CustomTimepickerI18n extends NgbTimepickerI18n { + constructor(private _i18n: I18n) { super(); } + + getMorningPeriod(): string { return I18N_VALUES[this._i18n.language].periods[0]; } + + getAfternoonPeriod(): string { return I18N_VALUES[this._i18n.language].periods[1]; } +} + +@Component({ + selector: 'ngbd-timepicker-i18n', + templateUrl: './timepicker-i18n.html', + providers: + [I18n, {provide: NgbTimepickerI18n, useClass: CustomTimepickerI18n}] // define custom NgbTimepickerI18n provider +}) +export class NgbdTimepickerI18n { + model = {hour: 13, minute: 30}; +} diff --git a/demo/src/app/components/timepicker/demos/meridian/timepicker-meridian.html b/demo/src/app/components/timepicker/demos/meridian/timepicker-meridian.html new file mode 100644 index 0000000..cf874b7 --- /dev/null +++ b/demo/src/app/components/timepicker/demos/meridian/timepicker-meridian.html @@ -0,0 +1,6 @@ + + +
+
Selected time: {{time | json}}
diff --git a/demo/src/app/components/timepicker/demos/meridian/timepicker-meridian.module.ts b/demo/src/app/components/timepicker/demos/meridian/timepicker-meridian.module.ts new file mode 100644 index 0000000..c0cc7f2 --- /dev/null +++ b/demo/src/app/components/timepicker/demos/meridian/timepicker-meridian.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTimepickerMeridian } from './timepicker-meridian'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTimepickerMeridian], + exports: [NgbdTimepickerMeridian], + bootstrap: [NgbdTimepickerMeridian] +}) +export class NgbdTimepickerMeridianModule {} diff --git a/demo/src/app/components/timepicker/demos/meridian/timepicker-meridian.ts b/demo/src/app/components/timepicker/demos/meridian/timepicker-meridian.ts new file mode 100644 index 0000000..89330ca --- /dev/null +++ b/demo/src/app/components/timepicker/demos/meridian/timepicker-meridian.ts @@ -0,0 +1,14 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-timepicker-meridian', + templateUrl: './timepicker-meridian.html' +}) +export class NgbdTimepickerMeridian { + time = {hour: 13, minute: 30}; + meridian = true; + + toggleMeridian() { + this.meridian = !this.meridian; + } +} diff --git a/demo/src/app/components/timepicker/demos/seconds/timepicker-seconds.html b/demo/src/app/components/timepicker/demos/seconds/timepicker-seconds.html new file mode 100644 index 0000000..3b5ecb3 --- /dev/null +++ b/demo/src/app/components/timepicker/demos/seconds/timepicker-seconds.html @@ -0,0 +1,6 @@ + + +
+
Selected time: {{time | json}}
diff --git a/demo/src/app/components/timepicker/demos/seconds/timepicker-seconds.module.ts b/demo/src/app/components/timepicker/demos/seconds/timepicker-seconds.module.ts new file mode 100644 index 0000000..52f792c --- /dev/null +++ b/demo/src/app/components/timepicker/demos/seconds/timepicker-seconds.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTimepickerSeconds } from './timepicker-seconds'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTimepickerSeconds], + exports: [NgbdTimepickerSeconds], + bootstrap: [NgbdTimepickerSeconds] +}) +export class NgbdTimepickerSecondsModule {} diff --git a/demo/src/app/components/timepicker/demos/seconds/timepicker-seconds.ts b/demo/src/app/components/timepicker/demos/seconds/timepicker-seconds.ts new file mode 100644 index 0000000..c4c58b0 --- /dev/null +++ b/demo/src/app/components/timepicker/demos/seconds/timepicker-seconds.ts @@ -0,0 +1,15 @@ +import {Component} from '@angular/core'; +import {NgbTimeStruct} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-timepicker-seconds', + templateUrl: './timepicker-seconds.html' +}) +export class NgbdTimepickerSeconds { + time: NgbTimeStruct = {hour: 13, minute: 30, second: 30}; + seconds = true; + + toggleSeconds() { + this.seconds = !this.seconds; + } +} diff --git a/demo/src/app/components/timepicker/demos/spinners/timepicker-spinners.html b/demo/src/app/components/timepicker/demos/spinners/timepicker-spinners.html new file mode 100644 index 0000000..2b02323 --- /dev/null +++ b/demo/src/app/components/timepicker/demos/spinners/timepicker-spinners.html @@ -0,0 +1,7 @@ + + +
+ + diff --git a/demo/src/app/components/timepicker/demos/spinners/timepicker-spinners.module.ts b/demo/src/app/components/timepicker/demos/spinners/timepicker-spinners.module.ts new file mode 100644 index 0000000..b3e1fea --- /dev/null +++ b/demo/src/app/components/timepicker/demos/spinners/timepicker-spinners.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTimepickerSpinners } from './timepicker-spinners'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTimepickerSpinners], + exports: [NgbdTimepickerSpinners], + bootstrap: [NgbdTimepickerSpinners] +}) +export class NgbdTimepickerSpinnersModule {} diff --git a/demo/src/app/components/timepicker/demos/spinners/timepicker-spinners.ts b/demo/src/app/components/timepicker/demos/spinners/timepicker-spinners.ts new file mode 100644 index 0000000..658f1bc --- /dev/null +++ b/demo/src/app/components/timepicker/demos/spinners/timepicker-spinners.ts @@ -0,0 +1,14 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-timepicker-spinners', + templateUrl: './timepicker-spinners.html' +}) +export class NgbdTimepickerSpinners { + time = {hour: 13, minute: 30}; + spinners = true; + + toggleSpinners() { + this.spinners = !this.spinners; + } +} diff --git a/demo/src/app/components/timepicker/demos/steps/timepicker-steps.html b/demo/src/app/components/timepicker/demos/steps/timepicker-steps.html new file mode 100644 index 0000000..d687767 --- /dev/null +++ b/demo/src/app/components/timepicker/demos/steps/timepicker-steps.html @@ -0,0 +1,19 @@ + + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
Selected time: {{time | json}}
diff --git a/demo/src/app/components/timepicker/demos/steps/timepicker-steps.module.ts b/demo/src/app/components/timepicker/demos/steps/timepicker-steps.module.ts new file mode 100644 index 0000000..b25b1f8 --- /dev/null +++ b/demo/src/app/components/timepicker/demos/steps/timepicker-steps.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTimepickerSteps } from './timepicker-steps'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTimepickerSteps], + exports: [NgbdTimepickerSteps], + bootstrap: [NgbdTimepickerSteps] +}) +export class NgbdTimepickerStepsModule {} diff --git a/demo/src/app/components/timepicker/demos/steps/timepicker-steps.ts b/demo/src/app/components/timepicker/demos/steps/timepicker-steps.ts new file mode 100644 index 0000000..77bc31a --- /dev/null +++ b/demo/src/app/components/timepicker/demos/steps/timepicker-steps.ts @@ -0,0 +1,13 @@ +import {Component} from '@angular/core'; +import {NgbTimeStruct} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-timepicker-steps', + templateUrl: './timepicker-steps.html' +}) +export class NgbdTimepickerSteps { + time: NgbTimeStruct = {hour: 13, minute: 30, second: 0}; + hourStep = 1; + minuteStep = 15; + secondStep = 30; +} diff --git a/demo/src/app/components/timepicker/demos/validation/timepicker-validation.html b/demo/src/app/components/timepicker/demos/validation/timepicker-validation.html new file mode 100644 index 0000000..ac54ab0 --- /dev/null +++ b/demo/src/app/components/timepicker/demos/validation/timepicker-validation.html @@ -0,0 +1,14 @@ +

Illustrates custom validation, you have to select time between 12:00 and 13:59

+ +
+ +
Great choice
+
+
Select some time during lunchtime
+
Oh no, it's way too late
+
It's a bit too early
+
+
+ +
+
Selected time: {{ctrl.value | json}}
diff --git a/demo/src/app/components/timepicker/demos/validation/timepicker-validation.module.ts b/demo/src/app/components/timepicker/demos/validation/timepicker-validation.module.ts new file mode 100644 index 0000000..941e820 --- /dev/null +++ b/demo/src/app/components/timepicker/demos/validation/timepicker-validation.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTimepickerValidation } from './timepicker-validation'; + +@NgModule({ + imports: [BrowserModule, ReactiveFormsModule, NgbModule], + declarations: [NgbdTimepickerValidation], + exports: [NgbdTimepickerValidation], + bootstrap: [NgbdTimepickerValidation] +}) +export class NgbdTimepickerValidationModule {} diff --git a/demo/src/app/components/timepicker/demos/validation/timepicker-validation.ts b/demo/src/app/components/timepicker/demos/validation/timepicker-validation.ts new file mode 100644 index 0000000..77fb1cd --- /dev/null +++ b/demo/src/app/components/timepicker/demos/validation/timepicker-validation.ts @@ -0,0 +1,26 @@ +import {Component} from '@angular/core'; +import {FormControl} from '@angular/forms'; + +@Component({ + selector: 'ngbd-timepicker-validation', + templateUrl: './timepicker-validation.html' +}) +export class NgbdTimepickerValidation { + + ctrl = new FormControl('', (control: FormControl) => { + const value = control.value; + + if (!value) { + return null; + } + + if (value.hour < 12) { + return {tooEarly: true}; + } + if (value.hour > 13) { + return {tooLate: true}; + } + + return null; + }); +} diff --git a/demo/src/app/components/timepicker/timepicker.module.ts b/demo/src/app/components/timepicker/timepicker.module.ts new file mode 100644 index 0000000..f39b64e --- /dev/null +++ b/demo/src/app/components/timepicker/timepicker.module.ts @@ -0,0 +1,115 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component'; +import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared'; +import { NgbdApiPage } from '../shared/api-page/api.component'; +import { NgbdExamplesPage } from '../shared/examples-page/examples.component'; +import { NgbdTimepickerAdapter } from './demos/adapter/timepicker-adapter'; +import { NgbdTimepickerAdapterModule } from './demos/adapter/timepicker-adapter.module'; +import { NgbdTimepickerBasic } from './demos/basic/timepicker-basic'; +import { NgbdTimepickerBasicModule } from './demos/basic/timepicker-basic.module'; +import { NgbdTimepickerConfig } from './demos/config/timepicker-config'; +import { NgbdTimepickerConfigModule } from './demos/config/timepicker-config.module'; +import { NgbdTimepickerMeridian } from './demos/meridian/timepicker-meridian'; +import { NgbdTimepickerMeridianModule } from './demos/meridian/timepicker-meridian.module'; +import { NgbdTimepickerSeconds } from './demos/seconds/timepicker-seconds'; +import { NgbdTimepickerSecondsModule } from './demos/seconds/timepicker-seconds.module'; +import { NgbdTimepickerSpinners } from './demos/spinners/timepicker-spinners'; +import { NgbdTimepickerSpinnersModule } from './demos/spinners/timepicker-spinners.module'; +import { NgbdTimepickerSteps } from './demos/steps/timepicker-steps'; +import { NgbdTimepickerStepsModule } from './demos/steps/timepicker-steps.module'; +import { NgbdTimepickerValidation } from './demos/validation/timepicker-validation'; +import { NgbdTimepickerValidationModule } from './demos/validation/timepicker-validation.module'; +import { NgbdTimepickerI18n } from './demos/i18n/timepicker-i18n'; +import { NgbdTimepickerI18nModule } from './demos/i18n/timepicker-i18n.module'; + +const DEMOS = { + basic: { + title: 'Timepicker', + type: NgbdTimepickerBasic, + code: require('!!raw-loader!./demos/basic/timepicker-basic'), + markup: require('!!raw-loader!./demos/basic/timepicker-basic.html') + }, + meridian: { + title: 'Meridian', + type: NgbdTimepickerMeridian, + code: require('!!raw-loader!./demos/meridian/timepicker-meridian'), + markup: require('!!raw-loader!./demos/meridian/timepicker-meridian.html') + }, + seconds: { + title: 'Seconds', + type: NgbdTimepickerSeconds, + code: require('!!raw-loader!./demos/seconds/timepicker-seconds'), + markup: require('!!raw-loader!./demos/seconds/timepicker-seconds.html') + }, + spinners: { + title: 'Spinners', + type: NgbdTimepickerSpinners, + code: require('!!raw-loader!./demos/spinners/timepicker-spinners'), + markup: require('!!raw-loader!./demos/spinners/timepicker-spinners.html') + }, + steps: { + title: 'Custom steps', + type: NgbdTimepickerSteps, + code: require('!!raw-loader!./demos/steps/timepicker-steps'), + markup: require('!!raw-loader!./demos/steps/timepicker-steps.html') + }, + validation: { + title: 'Custom validation', + type: NgbdTimepickerValidation, + code: require('!!raw-loader!./demos/validation/timepicker-validation'), + markup: require('!!raw-loader!./demos/validation/timepicker-validation.html') + }, + adapter: { + title: 'Custom time adapter', + type: NgbdTimepickerAdapter, + code: require('!!raw-loader!./demos/adapter/timepicker-adapter'), + markup: require('!!raw-loader!./demos/adapter/timepicker-adapter.html') + }, + i18n: { + title: 'Internationalization of timepickers', + type: NgbdTimepickerI18n, + code: require('!!raw-loader!./demos/i18n/timepicker-i18n'), + markup: require('!!raw-loader!./demos/i18n/timepicker-i18n.html') + }, + config: { + title: 'Global configuration of timepickers', + type: NgbdTimepickerConfig, + code: require('!!raw-loader!./demos/config/timepicker-config'), + markup: require('!!raw-loader!./demos/config/timepicker-config.html') + } +}; + +export const ROUTES = [ + { path: '', pathMatch: 'full', redirectTo: 'examples' }, + { + path: '', + component: ComponentWrapper, + children: [ + { path: 'examples', component: NgbdExamplesPage }, + { path: 'api', component: NgbdApiPage } + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, + NgbdComponentsSharedModule, + NgbdTimepickerBasicModule, + NgbdTimepickerI18nModule, + NgbdTimepickerMeridianModule, + NgbdTimepickerSecondsModule, + NgbdTimepickerSpinnersModule, + NgbdTimepickerStepsModule, + NgbdTimepickerValidationModule, + NgbdTimepickerAdapterModule, + NgbdTimepickerConfigModule + ] +}) +export class NgbdTimepickerModule { + constructor(demoList: NgbdDemoList) { + demoList.register('timepicker', DEMOS); + } +} diff --git a/demo/src/app/components/toast/demos/closeable/toast-closeable.html b/demo/src/app/components/toast/demos/closeable/toast-closeable.html new file mode 100644 index 0000000..801e3ba --- /dev/null +++ b/demo/src/app/components/toast/demos/closeable/toast-closeable.html @@ -0,0 +1,5 @@ + + If you close me, I will automatically re-appear after a few seconds. + +

I'll be back!

diff --git a/demo/src/app/components/toast/demos/closeable/toast-closeable.module.ts b/demo/src/app/components/toast/demos/closeable/toast-closeable.module.ts new file mode 100644 index 0000000..f9990c9 --- /dev/null +++ b/demo/src/app/components/toast/demos/closeable/toast-closeable.module.ts @@ -0,0 +1,10 @@ +import {NgModule} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; + +import {NgbdToastCloseable} from './toast-closeable'; + + +@NgModule({imports: [BrowserModule, NgbModule], declarations: [NgbdToastCloseable], bootstrap: [NgbdToastCloseable]}) +export class NgbdToastCloseableModule { +} diff --git a/demo/src/app/components/toast/demos/closeable/toast-closeable.ts b/demo/src/app/components/toast/demos/closeable/toast-closeable.ts new file mode 100644 index 0000000..6bf565e --- /dev/null +++ b/demo/src/app/components/toast/demos/closeable/toast-closeable.ts @@ -0,0 +1,12 @@ +import {Component} from '@angular/core'; + +@Component({selector: 'ngbd-toast-closeable', templateUrl: './toast-closeable.html'}) + +export class NgbdToastCloseable { + show = true; + + close() { + this.show = false; + setTimeout(() => this.show = true, 5000); + } +} diff --git a/demo/src/app/components/toast/demos/custom-header/toast-custom-header.html b/demo/src/app/components/toast/demos/custom-header/toast-custom-header.html new file mode 100644 index 0000000..9d8e8c0 --- /dev/null +++ b/demo/src/app/components/toast/demos/custom-header/toast-custom-header.html @@ -0,0 +1,8 @@ + + + + Fancyheader here + + Hello, I am toast. Have you noticed my header has been generated from a Template? + +Clicking on the close icon won't do anything in this example. diff --git a/demo/src/app/components/toast/demos/custom-header/toast-custom-header.module.ts b/demo/src/app/components/toast/demos/custom-header/toast-custom-header.module.ts new file mode 100644 index 0000000..6745f63 --- /dev/null +++ b/demo/src/app/components/toast/demos/custom-header/toast-custom-header.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdToastCustomHeader } from './toast-custom-header'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdToastCustomHeader], + bootstrap: [NgbdToastCustomHeader] +}) +export class NgbdToastCustomHeaderModule {} diff --git a/demo/src/app/components/toast/demos/custom-header/toast-custom-header.ts b/demo/src/app/components/toast/demos/custom-header/toast-custom-header.ts new file mode 100644 index 0000000..5071038 --- /dev/null +++ b/demo/src/app/components/toast/demos/custom-header/toast-custom-header.ts @@ -0,0 +1,4 @@ +import { Component } from '@angular/core'; + +@Component({ selector: 'ngbd-toast-customheader', templateUrl: './toast-custom-header.html' }) +export class NgbdToastCustomHeader {} diff --git a/demo/src/app/components/toast/demos/howto-global/toast-global.component.html b/demo/src/app/components/toast/demos/howto-global/toast-global.component.html new file mode 100644 index 0000000..238febb --- /dev/null +++ b/demo/src/app/components/toast/demos/howto-global/toast-global.component.html @@ -0,0 +1,11 @@ +

Please click one of the button to see a Toast being displayed in the top right corner of your screen:

+  +  + + + + Danger Danger ! + +  + + diff --git a/demo/src/app/components/toast/demos/howto-global/toast-global.component.ts b/demo/src/app/components/toast/demos/howto-global/toast-global.component.ts new file mode 100644 index 0000000..496a5dd --- /dev/null +++ b/demo/src/app/components/toast/demos/howto-global/toast-global.component.ts @@ -0,0 +1,20 @@ +import { Component } from '@angular/core'; + +import { ToastService } from './toast-service'; + +@Component({ selector: 'ngbd-toast-global', templateUrl: './toast-global.component.html' }) +export class NgbdToastGlobal { + constructor(public toastService: ToastService) {} + + showStandard() { + this.toastService.show('I am a standard toast'); + } + + showSuccess() { + this.toastService.show('I am a success toast', { classname: 'bg-success text-light', delay: 10000 }); + } + + showDanger(dangerTpl) { + this.toastService.show(dangerTpl, { classname: 'bg-danger text-light', delay: 15000 }); + } +} diff --git a/demo/src/app/components/toast/demos/howto-global/toast-global.module.ts b/demo/src/app/components/toast/demos/howto-global/toast-global.module.ts new file mode 100644 index 0000000..050cebc --- /dev/null +++ b/demo/src/app/components/toast/demos/howto-global/toast-global.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdToastGlobal } from './toast-global.component'; +import { ToastsContainer } from './toasts-container.component'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdToastGlobal, ToastsContainer], + bootstrap: [NgbdToastGlobal] +}) +export class NgbdToastGlobalModule {} diff --git a/demo/src/app/components/toast/demos/howto-global/toast-service.ts b/demo/src/app/components/toast/demos/howto-global/toast-service.ts new file mode 100644 index 0000000..1bc7699 --- /dev/null +++ b/demo/src/app/components/toast/demos/howto-global/toast-service.ts @@ -0,0 +1,14 @@ +import { Injectable, TemplateRef } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class ToastService { + toasts: any[] = []; + + show(textOrTpl: string | TemplateRef, options: any = {}) { + this.toasts.push({ textOrTpl, ...options }); + } + + remove(toast) { + this.toasts = this.toasts.filter(t => t !== toast); + } +} diff --git a/demo/src/app/components/toast/demos/howto-global/toasts-container.component.ts b/demo/src/app/components/toast/demos/howto-global/toasts-container.component.ts new file mode 100644 index 0000000..b36d129 --- /dev/null +++ b/demo/src/app/components/toast/demos/howto-global/toasts-container.component.ts @@ -0,0 +1,29 @@ +import {Component, TemplateRef} from '@angular/core'; + +import {ToastService} from './toast-service'; + + +@Component({ + selector: 'app-toasts', + template: ` + + + + + + {{ toast.textOrTpl }} + + `, + host: {'[class.ngb-toasts]': 'true'} +}) +export class ToastsContainer { + constructor(public toastService: ToastService) {} + + isTemplate(toast) { return toast.textOrTpl instanceof TemplateRef; } +} diff --git a/demo/src/app/components/toast/demos/inline/toast-inline.html b/demo/src/app/components/toast/demos/inline/toast-inline.html new file mode 100644 index 0000000..3703964 --- /dev/null +++ b/demo/src/app/components/toast/demos/inline/toast-inline.html @@ -0,0 +1,10 @@ +
Body only
+ + I am a simple static toast. + + +
With a text header
+ + I am a simple static toast with a header. + +Clicking on the close icon won't do anything in this example. diff --git a/demo/src/app/components/toast/demos/inline/toast-inline.module.ts b/demo/src/app/components/toast/demos/inline/toast-inline.module.ts new file mode 100644 index 0000000..6ad3f34 --- /dev/null +++ b/demo/src/app/components/toast/demos/inline/toast-inline.module.ts @@ -0,0 +1,8 @@ +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdToastInline } from './toast-inline'; + +@NgModule({ imports: [BrowserModule, NgbModule], declarations: [NgbdToastInline], bootstrap: [NgbdToastInline] }) +export class NgbdToastInlineModule {} diff --git a/demo/src/app/components/toast/demos/inline/toast-inline.ts b/demo/src/app/components/toast/demos/inline/toast-inline.ts new file mode 100644 index 0000000..8ca19d2 --- /dev/null +++ b/demo/src/app/components/toast/demos/inline/toast-inline.ts @@ -0,0 +1,4 @@ +import { Component } from '@angular/core'; + +@Component({ selector: 'ngbd-toast-inline', templateUrl: './toast-inline.html' }) +export class NgbdToastInline {} diff --git a/demo/src/app/components/toast/demos/prevent-autohide/toast-prevent-autohide.html b/demo/src/app/components/toast/demos/prevent-autohide/toast-prevent-autohide.html new file mode 100644 index 0000000..9a10d76 --- /dev/null +++ b/demo/src/app/components/toast/demos/prevent-autohide/toast-prevent-autohide.html @@ -0,0 +1,24 @@ +

+ In this demo, you can show a toast by clicking the button below. It will hide itself after a 5 seconds delay unless you simply hover it with your mouse. +

+ +
+ +
+ Try to mouse hover me. +
+
+ I will remain visible until you leave again. +
+
diff --git a/demo/src/app/components/toast/demos/prevent-autohide/toast-prevent-autohide.module.ts b/demo/src/app/components/toast/demos/prevent-autohide/toast-prevent-autohide.module.ts new file mode 100644 index 0000000..6069b6b --- /dev/null +++ b/demo/src/app/components/toast/demos/prevent-autohide/toast-prevent-autohide.module.ts @@ -0,0 +1,13 @@ +import {NgModule} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; + +import {NgbdToastPreventAutohide} from './toast-prevent-autohide'; + +@NgModule({ + imports: [BrowserModule, NgbModule], + declarations: [NgbdToastPreventAutohide], + bootstrap: [NgbdToastPreventAutohide] +}) +export class NgbdToastPreventAutohideModule { +} diff --git a/demo/src/app/components/toast/demos/prevent-autohide/toast-prevent-autohide.ts b/demo/src/app/components/toast/demos/prevent-autohide/toast-prevent-autohide.ts new file mode 100644 index 0000000..c05a46f --- /dev/null +++ b/demo/src/app/components/toast/demos/prevent-autohide/toast-prevent-autohide.ts @@ -0,0 +1,8 @@ +import {Component} from '@angular/core'; + +@Component({selector: 'ngbd-toast-prevent-autohide', templateUrl: './toast-prevent-autohide.html'}) + +export class NgbdToastPreventAutohide { + show = false; + autohide = true; +} diff --git a/demo/src/app/components/toast/overview/toast-overview.component.html b/demo/src/app/components/toast/overview/toast-overview.component.html new file mode 100644 index 0000000..5ae6e0b --- /dev/null +++ b/demo/src/app/components/toast/overview/toast-overview.component.html @@ -0,0 +1,98 @@ +

+ Toasts provide feedback messages as notifications to the user.
+ Goal is to mimic the push notifications available both on mobile and desktop operating systems. +

+ + +

NgbToast component allows you to only render the corresponding markup. Use it in one of your templates, and you are done. It will render a toast.

+ +
+

Live example available here.

+

+ Nonetheless, with this inline technique, you must handle the toast's lifecycle yourself, i.e. it won't disappear automagically or in other words we don't remove the markup, nor destroy the component. +

+ To make it disappear, you can listen to the (hide) + output and remove/destroy/hide it yourself, and next section details how to do that in a real application environment. +

+ +
+ + +

Let's take the opportunity to demonstrate how to simply build a global toast management service.

+ + TLDR; + You don't feel reading these long explanations? Go to the live example here. + + +

In order to create our global toast system, 3 simple steps need to be done:

+

    +
  1. Create a global AppToastService to act as a global storage for toasts.
  2. +
  3. Create a container component <app-toasts>, acting as the host in the application to display your toasts. + You could use <ngb-toast> with an *ngFor to read the list of toasts to display from the service.
  4. +
  5. Finally, use this container component in your application.
  6. +
+ +

1. Global toast service

+

+ Relying on Angular dependency injection to share some piece of logic application-wide is always a good and solid starting choice. +

+

+ The service manages a collection of toasts. It also provides a public method to push a new toast to that same collection. + +

+ + + + You could also create an interface to type your toast instead of using any[] here. + +

+ Additionally, a method to remove an existing toast from the collection is also implemented. + +

+ +

2. Toast container component

+

+ As stated previously, <ngb-toast> only generates a valid Bootstrap toast markup. + You'll still have to position them properly on the screen. +
+ Thus, as a suggestion, toasts could be rendered in the top right corner of the application, as a kind of overlay. +

+

+ To achieve that, you could create a dedicated container component/element to render all toasts in a convenient way. + For example, this container could be positionned using CSS property position: static. +

+ + + + + +
+ +

We provide a dedicated ngb-toasts CSS class you could use, or write your own styles in case some specificities would be needed.

+
+
+ + + +
+
+

+ Lastly, let's use this container. Common sense would suggest to put it somewhere quite high in your hierarchy of components. + Your root component would be a good candidate. +

+ +

You're done! Just inject and use your AppToastService anywhere you want to create a new toast. <app-toasts> will take care of displaying them.

+ + +
+ +
+
+ Note the accessibility attributes aria-live="polite" & aria-atomic="true". They are mandatory in order to be compliant with screen readers technology. More information available on Bootstrap documentation. +
+
+ +

+ Click here to see an example a bit more advanced of this how-to. +

+
diff --git a/demo/src/app/components/toast/overview/toast-overview.component.ts b/demo/src/app/components/toast/overview/toast-overview.component.ts new file mode 100644 index 0000000..7dc9210 --- /dev/null +++ b/demo/src/app/components/toast/overview/toast-overview.component.ts @@ -0,0 +1,104 @@ +import {Component} from '@angular/core'; + +import {Snippet} from '../../../shared/code/snippet'; +import {NgbdDemoList} from '../../shared'; +import {NgbdOverview} from '../../shared/overview'; + + + +@Component({ + selector: 'ngbd-toast-overview', + templateUrl: './toast-overview.component.html', + host: {'[class.overview]': 'true'} +}) +export class NgbdToastOverviewComponent { + TOAST_INLINE_BASIC = Snippet({ + lang: 'html', + code: ` + + Content of the notification + ` + }); + + TOAST_INLINE_LIFECYCLE = Snippet({ + lang: 'html', + code: ` + + + + + + + + + `, + }); + + APP_TOAST_SERVICE = Snippet({ + lang: 'typescript', + code: ` + @Injectable({ providedIn: 'root' }) + export class AppToastService { + toasts: any[] = []; + + show(header: string, body: string) { + this.toasts.push({ header, body }); + } + }`, + }); + + APP_TOAST_SERVICE_REMOVE = Snippet({ + lang: 'typescript', + code: ` + remove(toast) { + this.toasts = this.toasts.filter(t => t != toast); + }`, + }); + + APP_TOASTS_CONTAINER_TPL = Snippet({ + lang: 'html', + code: ` + {{toast.body}}`, + }); + + APP_TOASTS_CONTAINER_STYLES = Snippet({ + lang: 'css', + code: ` + :host { + position: fixed; + top: 0; + right: 0; + margin: 0.5em; + z-index: 1200; + }`, + }); + + APP_TOASTS_CONTAINER = Snippet({ + lang: 'typescript', + code: ` + @Component({ + selector: 'app-toasts', + template: ' ... ', + styles: [' ... '] + }) + export class AppToastsComponent { + constructor(toastService: AppToastService) {} + }`, + }); + + CONTAINER_USAGE = Snippet({ + lang: 'html', + code: ` + + `, + }); + + sections: NgbdOverview = {}; + + constructor(demoList: NgbdDemoList) { this.sections = demoList.getOverviewSections('toast'); } +} diff --git a/demo/src/app/components/toast/toast.module.ts b/demo/src/app/components/toast/toast.module.ts new file mode 100644 index 0000000..2ed4540 --- /dev/null +++ b/demo/src/app/components/toast/toast.module.ts @@ -0,0 +1,103 @@ +import {NgModule} from '@angular/core'; + +import {NgbdSharedModule} from '../../../app/shared'; +import {ComponentWrapper} from '../../shared/component-wrapper/component-wrapper.component'; +import {NgbdComponentsSharedModule, NgbdDemoList} from '../shared'; +import {NgbdApiPage} from '../shared/api-page/api.component'; +import {NgbdExamplesPage} from '../shared/examples-page/examples.component'; +import {NgbdToastCloseable} from './demos/closeable/toast-closeable'; +import {NgbdToastCloseableModule} from './demos/closeable/toast-closeable.module'; +import {NgbdToastCustomHeader} from './demos/custom-header/toast-custom-header'; +import {NgbdToastCustomHeaderModule} from './demos/custom-header/toast-custom-header.module'; +import {NgbdToastGlobal} from './demos/howto-global/toast-global.component'; +import {NgbdToastGlobalModule} from './demos/howto-global/toast-global.module'; +import {NgbdToastInline} from './demos/inline/toast-inline'; +import {NgbdToastInlineModule} from './demos/inline/toast-inline.module'; +import {NgbdToastPreventAutohide} from './demos/prevent-autohide/toast-prevent-autohide'; +import {NgbdToastPreventAutohideModule} from './demos/prevent-autohide/toast-prevent-autohide.module'; +import {NgbdToastOverviewComponent} from './overview/toast-overview.component'; + +const OVERVIEW = { + 'inline-usage': 'Declarative usage', + 'toast-service': 'Building a toast management service' +}; + +const DEMOS = { + inline: { + title: 'Declarative inline usage', + type: NgbdToastInline, + code: require('!!raw-loader!./demos/inline/toast-inline'), + markup: require('!!raw-loader!./demos/inline/toast-inline.html') + }, + 'custom-header': { + title: 'Using a Template as header', + type: NgbdToastCustomHeader, + files: [ + { + name: 'toast-custom-header.html', + source: require('!!raw-loader!./demos/custom-header/toast-custom-header.html') + }, + {name: 'toast-custom-header.ts', source: require('!!raw-loader!./demos/custom-header/toast-custom-header')} + ] + }, + closeable: { + title: 'Closeable toast', + type: NgbdToastCloseable, + files: [ + {name: 'toast-closeable.html', source: require('!!raw-loader!./demos/closeable/toast-closeable.html')}, + {name: 'toast-closeable.ts', source: require('!!raw-loader!./demos/closeable/toast-closeable.ts')} + ] + }, + 'prevent-autohide': { + title: 'Prevent autohide on mouseover', + type: NgbdToastPreventAutohide, + files: [ + { + name: 'toast-prevent-autohide.html', + source: require('!!raw-loader!./demos/prevent-autohide/toast-prevent-autohide.html') + }, + { + name: 'toast-prevent-autohide.ts', + source: require('!!raw-loader!./demos/prevent-autohide/toast-prevent-autohide.ts') + } + ] + }, + global: { + title: 'Toast management service', + type: NgbdToastGlobal, + files: [ + {name: 'toast-service.ts', source: require('!!raw-loader!./demos/howto-global/toast-service.ts')}, { + name: 'toast-global.component.html', + source: require('!!raw-loader!./demos/howto-global/toast-global.component.html') + }, + {name: 'toast-global.component.ts', source: require('!!raw-loader!./demos/howto-global/toast-global.component')}, + { + name: 'toasts-container.component.ts', + source: require('!!raw-loader!./demos/howto-global/toasts-container.component') + } + ] + } +}; + +export const ROUTES = [ + {path: '', pathMatch: 'full', redirectTo: 'overview'}, { + path: '', + component: ComponentWrapper, + data: {OVERVIEW}, + children: [ + {path: 'overview', component: NgbdToastOverviewComponent}, {path: 'examples', component: NgbdExamplesPage}, + {path: 'api', component: NgbdApiPage} + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, NgbdComponentsSharedModule, NgbdToastInlineModule, NgbdToastCloseableModule, + NgbdToastCustomHeaderModule, NgbdToastPreventAutohideModule, NgbdToastGlobalModule + ], + declarations: [NgbdToastOverviewComponent] +}) +export class NgbdToastModule { + constructor(demoList: NgbdDemoList) { demoList.register('toast', DEMOS, OVERVIEW); } +} diff --git a/demo/src/app/components/tooltip/demos/autoclose/tooltip-autoclose.html b/demo/src/app/components/tooltip/demos/autoclose/tooltip-autoclose.html new file mode 100644 index 0000000..d5163c3 --- /dev/null +++ b/demo/src/app/components/tooltip/demos/autoclose/tooltip-autoclose.html @@ -0,0 +1,41 @@ +

As for some other popup-based widgets, you can set the tooltip to close automatically upon some events.

+

In the following examples, they will all close on Escape as well as:

+ +
    +
  • + click inside: + +
  • + +
  • + click outside: + +
  • + +
  • + all clicks: + + +   + + +
  • +
diff --git a/demo/src/app/components/tooltip/demos/autoclose/tooltip-autoclose.module.ts b/demo/src/app/components/tooltip/demos/autoclose/tooltip-autoclose.module.ts new file mode 100644 index 0000000..915ac80 --- /dev/null +++ b/demo/src/app/components/tooltip/demos/autoclose/tooltip-autoclose.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTooltipAutoclose } from './tooltip-autoclose'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTooltipAutoclose], + exports: [NgbdTooltipAutoclose], + bootstrap: [NgbdTooltipAutoclose] +}) +export class NgbdTooltipAutocloseModule {} diff --git a/demo/src/app/components/tooltip/demos/autoclose/tooltip-autoclose.ts b/demo/src/app/components/tooltip/demos/autoclose/tooltip-autoclose.ts new file mode 100644 index 0000000..d74600b --- /dev/null +++ b/demo/src/app/components/tooltip/demos/autoclose/tooltip-autoclose.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + + + +@Component({ + selector: 'ngbd-tooltip-autoclose', + templateUrl: './tooltip-autoclose.html' +}) +export class NgbdTooltipAutoclose {} diff --git a/demo/src/app/components/tooltip/demos/basic/tooltip-basic.html b/demo/src/app/components/tooltip/demos/basic/tooltip-basic.html new file mode 100644 index 0000000..d9acf17 --- /dev/null +++ b/demo/src/app/components/tooltip/demos/basic/tooltip-basic.html @@ -0,0 +1,12 @@ + + + + diff --git a/demo/src/app/components/tooltip/demos/basic/tooltip-basic.module.ts b/demo/src/app/components/tooltip/demos/basic/tooltip-basic.module.ts new file mode 100644 index 0000000..c82d0bc --- /dev/null +++ b/demo/src/app/components/tooltip/demos/basic/tooltip-basic.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTooltipBasic } from './tooltip-basic'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTooltipBasic], + exports: [NgbdTooltipBasic], + bootstrap: [NgbdTooltipBasic] +}) +export class NgbdTooltipBasicModule {} diff --git a/demo/src/app/components/tooltip/demos/basic/tooltip-basic.ts b/demo/src/app/components/tooltip/demos/basic/tooltip-basic.ts new file mode 100644 index 0000000..7b63aae --- /dev/null +++ b/demo/src/app/components/tooltip/demos/basic/tooltip-basic.ts @@ -0,0 +1,8 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-tooltip-basic', + templateUrl: './tooltip-basic.html' +}) +export class NgbdTooltipBasic { +} diff --git a/demo/src/app/components/tooltip/demos/config/tooltip-config.html b/demo/src/app/components/tooltip/demos/config/tooltip-config.html new file mode 100644 index 0000000..d35a925 --- /dev/null +++ b/demo/src/app/components/tooltip/demos/config/tooltip-config.html @@ -0,0 +1,3 @@ + diff --git a/demo/src/app/components/tooltip/demos/config/tooltip-config.module.ts b/demo/src/app/components/tooltip/demos/config/tooltip-config.module.ts new file mode 100644 index 0000000..e265da8 --- /dev/null +++ b/demo/src/app/components/tooltip/demos/config/tooltip-config.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTooltipConfig } from './tooltip-config'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTooltipConfig], + exports: [NgbdTooltipConfig], + bootstrap: [NgbdTooltipConfig] +}) +export class NgbdTooltipConfigModule {} diff --git a/demo/src/app/components/tooltip/demos/config/tooltip-config.ts b/demo/src/app/components/tooltip/demos/config/tooltip-config.ts new file mode 100644 index 0000000..b360fac --- /dev/null +++ b/demo/src/app/components/tooltip/demos/config/tooltip-config.ts @@ -0,0 +1,15 @@ +import {Component} from '@angular/core'; +import {NgbTooltipConfig} from '@ng-bootstrap/ng-bootstrap'; + +@Component({ + selector: 'ngbd-tooltip-config', + templateUrl: './tooltip-config.html', + providers: [NgbTooltipConfig] // add NgbTooltipConfig to the component providers +}) +export class NgbdTooltipConfig { + constructor(config: NgbTooltipConfig) { + // customize default values of tooltips used by this component tree + config.placement = 'right'; + config.triggers = 'click'; + } +} diff --git a/demo/src/app/components/tooltip/demos/container/tooltip-container.html b/demo/src/app/components/tooltip/demos/container/tooltip-container.html new file mode 100644 index 0000000..b772fe4 --- /dev/null +++ b/demo/src/app/components/tooltip/demos/container/tooltip-container.html @@ -0,0 +1,16 @@ +

+ Set the container property to "body" to have the tooltip be appended to the body instead of the triggering element's parent. This option is useful if the element triggering the tooltip is inside an element that clips its contents (i.e. overflow: hidden). +

+ +
+
+ + +
+
diff --git a/demo/src/app/components/tooltip/demos/container/tooltip-container.module.ts b/demo/src/app/components/tooltip/demos/container/tooltip-container.module.ts new file mode 100644 index 0000000..635671c --- /dev/null +++ b/demo/src/app/components/tooltip/demos/container/tooltip-container.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTooltipContainer } from './tooltip-container'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTooltipContainer], + exports: [NgbdTooltipContainer], + bootstrap: [NgbdTooltipContainer] +}) +export class NgbdTooltipContainerModule {} diff --git a/demo/src/app/components/tooltip/demos/container/tooltip-container.ts b/demo/src/app/components/tooltip/demos/container/tooltip-container.ts new file mode 100644 index 0000000..37ddfd9 --- /dev/null +++ b/demo/src/app/components/tooltip/demos/container/tooltip-container.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-tooltip-container', + templateUrl: './tooltip-container.html', + styles: ['.card { overflow:hidden }'] +}) +export class NgbdTooltipContainer { +} diff --git a/demo/src/app/components/tooltip/demos/customclass/tooltip-custom-class.module.ts b/demo/src/app/components/tooltip/demos/customclass/tooltip-custom-class.module.ts new file mode 100644 index 0000000..04a4e50 --- /dev/null +++ b/demo/src/app/components/tooltip/demos/customclass/tooltip-custom-class.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTooltipCustomclass } from './tooltip-customclass'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTooltipCustomclass], + exports: [NgbdTooltipCustomclass], + bootstrap: [NgbdTooltipCustomclass] +}) +export class NgbdTooltipCustomClassModule {} diff --git a/demo/src/app/components/tooltip/demos/customclass/tooltip-customclass.html b/demo/src/app/components/tooltip/demos/customclass/tooltip-customclass.html new file mode 100644 index 0000000..dd8de2e --- /dev/null +++ b/demo/src/app/components/tooltip/demos/customclass/tooltip-customclass.html @@ -0,0 +1,8 @@ +

+ You can optionally pass in a custom class via tooltipClass +

+ + diff --git a/demo/src/app/components/tooltip/demos/customclass/tooltip-customclass.ts b/demo/src/app/components/tooltip/demos/customclass/tooltip-customclass.ts new file mode 100644 index 0000000..9e28986 --- /dev/null +++ b/demo/src/app/components/tooltip/demos/customclass/tooltip-customclass.ts @@ -0,0 +1,18 @@ +import {Component, ViewEncapsulation} from '@angular/core'; + +@Component({ + selector: 'ngbd-tooltip-customclass', + templateUrl: './tooltip-customclass.html', + encapsulation: ViewEncapsulation.None, + styles: [` + .my-custom-class .tooltip-inner { + background-color: darkgreen; + font-size: 125%; + } + .my-custom-class .arrow::before { + border-top-color: darkgreen; + } + `] +}) +export class NgbdTooltipCustomclass { +} diff --git a/demo/src/app/components/tooltip/demos/delay/tooltip-delay.html b/demo/src/app/components/tooltip/demos/delay/tooltip-delay.html new file mode 100644 index 0000000..d9bbf7b --- /dev/null +++ b/demo/src/app/components/tooltip/demos/delay/tooltip-delay.html @@ -0,0 +1,15 @@ +

+ When using non-manual triggers, you can control the delay to open and close the tooltip through the openDelay and + closeDelay properties. Note that the autoClose feature does not use the close delay, it closes the tooltip immediately. +

+ + + diff --git a/demo/src/app/components/tooltip/demos/delay/tooltip-delay.module.ts b/demo/src/app/components/tooltip/demos/delay/tooltip-delay.module.ts new file mode 100644 index 0000000..81c0201 --- /dev/null +++ b/demo/src/app/components/tooltip/demos/delay/tooltip-delay.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTooltipDelay } from './tooltip-delay'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTooltipDelay], + exports: [NgbdTooltipDelay], + bootstrap: [NgbdTooltipDelay] +}) +export class NgbdTooltipDelayModule {} diff --git a/demo/src/app/components/tooltip/demos/delay/tooltip-delay.ts b/demo/src/app/components/tooltip/demos/delay/tooltip-delay.ts new file mode 100644 index 0000000..e4d8161 --- /dev/null +++ b/demo/src/app/components/tooltip/demos/delay/tooltip-delay.ts @@ -0,0 +1,8 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-tooltip-delay', + templateUrl: './tooltip-delay.html' +}) +export class NgbdTooltipDelay { +} diff --git a/demo/src/app/components/tooltip/demos/tplcontent/tooltip-tpl-content.module.ts b/demo/src/app/components/tooltip/demos/tplcontent/tooltip-tpl-content.module.ts new file mode 100644 index 0000000..308b6ba --- /dev/null +++ b/demo/src/app/components/tooltip/demos/tplcontent/tooltip-tpl-content.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTooltipTplcontent } from './tooltip-tplcontent'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTooltipTplcontent], + exports: [NgbdTooltipTplcontent], + bootstrap: [NgbdTooltipTplcontent] +}) +export class NgbdTooltipTplContentModule {} diff --git a/demo/src/app/components/tooltip/demos/tplcontent/tooltip-tplcontent.html b/demo/src/app/components/tooltip/demos/tplcontent/tooltip-tplcontent.html new file mode 100644 index 0000000..d74943d --- /dev/null +++ b/demo/src/app/components/tooltip/demos/tplcontent/tooltip-tplcontent.html @@ -0,0 +1,9 @@ +

+ Tooltips can contain any arbitrary HTML, Angular bindings and even directives! + Simply enclose desired content in a <ng-template> element. +

+ +Hello, {{name}}! + diff --git a/demo/src/app/components/tooltip/demos/tplcontent/tooltip-tplcontent.ts b/demo/src/app/components/tooltip/demos/tplcontent/tooltip-tplcontent.ts new file mode 100644 index 0000000..89c5de1 --- /dev/null +++ b/demo/src/app/components/tooltip/demos/tplcontent/tooltip-tplcontent.ts @@ -0,0 +1,9 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-tooltip-tplcontent', + templateUrl: './tooltip-tplcontent.html' +}) +export class NgbdTooltipTplcontent { + name = 'World'; +} diff --git a/demo/src/app/components/tooltip/demos/tplwithcontext/tooltip-tpl-with-context.module.ts b/demo/src/app/components/tooltip/demos/tplwithcontext/tooltip-tpl-with-context.module.ts new file mode 100644 index 0000000..3ec3aef --- /dev/null +++ b/demo/src/app/components/tooltip/demos/tplwithcontext/tooltip-tpl-with-context.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { NgbdTooltipTplwithcontext } from './tooltip-tplwithcontext'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTooltipTplwithcontext], + exports: [NgbdTooltipTplwithcontext], + bootstrap: [NgbdTooltipTplwithcontext] +}) +export class NgbdTooltipTplWithContextModule {} diff --git a/demo/src/app/components/tooltip/demos/tplwithcontext/tooltip-tplwithcontext.html b/demo/src/app/components/tooltip/demos/tplwithcontext/tooltip-tplwithcontext.html new file mode 100644 index 0000000..27b64d4 --- /dev/null +++ b/demo/src/app/components/tooltip/demos/tplwithcontext/tooltip-tplwithcontext.html @@ -0,0 +1,26 @@ +

+ You can optionally pass in a context when manually triggering a tooltip. +

+ +{{greeting}}, {{name}}! + + + diff --git a/demo/src/app/components/tooltip/demos/tplwithcontext/tooltip-tplwithcontext.ts b/demo/src/app/components/tooltip/demos/tplwithcontext/tooltip-tplwithcontext.ts new file mode 100644 index 0000000..2148807 --- /dev/null +++ b/demo/src/app/components/tooltip/demos/tplwithcontext/tooltip-tplwithcontext.ts @@ -0,0 +1,17 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-tooltip-tplwithcontext', + templateUrl: './tooltip-tplwithcontext.html' +}) +export class NgbdTooltipTplwithcontext { + name = 'World'; + + toggleWithGreeting(tooltip, greeting: string) { + if (tooltip.isOpen()) { + tooltip.close(); + } else { + tooltip.open({greeting}); + } + } +} diff --git a/demo/src/app/components/tooltip/demos/triggers/tooltip-triggers.html b/demo/src/app/components/tooltip/demos/triggers/tooltip-triggers.html new file mode 100644 index 0000000..093870b --- /dev/null +++ b/demo/src/app/components/tooltip/demos/triggers/tooltip-triggers.html @@ -0,0 +1,19 @@ +

+ You can easily override open and close triggers by specifying event names (separated by :) in the triggers property. +

+ + + +
+

+ Alternatively you can take full manual control over tooltip opening / closing events. +

+ + + diff --git a/demo/src/app/components/tooltip/demos/triggers/tooltip-triggers.module.ts b/demo/src/app/components/tooltip/demos/triggers/tooltip-triggers.module.ts new file mode 100644 index 0000000..fa0593d --- /dev/null +++ b/demo/src/app/components/tooltip/demos/triggers/tooltip-triggers.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTooltipTriggers } from './tooltip-triggers'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTooltipTriggers], + exports: [NgbdTooltipTriggers], + bootstrap: [NgbdTooltipTriggers] +}) +export class NgbdTooltipTriggersModule {} diff --git a/demo/src/app/components/tooltip/demos/triggers/tooltip-triggers.ts b/demo/src/app/components/tooltip/demos/triggers/tooltip-triggers.ts new file mode 100644 index 0000000..2fe753d --- /dev/null +++ b/demo/src/app/components/tooltip/demos/triggers/tooltip-triggers.ts @@ -0,0 +1,8 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'ngbd-tooltip-triggers', + templateUrl: './tooltip-triggers.html' +}) +export class NgbdTooltipTriggers { +} diff --git a/demo/src/app/components/tooltip/tooltip.module.ts b/demo/src/app/components/tooltip/tooltip.module.ts new file mode 100644 index 0000000..229465c --- /dev/null +++ b/demo/src/app/components/tooltip/tooltip.module.ts @@ -0,0 +1,115 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component'; +import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared'; +import { NgbdApiPage } from '../shared/api-page/api.component'; +import { NgbdExamplesPage } from '../shared/examples-page/examples.component'; +import { NgbdTooltipAutoclose } from './demos/autoclose/tooltip-autoclose'; +import { NgbdTooltipAutocloseModule } from './demos/autoclose/tooltip-autoclose.module'; +import { NgbdTooltipBasic } from './demos/basic/tooltip-basic'; +import { NgbdTooltipBasicModule } from './demos/basic/tooltip-basic.module'; +import { NgbdTooltipConfig } from './demos/config/tooltip-config'; +import { NgbdTooltipConfigModule } from './demos/config/tooltip-config.module'; +import { NgbdTooltipContainer } from './demos/container/tooltip-container'; +import { NgbdTooltipContainerModule } from './demos/container/tooltip-container.module'; +import { NgbdTooltipCustomClassModule } from './demos/customclass/tooltip-custom-class.module'; +import { NgbdTooltipCustomclass } from './demos/customclass/tooltip-customclass'; +import { NgbdTooltipDelay } from './demos/delay/tooltip-delay'; +import { NgbdTooltipDelayModule } from './demos/delay/tooltip-delay.module'; +import { NgbdTooltipTplContentModule } from './demos/tplcontent/tooltip-tpl-content.module'; +import { NgbdTooltipTplcontent } from './demos/tplcontent/tooltip-tplcontent'; +import { NgbdTooltipTplWithContextModule } from './demos/tplwithcontext/tooltip-tpl-with-context.module'; +import { NgbdTooltipTplwithcontext } from './demos/tplwithcontext/tooltip-tplwithcontext'; +import { NgbdTooltipTriggers } from './demos/triggers/tooltip-triggers'; +import { NgbdTooltipTriggersModule } from './demos/triggers/tooltip-triggers.module'; + +const DEMOS = { + basic: { + title: 'Quick and easy tooltips', + type: NgbdTooltipBasic, + code: require('!!raw-loader!./demos/basic/tooltip-basic'), + markup: require('!!raw-loader!./demos/basic/tooltip-basic.html') + }, + tplcontent: { + title: 'HTML and bindings in tooltips', + type: NgbdTooltipTplcontent, + code: require('!!raw-loader!./demos/tplcontent/tooltip-tplcontent'), + markup: require('!!raw-loader!./demos/tplcontent/tooltip-tplcontent.html') + }, + triggers: { + title: 'Custom and manual triggers', + type: NgbdTooltipTriggers, + code: require('!!raw-loader!./demos/triggers/tooltip-triggers'), + markup: require('!!raw-loader!./demos/triggers/tooltip-triggers.html') + }, + autoclose: { + title: 'Automatic closing with keyboard and mouse', + type: NgbdTooltipAutoclose, + code: require('!!raw-loader!./demos/autoclose/tooltip-autoclose'), + markup: require('!!raw-loader!./demos/autoclose/tooltip-autoclose.html') + }, + tplwithcontext: { + title: 'Context and manual triggers', + type: NgbdTooltipTplwithcontext, + code: require('!!raw-loader!./demos/tplwithcontext/tooltip-tplwithcontext'), + markup: require('!!raw-loader!./demos/tplwithcontext/tooltip-tplwithcontext.html') + }, + delay: { + title: 'Open and close delays', + type: NgbdTooltipDelay, + code: require('!!raw-loader!./demos/delay/tooltip-delay'), + markup: require('!!raw-loader!./demos/delay/tooltip-delay.html') + }, + container: { + title: 'Append tooltip in the body', + type: NgbdTooltipContainer, + code: require('!!raw-loader!./demos/container/tooltip-container'), + markup: require('!!raw-loader!./demos/container/tooltip-container.html') + }, + customclass: { + title: 'Tooltip with custom class', + type: NgbdTooltipCustomclass, + code: require('!!raw-loader!./demos/customclass/tooltip-customclass'), + markup: require('!!raw-loader!./demos/customclass/tooltip-customclass.html') + }, + config: { + title: 'Global configuration of tooltips', + type: NgbdTooltipConfig, + code: require('!!raw-loader!./demos/config/tooltip-config'), + markup: require('!!raw-loader!./demos/config/tooltip-config.html') + } +}; + +export const ROUTES = [ + { path: '', pathMatch: 'full', redirectTo: 'examples' }, + { + path: '', + component: ComponentWrapper, + children: [ + { path: 'examples', component: NgbdExamplesPage }, + { path: 'api', component: NgbdApiPage } + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, + NgbdComponentsSharedModule, + NgbdTooltipBasicModule, + NgbdTooltipContainerModule, + NgbdTooltipCustomClassModule, + NgbdTooltipDelayModule, + NgbdTooltipTplContentModule, + NgbdTooltipTriggersModule, + NgbdTooltipAutocloseModule, + NgbdTooltipConfigModule, + NgbdTooltipTplWithContextModule + ] +}) +export class NgbdTooltipModule { + constructor(demoList: NgbdDemoList) { + demoList.register('tooltip', DEMOS); + } +} diff --git a/demo/src/app/components/typeahead/demos/basic/typeahead-basic.html b/demo/src/app/components/typeahead/demos/basic/typeahead-basic.html new file mode 100644 index 0000000..e5f8c26 --- /dev/null +++ b/demo/src/app/components/typeahead/demos/basic/typeahead-basic.html @@ -0,0 +1,11 @@ +A typeahead example that gets values from a static string[] +
    +
  • debounceTime operator
  • +
  • kicks in only if 2+ characters typed
  • +
  • limits to 10 results
  • +
+ + + +
+
Model: {{ model | json }}
diff --git a/demo/src/app/components/typeahead/demos/basic/typeahead-basic.module.ts b/demo/src/app/components/typeahead/demos/basic/typeahead-basic.module.ts new file mode 100644 index 0000000..e54782f --- /dev/null +++ b/demo/src/app/components/typeahead/demos/basic/typeahead-basic.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTypeaheadBasic } from './typeahead-basic'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTypeaheadBasic], + exports: [NgbdTypeaheadBasic], + bootstrap: [NgbdTypeaheadBasic] +}) +export class NgbdTypeaheadBasicModule {} diff --git a/demo/src/app/components/typeahead/demos/basic/typeahead-basic.ts b/demo/src/app/components/typeahead/demos/basic/typeahead-basic.ts new file mode 100644 index 0000000..63975a9 --- /dev/null +++ b/demo/src/app/components/typeahead/demos/basic/typeahead-basic.ts @@ -0,0 +1,30 @@ +import {Component} from '@angular/core'; +import {Observable} from 'rxjs'; +import {debounceTime, distinctUntilChanged, map} from 'rxjs/operators'; + +const states = ['Alabama', 'Alaska', 'American Samoa', 'Arizona', 'Arkansas', 'California', 'Colorado', + 'Connecticut', 'Delaware', 'District Of Columbia', 'Federated States Of Micronesia', 'Florida', 'Georgia', + 'Guam', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine', + 'Marshall Islands', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri', 'Montana', + 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'New York', 'North Carolina', 'North Dakota', + 'Northern Mariana Islands', 'Ohio', 'Oklahoma', 'Oregon', 'Palau', 'Pennsylvania', 'Puerto Rico', 'Rhode Island', + 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virgin Islands', 'Virginia', + 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming']; + +@Component({ + selector: 'ngbd-typeahead-basic', + templateUrl: './typeahead-basic.html', + styles: [`.form-control { width: 300px; }`] +}) +export class NgbdTypeaheadBasic { + public model: any; + + search = (text$: Observable) => + text$.pipe( + debounceTime(200), + distinctUntilChanged(), + map(term => term.length < 2 ? [] + : states.filter(v => v.toLowerCase().indexOf(term.toLowerCase()) > -1).slice(0, 10)) + ) + +} diff --git a/demo/src/app/components/typeahead/demos/config/typeahead-config.html b/demo/src/app/components/typeahead/demos/config/typeahead-config.html new file mode 100644 index 0000000..f3ce33f --- /dev/null +++ b/demo/src/app/components/typeahead/demos/config/typeahead-config.html @@ -0,0 +1,5 @@ +

This typeahead shows a hint when the input matches because the default values have been customized.

+ + + + diff --git a/demo/src/app/components/typeahead/demos/config/typeahead-config.module.ts b/demo/src/app/components/typeahead/demos/config/typeahead-config.module.ts new file mode 100644 index 0000000..afee426 --- /dev/null +++ b/demo/src/app/components/typeahead/demos/config/typeahead-config.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTypeaheadConfig } from './typeahead-config'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTypeaheadConfig], + exports: [NgbdTypeaheadConfig], + bootstrap: [NgbdTypeaheadConfig] +}) +export class NgbdTypeaheadConfigModule {} diff --git a/demo/src/app/components/typeahead/demos/config/typeahead-config.ts b/demo/src/app/components/typeahead/demos/config/typeahead-config.ts new file mode 100644 index 0000000..5165b6c --- /dev/null +++ b/demo/src/app/components/typeahead/demos/config/typeahead-config.ts @@ -0,0 +1,36 @@ +import {Component} from '@angular/core'; +import {Observable} from 'rxjs'; +import {NgbTypeaheadConfig} from '@ng-bootstrap/ng-bootstrap'; +import {debounceTime, distinctUntilChanged, map} from 'rxjs/operators'; + +const states = ['Alabama', 'Alaska', 'American Samoa', 'Arizona', 'Arkansas', 'California', 'Colorado', + 'Connecticut', 'Delaware', 'District Of Columbia', 'Federated States Of Micronesia', 'Florida', 'Georgia', + 'Guam', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine', + 'Marshall Islands', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri', 'Montana', + 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'New York', 'North Carolina', 'North Dakota', + 'Northern Mariana Islands', 'Ohio', 'Oklahoma', 'Oregon', 'Palau', 'Pennsylvania', 'Puerto Rico', 'Rhode Island', + 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virgin Islands', 'Virginia', + 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming']; + +@Component({ + selector: 'ngbd-typeahead-config', + templateUrl: './typeahead-config.html', + styles: [`.form-control { width: 300px; }`], + providers: [NgbTypeaheadConfig] // add NgbTypeaheadConfig to the component providers +}) +export class NgbdTypeaheadConfig { + public model: any; + + constructor(config: NgbTypeaheadConfig) { + // customize default values of typeaheads used by this component tree + config.showHint = true; + } + + search = (text$: Observable) => + text$.pipe( + debounceTime(200), + distinctUntilChanged(), + map(term => term.length < 2 ? [] + : states.filter(v => v.toLowerCase().startsWith(term.toLocaleLowerCase())).splice(0, 10)) + ) +} diff --git a/demo/src/app/components/typeahead/demos/focus/typeahead-focus.html b/demo/src/app/components/typeahead/demos/focus/typeahead-focus.html new file mode 100644 index 0000000..ba3b7b0 --- /dev/null +++ b/demo/src/app/components/typeahead/demos/focus/typeahead-focus.html @@ -0,0 +1,23 @@ +It is possible to get the focus events with the current input value to emit results on focus with a great flexibility. + +In this simple example, a search is done no matter the content of the input: + +
    +
  • on empty input all options will be taken
  • +
  • otherwise options will be filtered against the search term
  • +
  • it will limit the display to 10 results in all cases
  • +
+ + + +
+
Model: {{ model | json }}
diff --git a/demo/src/app/components/typeahead/demos/focus/typeahead-focus.module.ts b/demo/src/app/components/typeahead/demos/focus/typeahead-focus.module.ts new file mode 100644 index 0000000..147d260 --- /dev/null +++ b/demo/src/app/components/typeahead/demos/focus/typeahead-focus.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTypeaheadFocus } from './typeahead-focus'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTypeaheadFocus], + exports: [NgbdTypeaheadFocus], + bootstrap: [NgbdTypeaheadFocus] +}) +export class NgbdTypeaheadFocusModule {} diff --git a/demo/src/app/components/typeahead/demos/focus/typeahead-focus.ts b/demo/src/app/components/typeahead/demos/focus/typeahead-focus.ts new file mode 100644 index 0000000..2b2979b --- /dev/null +++ b/demo/src/app/components/typeahead/demos/focus/typeahead-focus.ts @@ -0,0 +1,37 @@ +import {Component, ViewChild} from '@angular/core'; +import {NgbTypeahead} from '@ng-bootstrap/ng-bootstrap'; +import {Observable, Subject, merge} from 'rxjs'; +import {debounceTime, distinctUntilChanged, filter, map} from 'rxjs/operators'; + +const states = ['Alabama', 'Alaska', 'American Samoa', 'Arizona', 'Arkansas', 'California', 'Colorado', + 'Connecticut', 'Delaware', 'District Of Columbia', 'Federated States Of Micronesia', 'Florida', 'Georgia', + 'Guam', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine', + 'Marshall Islands', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri', 'Montana', + 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'New York', 'North Carolina', 'North Dakota', + 'Northern Mariana Islands', 'Ohio', 'Oklahoma', 'Oregon', 'Palau', 'Pennsylvania', 'Puerto Rico', 'Rhode Island', + 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virgin Islands', 'Virginia', + 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming']; + +@Component({ + selector: 'ngbd-typeahead-focus', + templateUrl: './typeahead-focus.html', + styles: [`.form-control { width: 300px; }`] +}) +export class NgbdTypeaheadFocus { + model: any; + + @ViewChild('instance', {static: true}) instance: NgbTypeahead; + focus$ = new Subject(); + click$ = new Subject(); + + search = (text$: Observable) => { + const debouncedText$ = text$.pipe(debounceTime(200), distinctUntilChanged()); + const clicksWithClosedPopup$ = this.click$.pipe(filter(() => !this.instance.isPopupOpen())); + const inputFocus$ = this.focus$; + + return merge(debouncedText$, inputFocus$, clicksWithClosedPopup$).pipe( + map(term => (term === '' ? states + : states.filter(v => v.toLowerCase().indexOf(term.toLowerCase()) > -1)).slice(0, 10)) + ); + } +} diff --git a/demo/src/app/components/typeahead/demos/format/typeahead-format.html b/demo/src/app/components/typeahead/demos/format/typeahead-format.html new file mode 100644 index 0000000..0c3f60d --- /dev/null +++ b/demo/src/app/components/typeahead/demos/format/typeahead-format.html @@ -0,0 +1,6 @@ +

A typeahead example that uses a formatter function for string results

+ + + +
+
Model: {{ model | json }}
diff --git a/demo/src/app/components/typeahead/demos/format/typeahead-format.module.ts b/demo/src/app/components/typeahead/demos/format/typeahead-format.module.ts new file mode 100644 index 0000000..320d6c8 --- /dev/null +++ b/demo/src/app/components/typeahead/demos/format/typeahead-format.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTypeaheadFormat } from './typeahead-format'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTypeaheadFormat], + exports: [NgbdTypeaheadFormat], + bootstrap: [NgbdTypeaheadFormat] +}) +export class NgbdTypeaheadFormatModule {} diff --git a/demo/src/app/components/typeahead/demos/format/typeahead-format.ts b/demo/src/app/components/typeahead/demos/format/typeahead-format.ts new file mode 100644 index 0000000..5b68660 --- /dev/null +++ b/demo/src/app/components/typeahead/demos/format/typeahead-format.ts @@ -0,0 +1,31 @@ +import {Component} from '@angular/core'; +import {Observable} from 'rxjs'; +import {debounceTime, distinctUntilChanged, map} from 'rxjs/operators'; + +const states = ['Alabama', 'Alaska', 'American Samoa', 'Arizona', 'Arkansas', 'California', 'Colorado', + 'Connecticut', 'Delaware', 'District Of Columbia', 'Federated States Of Micronesia', 'Florida', 'Georgia', + 'Guam', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky', 'Louisiana', 'Maine', + 'Marshall Islands', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi', 'Missouri', 'Montana', + 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico', 'New York', 'North Carolina', 'North Dakota', + 'Northern Mariana Islands', 'Ohio', 'Oklahoma', 'Oregon', 'Palau', 'Pennsylvania', 'Puerto Rico', 'Rhode Island', + 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont', 'Virgin Islands', 'Virginia', + 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming']; + +@Component({ + selector: 'ngbd-typeahead-format', + templateUrl: './typeahead-format.html', + styles: [`.form-control { width: 300px; }`] +}) +export class NgbdTypeaheadFormat { + public model: any; + + formatter = (result: string) => result.toUpperCase(); + + search = (text$: Observable) => + text$.pipe( + debounceTime(200), + distinctUntilChanged(), + map(term => term === '' ? [] + : states.filter(v => v.toLowerCase().indexOf(term.toLowerCase()) > -1).slice(0, 10)) + ) +} diff --git a/demo/src/app/components/typeahead/demos/http/typeahead-http.html b/demo/src/app/components/typeahead/demos/http/typeahead-http.html new file mode 100644 index 0000000..c2ff11c --- /dev/null +++ b/demo/src/app/components/typeahead/demos/http/typeahead-http.html @@ -0,0 +1,19 @@ +A typeahead example that gets values from the WikipediaService +
    +
  • remote data retrieval
  • +
  • debounceTime operator
  • +
  • tap operator
  • +
  • distinctUntilChanged operator
  • +
  • switchMap operator
  • +
  • catch operator to display an error message in case of connectivity issue
  • +
+ +
+ + + searching... +
Sorry, suggestions could not be loaded.
+
+ +
+
Model: {{ model | json }}
diff --git a/demo/src/app/components/typeahead/demos/http/typeahead-http.module.ts b/demo/src/app/components/typeahead/demos/http/typeahead-http.module.ts new file mode 100644 index 0000000..c8aa753 --- /dev/null +++ b/demo/src/app/components/typeahead/demos/http/typeahead-http.module.ts @@ -0,0 +1,15 @@ +import { HttpClientModule } from '@angular/common/http'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTypeaheadHttp } from './typeahead-http'; + +@NgModule({ + imports: [BrowserModule, FormsModule, HttpClientModule, NgbModule], + declarations: [NgbdTypeaheadHttp], + exports: [NgbdTypeaheadHttp], + bootstrap: [NgbdTypeaheadHttp] +}) +export class NgbdTypeaheadHttpModule {} diff --git a/demo/src/app/components/typeahead/demos/http/typeahead-http.ts b/demo/src/app/components/typeahead/demos/http/typeahead-http.ts new file mode 100644 index 0000000..3c27b78 --- /dev/null +++ b/demo/src/app/components/typeahead/demos/http/typeahead-http.ts @@ -0,0 +1,59 @@ +import {Component, Injectable} from '@angular/core'; +import {HttpClient, HttpParams} from '@angular/common/http'; +import {Observable, of} from 'rxjs'; +import {catchError, debounceTime, distinctUntilChanged, map, tap, switchMap} from 'rxjs/operators'; + +const WIKI_URL = 'https://en.wikipedia.org/w/api.php'; +const PARAMS = new HttpParams({ + fromObject: { + action: 'opensearch', + format: 'json', + origin: '*' + } +}); + +@Injectable() +export class WikipediaService { + constructor(private http: HttpClient) {} + + search(term: string) { + if (term === '') { + return of([]); + } + + return this.http + .get(WIKI_URL, {params: PARAMS.set('search', term)}).pipe( + map(response => response[1]) + ); + } +} + +@Component({ + selector: 'ngbd-typeahead-http', + templateUrl: './typeahead-http.html', + providers: [WikipediaService], + styles: [`.form-control { width: 300px; display: inline; }`] +}) +export class NgbdTypeaheadHttp { + model: any; + searching = false; + searchFailed = false; + + constructor(private _service: WikipediaService) {} + + search = (text$: Observable) => + text$.pipe( + debounceTime(300), + distinctUntilChanged(), + tap(() => this.searching = true), + switchMap(term => + this._service.search(term).pipe( + tap(() => this.searchFailed = false), + catchError(() => { + this.searchFailed = true; + return of([]); + })) + ), + tap(() => this.searching = false) + ) +} diff --git a/demo/src/app/components/typeahead/demos/template/typeahead-template.html b/demo/src/app/components/typeahead/demos/template/typeahead-template.html new file mode 100644 index 0000000..988925c --- /dev/null +++ b/demo/src/app/components/typeahead/demos/template/typeahead-template.html @@ -0,0 +1,14 @@ +

A typeahead example that uses a custom template for results display, an object as the model, + and the highlight directive to highlight the term inside the custom template. +

+ + + + + + + + +
+
Model: {{ model | json }}
diff --git a/demo/src/app/components/typeahead/demos/template/typeahead-template.module.ts b/demo/src/app/components/typeahead/demos/template/typeahead-template.module.ts new file mode 100644 index 0000000..6f38efb --- /dev/null +++ b/demo/src/app/components/typeahead/demos/template/typeahead-template.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { NgbdTypeaheadTemplate } from './typeahead-template'; + +@NgModule({ + imports: [BrowserModule, FormsModule, NgbModule], + declarations: [NgbdTypeaheadTemplate], + exports: [NgbdTypeaheadTemplate], + bootstrap: [NgbdTypeaheadTemplate] +}) +export class NgbdTypeaheadTemplateModule {} diff --git a/demo/src/app/components/typeahead/demos/template/typeahead-template.ts b/demo/src/app/components/typeahead/demos/template/typeahead-template.ts new file mode 100644 index 0000000..f69f719 --- /dev/null +++ b/demo/src/app/components/typeahead/demos/template/typeahead-template.ts @@ -0,0 +1,78 @@ +import {Component} from '@angular/core'; +import {Observable} from 'rxjs'; +import {debounceTime, map} from 'rxjs/operators'; + +const statesWithFlags: {name: string, flag: string}[] = [ + {'name': 'Alabama', 'flag': '5/5c/Flag_of_Alabama.svg/45px-Flag_of_Alabama.svg.png'}, + {'name': 'Alaska', 'flag': 'e/e6/Flag_of_Alaska.svg/43px-Flag_of_Alaska.svg.png'}, + {'name': 'Arizona', 'flag': '9/9d/Flag_of_Arizona.svg/45px-Flag_of_Arizona.svg.png'}, + {'name': 'Arkansas', 'flag': '9/9d/Flag_of_Arkansas.svg/45px-Flag_of_Arkansas.svg.png'}, + {'name': 'California', 'flag': '0/01/Flag_of_California.svg/45px-Flag_of_California.svg.png'}, + {'name': 'Colorado', 'flag': '4/46/Flag_of_Colorado.svg/45px-Flag_of_Colorado.svg.png'}, + {'name': 'Connecticut', 'flag': '9/96/Flag_of_Connecticut.svg/39px-Flag_of_Connecticut.svg.png'}, + {'name': 'Delaware', 'flag': 'c/c6/Flag_of_Delaware.svg/45px-Flag_of_Delaware.svg.png'}, + {'name': 'Florida', 'flag': 'f/f7/Flag_of_Florida.svg/45px-Flag_of_Florida.svg.png'}, + { + 'name': 'Georgia', + 'flag': '5/54/Flag_of_Georgia_%28U.S._state%29.svg/46px-Flag_of_Georgia_%28U.S._state%29.svg.png' + }, + {'name': 'Hawaii', 'flag': 'e/ef/Flag_of_Hawaii.svg/46px-Flag_of_Hawaii.svg.png'}, + {'name': 'Idaho', 'flag': 'a/a4/Flag_of_Idaho.svg/38px-Flag_of_Idaho.svg.png'}, + {'name': 'Illinois', 'flag': '0/01/Flag_of_Illinois.svg/46px-Flag_of_Illinois.svg.png'}, + {'name': 'Indiana', 'flag': 'a/ac/Flag_of_Indiana.svg/45px-Flag_of_Indiana.svg.png'}, + {'name': 'Iowa', 'flag': 'a/aa/Flag_of_Iowa.svg/44px-Flag_of_Iowa.svg.png'}, + {'name': 'Kansas', 'flag': 'd/da/Flag_of_Kansas.svg/46px-Flag_of_Kansas.svg.png'}, + {'name': 'Kentucky', 'flag': '8/8d/Flag_of_Kentucky.svg/46px-Flag_of_Kentucky.svg.png'}, + {'name': 'Louisiana', 'flag': 'e/e0/Flag_of_Louisiana.svg/46px-Flag_of_Louisiana.svg.png'}, + {'name': 'Maine', 'flag': '3/35/Flag_of_Maine.svg/45px-Flag_of_Maine.svg.png'}, + {'name': 'Maryland', 'flag': 'a/a0/Flag_of_Maryland.svg/45px-Flag_of_Maryland.svg.png'}, + {'name': 'Massachusetts', 'flag': 'f/f2/Flag_of_Massachusetts.svg/46px-Flag_of_Massachusetts.svg.png'}, + {'name': 'Michigan', 'flag': 'b/b5/Flag_of_Michigan.svg/45px-Flag_of_Michigan.svg.png'}, + {'name': 'Minnesota', 'flag': 'b/b9/Flag_of_Minnesota.svg/46px-Flag_of_Minnesota.svg.png'}, + {'name': 'Mississippi', 'flag': '4/42/Flag_of_Mississippi.svg/45px-Flag_of_Mississippi.svg.png'}, + {'name': 'Missouri', 'flag': '5/5a/Flag_of_Missouri.svg/46px-Flag_of_Missouri.svg.png'}, + {'name': 'Montana', 'flag': 'c/cb/Flag_of_Montana.svg/45px-Flag_of_Montana.svg.png'}, + {'name': 'Nebraska', 'flag': '4/4d/Flag_of_Nebraska.svg/46px-Flag_of_Nebraska.svg.png'}, + {'name': 'Nevada', 'flag': 'f/f1/Flag_of_Nevada.svg/45px-Flag_of_Nevada.svg.png'}, + {'name': 'New Hampshire', 'flag': '2/28/Flag_of_New_Hampshire.svg/45px-Flag_of_New_Hampshire.svg.png'}, + {'name': 'New Jersey', 'flag': '9/92/Flag_of_New_Jersey.svg/45px-Flag_of_New_Jersey.svg.png'}, + {'name': 'New Mexico', 'flag': 'c/c3/Flag_of_New_Mexico.svg/45px-Flag_of_New_Mexico.svg.png'}, + {'name': 'New York', 'flag': '1/1a/Flag_of_New_York.svg/46px-Flag_of_New_York.svg.png'}, + {'name': 'North Carolina', 'flag': 'b/bb/Flag_of_North_Carolina.svg/45px-Flag_of_North_Carolina.svg.png'}, + {'name': 'North Dakota', 'flag': 'e/ee/Flag_of_North_Dakota.svg/38px-Flag_of_North_Dakota.svg.png'}, + {'name': 'Ohio', 'flag': '4/4c/Flag_of_Ohio.svg/46px-Flag_of_Ohio.svg.png'}, + {'name': 'Oklahoma', 'flag': '6/6e/Flag_of_Oklahoma.svg/45px-Flag_of_Oklahoma.svg.png'}, + {'name': 'Oregon', 'flag': 'b/b9/Flag_of_Oregon.svg/46px-Flag_of_Oregon.svg.png'}, + {'name': 'Pennsylvania', 'flag': 'f/f7/Flag_of_Pennsylvania.svg/45px-Flag_of_Pennsylvania.svg.png'}, + {'name': 'Rhode Island', 'flag': 'f/f3/Flag_of_Rhode_Island.svg/32px-Flag_of_Rhode_Island.svg.png'}, + {'name': 'South Carolina', 'flag': '6/69/Flag_of_South_Carolina.svg/45px-Flag_of_South_Carolina.svg.png'}, + {'name': 'South Dakota', 'flag': '1/1a/Flag_of_South_Dakota.svg/46px-Flag_of_South_Dakota.svg.png'}, + {'name': 'Tennessee', 'flag': '9/9e/Flag_of_Tennessee.svg/46px-Flag_of_Tennessee.svg.png'}, + {'name': 'Texas', 'flag': 'f/f7/Flag_of_Texas.svg/45px-Flag_of_Texas.svg.png'}, + {'name': 'Utah', 'flag': 'f/f6/Flag_of_Utah.svg/45px-Flag_of_Utah.svg.png'}, + {'name': 'Vermont', 'flag': '4/49/Flag_of_Vermont.svg/46px-Flag_of_Vermont.svg.png'}, + {'name': 'Virginia', 'flag': '4/47/Flag_of_Virginia.svg/44px-Flag_of_Virginia.svg.png'}, + {'name': 'Washington', 'flag': '5/54/Flag_of_Washington.svg/46px-Flag_of_Washington.svg.png'}, + {'name': 'West Virginia', 'flag': '2/22/Flag_of_West_Virginia.svg/46px-Flag_of_West_Virginia.svg.png'}, + {'name': 'Wisconsin', 'flag': '2/22/Flag_of_Wisconsin.svg/45px-Flag_of_Wisconsin.svg.png'}, + {'name': 'Wyoming', 'flag': 'b/bc/Flag_of_Wyoming.svg/43px-Flag_of_Wyoming.svg.png'} +]; + +@Component({ + selector: 'ngbd-typeahead-template', + templateUrl: './typeahead-template.html', + styles: [`.form-control { width: 300px; }`] +}) +export class NgbdTypeaheadTemplate { + public model: any; + + search = (text$: Observable) => + text$.pipe( + debounceTime(200), + map(term => term === '' ? [] + : statesWithFlags.filter(v => v.name.toLowerCase().indexOf(term.toLowerCase()) > -1).slice(0, 10)) + ) + + formatter = (x: {name: string}) => x.name; + +} diff --git a/demo/src/app/components/typeahead/typeahead.module.ts b/demo/src/app/components/typeahead/typeahead.module.ts new file mode 100644 index 0000000..617c374 --- /dev/null +++ b/demo/src/app/components/typeahead/typeahead.module.ts @@ -0,0 +1,88 @@ +import { NgModule } from '@angular/core'; + +import { NgbdSharedModule } from '../../shared'; +import { ComponentWrapper } from '../../shared/component-wrapper/component-wrapper.component'; +import { NgbdComponentsSharedModule, NgbdDemoList } from '../shared'; +import { NgbdApiPage } from '../shared/api-page/api.component'; +import { NgbdExamplesPage } from '../shared/examples-page/examples.component'; +import { NgbdTypeaheadBasic } from './demos/basic/typeahead-basic'; +import { NgbdTypeaheadBasicModule } from './demos/basic/typeahead-basic.module'; +import { NgbdTypeaheadConfig } from './demos/config/typeahead-config'; +import { NgbdTypeaheadConfigModule } from './demos/config/typeahead-config.module'; +import { NgbdTypeaheadFocus } from './demos/focus/typeahead-focus'; +import { NgbdTypeaheadFocusModule } from './demos/focus/typeahead-focus.module'; +import { NgbdTypeaheadFormat } from './demos/format/typeahead-format'; +import { NgbdTypeaheadFormatModule } from './demos/format/typeahead-format.module'; +import { NgbdTypeaheadHttp } from './demos/http/typeahead-http'; +import { NgbdTypeaheadHttpModule } from './demos/http/typeahead-http.module'; +import { NgbdTypeaheadTemplate } from './demos/template/typeahead-template'; +import { NgbdTypeaheadTemplateModule } from './demos/template/typeahead-template.module'; + +const DEMOS = { + basic: { + title: 'Simple Typeahead', + type: NgbdTypeaheadBasic, + code: require('!!raw-loader!./demos/basic/typeahead-basic'), + markup: require('!!raw-loader!./demos/basic/typeahead-basic.html') + }, + focus: { + title: 'Open on focus', + type: NgbdTypeaheadFocus, + code: require('!!raw-loader!./demos/focus/typeahead-focus'), + markup: require('!!raw-loader!./demos/focus/typeahead-focus.html') + }, + format: { + title: 'Formatted results', + type: NgbdTypeaheadFormat, + code: require('!!raw-loader!./demos/format/typeahead-format'), + markup: require('!!raw-loader!./demos/format/typeahead-format.html') + }, + http: { + title: 'Wikipedia search', + type: NgbdTypeaheadHttp, + code: require('!!raw-loader!./demos/http/typeahead-http'), + markup: require('!!raw-loader!./demos/http/typeahead-http.html') + }, + template: { + title: 'Template for results', + type: NgbdTypeaheadTemplate, + code: require('!!raw-loader!./demos/template/typeahead-template'), + markup: require('!!raw-loader!./demos/template/typeahead-template.html') + }, + config: { + title: 'Global configuration of typeaheads', + type: NgbdTypeaheadConfig, + code: require('!!raw-loader!./demos/config/typeahead-config'), + markup: require('!!raw-loader!./demos/config/typeahead-config.html') + } +}; + +export const ROUTES = [ + { path: '', pathMatch: 'full', redirectTo: 'examples' }, + { + path: '', + component: ComponentWrapper, + children: [ + { path: 'examples', component: NgbdExamplesPage }, + { path: 'api', component: NgbdApiPage } + ] + } +]; + +@NgModule({ + imports: [ + NgbdSharedModule, + NgbdComponentsSharedModule, + NgbdTypeaheadFormatModule, + NgbdTypeaheadHttpModule, + NgbdTypeaheadBasicModule, + NgbdTypeaheadFocusModule, + NgbdTypeaheadTemplateModule, + NgbdTypeaheadConfigModule + ] +}) +export class NgbdTypeaheadModule { + constructor(demoList: NgbdDemoList) { + demoList.register('typeahead', DEMOS); + } +} diff --git a/demo/src/app/default/default.component.html b/demo/src/app/default/default.component.html new file mode 100644 index 0000000..a339d14 --- /dev/null +++ b/demo/src/app/default/default.component.html @@ -0,0 +1,23 @@ +
+
+
+
+

Sunbird UI Components

+

The angular way

+

+ Angular widgets built from the ground up using only Sunbird UI Components CSS with APIs designed for the Angular ecosystem. +

+

No dependencies on 3rd party JavaScript.

+ +

Currently at v{{version}}

+
+
+
+
diff --git a/demo/src/app/default/default.component.ts b/demo/src/app/default/default.component.ts new file mode 100644 index 0000000..2a4328b --- /dev/null +++ b/demo/src/app/default/default.component.ts @@ -0,0 +1,10 @@ +import {Component} from '@angular/core'; +import {environment} from '../../environments/environment'; + +@Component({ + selector: 'ngbd-default', + templateUrl: './default.component.html' +}) +export class DefaultComponent { + public version: string = environment.version; +} diff --git a/demo/src/app/default/index.ts b/demo/src/app/default/index.ts new file mode 100644 index 0000000..2283975 --- /dev/null +++ b/demo/src/app/default/index.ts @@ -0,0 +1 @@ +export * from './default.component'; diff --git a/demo/src/app/pages/getting-started/getting-started.component.html b/demo/src/app/pages/getting-started/getting-started.component.html new file mode 100644 index 0000000..3f639e1 --- /dev/null +++ b/demo/src/app/pages/getting-started/getting-started.component.html @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ng-bootstrapAngularBootstrap CSS
1.x.x5.0.24.0.0
2.x.x6.0.04.0.0
3.x.x6.1.04.0.0
4.x.x7.0.04.0.0
5.x.x8.0.04.3.1
+ + +

Should I add bootstrap.js or bootstrap.min.js to my project?

+ + No, the goal of ng-bootstrap is to completely replace JavaScript implementation for components. Nor should you include other dependencies like jQuery or popper.js. It is not necessary and might interfere with ng-bootstrap code. +
+ + + +

We strive to support the same browsers and versions as supported by both Bootstrap 4 and Angular, whichever is more restrictive. Check browser support notes for + Angular and + Bootstrap. +

+

Our code is automatically tested on all the supported browsers.

+ + + +

After installing the above dependencies, install ng-bootstrap via:

+ + + +

Once installed you need to import our main module.

+ + + +

Alternatively you could only import modules with components you need, ex. pagination and alert. + The resulting bundle will be smaller in this case.

+ + +
+ +

+ SystemJS +

+

+ If you are using SystemJS, you should also adjust your configuration to point to the UMD bundle. +

+

+ In your SystemJS config file, map needs to tell the System loader where to look for ng-bootstrap: +

+ + +
+ + + +

+ Some components contain static English text or symbols that you might want to internationalize. Some of them appear + on the screen, like for example the placeholders used in the timepicker input fields. Others appear in aria attributes + used for accessibility. +

+

+ Internationalizing the ng-bootstrap components is done the same way as for any of your components, using the + process described in the Angular documentation. + The only difference is that we already did the first phase of this process: marking static text messages in the + ng-bootstrap component templates for translation. +

+

+ So, if you execute `ng xi18n` on your project, you will also find the ng-bootstrap messages to translate in the + generated messages file. All our messages are identified by an ID of the form ngb.[widget].[message] +

+ + + +

+ Please, do not open issues for the general support questions as we want to keep GitHub issues for bug reports + and feature requests. You've got much better chances of getting your question answered on + StackOverflow where the community at large are looking + at questions tagged with ng-bootstrap. +

+

+ StackOverflow is a much better place to ask questions since: +

+
    +
  • there are hundreds of people willing to help on StackOverflow,
  • +
  • questions and answers stay available for public viewing so your question / answer might help someone else, and
  • +
  • the SO voting system assures that the best answers are prominently visible.
  • +
+

+ To save your and our time we will be systematically closing all the issues that are requests for general support and redirecting people to StackOverflow. +

+ + + +

+ We are always looking for the quality contributions! Please check the + Contributing doc + for contribution guidelines. Additionally, for local building and testing information, please see our + Developer's Guide. +

+ + + +

+ Please take a moment to read our + Code of Conduct. +

+
diff --git a/demo/src/app/pages/getting-started/getting-started.component.ts b/demo/src/app/pages/getting-started/getting-started.component.ts new file mode 100644 index 0000000..cf5381b --- /dev/null +++ b/demo/src/app/pages/getting-started/getting-started.component.ts @@ -0,0 +1,51 @@ +import {Component} from '@angular/core'; +import {Snippet} from '../../shared/code/snippet'; + +@Component({ + templateUrl: './getting-started.component.html' +}) +export class GettingStartedPage { + codeInstall = Snippet({ + lang: 'bash', + code: `npm install --save @Sunbird-Ed/sunbird-ui-components`, + }); + + codeRoot = Snippet({ + lang: 'typescript', + code: ` + import {NgbModule} from '@Sunbird-Ed/sunbird-ui-components'; + + @NgModule({ + ... + imports: [SbModule, ...], + ... + }) + export class YourAppModule { + } + `, + }); + + codeOther = Snippet({ + lang: 'typescript', + code: ` + import {SbPaginationModule, SbbAlertModule} from '@Sunbird-Ed/sunbird-ui-components'; + + @NgModule({ + ... + imports: [SbPaginationModule, SbAlertModule, ...], + ... + }) + export class YourAppModule { + } + `, + }); + + codeSystem = Snippet({ + lang: 'typescript', + code: ` + map: { + '@Sunbird-Ed/sunbird-ui-components': 'node_modules/@Sunbird-Ed/sunbird-ui-components/bundles/sb.js', + } + `, + }); +} diff --git a/demo/src/app/pages/positioning/positioning.component.html b/demo/src/app/pages/positioning/positioning.component.html new file mode 100644 index 0000000..5798308 --- /dev/null +++ b/demo/src/app/pages/positioning/positioning.component.html @@ -0,0 +1,125 @@ + + +

+ Some of the components we have are designed to be opened inside of the popup: +

+ +
    +
  • Datepicker
  • +
  • Dropdown
  • +
  • Popover
  • +
  • Tooltip
  • +
  • Typeahead
  • +
+ +

+ When the popup is opened, it is positioned correctly next to the target element and fits in the + viewport. It is also possible to provide some options, ex. whether the popup should be opened to the + top or to the bottom of the target element. +

+ +

+ For instance here the tooltip is always forced to be opened to the right, even if it won't fit in the + viewport. +

+ + + + + + +
+ +

+ Bootstrap uses popper.js library for positioning. We decided not to have any dependencies on 3rd + party libraries, so we implement a subset of the same functionality ourselves. If something is missing or you + have a very specific use case - please open an issue on GitHub and we'll discuss. +

+ + + Since version 4.1 we position the popup using position: absolute; transform: translate(x,y); + to match Bootstrap. +

+ The position is calculated after popup opening when the zone is stable. At the moment we don't reposition the popup + on scrolling. It might be supported in future releases, please vote for issues. +
+ + + +

+ Components in question have two common inputs that help with positioning: placement and + container +

+ +

Placement

+ +

+ Placement specifies where the popup should be positioned in the order of preference. +

+ +
    +
  • We go through the provided placements one-by-one an try to position the popup
  • +
  • If it doesn't fit, we try the next one
  • +
  • If no provided placements in fit the viewport, we use the first provided one
  • +
+ +

+ If no placement value is provided at all, each component has it's own default order of preference. + Check the component API docs to find out the default order, ex. + here is the tooltip's API. +

+ + + +
+ +

+ There also a special "auto" property, that is equal to "top", "top-left", "top-right", "bottom", + "bottom-left", "bottom-right", "left", "left-top", "left-bottom", "right", "right-top", "right-bottom" +

+ + + +
+ +

Container

+ +

+ Container specifies where the popup will be physically attached to the DOM. +

+ +

+ By default it is attached as a sibling of the target element and the only optional supported container + is 'body'. +

+ + + + + +

+ There are two things that make dropdown a bit special at the moment: it won't be positioned dynamically when inside + the navbar and the popup (dropdown menu) is always attached to the DOM. +

+ +
    +
  1. +

    + When dropdown is used inside the Bootstrap's navbar, it will not be positioned (to match + Bootstrap behaviour and work fine on mobile). You can override it by using the display input. +

    + + + +
    +
  2. +
  3. +

    + As Dropdown is not a component, but a set of directives, the dropdown menu popup is always attached + to the DOM even when not visible. Depending on the container input, the menu will always be + attached either to the body or to the dropdown element. +

    +
  4. +
+ +
diff --git a/demo/src/app/pages/positioning/positioning.component.ts b/demo/src/app/pages/positioning/positioning.component.ts new file mode 100644 index 0000000..7a37264 --- /dev/null +++ b/demo/src/app/pages/positioning/positioning.component.ts @@ -0,0 +1,59 @@ +import {Component} from '@angular/core'; +import {Snippet} from '../../shared/code/snippet'; + +@Component({ + templateUrl: './positioning.component.html' +}) +export class PositioningPage { + rightExample = Snippet({ + lang: 'html', + code: `` + }); + + placement = Snippet({ + lang: 'html', + code: ` + + + + + + + + + + + + ` + }); + + auto = Snippet({ + lang: 'html', + code: ` + + + + + + ` + }); + + container = Snippet({ + lang: 'html', + code: ` + + + + + + ` + }); + + dropdown = Snippet({ + lang: 'html', + code: ` + +
+ ` + }); +} diff --git a/demo/src/app/shared/analytics/analytics.ts b/demo/src/app/shared/analytics/analytics.ts new file mode 100644 index 0000000..56bd1ae --- /dev/null +++ b/demo/src/app/shared/analytics/analytics.ts @@ -0,0 +1,42 @@ +import {Injectable} from '@angular/core'; +import {Router, NavigationEnd} from '@angular/router'; +import {Location} from '@angular/common'; +import {filter} from 'rxjs/operators'; + +declare const ga: any; + +/** + * Simple Google Analytics service. Note that all its methods don't do anything unless the app + * is deployed on ng-bootstrap.github.io. This avoids sending events and page views during development. + */ +@Injectable() +export class Analytics { + private _enabled: boolean; + + constructor(private _location: Location, private _router: Router) { + this._enabled = window.location.href.indexOf('ng-bootstrap.github.io') >= 0; + } + + /** + * Intended to be called only once. Subscribes to router events and sends a page view + * after each ended navigation event. + */ + trackPageViews() { + if (this._enabled) { + this._router.events.pipe( + filter(event => event instanceof NavigationEnd) + ).subscribe(() => { + ga('send', {hitType: 'pageview', page: this._location.path()}); + }); + } + } + + /** + * Sends an event. + */ + trackEvent(action: string, category: string) { + if (this._enabled) { + ga('send', {hitType: 'event', eventCategory: category, eventAction: action}); + } + } +} diff --git a/demo/src/app/shared/code/code-highlight.service.ts b/demo/src/app/shared/code/code-highlight.service.ts new file mode 100644 index 0000000..3e1539d --- /dev/null +++ b/demo/src/app/shared/code/code-highlight.service.ts @@ -0,0 +1,20 @@ +import {Injectable} from '@angular/core'; + +import * as prism from 'prismjs'; +import 'prismjs/components/prism-typescript'; +import 'prismjs/components/prism-bash'; + +// Prism tries to highlight the whole document on DOMContentLoad. +// Unfortunately with webpack the only way of disabling it +// is by simply forcing it to highlight no elements -> [] +prism.hooks.add('before-highlightall', (env) => { + env['elements'] = []; +}); + +@Injectable() +export class CodeHighlightService { + + highlight(code: string, lang: string) { + return prism.highlight(code.trim(), prism.languages[lang], lang); + } +} diff --git a/demo/src/app/shared/code/code.component.ts b/demo/src/app/shared/code/code.component.ts new file mode 100644 index 0000000..cac00af --- /dev/null +++ b/demo/src/app/shared/code/code.component.ts @@ -0,0 +1,24 @@ +import {AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, ViewChild} from '@angular/core'; + +import {ISnippet} from './snippet'; +import {CodeHighlightService} from './code-highlight.service'; + +@Component({ + selector: 'ngbd-code', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+ ` +}) +export class NgbdCodeComponent implements AfterViewInit { + + @ViewChild('code', {static: true}) codeEl: ElementRef; + + @Input() snippet: ISnippet; + + constructor(private _service: CodeHighlightService) { } + + ngAfterViewInit() { + this.codeEl.nativeElement.innerHTML = this._service.highlight(this.snippet.code, this.snippet.lang); + } +} diff --git a/demo/src/app/shared/code/snippet.ts b/demo/src/app/shared/code/snippet.ts new file mode 100644 index 0000000..855b322 --- /dev/null +++ b/demo/src/app/shared/code/snippet.ts @@ -0,0 +1,36 @@ +export interface ISnippet { + lang: 'html' | 'typescript' | 'css' | 'bash'; + code: string; +} + +function removeEmptyLineAtIndex(lines: string[], index: number) { + if (lines[index].trim().length === 0) { + lines.splice(index, 1); + } +} + +function findIndentLevel(lines): number { + return Math.min(...lines + .map(line => { + const result = /( *)[^ ]+/g.exec(line); + return result == null ? null : result[1].length; + }) + .filter(value => value != null) + ); +} + +function fixIndent(lines: string[]): string[] { + removeEmptyLineAtIndex(lines, 0); + removeEmptyLineAtIndex(lines, lines.length - 1); + const indentLevel = findIndentLevel(lines); + + return lines.map(line => line.substring(indentLevel)); +} + + +export function Snippet({lang, code}: ISnippet): ISnippet { + return { + lang, + code: fixIndent(code.split(/(?:\r\n)|\n|\r/gi)).join('\n'), + }; +} diff --git a/demo/src/app/shared/component-wrapper/component-wrapper.component.html b/demo/src/app/shared/component-wrapper/component-wrapper.component.html new file mode 100644 index 0000000..76a9376 --- /dev/null +++ b/demo/src/app/shared/component-wrapper/component-wrapper.component.html @@ -0,0 +1,83 @@ +
+
+
+ + +
+ +
+
+

{{ component | titlecase }}

+ + +
+ +
+
+ +
+ +
+ +
+
+ +
+
+
+ + + + diff --git a/demo/src/app/shared/component-wrapper/component-wrapper.component.ts b/demo/src/app/shared/component-wrapper/component-wrapper.component.ts new file mode 100644 index 0000000..f5bc338 --- /dev/null +++ b/demo/src/app/shared/component-wrapper/component-wrapper.component.ts @@ -0,0 +1,90 @@ +import { Component, NgZone } from '@angular/core'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; +import { filter } from 'rxjs/operators'; + +import { NgbdApiPage } from '../../components/shared/api-page/api.component'; +import { NgbdExamplesPage } from '../../components/shared/examples-page/examples.component'; + + +@Component({ + selector: 'component-wrapper', + templateUrl: 'component-wrapper.component.html' +}) + +export class ComponentWrapper { + activeTab = 'examples'; + + component: string; + + isLargeScreenOrLess: boolean; + isSmallScreenOrLess: boolean; + + sidebarCollapsed = true; + + tableOfContent: any[] = []; + + constructor(public route: ActivatedRoute, private _router: Router, ngZone: NgZone) { + // This component is used in route definition 'components' + // So next child route will always be ':componentType' & next one will always be ':pageType' (or tab) + this._router.events.pipe( + filter(event => event instanceof NavigationEnd) + ).subscribe(() => { + const parentRoute = this.route.snapshot.parent; + const tabRoute = this.route.snapshot.firstChild; + + this.component = parentRoute.url[1].path; + this.activeTab = tabRoute.url[0].path; + + }); + + // information extracted from https://getbootstrap.com/docs/4.1/layout/overview/ + // TODO: we should implements our own mediamatcher, according to bootstrap layout. + const smallScreenQL = matchMedia('(max-width: 767.98px)'); + // tslint:disable-next-line:deprecation + smallScreenQL.addListener((event) => ngZone.run(() => this.isSmallScreenOrLess = event.matches)); + this.isSmallScreenOrLess = smallScreenQL.matches; + + const largeScreenQL = matchMedia('(max-width: 1199.98px)'); + this.isLargeScreenOrLess = largeScreenQL.matches; + // tslint:disable-next-line:deprecation + largeScreenQL.addListener((event) => ngZone.run(() => this.isLargeScreenOrLess = event.matches)); + } + + updateNavigation(component) { + const getLinks = (typeCollection) => { + return typeCollection.map(item => ({ + fragment: item, + title: item + })); + }; + this.tableOfContent = []; + if (component instanceof NgbdExamplesPage) { + this.tableOfContent = component.demos.map(demo => { + return { + fragment: demo.id, + title: demo.title + }; + }); + } else if (component instanceof NgbdApiPage) { + let toc = [ + ...getLinks(component.components) + ]; + + if (component.classes.length > 0) { + const klasses = getLinks(component.classes); + toc = toc.concat(toc.length > 0 ? [{}, ...klasses] : klasses); + } + + if (component.configs.length > 0) { + const configs = getLinks(component.configs); + toc = toc.concat(toc.length > 0 ? [{}, ...configs] : configs); + } + + this.tableOfContent = toc; + + } else /* Overview */ { + // TODO: maybe we should also have an abstract class to test instanceof + this.tableOfContent = Object.values(component.sections).map(section => section); + } + } +} diff --git a/demo/src/app/shared/fragment/fragment.directive.ts b/demo/src/app/shared/fragment/fragment.directive.ts new file mode 100644 index 0000000..3036759 --- /dev/null +++ b/demo/src/app/shared/fragment/fragment.directive.ts @@ -0,0 +1,12 @@ +import { Directive, Input } from '@angular/core'; + +@Directive({ + selector: 'a[ngbdFragment]', + host: { + '[class.title-fragment]': 'true', + '[attr.id]': 'fragment' + } +}) +export class NgbdFragment { + @Input() fragment: string; +} diff --git a/demo/src/app/shared/icons/icons.component.html b/demo/src/app/shared/icons/icons.component.html new file mode 100644 index 0000000..7a40789 --- /dev/null +++ b/demo/src/app/shared/icons/icons.component.html @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/demo/src/app/shared/icons/icons.component.ts b/demo/src/app/shared/icons/icons.component.ts new file mode 100644 index 0000000..5862442 --- /dev/null +++ b/demo/src/app/shared/icons/icons.component.ts @@ -0,0 +1,17 @@ +import {Component, Input} from '@angular/core'; + +@Component({ + selector: 'svg[ngbdIcon]', + templateUrl: './icons.component.html', + host: { + 'xmlns': 'http://www.w3.org/2000/svg', + 'viewBox': '0 0 24 24', + '[attr.width]': 'width', + '[attr.height]': 'height', + } +}) +export class NgbdIcons { + @Input() ngbdIcon: string; + @Input() width = '24'; + @Input() height = '24'; +} diff --git a/demo/src/app/shared/index.ts b/demo/src/app/shared/index.ts new file mode 100644 index 0000000..6ea661c --- /dev/null +++ b/demo/src/app/shared/index.ts @@ -0,0 +1,32 @@ +import {CommonModule} from '@angular/common'; +import {HttpClientModule} from '@angular/common/http'; +import {NgModule} from '@angular/core'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; +import {RouterModule} from '@angular/router'; +import {NgbModule} from '@ng-bootstrap/ng-bootstrap'; + +import {Analytics} from './analytics/analytics'; +import {CodeHighlightService} from './code/code-highlight.service'; +import {NgbdCodeComponent} from './code/code.component'; +import {ComponentWrapper} from './component-wrapper/component-wrapper.component'; +import {NgbdFragment} from './fragment/fragment.directive'; +import {NgbdIcons} from './icons/icons.component'; +import {NgbdPageHeaderComponent} from './page-wrapper/page-header.component'; +import {PageWrapper} from './page-wrapper/page-wrapper.component'; +import {SideNavComponent} from './side-nav/side-nav.component'; + +export {componentsList} from './side-nav/side-nav.component'; + +@NgModule({ + imports: [CommonModule, RouterModule, NgbModule], + exports: [ + CommonModule, RouterModule, ComponentWrapper, PageWrapper, NgbdPageHeaderComponent, NgbdFragment, SideNavComponent, + NgbdCodeComponent, NgbModule, FormsModule, ReactiveFormsModule, HttpClientModule, NgbdIcons + ], + declarations: [ + ComponentWrapper, PageWrapper, NgbdPageHeaderComponent, NgbdFragment, SideNavComponent, NgbdCodeComponent, NgbdIcons + ], + providers: [Analytics, CodeHighlightService] +}) +export class NgbdSharedModule { +} diff --git a/demo/src/app/shared/page-wrapper/page-header.component.ts b/demo/src/app/shared/page-wrapper/page-header.component.ts new file mode 100644 index 0000000..a6aa441 --- /dev/null +++ b/demo/src/app/shared/page-wrapper/page-header.component.ts @@ -0,0 +1,22 @@ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {NgbdOverviewSection} from '../../components/shared/overview'; + +@Component({ + selector: 'ngbd-page-header', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + 'class': 'd-block' + }, + template: ` +

+ + + + {{ title }} +

+ `, +}) +export class NgbdPageHeaderComponent implements NgbdOverviewSection { + @Input() title: string; + @Input() fragment: string; +} diff --git a/demo/src/app/shared/page-wrapper/page-wrapper.component.html b/demo/src/app/shared/page-wrapper/page-wrapper.component.html new file mode 100644 index 0000000..8066c49 --- /dev/null +++ b/demo/src/app/shared/page-wrapper/page-wrapper.component.html @@ -0,0 +1,66 @@ +
+
+
+ + +
+ +
+
+

{{ pageTitle }}

+ + +
+ +
+
+ +
+ +
+ +
+
+
+
+
diff --git a/demo/src/app/shared/page-wrapper/page-wrapper.component.ts b/demo/src/app/shared/page-wrapper/page-wrapper.component.ts new file mode 100644 index 0000000..ca3a9b0 --- /dev/null +++ b/demo/src/app/shared/page-wrapper/page-wrapper.component.ts @@ -0,0 +1,26 @@ +import {Component, ContentChildren, Input, NgZone, QueryList} from '@angular/core'; +import {NgbdPageHeaderComponent} from './page-header.component'; + +@Component({ + selector: 'ngbd-page-wrapper', + templateUrl: './page-wrapper.component.html' +}) +export class PageWrapper { + @Input() pageTitle: string; + + @ContentChildren(NgbdPageHeaderComponent) private _tableOfContents: QueryList; + + sidebarCollapsed = true; + isLargeScreenOrLess: boolean; + + constructor(ngZone: NgZone) { + const largeScreenQL = matchMedia('(max-width: 1199.98px)'); + this.isLargeScreenOrLess = largeScreenQL.matches; + // tslint:disable-next-line:deprecation + largeScreenQL.addListener((event) => ngZone.run(() => this.isLargeScreenOrLess = event.matches)); + } + + get tableOfContents() { + return this._tableOfContents ? this._tableOfContents.toArray() : []; + } +} diff --git a/demo/src/app/shared/side-nav/side-nav.component.html b/demo/src/app/shared/side-nav/side-nav.component.html new file mode 100644 index 0000000..d7e407a --- /dev/null +++ b/demo/src/app/shared/side-nav/side-nav.component.html @@ -0,0 +1,21 @@ + diff --git a/demo/src/app/shared/side-nav/side-nav.component.ts b/demo/src/app/shared/side-nav/side-nav.component.ts new file mode 100644 index 0000000..37647e2 --- /dev/null +++ b/demo/src/app/shared/side-nav/side-nav.component.ts @@ -0,0 +1,21 @@ +import {Component} from '@angular/core'; +import {Router} from '@angular/router'; + +export const componentsList = [ + 'Accordion', 'Alert', 'Buttons', 'Carousel', 'Collapse', 'Datepicker', 'Dropdown', 'Modal', 'Pagination', 'Popover', + 'Progressbar', 'Rating', 'Table', 'Tabset', 'Timepicker', 'Toast', 'Tooltip', 'Typeahead' +]; + +@Component({ + selector: 'ngbd-side-nav', + templateUrl: './side-nav.component.html', +}) +export class SideNavComponent { + components = componentsList; + + constructor(private router: Router) {} + + isActive(currentRoute: any[], exact = true): boolean { + return this.router.isActive(this.router.createUrlTree(currentRoute), exact); + } +} diff --git a/demo/src/browserslist b/demo/src/browserslist new file mode 100644 index 0000000..c8143d3 --- /dev/null +++ b/demo/src/browserslist @@ -0,0 +1,7 @@ +# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries +# For IE 9-11 support, please uncomment the last line of the file and adjust as needed +last 2 versions +Firefox ESR +IE 10-11 diff --git a/demo/src/docs.json b/demo/src/docs.json new file mode 100644 index 0000000..88cf1b8 --- /dev/null +++ b/demo/src/docs.json @@ -0,0 +1 @@ +{"NgbPanel":{"fileName":"src/accordion/accordion.ts","className":"NgbPanel","selector":"ngb-panel","exportAs":"ngbPanel","description":"","inputs":[{"name":"disabled","defaultValue":"false","type":"boolean","description":"A flag determining whether the panel is disabled or not.\nWhen disabled, the panel cannot be toggled."},{"name":"id","type":"string","description":"An optional id for the panel. The id should be unique.\nIf not provided, it will be auto-generated."},{"name":"open","defaultValue":"false","type":"boolean","description":"Defines whether the panel should be open initially."},{"name":"title","type":"string","description":"The title for the panel."}],"outputs":[]},"NgbAccordion":{"fileName":"src/accordion/accordion.ts","className":"NgbAccordion","selector":"ngb-accordion","description":"","inputs":[{"name":"closeOthers","type":"boolean","description":"A flag determining whether the other panels should be closed\nwhen a panel is opened."}],"outputs":[]},"NgbAlert":{"fileName":"src/alert/alert.ts","className":"NgbAlert","selector":"ngb-alert","description":"Alerts can be used to provide feedback messages.","inputs":[{"name":"dismissible","defaultValue":"true","type":"boolean","description":"A flag indicating if a given alert can be dismissed (closed) by a user. If this flag is set, a close button (in a\nform of a cross) will be displayed."},{"name":"type","defaultValue":"warning","type":"string","description":"Alert type (CSS class). Bootstrap 4 recognizes the following types: \"success\", \"info\", \"warning\" and \"danger\"."}],"outputs":[{"name":"close","description":"An event emitted when the close button is clicked. This event has no payload. Only relevant for dismissible alerts."}]},"NgbRadioGroup":{"fileName":"src/buttons/radio.ts","className":"NgbRadioGroup","selector":"[ngbRadioGroup][ngModel]","description":"Easily create Bootstrap-style radio buttons. A value of a selected button is bound to a variable\nspecified via ngModel.","inputs":[],"outputs":[]},"NgbRadioLabel":{"fileName":"src/buttons/radio.ts","className":"NgbRadioLabel","selector":"label.btn","description":"","inputs":[{"name":"checked","type":"boolean","description":""}],"outputs":[]},"NgbRadio":{"fileName":"src/buttons/radio.ts","className":"NgbRadio","selector":"input[type=radio]","description":"Marks an input of type \"radio\" as part of the NgbRadioGroup.","inputs":[{"name":"value","type":"any","description":"You can specify model value of a given radio by binding to the value property."}],"outputs":[]},"NgbSlide":{"fileName":"src/carousel/carousel.ts","className":"NgbSlide","selector":"template[ngbSlide]","description":"","inputs":[{"name":"id","type":"string","description":""}],"outputs":[]},"NgbCarousel":{"fileName":"src/carousel/carousel.ts","className":"NgbCarousel","selector":"ngb-carousel","exportAs":"ngbCarousel","description":"","inputs":[{"name":"activeId","type":"string","description":""},{"name":"interval","defaultValue":"5000","type":"number","description":""},{"name":"keyboard","defaultValue":"true","type":"boolean","description":""},{"name":"wrap","defaultValue":"true","type":"boolean","description":""}],"outputs":[]},"NgbCollapse":{"fileName":"/Users/wesleycho/repositories/ng-bootstrap/src/collapse/collapse.ts","className":"NgbCollapse","selector":"[ngbCollapse]","exportAs":"ngbCollapse","description":"The NgbCollapse directive provides a simple way to hide and show an element with animations.","inputs":[{"name":"ngbCollapse","defaultValue":"false","type":"boolean","description":"A flag indicating collapsed (true) or open (false) state."}],"outputs":[]},"NgbDropdown":{"fileName":"/Users/wesleycho/repositories/ng-bootstrap/src/dropdown/dropdown.ts","className":"NgbDropdown","selector":"[ngbDropdown]","exportAs":"ngbDropdown","description":"Transforms a node into a dropdown.","inputs":[{"name":"autoClose","defaultValue":"true","type":"boolean","description":"Indicates that dropdown should be closed when selecting one of dropdown items (click) or pressing ESC."},{"name":"open","defaultValue":"false","type":"boolean","description":"Defines whether or not the dropdown-menu is open initially."}],"outputs":[{"name":"openChange","description":"An event fired when the dropdown is opened or closed.\nEvent's payload equals whether dropdown is open."}]},"NgbDropdownToggle":{"fileName":"/Users/wesleycho/repositories/ng-bootstrap/src/dropdown/dropdown.ts","className":"NgbDropdownToggle","selector":"[ngbDropdownToggle]","description":"Allows the dropdown to be toggled via click. This directive is optional.","inputs":[],"outputs":[]},"NgbModalBackdrop":{"fileName":"src/modal/modal_backdrop.ts","className":"NgbModalBackdrop","selector":"ngb-modal-backdrop","description":"","inputs":[],"outputs":[]},"NgbModalWindow":{"fileName":"src/modal/modal_window.ts","className":"NgbModalWindow","selector":"ngb-modal-window","description":"","inputs":[{"name":"backdrop","defaultValue":"true","type":"boolean","description":""},{"name":"keyboard","defaultValue":"true","type":"boolean","description":""},{"name":"size","type":"string","description":""}],"outputs":[{"name":"close","description":""},{"name":"dismiss","description":""}]},"NgbPager":{"fileName":"src/pager/pager.ts","className":"NgbPager","selector":"ngb-pager","description":"","inputs":[{"name":"alignLinks","defaultValue":"false","type":"boolean","description":"A flag for determining whether links need to be aligned."},{"name":"noOfPages","defaultValue":"0","type":"number","description":"Number of pages present."},{"name":"page","defaultValue":"0","type":"number","description":"Current page."}],"outputs":[{"name":"pageChange","description":"An event fired when the page is changed.\nEvent's payload equals the current page."}]},"NgbPagination":{"fileName":"/Users/wesleycho/repositories/ng-bootstrap/src/pagination/pagination.ts","className":"NgbPagination","selector":"ngb-pagination","description":"","inputs":[{"name":"boundaryLinks","type":"boolean","description":"Whether to show the \"First\" and \"Last\" page links"},{"name":"collectionSize","type":"number | string","description":"Number of items in collection."},{"name":"page","type":"number | string","description":"Current page."},{"name":"pageSize","type":"number | string","description":"Number of items per page."}],"outputs":[{"name":"pageChange","description":"An event fired when the page is changed.\nEvent's payload equals the current page."}]},"NgbPopoverWindow":{"fileName":"src/popover/popover.ts","className":"NgbPopoverWindow","selector":"ngb-popover-window","description":"","inputs":[{"name":"placement","defaultValue":"top","type":"string","description":""},{"name":"title","type":"string","description":""}],"outputs":[]},"NgbProgressbar":{"fileName":"/Users/wesleycho/repositories/ng-bootstrap/src/progressbar/progressbar.ts","className":"NgbProgressbar","selector":"ngb-progressbar","description":"Directive that can be used to provide feedback on the progress of a workflow or an action.","inputs":[{"name":"animated","type":"boolean | string","description":"A flag indicating if a progress bar should be animated when the value changes. Takes effect only for browsers\nsupporting CSS3 animations."},{"name":"max","defaultValue":"100","type":"number","description":"Maximal value to be displayed in the progressbar."},{"name":"striped","type":"boolean | string","description":"A flag indicating if a progress bar should be displayed as striped."},{"name":"type","type":"string","description":"Type of progress bar, can be one of \"success\", \"info\", \"warning\" or \"danger\"."},{"name":"value","type":"number","description":"Current value to be displayed in the progressbar. Should be smaller or equal to \"max\" value."}],"outputs":[]},"NgbRating":{"fileName":"/Users/wesleycho/repositories/ng-bootstrap/src/rating/rating.ts","className":"NgbRating","selector":"ngb-rating","exportAs":"ngbRating","description":"","inputs":[{"name":"max","defaultValue":"10","type":"number","description":""},{"name":"rate","type":"number","description":""},{"name":"readonly","type":"boolean","description":""}],"outputs":[{"name":"hover","description":""},{"name":"leave","description":""},{"name":"rateChange","description":""}]},"NgbTabTitle":{"fileName":"/Users/wesleycho/repositories/ng-bootstrap/src/tabset/tabset.ts","className":"NgbTabTitle","selector":"template[ngbTabTitle]","description":"","inputs":[],"outputs":[]},"NgbTabContent":{"fileName":"/Users/wesleycho/repositories/ng-bootstrap/src/tabset/tabset.ts","className":"NgbTabContent","selector":"template[ngbTabContent]","description":"","inputs":[],"outputs":[]},"NgbTab":{"fileName":"/Users/wesleycho/repositories/ng-bootstrap/src/tabset/tabset.ts","className":"NgbTab","selector":"ngb-tab","description":"A directive representing an individual tab.","inputs":[{"name":"disabled","defaultValue":"false","type":"boolean","description":"Allows toggling disabled state of a given state. Disabled tabs can't be selected."},{"name":"id","type":"string","description":"Unique tab identifier. Must be unique for the entire document for proper accessibility support."},{"name":"title","type":"string","description":"Simple (string only) title. Use the \"NgbTabTitle\" directive for more complex use-cases."}],"outputs":[]},"NgbTabset":{"fileName":"/Users/wesleycho/repositories/ng-bootstrap/src/tabset/tabset.ts","className":"NgbTabset","selector":"ngb-tabset","exportAs":"ngbTabset","description":"A component that makes it easy to create tabbed interface.","inputs":[{"name":"activeId","type":"string","description":"An identifier of a tab that should be selected (active)."},{"name":"type","defaultValue":"tabs","type":"string","description":"Type of navigation to be used for tabs. Can be one of 'tabs' or 'pills'."}],"outputs":[]},"NgbTooltipWindow":{"fileName":"src/tooltip/tooltip.ts","className":"NgbTooltipWindow","selector":"ngb-tooltip-window","description":"","inputs":[{"name":"placement","defaultValue":"top","type":"string","description":""}],"outputs":[]}} diff --git a/demo/src/environments/environment.prod.ts b/demo/src/environments/environment.prod.ts new file mode 100644 index 0000000..6ec7b3a --- /dev/null +++ b/demo/src/environments/environment.prod.ts @@ -0,0 +1,7 @@ +import { versions } from './versions'; + +export const environment = { + production: true, + version: versions.ngBootstrap, + bootstrap: versions.bootstrap +}; diff --git a/demo/src/environments/environment.ts b/demo/src/environments/environment.ts new file mode 100644 index 0000000..8e9d78f --- /dev/null +++ b/demo/src/environments/environment.ts @@ -0,0 +1,20 @@ +// The file contents for the current environment will overwrite these during build. +// The build system defaults to the dev environment which uses `environment.ts`, but if you do +// `ng build --env=prod` then `environment.prod.ts` will be used instead. +// The list of which env maps to which file can be found in `.angular-cli.json`. + +import { versions } from './versions'; + +export const environment = { + production: false, + version: versions.ngBootstrap, + bootstrap: versions.bootstrap +}; + +/* + * In development mode, to ignore zone related error stack frames such as + * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can + * import the following file, but please comment it out in production mode + * because it will have performance impact when throw error + */ +// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git a/demo/src/environments/versions.ts b/demo/src/environments/versions.ts new file mode 100644 index 0000000..1e0f1b5 --- /dev/null +++ b/demo/src/environments/versions.ts @@ -0,0 +1,8 @@ +// extracts only the minor version from package.json +// ex. "bootstrap": "4.0.1" -> "4.0" +let bootstrap: string = require('../../../package.json').devDependencies['bootstrap']; +bootstrap = bootstrap.substr(0, bootstrap.lastIndexOf('.')); + +const ngBootstrap = require('../../../src/package.json').version; + +export const versions: {[key: string]: string} = { bootstrap, ngBootstrap }; diff --git a/demo/src/main.ts b/demo/src/main.ts new file mode 100644 index 0000000..fe43640 --- /dev/null +++ b/demo/src/main.ts @@ -0,0 +1,12 @@ +import {enableProdMode} from '@angular/core'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; + +import {NgbdModule} from './app/app.module'; +import {environment} from './environments/environment'; + +// depending on the env mode, enable prod mode or add debugging modules +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(NgbdModule); diff --git a/demo/src/polyfills.ts b/demo/src/polyfills.ts new file mode 100644 index 0000000..0d7f02f --- /dev/null +++ b/demo/src/polyfills.ts @@ -0,0 +1,38 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ +import 'core-js/fn/object/values'; + +/** + * Required to support Web Animations `@angular/platform-browser/animations`. + * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation + **/ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/demo/src/public/img/favicon.ico b/demo/src/public/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c8eb88ba34fbbcbd6ccbf00e1ab6478461038a00 GIT binary patch literal 15086 zcmeI3du$Zf6^GBx8WBJ(C{3Uwq8LKjr2hnoPz9+Hu)8*p6q*zgP>I??C6b1O;@u_C zQ~^^+)Ygd_Dv>5?d3hJxm{4ABfV5x(t%2|g?5@G(VME*i#(;VHwe0OV_} zsnDk^Mn8Jpt$r}P%p!jGi?M)GN8G$5`MDo;VK2N$``%4{MDpOtSioJ$nMG^hSN#Zk zU6qA7@Z|s6hsZCXUnHO`N8W_UEY{8VliLy@pR&Bj3;n)qKMu@*ZZW^&N6xDvt6ANK zpPy4#LtgNII1IXd8Gc0OWaT2~F*8=PZ4&&dAB7&*v9Jen@01U8Ka2!i>&+PLw#o7f z`>pTF`|W{z=!txbu&DP#J65wcfs3CnGPWt~Q5JnT9(s1>ctnEkXOQWv8JFy~4Efv7 zDX;5n$Oq44>H|JM%VjyO_UjCPvTu3j{K_dSF4^r}z<(+eewGgkGPc@rUMzoyCEA*8 zOT4w2#ao)#txI39`dB5a*-^|Q;l*tI)WI^>0qxV8k1S4o_$&_|wqvKZhFwcorpxWN zR#v@nDO)?SUs^v_dEjBE{6~W@;!g5&zb~IU!it{mY3su(FN|=?9}T*vIOV^*Z#g@& z;yGQZnExE3vNL}YJMg}rojbIhqigo$mSwj5ggjDzxLkA`pze=*#e->WQj${!23 z%B;Bg_Ei3$6h9r4&nq|S_;>wgvJJnh%qc&<7e_d!l{NC8-#kZ}|3CY;>iD|3}ZA*kme66 zuiA0*w$AV$`tv+F?)LZ#3whvG2mA$`(aswAuO9h`)s;-smCD~wV^rw;xpBT$J1U99 zZFYFsuNi*B%lY9`3S{}CL8a7=o3|PH&;RXLX|LbCe|TB*fhtD}7fSJmeM+e`f55$u zGuqiq`03eor6!{J(0Hawioek7+9%JioU`NRZJF|)*|}EZr^;V{oGs(SIT`*FuR(X@ zvy4Y^cCwlBUx@71@o%3uPKH16$8pd-oex!W;8Bs$uAT{h*^Hr#zFov+-IQ<1@Uzu9 zfSC^aCFala+A)i5M*b@YLb`W<`WE2t{|7z@vM=^;Q@o*n>W z$kRneySkBIS*tkA10l;B?}GTPOh@Y6fy z^>a1q_hY2!ZMJsOH>JnD<6Kf!rssFP+}wO*tTyAc+UAhI8uNSeS9;(P8KzA4i3VUe=8u*j^93tzzFmU< zLfES;LGIz1@?_et^m%Z{^0LNatRjrOH#oCsE%K?l zK!;5%U^qd6vF3s8wy}4+t#G$nYQvHMSU|QJU?ACAfbL}TadHD$au5IDZn&d!8>fCF z0W!6SgiShB{*TXo1bsdh*TX)TCleEp;lzB~c+@*7YKnCeea>;R&;#RT zG|cwFWBeHA>9>knW_25Vl>^`Hw-)89(zbNU<$hUvH30S&jIWgW~V= zx{Q7}w;SVmpsSx(o9Q1+5j1Q-y#}r+S{Z%Hb<^>ug>q>o{x=xpbgRiZS-SD%4C(Y& zkLj+)t@!v?7W~{ypV@t@M`pw0T+e8^J|^|qwdh8zoc-4ZR=2l|wOqf#rPaAGB@_CX z>f<%=xXx)|B^t;YW7OsAn!cuDUa`9N__09!F@mJ7igQ(Ga6O~t`iXcet9gI4RzC5M zW)$l@{r8Xx)O@vpeG>4P`mQqgV1`aqSV_)L&gl5lZ(pq1qcSws#w_(|zOx{ytD=6$ z=r>>2{I|LO>0ZQ_byEgdFeo9NsIY?Zpq*ULXl3-p{E@Z3Yo{QotDP>%Q*XmAi*VT9|Ki^Djtxhz#Lh%OFvW)9-eNDuoLh}#o=d8He%wii6KMdf$npwN& zZ_V~1^%+!IFr5`Y-OJ**cf@Opb76pS466L_l$mb2Z6@QJRv+I_pEByxI?YeOYoGw1 z!MeHjb_@>m>oBKWAT!^9<|xL3SsvtAhNZC+`i+>IScdPxo=oN%(wNlb!+m&PoD}uD zHXuifk;TOS7JTR3fN{S`I5*@z`i-tyVtH^s#wTA&etW6kKuP@s+T9ofd#Q8`+}~At zcE3{M#SnZ*Rz`eO>w~_crriK)1yQ?7v3d3NGW1v_l6k%6tF;rt!sGl0z zp>R+0K*kq}Q}n@-jYW%7WZ{Z~<zuI2fVg>y1y(=~?YPb2=H!0hr- zAf-<6W$atUuLt2esG|6>6$_rjL(Jv?3jO*0t1L7Mh8TYbHCJ)Xnh$(hUb+0uAFB6# zn#0)fTgob9|5(sn!TUGYA7`zNb?nxey{zHbKGxQJ6X#$=*_vm+!GEJXrGG#5Kho?sOvF#{MxUhxgabkv{%9JjAuh zQvdkP2G!F0KJ}$}FX9*!=fFdp!T5}!{*-I2erqt>@RM(|6CeMDoj<&TYpYg#R#3dj zs4w#GS~Nvv&^-f%_!NHI1daV$FV`}f-&P;}C!^eR%A8*h1UY{)H>15DipRg=&1Z4$ z`5+xT9Rte2@cxx~&;#cIOvbpzX5`S012J`+GwO4?6?3Ya@g6?8bOiL^{Y9(h@B+#! z{QtY!wOp0h=anLdxwWHtFnF~O28+LMtW#_=u>j+)c&Dw!*(Md3ov|zVuJ--nBPk{d F@LvID@KOK( literal 0 HcmV?d00001 diff --git a/demo/src/public/img/github.svg b/demo/src/public/img/github.svg new file mode 100644 index 0000000..aa05db9 --- /dev/null +++ b/demo/src/public/img/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/demo/src/public/img/link-symbol.svg b/demo/src/public/img/link-symbol.svg new file mode 100644 index 0000000..1f4fb6d --- /dev/null +++ b/demo/src/public/img/link-symbol.svg @@ -0,0 +1,3 @@ + + + diff --git a/demo/src/public/img/logo-stack.png b/demo/src/public/img/logo-stack.png new file mode 100644 index 0000000000000000000000000000000000000000..1a02c710eed6cf37f5a9bc32e38f3a1a63095005 GIT binary patch literal 57958 zcmc$^^;27K*F7BEwYY16;_d{Omf{q5FYX?KJEcHzYjH~P;t<@eIHkC|J4s$X_cPzW z;XS{cGnvda``WYi+G|U!hME!%CIuz{0KidEme&FR5MMtL0qCf&uN%L)CjelJQ$=1z z#}DD8C-n6Y0Y5U=x zgqrOE1NzKZmix&{!jX@MM_$Vi^)V~u*MNTbE^)Ec_$u_$Y&oZVQ$;J9)j|KXXByK;(ju>cCQi(# zq|>8C9$JAAnagbP45^6mjvh|z>WJ_ zaAei16>x+#GU|wNh(hLwlEdpr)7giH3@ClWHjf%*nVNjUP%3Ox^vFxoe(ZSict_v$ z*o4t-jQ)QXiJ6`t;O8`@W%<6(g6T6*i)87WDo?kPQ8MBD=)j%RNTw5STMGZJnVnd~ zD;uAzN|kX`Y^85D+mJvl5dk2 z&|)p94cDfEYU;*D8w|HUW)CSS8qKsxT6Iy;2TR#ILV3U(Y%z#py zz6cJj_vS|fr`GbsTVc((G>iaj9;&i-cZ3ZQnlAnnkLMvz)2nK+&N_LUNy4;h#sdab z_TnJ)R0PQwRYm!?rRIPCLIJ3!@i^q@@rRuxDMli-_tYny$3kb%Tda7dtH@VYpLqWd z@g{un+@=qUHhXt^WH?`81CHqKY%-)jYGu;7QFKG4BCMGJ=)cY0tgPSqwrQIWOXF{u zS{^>9H*;;@aYJ&HT*b#1rqmHpGqDite7660U*Fj;EH`=iUq7a0wp(+(A59AoDhf(e z-4C5WO1BVTK1L(QFXO0kK3`km+{!Z>E2dK5=x@g;apiSCC<@xW?~<+H2F~E{bQ>d! zG$8#KRGtq`aoBKpLV($~PubHe&6yk~k-cww=qzI-)WKr-5$^!z**|L4W~(87J?5C4 zvY!jbs^*^riUM6WQ{hk$UBNP{%qvVbM9$`DK~}_X-|Q6_vwU<+t31Y+QVD7oV5DV44S@& zW?r#_&R)LkFiF@Xv%PXHLJ^t)cPVDQj$$u{#VN@aF44A7!7+G=M?i%mQKn;=qR7?i zrQ12^Dn#Po{~gsT=<4?&HWpOp2LtdPf1d%$&jrZVlaHyBG+{H!eEM2}2v%h={DH~y zIH3_DEg{WuF6WFW`?f0a)U+E$J)O|2(a= zx`$Ax|2xgAa0Aa-MIX}A``vjk_qNP@9&`7kHFJUO2=VF@Y7>#b+aff;_q%wjp0^MK zdNOM|?8L9dz7$XI(0oA2O0EIPMlk=A{@26)qt+Jfkol>S+tYZZwKeQ zZB^L8vpf{$-qK++jid`_0*~Gs-lH1bL^PVpE+aXq1-p|*HI_rVMG^tp+aub@|L-V2 z5uS^X0L>Vt?`jLpnT>7fh5Q6%WBLFg@Ym3H+Wp5qK6?1f>taaz;;WX*Ri?9TQ|k*D`QNQVZ;s({XdsR zslkVX#fM?nHH7;;>9tda?*9-A;s)91#u~aNDss%~{go%2?A7PikJOY)ADIh}DFgso zFy&0*db>YF$ z_(lA8!Ntp1-Kg|Hbw%Vo1kPA&IdpGpG*_w~yC?CZkXl?pMcMixqH{*qpqRdA$hfl^ zD6@TC5SJ`@iiBW8q}Tml1+57D{QdYb!EQhvAM^X)v-G%OQ6)h%_BgE{>7ID8QnnJj zr9RY{^U3}< zU*#<*rR!UNElCPLb6(P}U@b*yf_YYgM)CL4Wj}Yk>F+0@lgDgwQE5dNBJ{08+1s=` z-FIW64@*+jYaYY@hh2Vc+e@>{1-^;ch&6>^YXj~&FHGM2(3Hd!=k6-pxo$1_8i3PN z-jl-QO*;@==E^ESuH{1Cx>8I@w(<-*>$(%d1&R!I>NUP(Tl|M!iH3q9gs6eBwP_`| zG{$@HnJdeb6R%?W5mcFYo`B@f<}&&VkLkf*+hJVjds(pbkfHu46;7i2?>jl^!ctR{ zD^e4ui@?c+DdEyUt^cLJ{4$8ST;$qXLSqQnqXe~tA0rpVLSua6$o}3t2e^@MQ;fR!A+cuCril2QP;8w0o%K7MNy* z^G`(4K0!HC&|xHL(2rMYdS&uUKNEaL*Q$%tc8Xm7ju?4z1Nd^xExsAuSRi;1(~PK9hWpTT`*384{wy(5FQ&`ji+CO>;V_fT$7mQ*lAtz z&cQ5^$v{O}n9wV#b~lq#mWd2WImKXfj;P~U7~K02bcR=`tujy-I~skWU8Ki?nql|o!UNpY9?4}Mnu0LGAv zItX!QLjW#CPH{5`_1&*QuY7Z6+cARP0~u>R25A{Js6q0M9o2WEk06wYN231-S#^_M zyCZvVXV>^%DWlFi1ncbfg6{P|ek|Vy0Hf>a?n(jqen)|^q9F~P3KuF4rX<=G$)Icm zm`^cnI)S&5Txu5h;UmboO1H(FXHNb*O52F+14q$w^)^4;1MS}&vd0vT+iL{#-+S8F zGugm2%wZ`~mYvZax#}DRYN}J;>PVL9($R`BMVV!!?FWf3mw9K+z|w4mt9a%pjn7yT zZ+UuOQxNt^k-!je1QzYOUupIiX-A6d;1$lc?~Q;LqmU|mQoUgcjAgc;%3B_?5@wdv zJ3qU%G^qQKOR|jm<1rA#+}j?}k-OvAUiimp&8KDE)9}xmK3!9*N`M#(U`4duosVAr zNK{JTZS;*;--kZtqhQ^D&(O3KgsR4`!-Ygb^dw+4RJmz9FhLn)m{`Am0+aKFi>$O6 zpLCUt#|xdg{o@ng_Ve1SeKZ^FkOtCPwLl~)mYz$|m!fU&XSJzJvkGa~SdS^o`m4{y z{1MK`pAdBmqm&~?0bm|XwFn`+*#%`C6#2p>PbV3P51Pk$k0d)@^KgXAq7vNH+ z4|nW-CoAnO(2v_g60F^BRObK@XVKf}b^Q_u$2&S?DOZ~x)O~edhnrTe0A-SCu34vfCa&cG$+d)8C*T;}e?nZO+jY!Ba zV-#C@20eD`70i}|{9kB?NJKYVtDcaSIh`02aocxBhk(E1n8YSHvWlx(65vp_BBhfum3E~!H1fhA4{Kc`0t zpMGMo>Y@(6-?zek--bV?ytnLxP)=WJSA4hziTLTn=^1%Oy!l?5O4JH?8GAQ_*=%jYwGw9qta*2|a+ zG5`by@(tdz zVBcyu7J&Qdzhncd`f_CPr@@VjJ}Y`=7X1>tAJrnBAXu8b2)mf@NSSiY9`E?6J4dLN z>tCnu^?)Qw)jX=Ah<)4_i(h!tWN?O+v$peL!9G;7wNAE7}X_U7>*LzYnGO?OWYgA6ze=Y}# z|LNEJ>h{*B0v~E#^u7OuX(kS8SBUGm*-f0xj2AU;E|(Y0LiQJZK)OX5vQ26C3N+7n z>=m9;BQ?h;a%f`j`K)atkXV;p{Ewk09nZl6_lX`*Gpbdbje`PlwSKnx2P}MizfH3Y z(PL(A>~BY3-kg-#Jhmp+pzdJ>MDWRs5)n>f;}Onh)K3tiMc9bs%j-!VhSXYJ#4rKIKE(Wf^yVJ$_C^ z%icE60|s(iH@Y6#LEF|0cz@VUnTOSSE1_#3SZLbv^^3fB;cfUS+PK|k470=Q3+Lt(p-5=IPGMLMNW-526Y*n9P!qDka_-k{mvb^5{8@7BCW}y?yZ4$`# zpO_zse+P2ICQ&dfn9M)mj}1SO)}Q&;-d{$)I{!YFCE-=Us}23RoCF~w({-1oeSq)V zzJo$4@IPUV0+jIyHINl*scJ2;;8m?6gPEWzK;5PaXaF;33kB(JF8^zCE%*=1i2xBv zt=B9o`(#w0R5Mz#Zv^$GDhs0DOdh;N9&v_Ap-Z;A@c;Z3w|w>9UN?lR{9qR?jOrrB~J z)`7^R^(kC$Rg%?qgwIp8ZW~N(I~d7vZ4>q7FlcP}>d@KL4t|H(tkYV0SaAoF(?0PcwF=>bT>rD|suSjzJpKm{hV-^Ht+SE{21o zUm1sGNH7I-3ULfxjMV~O(!gapG3FM^=hTvj)dPCOj7 zSh_(&OLr3VSweMyf1- zP)$F;t#9?H45EBL2DH7JCTfS|S2~eaB>>)OzO+I-k81C-$=GYw)f&&CxFhOg5cYg+HIUFq{O8Ng_jkD@sShtVH2BiUzZ9W91a|b^r|}gA0`lHt ztSqTg*ttzvqR-{>C4lmAD0R9S<=zjV_+o_v2x9KJ;R_jB9r*|2+kSOdW3OJn(cE%b+=5}@A=nC0Cp^-Rq{5- z8f<_oJ2+Ryizf9x=;5A;3JOSSziC{BoV8eJcPVv;88y=f{TB9p#|{2+^0cpGQ53f|9I@r|9oDRp01GSPKCznG*Ajh? z5X^qA6^n^hI0F?Hj$XRk|LBz;$oTb09aD>u0XJ+MGu6a;?+Dz&Z!Lp>bHXx|@~yN) z7`IaSd6c(qQuEj>{vaZT?mhB+0D+yPOI6;}q$Y_;{>}w=8KB|;RRE9Fhgcb@i0)%|u#5MYvG%lBnOZPV{O|oA`Fvlb^XZh%zz=So5R=KpKQm&-*JTzBX3X>Y1KZv zE>YNSQ;$^`To`Qy-ZX&-`YXOgC3)?RkA2XEf(Ex99-}#OTJ&yzjXwpv zq7P(vFE9i;k#H^Pir2WyRHAqN@#wqkrANF1e~|2_gy(GtU`32hk^&cj(!_`%wj9aG zIHT%(O07TM5Vt@WPIJag0d|+@aoO=PH6l>_AW9&V4vLn)dk7anV8z$j3Hye)kvI56 z+7$F7`}g9n@BZ_&BBl>6_}90D*~dhn8sj(gxAZwtPW74R4rYoa#M}T%v75N>Ur<2Z z%d3tECmLXE5fz03`e9J|VQiD@91d5k*>~SEL%!28@6VrZb`74iNPcJQ7}kA{d>()h{zR`ai&Xogggl>WsUUZ0n9 zvYcGY#Ski_x!m{kuk^47Erooft=Q|pGo|--hO70j0MNsT;t^a6Xp@7jIPf2KIZqqD zYMfppz0ZBvuOpRai^Kli$zC{YK9nO>R^BQNOj=QRdnt@f6&z6!Tyz~W;r42-J*;Q9 z54e0b=N+Uf_w`tzxa2|Q-yMgJYVIA;wH&@p*Lgb`f$EVvdL0_iFW}a+2Txh#OJrv>6Xhyf&W~dzd}lI>rNoNt z9zB?dJly*56f9`NVfA&f#U+rGJ4iI{gQhU_n<*q;oP>uZhi|c!{&ZHf)!nT`f0>(%15E7-7Y&Y({$^7CPu*h&Wcu*O<*ALomPKSDN~Ywc=#|3+;Ql_4Md*-V zW7Xq_%U-=$Wyj0Bmy-|cnD5b`P6*5910z$iTLt&i!LPPHbXGCL8iLdm_`8`#r!E?R z)+b9`G|#;Ns)M-b2a<}bKB-4RK35INUMDOqGb}^?#!LF4 z&zw%A5y^#uta%X(D=~BE@zvzr4$6El^?1rC71arVQTNR+sh$x=FeULk($#l=+P=hX zJ9BoHLgtiZl5)&>_(?8ir8U;nK&|&W#lgfWW`|g)lmn&+DFwun@`cF?2Yenq@>`S% z_Z@??@M>E0cmIJxBPj=Ft5!6mKx?RQ8>Sxn<}ZWnk`KEry=cCqx#ctf&2t{0!|64< zAO*i-c{EV=xD&tBl4JICVx(6%e~O1b#TYIeW z#(Tv2Thm{3Oo7FSio^iFYB@W(v?|)#T3$tg9SzEB`*O@pFI#_O9B@3M>hu;7Lg>-D zsiL10R+y+D1m7`Esvdr8@DLTy0{BtDSZe4mbL0BboTv5NuXZ6=T4dC+#Grfgeo3ft z($|sG_Rd7PB}knnol~kvYV#6ObeA8O^uEN8ZJbKkBV&mol$VH(wO+_wG^3?`&y#mG z;qAvKx9FDgwF3J&@T~@c;@erk9^~sS(u+B6GC|h!Tj@LOV2*C6Au+ZhSmQFVE(GUIjjtcz#zQEJlAjl!2L~o(tvI&0V_s1830J-i% zM?)wP(XHflI<`$u0WX2p$_sdg6x%`%-5<9u*^1E55PXv5dpqELgb`wLWY1Gc0Ge%lv(d++;eTNvCf*^YTgy%Kk&DsXcZz9UN0ak zcQc(<#(eOev$Bv&^A!8J+)|z{NIlQtn)v1p+v^MC+dnz-oQMpvb#l50!w(2qRoTY} zUQtyw62C(tR)>cCHV?>#Pze*5`Ba(e_wI>)ceV_LaWrsz1i!j^K`OPuPkrQ&MERXW z*!jAghZ`Cm^pT#fJQGb96CfT1Y(?o_Sg)|UZm&+MY=S3ja}ZpNeyuQrMG1?3M!=!V z0g}{0a|c)$1Cr>>>0WC0YyDL?jWXuld#(b~n?L;#;V=54WGtbly2WinwgVV%p+ip# zjTIO|D|@_kl#>XtPRm16!q6elCnqnTBxqS5^_+Y~8Y}kw)Tg@d3>axrb8y_Oekk@y zaHI*`m19Uz->%@J0UO|54ovEz=$8P;LrcUDUra4DFr82v=(z|5%T}S)Dpb4vbvRiL z-RIES@~2!3A)YpBCVOX=oT_CqK@$1geT_R11n>p_^=wBG4^uN?}l3q07AiG^+C`$ zgynVo92y8&?vD}&a{W2_5Gaf~e3kQT=s%zJMmo8{NIG290@1oHxTp5giaw)# z(slnSk(0wIA-k9stDQf&i-(j^gINRH!p&S_0?K`Rc>}YcS>{)MN6GW;YX5nQ1`AmR zOAYjyC6*LUgP?$*<3*^XcnNjoow@Zd9c1y_3`xiz6qc^&UttBjYYW-OIebsg(Hn@4}du&jv3R^TN) zlo3%Dx)eoKlNh|oSGfxUVw8&#Ul+bda3ewKKFol2+_O76i0-R15p}8cAJ#IT{=7g6 z8fNkwLSz=&`;%3iba?W!my5R3i(t{&-qzjo-}tj!H623?4<6(G5dZa-#yL~;bJ^nz z`LDWTwZ31$-zr+~P&@hyLmXOVWtjJj*|7_!v!^H6FGNrj%0!+! z@>H~C3dpE-;faM;dQUViD&Zh*OnAL$#Qv#h+(Zi6ui%op9_uURAtO`I0FF$7dNiWq z15bRQ3eavMT6CiP!F?s`sDhmyAc$O}8`$6~&0KsHT=4zbtV>Y!Iq}x1>(=a*T`UUU zcjq-T0moMEC-Z3wlk*0NkSZG%Zm~ARrcG%rbd89cT^zRS1rw{x^urk~i-W(3aH@AF zAP|xlzyr@^KzBjhP%E}lt>spws|LJ#Bi}3bV1!Q+q7kyF%pW#FaDd);Rzs$nhwz1h zSzct<(+PGI>BtA)Q@Fj|h<}Dk$D_J?#{?j3G&g72=-}xT^cJ%x!HFjo`4)lm$FvSq{R*UK&Z!14f zGUyD}H)uphSxz0BuK?=)_nQ1U$T<9f<$RGmsvE%Jxr;It?A)C!xD>YiS_KiHRg?F{ z;m+?IZt`(E?*4u8Q!2xZ*22pDS{;g9l417}X4$xCPF+0X9?1fC)m}%5cZcf=ViVUd z{#Xd>6LeI+TN$-LQlwY7KxA?|FdAF7@bhaKUA_77a1&&t3f)>a%H7vFkSqB@cdhW0 z1j`)RW;}V;`k{rK+XX0Jg8G=g*)9$7Z0GV!tw5x4M!iT_bBi8ds*6PO2Uvz&wWxodVuJ+mKocH`|U64Pw9fZv$HwR2#I1MU%Y z8uC0f2*zLQxeO6?XR^8r5ei_t^sijl9oys3_(FQGKWbq(=mtR{iu_UZ=Fy|aLZ^T} z2SzTZbhdkt%7`tIErCQ>c-~m7zkK7nVwXPCx?4mT*JVWIt8m!1HAA^JK9TnvK(TE8 z?tY3MjBa~+c8nDu1zKj{GS&n(d|79Uw=S4O-LnoVsuRYL%DN+0w^p$k-;-hls@frX zI#=N$)m90*2;I;3OUTomP%t5Vy`r#EecSMgm5o4k@5^1KBwmScyb2#xQvC?l8eYqs zr+za!L>)xkRlJ_Q;!@D)pFm%a3u#Q;r}O?gu=x)}Xv0&J$V$jL=I6igM3G5R2153K zw^PAnt83$HpDa=#*2BUI02z`+4Et)_hDymvv?gB)#)9O14dTo9E;Tvh52~d>sRv*1 z-ipi#4{U}yB;lydO6Ho|Gsy8#llhwr6A)&BliB${1O^>h(r=GJDm&w%j_{Ag9rb6Z z_O}E-wjfd`YU=PtCIJ|-+JCdvEKb%*%KV#|kcvaR*ig4G=XZfLM~pK!dgWLVn5i24V8p7GoL zap*X4G(eBv$sW&rq77Bb=TMeJXkgq0M%a3v6@fR+pDREO0?1$WBY=*siWB)Id*K&P?$Y7>0s-vM2tmgmH8{e)M1D^)+@dG zJ2L!YgP`m2!F`){!|~AIkT_+Xg9JMbtWjRZDNC#jFhPb%GNs;oY{h1XKB!{i+OD&C z$oQlA!uK+FLJrF)mfXf%Rmz`y@f}DAnmL-zM~q*IPO`b1Kz|N@ueXHvZ0&D9GrMEr z95;#22RfLYU;F!z4DCiEm zLgf-K!=x_v=F3iKcxKDd^74w+cj^;Mb_1Og*aSIAI@hr-|v3(6ypo+iU&eSSaDl zd_VTdQfQ4x23k-5p}MTjNgFv2JDoRBXE}kh;&t<%h604(m+8A968f zAAi34DPC!?U^o!SZPEut`H1DF(<@ohEH{((zm|~nq&}eIoRZSP@KR|@vHFd>AppOaX}a@w ze+}UJN|YJ_Ve&A>ePs0WT0_ZKPg32iepdmgC3l?-QeVKn8=MI8mw(tQF#2I29*P=J zpltg=%|1TT*vh-ZM@Epg!;8QueW9+hlVXU#lj90uoNVX&D)eP_3h&}oHZ_tJ#76d; zxnIq`wg?qgxRk+1D&{N|hur*vPnhCo=aEVc(mc&Nh~E&UF^K>)dhiUPS5Jz6eF72> z88B_6)gE()F!xaDv;Q;Pt_$`T6t1q>Pny7ad#NN5cqO8%5L$PbAZiv|70+^ylk@nx zaOZe)5eZcP86m_^_*w@U|L%&!Gp?vgZQTZ+vodvxfXx(_flHi3`d*5Jnt@+BR`t9pa6BcJrX2QxHo7*j$ z9~8<_QOZRgiCfD4p+_=+dr?08Z1GbNJcks<{yyccWaMJF6I+)47AwCn{1bzly0sfcgowkVg+; z7C<-mg$RE|4U{gGiZ~Ee$@&93OQ|c}2vL3n5Oh6RYI)ysa$`sV#^eU%R0TTP$1 za|{Ac1qq!W7~TI}@3p3`cLb^NQvd9GLg97MH=Ck0S$0u%?QPlwMWz<9nM(0a-uvEM z9Q+KwQ7_I?dyIrY_=Gwt;#xTDV?3TwsDn=81~?q5`OI=ArWWuK8$#KBz;=aA_a6I- zMAgf74_?`RK$7?)ysB$a6`HT`n-V9VE~hOcd`hDGk5a0e01{8eH^eJkqq}X219%hM zkH9+*ZqRSS&hYy#wJ3NP)I}tOD1vxQ(!bPH75Bx()dCtOa#<_hP`Oyz+&RV0^7dgF z3;N=v$1Pc;62&q*@Yl+<=c3e&!y&28S(gzKe9G4ZpB1F?aI>ug0G3uHd*Dx(QfGlM zCdSmmtBgAXWqo26$P5?1)zukBlim6&}=b0y344^4r-j?YOZV4v()H8^`T(}DCmtek%YSXDB8Vmn_<)vB2($Iaup@ox}W79uk z;PMhH8KzPp*=;Wp_bp}4mSG=Wc~RH9agIn{&LgLoY+0-Plot-kvdDhd&>?e#K{l$e zOsoi9UH9BCgcWOQ+6N6z&t~ExR~PP2G|C0GOmyH+_+Uax7+DQ`;@p&R!V#Y3_caI1 zL$}M(+;CxPqps+Mk}jl_(0G1!ozr5jB6#bbk?feUQ&!pG78FMvJ@~knYSOVF`1vG& z9z2u!Qe_=Gc!aN4?YfCqW9H%%X)%l zWg=2bH{A0F-GuOhf^s{}DAB@N8Axx|=qE$mCVx@Sad(mrtpMatrh1+Au04%r&oLEq zK$|n~T!77?IYM)@#;2NZey`us(yF;Ti42g^9Pf5zC@PmW6gSYjA)*7qwpd)-8(_WP zdH5a!EYIu=J`l%eZ*jQ9zVV=rqpn6z0$2V$FZTwezQn;oC!hqw2GP<~*0FKEXgFis z-(7mvQ$Ggb34gO*SAXN<^Qy}Mulr5SchzJ#@r#6&ZRGBxot{}wi1r58NK`%jrIGGj zW+r_mIg~gz!0jEo`Q{Lb&4wD`o+&e$&2yR~`n-Aa7PD>)IT7S#q6duW1_%0+LJ0+b zRs`LIsh-*A=%vQuq_#@@2|0uZR*Gao<|F=>WSMvUDNwkb^GE zU3*oV1AFZE8s238{7r4gC=5W>YV=P!fXCn2T8aL2yxik{3Ku>14D^Vxs2yRJ;$Z!6 zcbw&T?wd28U6%EyPWOehh~3fKecpr=x)lodcq*|xZlwly%m_|+iSaCSDOW!ygGo_^ zk(Zes$fwQMpy63?qvwFP{tvsL1`T@>OaLPa6_q;gx6yoP(bVIBWXZXkMYZhx=R^40 z>}^5Dcf+Xjg{jJNocpVH06C@e?Z|VSzCTWDtkR<;_{opqfSqILgu6koQNmC70BtPk zGx`=6A-xXlwZO}Py;oNhP=Wo})irKyOUE;w9{({|`}J-);u$JUo;~j2M%S~LPqDF< z5tXHiqFVAY`!D%k)Bzx~K!k`4f*sU&y?_k@YOd4guYpo6`5&en-coqPKX~%I18k!{ z%qxA#BJODtfRZSKcBk;nxm=kVQy;KiqP~1CM6Mxt05X2_OE!bo*cGz8&I@;U7!D@J z{2{GdnLCi9zdrwl2l%HUdI2r#hKumKb#>e3C4AM|O2OXBCRT{20i|A5wcr|*UQa`p zoeAP(j%_b8ov{-t`MQ8y0a8mUA^i1n4j6YBP9pOfoCVpYE)&_?e3Lw;LSid$$ z6Ll|Gs*Ojlnkp@cKMy|aD@}P_!dF8wNs|)(H;BbhSD1B$95)LYYRt0Ck5v4zTB)ZvSxvUx39R)g-wpr!0tm($|BgFmQYTnxMy_;?VR3lN6baYiW zN2#04^j@WxU!FI&#IJc}zfmbzWhs5!WoP_%{Zf1Q#G=)+!rk4Epqb1n!v6xM!>WAM za`88&24*UDK`&cB1Z+7k-ULRA@QRp};(}fkbWr*oF*go-c{!cCcP>iIy?DQoP{Ym@ z_iP(TSR7_0T)3AuE!>@x(GJsdjjwkbd)s@-4o(JZ|A1iJeHY?fuZv)>EtDEhS8$i| z)8yBysw@%P!vE4j@&*#t%PxtLFDlV0HcD_ zNzS-?gDuCi5}oihW^qPk_Rh$l>$odu@YE<|YErN+ z)4dMyI4`&IF-K}HB=2xiCby&zG_(AP6?0MIOl3rxsG3h{CZD<2ZM=NdaVa!p0Bef( z=b1laZ8L22erv9JG>EJTh1Pn%0`<1Svv*|5la9p4^!zEY6uxSdZC`y^-f+HS*Y@)2 z*oVUpj4y7B_^B!djllSL+kRd&k4FRtx>~kVb}s_rF{H}NfVgR)V%`qbChSzd~=pM$U@^sx6C^Kdd%DOi;H z95UbAWU1MZ=Z)guQVqT_(3W7D?Rvb1jV~yPH#p=Cy(;p3w6rZzlnS(FQ|gM+ic_l3 zT+gV2SOxnfq){7xoGe<+O907XSRvKWF%mIh69!YioDG3cOv42%_t9bG@&_iMlDUmz z+SsT0i?v^gPNw=cCbKn_Z%2S3PAM$736xRrKi|{7-Wf0&#$6A(%WkmrOd1r!FGI6MGyd!yX3EmM>4II-5#tJ>0RQjrmdZ?a_9$?9;eqi< z@80d#W_z#7j*r`PzQK!xf{F|K>0&*xDsmDNoGlZX^g zB%`HjijK_K-0OBKQ~zuK(dk@Q!LG4y2;)Ubg(akWlf?(0N^Pcr9p@lx_jQ2SJ;DvZ zzC3S&*CwOn0J*+D49ORx{LFzl5h9PRkDP}wzb1*&ba5f!+9pXw=Q#!Ws`}^XQa-99 zb&7I0Eoe2K;cb?_tR|-11qJvi`D1tiD$YqC)Xx3!)mG=mImNYwsR~z) zfxhcN0+NK#OJ%W2jFd_X>G0yUsri{+LGYi`liCk9b|&pNcuO!R3Neos5l#m>shX zW)1q{nm$WReSf!=HE9YU_&(m706^4LWSQN&9UC%b;Z3@j{?O@#$7~yy9o95x)6r6P z0ksJu(^p*^;3m^i0pD%f(aJWg?>}gZi#$y9JB^{(;HJQGUl75O!u?Aga%k~furFIt zu86T1M`<;s>-DJxkzwD(EJ^O&ZvTo&wf!3Ch7(MgZWUng13@Kr*&%+78PE3#eCZeK zdsQJM7-5g~I^pQ~T38Q-G9${MGK|z1*R`3+g7vnzUt^TxR4^y2ci#jP;EQd8A6bHz zMLAI?LpsOqsd$UKT1ldq$XNSI$u9gaFJhK+F2=j)O(l6Jqpp zm*d%2{IbK@?rMEI9?}s=eZIV=B5|@Qa^;g!LO;*;-n;*$IVQq*Pm_$#;@vi-Zr|9D z;^VTXu;^ohppUT5YYdM{&sw z0q_LExMABleNR6^iFg8LIW+m}HCv;*-uBeJh4bOXR}S zkbULQ9rI}Dx@5Mkt7}*daCJ%>ZRn7CELj;%;YF3mJUtE-B~MhQIyTwt<*R(?Qd>v9 zugTm4bWkR&+$!?i=d*Y6pAD;sLQuNdQ`yTUG9Rqx;;XbqR zl6}}Sb+rW#42wM@!r!41Tx1|bT5TPB8nWm+o#%i&3H8>sOB>4f@2Y+m5}f<=#$81e z*hOZ~_JnhXE?312q-}L(oYjbI{8uv!bJ`a>zG7@q!BXF5_u3tL>QpF3GY1 zGE$txG6L-Al41?`KjE9YNFJiG5}qLh{alBzajOJc|FEKayggVbVolc6>kvyE{_Xsi zDZt6Va0=4lh$HNL z#`9ME+m2ZWBqRSgbE>6*cd$+2*0`w)1*joMq)M3~yWX|@@zEE$&?WqQ5K+`}#xwG@ zz+r9in#Ss!wBktdud+cy&L3V}hy097aShIyn^96L%)5k@+6& z6`#xQ)?qy(9Q5kg6!>`J%&G3N=t<~x$#UAdG4rRq)y77y)}ckQ93XRJ#CW_S((CJt zEF@Qb%y@A*vbi5pWyi1o9h;{#dYlvd(CVVy&b!9em0d*07oIZTZ_16lRb{3%b%Y|A zpjwi^XDOD&oGBPF1X^@unHz)-u%N|ROXKqV@uaN@K19bSxii9`_`Z_yUuUt`VW3n=H zAbXG^C92wqie~TS??*q3_eqxek>}mTUu5aaW2=1!EF(X(;{I?oQRgxbx)$dCwV(cr zI_jkPb>Tx|OEdjz5#8`p9Ah%C9`=uIG1S{0-bG6~(mRZ*U@Mh|=#BFbcQBf%$cP_< z)dNBkQ{gUNkx}x#fjbTnip+rMq}Teo$HS)CpI|s38RaF{k;hY)jy0V^dJpw@o$9-V zV_x%TDs3#JZ2u$<5V%LH*)b1r)N??HoE&Ay=;)6v~y!vu2m zVee4|S-ZxY{B1He6K4teb$dht3$J%p9mrG!eLqNfQ4lPqcq0@Du$Q^A?j{yw3C7PA zWL>JoZA3{C2Z0+Si>MF1;b{K14=wJ*bM>o#%kKX8Jl}OHbm{+z03iUt^rJ3d#{jlRwK)o38om19lB&NNJ#fhd_%=iKq*E0#73Hc)>8<~l zXt(;BBGGE26>Gbn5x06fP2q>=QpUz^wF3CpF}ydU{n}i4qWm}H@Cep3D{y0 zdAZ?QjlrmuZ2(VT&p*6C8`K(PbKMWM@S&y=GJSd-9BVmjw@&HI0Ot=OyYjpKmSP~i zem4p}Pt<`JaeV@R)!ya*)8Hzg`t z>7QsbX!2?Y@bKrp_Nm^$C6#D`V)CO=J>G=C*qZ2*$1xUw=;u!}hK8R)q&p4*6>ZA3rdJQDnVFI^=QtlL?n# zrl&mdm*mUI1XTd)CfRyxn2cF$pu=Yh7Cr~}MhsT*;b4D{0hM8*f5#8=K8lk%Wy=0h zSZscpRDcrcM+=tqbo4DSb`~Ye5f6$TB};>0ne)*NePy-f3S+)vSMo6;GONS@JL^yD zm4;f5yKW@Azp`^$cluJb zHQ=Df>t68K9c+ROFi*TO1OX}-pGtldr(`;)> zd2~h5P0vUB0&FTPCDNM|9FDv)u(qG?Tw}sh`StdGXA1v0en;L5$bJ7IvI-p@n-59> zBBv7pUP;?|b)@`(>xgxo!iuJEvlhWu@b36lJgzXduM&@6UIS?e8OJG1@ZZ0FHLBUz z=yCl0sl7mjqZm!5mV+L}=>kf${nosSog|67w)bSk`Q4gE5W494! z)ynz^sp;PGnvNqzowJ_uaE1FJfWa}dt;5sq=VJD3!8$PQZ-I#>HfkR;FTP59Top^L zKo*~sR!4a6uF|?2ddEPjNt8& z3Ak+bI51Nf2i(4mkp0+nB(C!gacV?h)@IgY;lyq?>dgzU4Hn*@U6nh{zsR|rXn6Tm zTGL{RiC>_;BQ+GHyd>E(58LNVCD1Ago9cYTDGHVp&2StwS_w1~Xmoq$q!Jv_x1Vw~ z?&c@vmWC#S?XRO~ABdNgF*B7wXuKIQwXbtHzDyYZR|eOvpvyAHW&cUlGJE!HSI*Be z#L0F+a-+hZ@-`tsC)A`j`YA)W>bHVApd0L6t8>AL#@7pc`cFKq?JiH97w%xJ0)~K1 zAM`~bfE+4a|M2fz^Xh%mnb*&E4`eYFxSr2bo^(F`Fz6u^K@&y29T3%t3OO12RB358 z{%1^vggM_vEoc4668vvz+yx3A{A~G zE%uM3%FcJ6Sm-vVZ;rQW%5BBHxKk$5& z@YUp#%H)qsK#VFfa!c15l5ZtB*64fbU9CKL`l12+1ok*y^Xpkp+#KHtVy-VShheWY znf#1Neo$HSPXpXJj5aypJeiDTyJL{xPjq_eKQ8N$8?h%noo7^NRF1IsTA4-&y3e#? zx7117)l|D5%ZhbToOdizv^^#dYn$0^!w2u2r2L&W&zQ;x%Zph8h zCmN}U#0&tIK79hteV;fk#cl7goe4g%zC-4J4-I#~D7Bha?!GP8gWFq2WWaMf=$RMbfpww@ABvjCjfqGs#VeYt==Rqtj`XADJ)!$uY#)15lJ18Txsv7*2#L z_$n!Ft*>`+BBy^#_RfzSSv>ay$m2Hjedwd?@!KSjn!6^sDsJKc6O>YP`^CMgDk%LYy9Kynip8DnrebCLpAQ6E)wy_-%}Gto%_Ogkvz3 zg{VoPAf9W)upqTp#Qc_DU(pU_Mng@ox)3AnOA0TY7og1q&aaZ1yQO4x-zEFfANp^P z$3m|2bwxz-v+C=+5=~k3X4#eeJQ?AEvtjg{jPnfd#4_z}Sda787qQiX&ek|v1Kuo) zJk+V!ehU=-xt6G1OF2!Cy5|k_cS5j1d=41uMuizZ{Xz%(sGHBc=xq_UP@Ojn>$l$H zoR~r*nl8O>x-Lu}#`)RAG~OUhH3ItpLO4h=wke^J zgeut?0JmeVgg?7PR?-u_&}4pQ`MSU4nX9-!2(7e+XYN!o&P?@+cX5=hEHtdS5` zF69<26-~7SH`i>x`QCWH@!t5v(3FjsE2ECn=W)Uc9x3C?jq~PwO^|L-Ykyg^WPl+2QZ-fzC55p_C>pswwZ`=t#&aph zHO*G~+;EA7%90(?Z^dG71g(CjS%KIQ>11S&Zv+!@SnSH52PjC~e?f=k#kP)=VeA zr@;*fv()q^$09b(49H2{Kc~acuHK#B)kQys_G5-TBl?!`|ISHW^pq#+9`1>=(0Gpz zj;{wAh0S(|B*NrWT|A&K%orxRoIiC5yt{z~GBiXj1Xxl|-Vb{$*RxHgbv}Nc7<51W z%V4QS*q3g>V|-d)a(_GPS{QV>{<2c3Bx&}@lcUe1Ai^FP%B^R#2z&n#6fUw`%(sK57hx3H8nb9--ZGfzhuJLp@Q z&bno`kV$TuOOdwGlC{esFoIHc>#w5Xw|Wpqi!VG{qws`$to8O)6VUYKEkUKbH&W|SO6mB_OKAC=Yu|>EUJ}gN=ibuR3}1 zQMq7ek7cTGFPEc3*!HHJO14PQ9DSA3y?p8}{5eV;tzzXaK+etkbkJz0^!K_B#hw17^BHUqO4qOlehb?GOy!eU?x3 zs%h`42SN3tZ#^HRYzVl{&2*TbBxt{K5FTe`mQH{u=Kz&)_QP>$()|2+Q5YOx>{ZA~ z@$9k}3hSlMjQL~Br`=5u`Xt}%t~0%#j=>f>ECFVw}Gec{aKdE~hzwI@;`xQFx_l$n+ zM6}s)v$npWH#Hi6MIk5hbyL%TdB+`c&Jftrx6Qch4r4QqfV0MJY((&6G2FUZKQ58c zttbNt(vd_*C}I8c#&KXu*f~7%FQYd7J%f2b?rNNXpCYE=8 z*Ao55|9n-(*JffwUBD#~0^QKk{s$^#H=kv}oFzS9FsuASM3}<26NiS&W+6 z^2S$h#^#GwCn=$_??2=sOmOXiH~|{QeOkM;Ezu)bFkct~vt~1wGM!DQSwT^o{rQ#r z?WbDO@}xiTQB$*ed}oeV&Iu z#`d4lm!q_Id*ZKgTzVXB9Ok--8%&-=uSSd@$}>wR?{ z`HPs4Ieg&s5xm5XsL(`Wuf z87uhub)_6UGq>(8yOzP?PA7@Ow=;NK_NkhV=L|2j_GXhRxmB*qjP+n`!FK4qG_m1| zm(~iAG$;uV#UBFA7Wd6sA6ks&uijdR7Hxftr(*8 z;eO-Sa!FW9sBJ{eaT~FMGK#0=kve{DDH7$q2{A(-BZCV%{( za<=8=0uL0%beizp;JLhy++)zcnCbKs_r)V#wsuRkv#A|eT=4ah=-mj^yo-)*xxG#g z&ZX1O!rF7|?!{tT_1kXkB${YeITt_$W^vexWv!)ozp|LYT!AN(x)yw*c1VfOy1v=f zTGUTnjYD|hb%Da_eW!K8Vga|FQTdDt|Gu;SeF6qxFtn=0jVS|3!aUmJoXzE=X4ea3R{2;N*Dv?BMrf=)i%VzYQY$yyi^P~J*XNg&3hhMh-tbJ z{)kgd=J#%7D|4#x$H%S2xH2IhEdV{Ycw6$%_Mg+s6?Y{bQ+yCoXzsPuIf{YCy2+?3 z^@w9ChRqQ}b|G)d-rf@->=B+5bofU{S;c`ey~mX4%fWU6mS11gRn`#-Cg8juBBRD| zxTE42wr3?tDPLaUgA+Q7EmN__3gppA?a{T$>x{&vG91;Z-Rc(JOMvf_FqmE?dcAMHGMepMElYY@L5}h=_}ifG@)r?J zxs<@>Eg?H*!mIor;wod+IpfOCVQ9y!Dyep30_5xFwmmnhS4?icKwh)IE@hdG`Zh=` z%#yPj`ma0a;HPz6jc^vpA;rmafR{tKHhyh^%1`3pL(7e)8rclCyvFZfot`EhRP6qi z(Bog3)D~Y^2n)Cf#QN#vebQnJ-|3RKQiD3W@H<|e`!?Off1AwLGE4gqXq7weuAr9N zF8dT{2y;08nFZt+*iWPnIbpW>ko;xkA-sl0&))0+DdC?|b1g}n-jxSP5u`?9_1(ZxmOMD_9T`#?~NCd$%h;zVYV>#}a`CtAgF2hEpLg zYhfj2+owm~rOJag=yOo=3FF4&-&eBk+LDa4rWt5+wvq#7(a%M$GQB&H+0iy(V!Cjc zL&HpbpjRJO5>%X%aiqwLVDjaY+M{O})~>XQa6^_9zmCCVP>>h5n2t$@;%&3*{ZTr_ zFE1jb&*#fq^@xK{(|`Q4YmR}&*y;Yhxoe-{#D;|9sGQehZOvajMt?M|(6A}pNW$Nu2`#e#wlq4&*|f^^ApSwZ zUd&Y(G`RO$rm(I;pUP0gj5HD8(i94Uh<}$8z;K5aF%3!Ani~<=PANhN!v0QQZIc@Tv_m!JsE zhX)Kh?Rl?TgM6rzC#GDsboMt(xPME2>L5{Z!5`FZS~pB)wAM2HE5$%ltd-2%_bYnj z?X}*Yka}q>KGY$??yL_#Z>N&`720yOLc>&$w|~k#BllqrPP0ma?2XMDqE6r5AeuQJKhi<5*0F_BX$=(vI zp_W6B7kTrd_VF_+-RsO*r3r0`GIM48`dL=|XlJbG{% zsgr4}#_i7%hqX!25y9QUoib1|jNPGkZ^pK~`0sN5&~poJt^1f}*_&t;E<*xeOoHI# z5g`HfZ$YI$4&ruNKs6$*A673h(K)hqvG>>a((_STditolJW52^Y30vB(vOJgG!S}Q`ytZP2c=6#GAsGtc=j}=4& zT;dy?FgwU))hpM{zv%r+4NQg0RFMns|J9oY&kl{JGM6+MkHS&h;AKI0-@AkesdW5ck>NO_#{4MtnYB~1_ zTdc}}ZYUoc8x(&VlH?cG5@q<7drj``Ql{N%WI8l=Tdycr17JLH>U{KCRI1grp7Liq zP5FLdRLYgQ><`>#~*$T*kmkY&1gcpN0*(W>|iqy>_k?MI)g*TtrmluTJ+{`ixXFhW99e{uU493u%B+nDT~t>!cQSYLd|Q5_!2LpoBiCx z=V6V|hWmvI0cFy>+*R?LW64watR6&4L*iu%ilYhr4v^(Jarm=`48P;&{IjGDj#}LD zM>83Wvp2?z`nqW-tr;SR1#0%{Y@O%U1aOf7J-m{tACqY&Ti5^jrIxGbN`Z$Kwi}Rx zF&AyN)pY0)s}Xkc|o`P)bGuW4IF410cBk;o(@-8 z%MH?=D0fWw#j){S95aK3x*uusO{ghB1YDAGCV&A(JYMG+Zv6FF8KWLc)z&rrLQy?lIZ1nUnS6a&w{HNBNa{3tbgWIhWa-;v!$MmHbZX6liQ@a zqMyCn3C0W$O|q)qdia_}4)#E3aW?s1v-!FL(JmZ;{oCUt>-;6OJ#hNkqE@ii%YmF<8c@D z64e_xS7+zh#ts~Fa;OEq4xbLXFpIvM)oCl5Qn|zIlj}02U*k!Dqa1tu_(9}A*{hFW zr%J6cV5Df+3}+E99xysWclH)kj?;AHhhgOr{U`-g6 z`NX9qK4t?Nj*yZxuIMm7K9&<3TSFDGZ%4k4Ucy-0#;PIE9YP^VyO#~UB-$dIaYhmt zL!`_0?gE?3SumxLMug(?i*RdNGS8Jbo)zCav03P~8=$!eOGPyW`e-6d%qaDYOy7^i(@qo7&C-2)sy%WEvn%WI|xAU zz4+R1v$v6x%(OjdhNo3j=eYgDW~x__W|DyWhFS$zI}r*4ctUfFV1FMuWfxyW8T}jF ztSO|zQ6<=8>l^*dVW56PF|Gqd|b80 zaI%e7*UPcKsRaLKQH@uB@61L)X%LnFj;UXyH!+sn_*Ff02S0I^9rNdD`eaW@G5*b& zD>5cko<=^FRvH+SX^)k4Mmk{1d{FTdTfxkH(y04ihTs~u*|UwQt6x2ZEAP>QB>zoI ziu9I1{v=0ZJn!dX7h_k*#z*T{tB98nhCzLBijv;C@3|jT>_T#nL-NWS*E;x1&mf3r zqKjJ4cps~vW00SC%KpD`?+q?o+}(dSptC+;?2HgVFSbb%pYZ7?$;UVR{^P-v1^#=A z1%PWpTxF3wNnXuqg_`l|)7U8HaNN%Uf~zA5IN7dU6?>)Y>ylTv&2*To-~*7}Cf?v& zU@rI$2>>xkLmI)8E92BL>Q)VvZ#Oy3s#bm7UyYXG)JG`#&dH;}qIqZtdTdysy(Brb zW)u0hz^pQ+jHVX#kxenmfk!1gHw*F%qslgKlna(siH00T`zU{WQXBef6w1;|q}Wa1 z6$eCE85}Z2=b0q11cJr0@S699PYOP)nfgsS%6y=V;bgn6vHq*2);n1BTe}qlcMlf1 zk0s|buSOP9wjPANto*kyAS>*&fZe|f$`S!uatF4C-%3%6$D>$nZ=Wz`NO&AX68VpF$0xSf=qWhZPo|502srBDL*GOP`n@q=UF@=C z(E;2L6>MpqPg@AntxTX8Ey}h(BE{L$@D$>)!W}5_w6D3ghvaN!KMI}JcMUSKKEbBv zG2oF!-m);=h$PK(ei-N^kuLlmCIi6qUp3pzJPg_EegDcoIMwAnM(0R(0N`yIMv8@7>s zgmwqs<|Qdqrr91!9173qnJDyudl4IF>iD=WrxY;ig*p4As-5hZKhD}Q+0E@dmWe2&t~Wo12Vsm*JqnT3f$ss=u=rpz zr3d|xYuI#Xhi2L%dC@snS@RzVF9Zc+%kdAZ$!AVDVgUuQ@fm_X2Z0K3PV-kKw(7T; zZ+;^-FAK1glF{&gf(ss>f6T+n++3d(*bR<0t?_tvvG5t*AGfVS-E_Id6^iynU;?%Sy%s#4 zEjZx-d>M9pe{uUzpUNNA&Ck0?%jH~}H*;x1ZnkFy-~IgFF83ZM)uzL}~zEDLsW^X@SS2x3-Z> zz*YqZut=!pPV;Dwt8_}t|RozBfZSD5#CZ0I7xn#zJY-;kWQ^(UQo z5GgwKx@${3stZXSVg~;!Qc@5wjU~DRk5_YFbA=QzaE6*EsEu{jP9)prewdb=ew8!~ z{e2n)1EjQ8D*J*~N37bAXr6C>$P)!JjAp$EK9s6U5bY6XE0RvD8O{=?21vZXDS7H$ zR88_Wl2rsE{bUK@VwSR5qRuB(Y#eH_bdiOpUwttl+KNz+4bc%d1!C=9AdbR?>mx@;^ofj|H;slCTC89 zgB5z}bF%NwtN!=}p!qmW-ibxt0beFKFm+Pt>sY>7Zu${PW<59feLIuiw!?a!Y(hTG z7Gtd;{4e>GgU;#F3B;9Z%J+nknmh67HfJyc>>h)W4)j=7=y_IoS+l;%HvV9&sB!pS z7pP+~v|AxwhnkwaV-Vq)d&3|%{>!OzTmQ`!?P|o5p%p%uX#x<|VMXDs9!AU+J$VUw zLLX{Q`JB96SJOzUFE%yxaiNaN8Q;1mJn_Pb zw?iVyt`it75#!4(8I;&2x;%C2Bi}A2yXHBT(J$T4(#ahFz)?pOQAt%FGkKJ@p7Qu+ z2i+<*70<)BAJSM)liZ>0d#~26e5;DDat&f>&Yhk%2Yxl;I!zssa42R| zjExDG6TeOn^R8M@KO2T)(%!uAJEEK6zbFe(Srp%b^4IOqe`=Vfr-XN8d>=ilHhEO- z40hj&*aM$gS-2xFl1X(bK>-XBVCR2i1>cr7mboQcT`ln-o_`6)EUleqh$}HoTn&Fh z>^HrCyQiTa!_98@)$sP{|D)rM3?@Q!8F}$43(bdh0`)u5m};PY6fYTy7C`MuqN*Q`PNjx&-3&BBC zcl_$2Us`axZ*qrOD+AKy{G(N?+{JftFWif z%q{w(g>43kx$A$=ElytUWZ&y@i6!V3-}Y6JUZ5Y}v&O#oI{8zV#n|d;XB(zzB_uuY zIh!GuJWdEt{ z2Ao_bW0sI!BPz{rCm+yNlOl*Uijf9a;9RZ8(Bc47?(ElG5qKT##WsLE9S?Z0&l@@B zRmVsJ1SHqym}S#GLwNQ3mn#fP9S%tP{6P><{at7*EQ)XV@47<0dxX34N_Ts@iH4}* z%m!!z^3dN1QIK#V=x?fVT~O}$tL%o)W?{f*q@ErNF6v)!R=F^^Y*~c`HJ>$d3~m= zX@<8h9xfozfF%q+CS4h8z}IT1jU!uKjKBoHz=9m-3~U7^&#J@7zWM2K#md6JM_}TH z;xbFo!k* z|9|MH1&(68o;MaB#>)2^>vBPz_Wj6(e^2VmuC{x$l)N0o^&!zcn;_!7n=6c#os!oc z_xL{WL=or{((x##Nr+2{rP9X*ks!jyXS?Iq2Xi%I<@4mM7`R|^Hxeq(n;Zr zqCFu>X)+b98WRqFv5h7_YN2tM8yBkD6hyd%K*7V~2>KcOv zp(92*of>a6xiZ2(40b3pw(GD|Al5s-oS}TC6mS4*tlopF1)ZaCW%o-rYhHic>@;0_04ex?QM_z0^StmELJp1&H+^)? z!MtU}-oG@!i9h3m8fRhHa&Ec58wtj{y*gIX;4!`p{n{~6Qgjd?gm8AxWyde*dq8BO z3!Ed+-VUL0S~RJYx@60m^J)*F@9i#N7}AIQVFF?etX{u&nM3%OJaY=~Dp~TE44`-=cXH6~uScF)F1t>rW5@B)um!&-xLXapU2QbhDsovv26y#D zQV5QG82|81UvH%Fr(BQFPW@>m(O+|T=@Rl!hj~Xf_Xi9{h0R-XyWag;q1iWc?I-ax zKMd)CvtOqA-sbfhczE#hwFyfG*Y_lo^= zcplS_(=jvO+;Hm5)1&f8EcOp)&yze|c?8G{KkET~k4sQFC5pNZ%HmW;;N61ftS-j) z%&;)frXXPRHx775(sQi&Q=!THwY=XqUziJ=$ak5?6V}c*0brBf+o8r)0*S+U2878C zrHS?N%a#fPfPnP~5%yQbVv3k15de+&nI)aj0&C0eJ2#Jv#$FQX)$A)hjin{ zA)20>T+^JeA2*A5bU`8HTG%0>SZp^V%@i25UZ#YUab;Fz`uojlR3(MyE1>KJgad}h z#dcR1x-8&db%C-vj_YHR3@vrnz;-CM+b_D4tIQbQe7+4=Td4>DzR>#mU#FowY|v*Z z6aQ>|l7RF|ZzD!caDBoM6A(T!@AxQk-uB~{XXWC|8}rA&mJ)nfRoCE%oQB`$lLEBa zbw%7C8aU(3!yZgYD$x6}+9CCUP;~G(FysyiT^vuj0*>;A$=(S>56>FUueV)0MySV+ z_~qX~LP$BJcks1n`u7~m9$54b6>#@_>~2}lt!Wi|o?XJcUwb%G=eVzEv>qWa)nqtU z6h+`>l3n_cCTCR;@iNRCtHWwsgk|84p&$v*nyE~l?FGr&Qvm3vIeP3dQu&%5<$PuR zY18g3#(sO-*Lq@T)biEB@20+itLJ=o=HSr$wIW$ExPq3OLQi6jWkMJxz5Gi_N6PSC z)8z0df73NNca?VhhLINjv&aS9Mw)?OcF?H2WhG%mC}2>}Deomr zv6|a^L4_Tkr?zrmRu7V|PB&L5j#~>7U8SUIJdF}naIE_I_lDNS`bv$6D&8jN(8D?3 ze0+gvE>qK5{wryhhB9N7?X*1}+E9fBS8BG>Vk+cK@NdC@1u5XVphLvPKcWIdt)x4H zknCZ})=?6?2~lH{aj}2m0bl!+#mpR_=iBPwc%6qXy3~6e4^>)DV?3?3O&FuUw%Pe% zu(~J(=KQ{)*rvhK&;KD(uvq{sX(wmfS91ZSb@v&im~%4x#E~ZCh%whm`Y|r(@~C+9 zaARK+*vLODh|KNFzTVofNFw9$yPlvs`Mc7&?I>AcVMqc!`0OEQJA*~>UZcQ;4yB2y zTMR(pBmBR|?~o;mib>Yh8xZ<7l?Z`y$z@m^rdOT8!QkOW%Er`I9gNDDyM%mqYU1y& zrIITy2uK55;nDxSVN#3kzqoJ+>38?e-?P|7dD{>TH<;BNwo0z63sa{`QQ`6D-V)6* zq(wg|g@VMZfS&p%HEd!RV*PK-z0x6Lt|WkzO}1vwF0|(kD>T4%R&u`9p!d13ll>iUxWZ6(3*H{HPU;wq(X?=L3^W5#N|i(VhT=8G zS#zsMrNN&n20ovLZVsT&7twDjEc#`S>1XUN~U& zJL0&&?#y((cO_Bg2*82A`}ekU6OrT2;`2up8uXQCVDBZ#4lR~Hbw2)^L$;F15s9J_ zr#Z5~RT+Hon}V&H))M4o?3C-h4k9+=$A+z3xsyco!6}{QeaX)_KN&2&db^p1R~=Yu z)q#A2g@IAG`-Qx>$nbt*B>MUf3d@Gvf1`=L14!~8@eLY%`1sL-OLOj;1YKEi{8EQO zyW8GcIAGAa-T$sQnqGPP^r@+{vtNwMXa4Qv9$4+jWpDV_tKT;k4{i7nZME2{FP&22rBl9Z{hOi3 zQquCfYyS)IpSV44V*>zY`|!{piESKqCCpcd?nn2KO(Jexy%qzxuedJT9{n$h@!oX1 zu>moUUWC!nYe}$%{VcOpikN&x#W~{0#;i?Ljxz-pdV5<}y62wBO1{;FXFpyquNHDG z;4Z@S(ETS+);u|fxPIoAtUq5^sbz3HXzwGc3cco!I+f2kBqb!Y>9!pmB?DODR{H+# zufXwuTyTIfyMmv@AA&n_krY*&jsg1B-Lp-A-X~_I@y%Mzy~?1tm8Hxt@hzJn(LugU zlAziKb^w-^7-}4LavqEB`YZErYGd4RX_7Vvn2mn;N%!9=2t-~2aH=w2qKIL}Q#hv_ zz$>>(ank#slZ>B7QKW|+f_>3jeSt0aV-vH;t@YvEGf=_JV9a}e# zpZ`26b7!sNAn?uHG1Ckk_q6mi4PKrVQwNOPh%+k;{N2=y)Dh= zA~8_^@j#-ik4yPEh;$mu0A4$MZmhx6)H>M>&=z95qnGyWQ@A=Og`lNv)*!dZu1w2O5?4Z!pN=cET2byQj_Q-i_SAP006(hr+O{S7`O}oM|a2 zp(r~mfiI~1#uFB5?jj@SUO;>xsD@S$ghd|{@|7~A2uYwVXPcZm$zuAw#wQPCP|IXi zvhOMx(%YRCpQ%{FsgGiN ze&HBF)^F&s6qew)8$unLlW=TGQM+Zpd9XwiPA~NIK84At*h~b?A$4bYX!W4?p{;?$ zJpm~H;Tib4*!MnuD@6GY;g9ExxXzfhxUa~476$M^qIGo+O=avuz{b`xw*JW+f40qL|5B^$xZ!H*oyy2DW)vKL`2@Y&XtS00i{gP3@tNdTCz)XijUioW?z;7( z8{6HF>B+S4Hxanf9p{E}O2+8;W;<>qj0}LSKAyo_%2`K}$A|k-gzQfS{(@alem6K)>DEt981c@VsK7Dns!<6?M+mPf@88y$kJtdFMu01v&@>Kz-_A_72 z+m34D>qJID*FS^($JUKIsbZ@S6*3K&fqj9tb~bzG3__og`}3F6PcRExtJ71CKYf{$ zanq_zb`^n|JlJG;_UmTX;XC4E|1o|hJ#O?+F#KxNGCDC3vohL8>Z*?F#0P59x$b7A zlZ;+0tn$9~!F$iNh%H@5SpXO*F6u-AMoJ{GIOMy8f>1gMjqt89 zoxjvv;JnXN)lXBQjp#k`+_uXmIUR_#elk`W<&|`mcU2)?+!zr!w5C{r#t+-SBMefF zH$90#vxhDgs7MC-7yMWQT&ul)9x60FiLZc4S~-<$6Sy6HSe|~0`G~O;=KuDNNqZ1? z^%-~dyzZwq7*`Ux@G!=DO|$!vSznHZTN%6kyld|D1|$b^fmd~QSpRdaUq21#-p}RF zQGlIS@m@>+va=-r~b4NBvG*p#gAtXDdoA{+G+GZ>su9Jo*3@A_mbYm zn~A?qRIBeo1m8%r6^b}Di5gBq#E6)n`=G@F*v%R2gx|nsXk+Usif-*QllA=SJ#vfTx z1y}d5ueFWgx!?$Xz#-kjW4}|i@w-p}bxbTXu5p}T=2r#{%_Oi=#2O8iy+f4aYcN#J zg>i5^reAe%e2M60UIgTCcSuT9iJ)Im$No?wK0r2&^>3tR=3_k*P@EL9!m8LZ2!AL% zz+^>d>lE(Rr5*~`H!+(V`F!lw#A~U%C${|skCO`DszsfFVD1Nq57B!uHG;WOk!{i! zwdno_yVHyaG&gUNB!7x=MdX{HNak5v4|uE z>&a>kC$NG?etqxlQ%`-s$Onn%>sGfiWvVIupIL1;6!{oQQ?Gz(UL=hD+r4r66w8w? z+;E1Cg@g@{^+o1Q1>azputG2+_FRU1bXwOK0q>~#xp=sHTkY`cN0b@?0)5&$Pxf$W z5O=T9`i0QHE}Ph5iQ+9wl)G(deh4(H(}7kG>j>TS)h4?WTcCLJgK}8{s&lBnb-?2Q z=CQM{LnUuNl?Oc9q<~5wg`+Ir?0g9e&$C?iyAr6VfNRV#vo{h8vi}kDIro6%;R}e} z6JBeHa)>Zq_ZuZGh~hi>&+SY{soen+e8G8p&$Z-ay|MsH2IqkX;#WDwOt}ad(mtG= zK+61}chNi6h!Vu8Ip$gHiWg=?nR<(lnElA_e)cu0PZ;hqf2-VI-Wbh9tmp)AUn@nF zve<$4<-V=Gi}{Io(ggEM#*RySgpda}@)`n$e$W-1J-f~QjZGf3Cfn!N-j`VQsDnFy z4dswR^?!M?%al{Q>T1IPkhMfSVjen1OE;b0YPwww@$UT!B5bZplQ)4Eql*Ykv4~YO z5$i8P$+W_0K8MqpvmOYB9&lJ)Ou>Jdo(Brd zUp98s#P;#sv^K~pyl;G%rER!1VTE_u+^*sIA=F%RZ4T13dy>FwC0%L;@IT#EA&=Ya zE$F$iTf;%JSKxEkdtYrpicqNBD)00=e@YJU5%yNDZCXYj9AK=7<}B${(8XQl1;m0y zK%I^SuaUR@*X~D@uTpR|b~0<2?H?8?{+hX;E}x)qlY_450f^|o(a7MlYyz-VyIEzhKJNsbDzE@X3WHeB*`<~vwk{U3qB~?a z?*Rob`S0qBJd+s?+nr0uCuKsS?VM9s#~`S{sXg&S0JNWK3y+cxMr=Fou3X9N-^K!IYuP^cZ5(mj|C5Ey{QCP8JX@J{yQpYHni==fx` z3|yt$r-7R6+3qAAp1&apE%g!k!jJ;p^^DV{JMQHiHc|q&RE5>|Jp!-cVu+ik5st!)$_5ovV&&n&rQpj>=29 zm3>KO0YxPG5Eu<9;IJA|a;k%$|iS=5YZ`$lW|2VqtK&b!! zf6JY{ve#K3$q3maceY3X(Kq#a;+#!!cr;3kGS zCeMv`X8$T%`|S@Ls`mwLA1m0gay-d-QIKiC<^5ETFC%()ycdZdm;xUv1({aAjL?Y{ zv0an_sJD}T;?rzqzs$MWaaQP3I)2{#xzZCKfj?%9ef&-`kDyZ);1ttA$?)jSG6_@2 zwRTG@x(_oja>(=ap8^{4HTw1-z7}^YHL7JW%EpTN$4dVCmRNQnu%i8f8TrNt)zJ(n zu+}tvnS(^sKeoa{{<_@2sDNV@qqj;q<`n5U9lxFo<;WbRcB;>Xdkrj6icq>ov(r&$ z2C{IGprbjM_S!Aau!K9OK$yp9QCrmu*tulbvr;YbTQX87rcwL09cQ_AmyfZ$@naw( zNkuSI!d2FNVs(_n8^8^F>|x3>0%U1E7)*cv=Szhh9qi1d0lu%4cd7 z1Il{l%%aPhX~AK+3>`Vy73n|fk4{N!ZnZv49=0NXcH5lOPd8k#^2mlpZz39xN=AU{a_QKe zWFE=@@((gzl57fB?3ahYo9QG$pz0P0Kt`m#wPhs_Wx**982tvO5ogwr0#nKrmoqT5 zuL7WT0L%{5)maCRZ%#hvsH}Y44kVLB4Kg1=1z$E<*b#a+J?oWpB#f(8t>b_h}uD<>?nBxldjx8drFBDh1$V`nK`NiN~0v zV>^)9)b}8_;17;h-eS^+J?@8a4SVh$FU2z#K`-%6dZMS?_*QbLgEH!E%L7F=8|h=~ zjNSKTA>{oO^^}|S1$R^U9--h$V|Gv~n^wwA=DyYlsEt`Ru2NLYSWPP&l7WcR2s)u4 znZa4(HQ5a1O-IFRIL7nEMQ}BJ=c}(8SKd36cMJZv6pZsfmEN4Xmb`;7Pz*9&@;|ye zqTEUNdV*bn+nu$;UrhNMW?+>?6js0tE|!GcI&@f1^qs)Q6O@%Yhz1;qezGXrq5YI9Emz*2%dG)f-$ zi*>Vt=W9ZCghwy29;v0ydgW@o4lqpbonLOi>HN=*DxG7e*Vzy2TjlL)E%`rREtvJy zBIj&+V~q=sqC+lJY3+Zq*4}AR;;h_%SjDRTA=<;KLwbK9*><1&tGs0~shkhtthBrS5Rtef`(bgZ zq+0h6{S$c6N(Ez&H~aF@vrYO43!7@VX@eIB{~P8VU;{~WbD&f0Y!fL;KAMPh9~1Ta zEo!s^b13^L*5Pz}Ig`G^YlIAh!s&d>Po+Taj#%w$L+!U5aJDnr9u5flvw_Ldi(TPb ze3Iv=R(y~2JJ!-C<_MgbcBoQxFOi_S9cl>XZj~h2=Rdhf`I~$T@4$Ux1@IRqw{=O; zo~e1X@%2`NRbjY~%)AQAWCwXDnt`lz)FMx}N0ossZXe4X zj{9InOvr$StIgsPRY@Vj5Od4gOZfEd{=lcMU;%22DmpK4c^K|}IkC_627LL~n)dis z%e^^|Mie%bRKSNtb*Kvhtt;u?S>!37k2sUWN`KmA_zztXl{u+yRd3o10MN_P%cWsd z&M8A4hOzyOXR~IfcvUKeZlmn}GlEDEv~>)8qeL$y9r* zlV{xMF`{0i?~F%1pd5&w1h!(WAeWTCLSLaL+~?si+YzJLPqjxB5z`!S+(Vf6*Oy|~ zQG47jZve}i2=bmd(epP(k8we12EiJekBCXlI|Dm;Q!_sTP^7J9URt|hC^uJfBf z+ngKmnBDt%ZDX_mO)oZXd%OiZI|L+v`fQwMxE9cG-ve@}^Mn1ztq9Zn16B;lh>_}7 zlFhKLH-O!Wven<9wybx;!%+_|Kl`FfX7d`k$dh)^xe$D1_qn9;b9PeLCE&&fI!5Zy z(x`UXrrVL{!6R<5D)-7H*1M(u?o11SRmU&V41%!#MIZsra${>|h&zS>yCBp0bB-%@ zONv`DYJlj~_C4h~wj09kdtMtV<=)V#{fKf{4jTjEQdx8m^>U0QH!Bz+OW&O{VJql5s&A zK#b*!4VEgeAV*U_L$9=)f3#g&)-D*L$TYRcD`c%<0ctmh3t!1Eu}CfUQjG}bIue$f zDi5Ty?^t@@XGnGoaLW?DGk8dAiQFaBX^|F0m)b0~0FGb!n(6N62e3#%*ORnR$<|#c z<9DNoh_m5>ec}vo$%@x9p1P_{a?&hmKAs*Og<`8vq zx5NZ&w4t0J5iUz^wJ`5AL3wAP(6U;P7$=vqq);$eNsuD(63DH*;~wU@_jth6_r~n> zH%`-LABGW|$)qgY?f|j%~$V@z-_R$dhE3F8Y!P#pr{d zs1uPq9c7~ozafl!%bx2uvfijZGiuI62Xu^CM%|n`h#OAzLzT#umjx4-xvH7OxK0Tt#Od6UGLY z0d?di_+aeDX2JwgY&epHnzvi6Q3LWEhV*D|E1{=QZiq{AJkog)aF0?3bS*s7A)p2@ zq8E5ArRQ?NoN)E!ypw!nl!mAz%25q{C>_$nM$474o9>Z8ug2Q00yZAXU%@N4Qifv<#AK^~c6c9`N7VL+9}m^O=( z3{D$#wE**+^WvObo&qDie27PuSP?<-Yv17=w!sEgQBtO)A2Nar%>NJ;y(fAs$dAT$ zq_TdSTWjn0G;k!`{R!2SeG!kuv78zJ>9^OuV-MAspR)iVOOHh@*qTZk+c{OsF*Kiu zKp)>p3oFS-97#w>BVl!@wMSD3*mhny$^Wjpiqp#PPx>l>)kWW59IlJ$(R5CN(t!UV z(AKN4&frprd_*7l1?T=0y(eS*ko3!SoF|}4IgggShBf*|n*8-&kO#KRPI{IXLQ^hs zBgA&w$wWVY4$-u`%U+dF6qt~FrB+Con%on|X>^Z$YzqWN2gH(dO#|0^QrR3&$1=mN zodS-GzIVEPdO&&BL5|F-r)U$$mrXCfK?UWjA8yLMp50#6X~q_W00BA-Cm&x8iH_ep&O zL$!xtpOjbTcY0 zr~sg;h-U!$>}s2>--PBCkK+cC{b`I3P21~nczMSBz>=WeDjG}lIi;ULobrhzC2bvv zYAzmIDj(PUkRlZnUhYu{G1w%uu{_CMx!g^iBxL#vMeq$b)c$KF;NCtm_V!LFzOHq+04t{nExl0^BzIg_4ua zZ;Br|Un3iSC$wo7C+)hO=-jB@zS%q#iOB{yO+^kP1OmZ=Z$bU{qH~}GZ>rzYTsC>c zoiQjb@rc^6g-fEmnA3#X#*E7A%%h223-~yU9MW` z1<~N(7y$aGn3rgjO#?4DQ0!`tQtK$id1Y}%K?g1jW<9#QkB5iO{zUO0(xevn$kBl0 zB!rjG?tP(ecZ(K@J9k9VIbCiO%nm_lu`Dj+Y=>+k@2s`$M%xivEQPS#^R3UEUde93 zE`_`(XatBl5}m~nzXVXZ7SBQe{^N8L)~ci7!d8`2FeEe41C6CfNrJFZ1@{KsV>8t% z%Zs1rLX~YpX-)Vl+bk~}R&x91M_cMGqN!w|Mc^s|17&-KXs7k=#d%cynd|h`)QS5l zJRMZLh%>;e7G7=JKAWC)4F`dUXWnZ<8vAB#Z(NgsG=Rx?q*YvQ51`A%b6|eBX|c!9 zo|0ROC8X4phif5)qx;Eg(^ zjEMC(pOf%l1ywm+OSUhL(&b)#d4u_PFP|2N&%skSOr;6dBjXx402!$Z`p4)(rn?Pt zd7S<~*E{*pojR@+3nE-y7myM;2AuZ!G_0ikGv$2@POEyi5Sxs2Z3OBm1{AkbsYk+! zy58}!=gNTi!cow#U|!q$O!2$3tUtf@Tb(tq9x1yCD5MM% zjj+-!C|Q3Onkl)wr;VwNU+w9WCH)qexTCz)>l>UaOmzG>)fJ<%jHv&hP~e7Yah62@ zBEQBMR2?6QhyTH4H>c-XSIYtdRPJ(lSTVZ{1i_%?h1{Sp(R3Vl&b~J0A@M!+o>u^Z z$mj{Ak34Nd^eJK%OD`;qt|kyE`YJ})WeRbMhUZBp0fhj<)ZICZpF;_?zS+auz9)_I zAo%!&e~>3^Wdr3n!GIaX!(tI&ez@hSOzR7>yRTRN3+>#kD;^95i$k`HA+_jB>7w8V zOSoHJ96Sv`{awp@F=9M0$dpw*6XSXB*GKDvwcw6_Cz9v_P68Cbg%cQk;V$=k{*T?- zT+ik|9G_dOFzmT?mA9#k{U=;VJvXK1DKjse1i;&!sO|s6#FebV94o`W*jTDid~tLL zp^35s&@sAYR9ITFWrqZT&<--TYwmMSdB`r{TVaJzlPV9y4-bM>jcWG118=+%;Shr-S<{bNU#2-M5btYe4C%-3Gx_fQfmq0-M9Ik31Me4GGzt*xC>#Nq>WhLdM)HOEuzn!~J*-aEr2AS`C zc(;WpcmBLQWh~Vo16;6Zh1=K18375im4-0Dvc$G8PB(k?Ms`G-aJ136NJ|O#B;(3S z^|ITeS=CE<5GnXK7+LBYguH%#WBm71@m-~kX!lV?<>kUJ`I_xow4%5I*m>PtRH;yw zvK?0zf&(Hl9dYz2kQ;5UUE)}~>DIFv=f^lc8ei1Sy&IvXy$?K9gJzbkq&R~3-X)0J}kP3V3@XY_Gi3|w6qpGN=>X#j`WzyA(Fq7xSl(L(v z#-oeWGuY*hrI}tUiYQO(zMw)n>g&zjQ8yd>D6WY6DOOq3O37cwaCELYV#E8DP6y^3 z68^hwqwU;3da$yPy0hX|Ls-$Ep8CihN#x0`rABc(GNah8z4fyri?oHgdn*XhG`FqYZ zF;@9RwU$nd!SBlt&9r#F>eb?!r(_IkCtbXR-Dc0PCFM){@a#||e#3Rk6qd5?LHDpo zKwuN+m^L#m?Ew(_a9lNE{{vic#*S(u9p@BR?J$QUQek26iEYzd zAj5&>%A4ANpt3k2dUlY^WQgQsRT%{vA6`ZK+ZxH7cRW4(7P1%_CS4z=Dl0tSo;C#M z6JLI$JcT~-Azi89^RXUG-xP;qay^nQIy$lI!FF)l?R(Y=nJu;l!J%USpUNn zb_Ox_L0h%hHuDa+sqIf$jfpFhcmG;*3}DDkC#hli*|mIlP{y0=vpm0l5hzZIi&D+0DT6|kF(UWLUme zCRFIKdpf5wUN48Ng6wT1DK{X2SXTbN;rZQHw4fsAEKuP-`>qfQIPjNoS|AmR^~gw7 zR16(d>gYOtS3Os8(u@X{sLlQN7SeuAx1dS$2|#L-e~&gFAa8$8S!8j{v0C_EJef`&QVo`Dtz-!FG1KF3CU7y^q zG4;E_4Qw|o*gEVD8>KZ{jD@O$SO8N`CoC46XnWDqJ`YAfZo1ESRSB296o69bYA;*4 zHA2$YtV|F6oJNckJ;;)7*0s!M&tG%cqQ0f95jOkKY~c1bPj+awV9N3OBH8o90m1zg zh7GZAt`g6Xilx4#4Y$I6Br><(kteOAs88v7dp>_$XHPnzQP)D9I{qenovr*DXnSlh%eT8%1n&W_{^&z=Uz3K#Z^0R>uUxQ{s`A z*|dwEVm{UyG`sJYpZS0P!_EsiusOfq!|q$7_h)=oBQ|qyMXHPLJ8we- zB>RUX3gt%!Us28xs_y2$+H$14$Lljh8hte36ZbY>k2YJiN|T*~)pPZAkF9b+F0CvG zZHW%49w;pf8M%nyeu^d43DBCcw}+(seelF>OKwUQEVWv@Nt?(NBi7b#Nf^QUB%c{g zLGwC>$WV8EsYf_&aUk5`-r>D|EmYU&#^kl(dvQkuTY^+DqUJ!8fH+a+B{}S=NAJ1j zAOHr^dEPttNo1N;wMnQd*jh;NpQoQ{#8sNGeEP%4g`n42dta0!9{%iKOOWfjx`h`7 z{~J#`n~yV0tM$6!elJPHT%)dpu3sGtRk020#|j9W8pxC1mVzkrCFDc37+kD1H%7*w z0u|Oq_-P6IOb7Gz6*Ex>j9p08|T8vrT$ zm1_KA{9)^3wwl0Z)b&3}BYB)0MbO2QDfk5g|3)xVx|zajvfe~JEgz?8e7=$69tr~G zEeot86xWF*9enf}0FB`DsVfeIs_vAw#tXWjJqcle(#z7C{dwL%Y6n8)bA!>xBvc{2 z7x!dA#MzZr#+W4dQ&vpTNmT%&DXf9*$X-|TkIL6pzhOlZENTDM+YW$*>e9?aueGo= z08t=zT15O&h^!grwaQ>fm6rkHFHm-e>R2uoTB|s8_|Q76P}ID?64+x2dtlUoSu5pc z3yD-+_V54kAY41i)n(zuV6fQiM^M$Y$NR!agFG(?zCKy*d}m>C_mBBf`=7hsq1F}8 z)8Sjla%}HI0D7W)sd8Lv7%BQ^Q?+{mWP?Sr{Tei25U3=vlwh69RGjIkaB$4?hE6De zbWT)1ty73-IuUZyGO6SVZvv>VfK<;Zy}elAc>$!Mf2R9w%Y)fWB|RXH?YIB$()4#l zi;n6wnTsZ3;awK5*dRU@)cYE$fg;((JvTO23|Sw!z{HAnm}wnZAOUarx~?-ff9%g! zp?%NwkBm536Tfb0GY5rUoLwAwrJYk#g)X@Fp@45=mZn}fw|;x9R=dDQod?v&j$uJ- z)aGU1v)*aIzj)|Oe!Er&{7Y}ZMImf8BQ!$N|MR8;{cFsd(JMq6NH! z@V=@YKcm9&gm3Cob%8luu=f%5Fc0|YG&L7w127&9lP&;tG|H!9gFTp{?a;Q^MlzE0v)@9f|tk@ zn@+Dlxr{aBhZGpEVyHRwe!q19)uZi%i6blY2!Z&qd_oJDtI&v`%NkuOamTqQ+=;Hl zCMg2OOo8_dx4mbAZw```R6gIrdzzs~PU666mz&L|i{@sp!Qemd&}BqufQW$&<|rW{ zP?$AQVT*9r^w0WwjnFan9-eN*Z3vq9CCD^;gW2Dd#RZ>)Bix^G^Uz2RMj-?+93+K$ z4U1{EC^j3l4IVJwZ-|Fu8#=+@#sLPSO+qmjWk-f!^t1wqU1ELfIs(Sh%g#f%yfP2Q zgxSlxAmnC4iae+P_KiWe1z?w*bdj*{eUanIo2*|BoDCt9Dy&;bRJk6%-6KVya? zBECce4dun}9tnp$TAp4Kk1VwK&qwni`1fNEQ2Sh68GFDNiNmej_Y!wy!pGbn>n6^0 z7ZaDCUXFTYCbX8kD8%BoOq_CF|LHiOx|6gID&3#oG{iFwOhe__qHJm#O*?@Zkv(@4 zC5A1BN&-2TRi+!3RiqDOUnMVy2yYEc7I-dMixOrLzDpqDCk(-VklqxqygeB&!wfmj z`Bqu}5HV!c>n`Zwm2n+C8@)wMF)`hAh|9!h$KMe+JFK|9-vNCkC<=7py!?*5`rXpw zZzUhO9FsUL``CgMOTQ-bA^C-i@i)aO{4TwkFJqRRmKlJ{T8sAR>$drV%me}@yS^Z)Ii`$W}p@zUAiT5ayK-xEvNi{bnj zHHQ5s4T!oWHb{4aEPv21AD=NZvO2?vvt3>!R0Z5wpAH46j`8WGl7F;kwkn*h`i}*J z6z6xDklXSEiA|Cl&LeMY63-Nj#)vBKJYJ7o{JANLqXX#}unpC_cB-~SW@C(o=o8QK z>f~u@j2lQ_^-A-p;r$o?BySvI43ijVm^V(_O(UsYeKxS%6G&F4x%jf3FPoNWEmyD{TpKy)zLD9oz4R zM=WqezkYstzY=wog$iK~6O4?CLm*YeG#iX}DG9T)WI(rMo4HC~NggTzp5Llh%`X0y zxS8}9kcrLVqY!CqC#AtgY$(}rOphBue|H!hL=@Gal^#0D_#u2(2vG|#-QWwhwyLUb z)=(?cW3_NK)%k?tjRzs;OvrMrr05`^1NjQvQx7tUuzqVyP31vxC|#6d+O}JXo=+KV z;p-25LNT5dK5hK@w(>UJ@)5qEVPP=xPGpt%#oPDI%s5SRT>Am&9_mp97`elWW^w>h zaRDdokrHnpJclC`t)bsz&TEb~b4$i?gk#mKX(Bbm|NcZV-+5^cKsYJk5zq&w%__ek z{$_TwcBo6aQ^~}E#GRHc-PRA{PB$IanhYc{*)AtU++98A5~{@UU-tx*>}XAzDgH;W z5W;GS_FXX@sGiGn6`f|#t7dm)$N)vZddsga_*a4p-3zwp1ryql?**O)`f!UJezmpd z1bBS)v5qWmgLOPYbO=O zqbLnE_D7~fh3^f`=w$+(FG0q}{IYAd0CfdrK#A10wzQXlB}A?>5}m+`xsbo=wIqyR zL;iJ`S?mWVzTq&5-Zt3FAT!NsB#}4}MggGtjX7&*YzWTfL+E`t8m^u1mxY&OO}!g? zXjG1%&qKJ(6bdvYSih8c(r4&1Tt??ws>(=Kdij2#(1h+~i1q`-rl=+Z6lNqLRiC`v zq<-F5A*hkZ3fh}RBjDcre*?ECze6) zdBstm8fZJa5kaQUY|)kjFN@1Uj3`Vg09#UArv5SM>`aqf635WgEfxaZ0Nv=O)Jq>$ zc{YrY(V55i#w0Lc)kkgQ4Q&i-(J)DswByVdmT*TUw$#?*U^}L<1bNN%eBN=;#pwX& z!w0o~JU#KcjwbELz*+zAtS`BE>I-3>})mI$$z1 zo&!QV%d0K!+4Y%5cn2I0R|Wj58EL@#eP9D&dV==qN@cFyH>2NlhkET5#3k9+xTV|} zPp66_k=~59ft2V>o=aA6aL22ghnn{YfOrqz!tmj=+OR*xSFlY)S%^S|(m5%wtOi-> zhP`xY)7rP0}+WdUb@%F8OV5gyC&CQSLA*9v90S@;^d1i2Bsior90{W!UbRbqE1kMJfd%}OIR+T#`L;iiS9>0kYl|cGw%+7+-ocx zj@`b_eFUhwU@N{NRn~}2{e4mYJ2dy%1{ze3y#9^)ADtLFKZA!y;W}3b#$F2~I{3W- z`^7z&3$2Fu0reJ|)(1tdM=B$U1JvQ70wtcbP;sjF1J8#C-baVNiFAiOdHX&;)hjrv3Rd6vBvRw7msbR%+fqn>tS8+))>jgfE=vMX zJ75mJdASv8mn^Kc-Jc(v?Lf4T&Gz7}e(>`96F@v6Sl>$5hkvm4NCD#H5L_tZJz}c* zhd*jDwMaABWrw;atdW$(K~t1;UPxUgMC3zFLqNzLuwZOlG?Y9NV8hLSw$Wir-T`Oc zkuLD5*=F+jFSb_qmR4x;=>XBMerddjUqBF!@BkchFwKV@k*l-wP2)a%*wx%M$1R+6 z|IQ5`Ma9ns2OG11atanIYvYmPYV3huy&zSwb-2xY<$rOyjXQ{^Q@*ku{LvlN+maTA zWQa1$(bS`8h68~75+P-_(0#yPd7FW%0LyJc_CzQ7?Ml~+FJxIN>Q4l^9M_0NNt-K6 z`2eeV{?Hw*uCU&Id9dM|jaZ4TbLPBiivS-4FNgR2UYO3)_MjtM8l$iFjg6!Mx|Tne zlhE5#!t|uM!nJhFm;GW27kY169=|m%CoaFxpz6x%L&+2^!$ zW*C+{RL^Zw;z>+i`N*oyyWni55J zaEuR#S_;e`@)2UqKvn{E4`4QFlzIcLHh;jwy%@$!O5lA48rdKm7=q@ijuTk2sFaG1R#5B=pcIxwa7QcT)9C=I=Z?Og6<3r^m z*b`(D-+yMcm~D5w-+bxe?5;;(rGj<~H^b2!^tHWCruf?a+VI|ok5TmXZh$gYAXra< zh9)tIhosx7E$)8hLXKqp0oQLS&#gM?^WKe$rhj1GCW``!{%7R-m6p9WD55x5Dl+;X zcd;y(X^z7eYIzJDZud-DA}dTobe^g^W~|z6y*Sxd`m~ezA|6}*8L2n{i&rUSWydqC z>8hSyupJ95c~ttqnmH?CP6#Y#I4#ryfJk5pvWv=CyC5@!$5cfrqWo(~C@c(X{?vY) zh1BPPOFfy}gGbrwvNTc0QiB{FCQm-T5P!}fOFPM>pJo{KRPX26PO$cqwOKH&?sqJa z+O0^EZ#9JhcXp!>Au94*NLZ%cK!3qvk5qsK?PI9&O)rwPGw#F;M8-Qjh=iHrX1{nz zzOFu*{gNI08(!tNb~0shnOEHruEeTikURIAyHBfAywyP&JC>($Nf0b%3b3W$T!gaB zwGWwoSpVJ>sQmUgIJn|52wb(DdRv+X9cgL2Z?ZxMJaBOG9qCj>RvCAi5VAgNAPzB6JQ`Xq zH9LQnDZ2dH`@L?k%|)Tpm_K?3kk40cPw?Ep<#gN)cQiTmqQhLJ*LsTJAmseL_|42S zQj5Gk`3pI6kr(l)9NtKgfV88jxaq@}TQ@>wfOy@fr3F@b#+su{u{En%@X;}3*)!CmpD2HQm3MDw@@M z1SV*3*#u@` zBPLm@Rn7XFM$N>kcHXpU zAAP)a@s1u@RV)7=$-iNRACPCIX)CX_(n5lHtUwHt?TzH#2-9F^5{@_yOIZz|oIk}Dj(Fdlz8S$ZC~&Gwf$)j}Jw zVxz0-pTh61c=nW^6`gs|=IyG)#7hnhDEucnPj9z&!0mP<#6QgUfEMyUlW@*2#vi*1E zhY(lp20}NGuP2lbn>bS_zy5F5gHgd?!6|ngL(dRyl+k z&VKv+u=0zutOrZOdWz(HT++?9?5qe!4Rj4MQowpaWqijt2NpSH3iiE0Id_zu7>So&7n>^Ej=keHzf?*@Qnpoq@c_Zh@@u#<4ZnczF$vCoe$OFYa8MDb^qLDG~qNhdFChFb&X@4 zbQW-Z>dwayb>uWae$?v($S8Wq zO*6A!Y88Aj_?AUcmZ1Gxm^1pfHCME_-9-g~8)A^}@&ix=f&HCsrp5qWc`O!CsCaDw zKruXI&ZKR{?z!IN&iHQe2s@fz2V(#eS|pu{iocK$(^wF9*t_a28IiXZ4IPL71 zFfhaw?U3nZ#@SSmy3xVoNOJqF>D7=O<~uPa$}h3+5@@9ej&&iLAj5BkEqn7Lhvcz8 zs_wdLt@3JQ-ZP}h#+TwQ$P@x9t7byP0UA-g7rdO`rt7(x2)l?AJ=3-rJtG%>aUi)S zFLJy5X)AgxZ?xrqF9ao9n86+^W=;Sb9tkc2EkbiSsz7W0I;-cWcwb;bt){$hT8@b< zQaAsfcd}~m5L_13V9BT^-Z`}|s;nz;3L}{X) zi!th2BNSdbd@?}Tw%_=m<~?S^GRVWQa%(~%bZttAA^`fA7H<>w+aOPH zpV2)C7-iR9l)6GH4_`yHd(}&*1dr;Ibt@r$dBn^WOsdK~!!FVo9`s+&x%UWKRGV&XG080Cx-ky6gukY zZp?1>Somn}ZfLOSL}CMRL997|=m^xlA=t^eq#4Uxohtl&%)^LVxq1FP)(!R5(dn8cSIlSO-2|4nrK!^ zH1>j3p&4GPx8yq_5#_RcFx3z!ga;Pj}|uJBzE_ zb>TooPo@HBE9$vwXqZThaW&s&?RyW%a;9eh(ceAoCD{W7#jhj`o2#AZZD#7Cmnzc! ze|KX4O*g-m7%S{j4f}z;jbpz1!6mMXN>bS?4K%%jrMRRXs9ggywg71J`|w~)a~A%Z zc49t^a%h{if}liFDXFc3@a1b8C$~gF!j{*w1I`w=W(E&;G0e*{8|a6N+*=pZx2|sy zA&fG!hGB$>NC@ZJ`iEWsIs$Jf4b+Pj&V`M&e@iy|ek9-gpH;LEjl+G8if@&u%5CNl z3C5(U$y1Xn%WHf-T0yL6OSxtgz;Z#vG%s(;^~qTRw3v-Ce^q@=vfFYY*M-V>3n=5d z`zH$^DPfoYk;x0Z&>iv-w%lnru)6ei%W>k(TJ%c*G_Zq_)PO0%Ig1X-5nUQ4=F({@ z_H5CQF~6s&7!L9SB8`AX^ls?qA5t5sdWo2+^u;E9Q)IPfIk#PxzD=y@)`EJqv52V^+%=Ie>*=Reo4<<1084P zl97bsrL^X_o-P{&aHQwtxy^;0(yal~Tn4mamIzj{M{@C@0ksG}XOpODd=iUk{tx%*)nNs%AK`V$lGy|Fo@_hy0u_)6{ex!&s)xv>FK z@1XFCjogSMd-985UFbz1kH~K5Ni;2_!^r57A)Dw-pYJ8f!s>3}Z^OXD2Sf75A_hY^ zqZC;R@B8Y>>g2Mv{TA*W`FbHLv_Z)*&l+Z`OOe?7yl_QtT9cNrH&^Z(ZA^;tgKBnh z%86HO9R5xiDu+hrA9vw&0Uvj`#2uFS6S_F901_aC%FUs5Z%;2zB0Tjs?YREa$`pQh z34ff9%;2kdmH`X)g`dPJA}F*g@*f_K)|x7@;}c77p#q~$SiFi`k^XuiCuDk34Ad~3d%L5T?7bzC@R(qv-J2Sc>Idtb9dIpvR*Y`Es=r|{YnqUi z5rwVhq^-NFmSqMVLr8I)kBQS&!n;4CPaC`B*sDM>%oaxEL635}>P^))59cUjx5RmO zVF0=T=agp;!ns23IxLg(ofYNp$U`dx8E8+O?yMIO`6 zy88G>Lj`i)?GyJmqR zh>YpL=6rNs3@;i;VEBAnCS<=bzp7e&$zA`l8YY>~@UyoqZcmyMH$?=B>XI5_1M8hK zy0!g!RUzt6;U#$`P*Lf2^!EUDn?F2ab{ko}-<0TwVg#t4#VK_#UDAC4hYP+DoK%!F zzTG_`oajm$R`6y`n^GYEet+8)8_9rG?5lKN`3vfB9SuzE5w1#h+h{Cm{}^FK7^K)x z(DB_1z{`qn)<0Z}we&47_^j{d-1gnQ6W(#h>t2CG*~11}y7u{TYOjMYcQE_GAE)km ztVHtO;a|CgRxore7*C;-M_@ju+Jsogc1)QiuB0+-E}bXfaC93oqwpKb6-f z>9B~DumE%45(Zo_VA)MRO0XEqz?$DAax_RV-$5xu={jzY=3jhSwY#q$&&a4N8@)_G zbEp2UhCwUg1biq>1GbTlJqKXmp%B14mN7$rGXvuOmyvoj)3^XDA(b~Juj5l$A%n)DrL;^vLhDc$E>!ZDp zUM+u?#;(6s-C_Olm>^-)?3N>2_Wm4k24xuug@llnbDNbY+chB-ArI65L$jFo;s?8V zLzpQ&jjir8`E6Hu4^!UD$%Q{Z3CBq%ex=7y#0CI^NP`}k#Srl&^xYrd=nIvYhveok zglq2<6-7MPK{od3zghWM%Vghv*yB&i=;3d8)Z~o7wc4F*eQ~&eQ*^E(TNeQW-H%&$ zobd66OqJhWd0FmjM-hhpn}4$#E*$WRg3eo&5vBh41ge1S@Ua4QuJ=rbsJSm-O0Tt@ zR2Iw$o~tBXt;UDo@iw!;fi?;HGf9eb$^Bi%we4?-7a9htn~;VJqG^V;mE>4SXaKg9 z;%e7gf$J-%YTE}I^HD>2J4(G}_Hzb@?87f_X`Yk61KO`2h-}eDAGO-;$l@Y%=;r~) zwMPj{I07O!k@flo@yL)#dw`9X(5dt_Rs}yTOYx`6$C&s%HQL|^6SmdDmlU;nDJj7Z z@LJrW9PKK0nktXvLr0oW#~8H!VKgNM|LY3kTMC!+mkTq8Ds%SGcS!iy)>92sx7cq841m3pS0ta=`e!0Z=jJ zeacVB!CW-14LFrAcqe%tT=CwXAT;c@lzMZ3SJY)Vf(3vApZvj!lcEwEb6`P_$c@8j z+175(hhnJAmk}e7bXaC3g)2>ULasgj*uHVF*v+sq{A>sVtbG&Wbz08Ph+gVVXyaKt z(UF6##%Jprp+1r6QCaB8fRY}nUepXI2|Od=Ui8zvo%hjGJ~9}*U;30|x;aZWrK!*6 zVz~Zf7qPtfj@fe^M}}Gg2&kHEH%T#(=Wz<=HH-i@s{(>P)ExMt<6sD7+#J3bB9LJ; zWv-t^*Ld7_%u$V-hq!w!_#4-vlCLGjr+P4&)|hL}Bx}G{`9NKo1dskNcK zw^nC~kX61j4k8Z0Q(~B6$5ISo|H6oue^-)ZNcB2FExanZ3>?dg} zG0~Fy1?Any1gojul0NwBi6FqDDKH20DG5yPO|vk{Y|59)s(dxtzO zd;9gxXrsG1XHVI)CQmS+EGOmeN@#oP>zpmqets|bWv4QqJ70&oZu!sH#blG1y~BtT zoY+5Kdr6r!!HZ~LfK@)wyi}cv%Xk}-6L!ecpE=||U!`UcbXw4`Ba}8oVRnaNTtDMZ zkB-a_ir4%l29YoJOlYo{T$3RSX;=l((f394_XYyP0Z+6!R?f4R+!z`Ea*M!J#gnY? ztzV)gi_7CaGtl3o=#eN(O$NVDe^c?oCvcss%yGoBW>xDsTI1WtJgwfItQl~L_Px=O zmy0*wl}l#ak!s8&E0(A&ck{!qzBy0MC!gf`mUXu?mfpR*Hv5ah#dB`p4c!oW(`1~V zpq%^WT5rhin~W<;J;rS2{r2XtodSQH0tKIVG%e*hIitVJuH{8~o$g69X%1!{eS`H$ z0>@s`>dY+Ceu3EZB_%_GmUFJqz7aU?N_`&9XjD2dcrCObr0D4zc<@#E{7>C{l|Nt% zy)>)qWePP9#~%FgMa^F$y`O2`Bdz?BwA3NBIsc2!%SupPdUaU?sWs(>(UM=ge%NcO zKi-Mu75*Y2$tVkWC9uuCg3d4OD@A3etgelufNEv{C55&?$6LAlpo%S-lq;*AO6rO2 z#6huBYxN`?HAM}DEjW&GuDG0EtFKVfe}7qAvn{*QxgrLkqPCP&bhI(c3otZX8iG#v zC%-J8O*TIzPQH`CtkP){@v5{j=OWEM1j)&;^*aeL?7KW7+1qT3&i!EX#7lDv)``?n zs}>lTUQ0;T|n zA32NBvfg;%+VL?_Z^&=nF1aRTAH4pQa2;b{N~c*<7EQ(R#t9X@9U&fAe|mzUn;$4U8L&->v#T{6h(ZJjjO-t za^Gi&mnK;S@NEH-#P~yEi*V4&{XL^!kBH*BdPBJ$EIZ&zc|-8*4O0;Ev>`z%S0WZs zL76+sXr%|c;)EVi-ajb2^*-M$vQx*=zi}8m=EnkLKU;KpS*Bpj>Tz14~PkcbZ`1v@Aor?UN z$9F5#D~B#Lt% z0=5y~RpNA__I~_;TuU8PsSX-FXqUYMReC#&sy^G(AumIL7Lp`I+f|L6mkiEK6=wyb z!u``__4daSl3vr?dID@5&+K~N7IRVL%J!a#`r9J|l<&gXI##J-G@U`A1})iJk;lfg z5fA*S$kJL6+Gb3g+2NzU52#jncd$9wYxMRBiVbft4G?bnyImAVu%=CRBA2t zL*JD)(GldQD;@mMpuYzrYO6yvBxEG}Y*xV$=_Yw)-~9<^-%eBdlgSEhpF0-6sz^FY z{;5Xl>1~28lDAdw&BgY7_91$%|3)U4Q|tSJn8cPpdEI5RZKTXy?zsM0H-nAq1-}M9 zLrep8@(f?pZkHOFh{q3I1umyZV3Aj&6Bp)NOe#K+?BY z!=s8d-9^Vlj(+aEjzE5$9iCFL!3@2{fFPmUQcRzLSZm`o`*vp$OgEAJ(hd(Hi+>v%6z1zGEiGwj&7V{3n@5=DRf~ z1VT|XKF9J&@kRl#ga4y1W12-qO&ysr@#F?AseB7YB0bxDG>mZvu$Z5c$MQ-J9~U@) z)g%0L3aa&Lx!=tSF(i}MJ1qNZJPtZHxV_?i04Br@mARdnsB@U^_>m)ggA9B)gH zR3b0B$Y&Zsbd3@&$uI6j+2D+SjG6NEO>X$|hEqk|fzZKiHEh0$yegv$mJP13=^I*c zIV?#{^JL`9&VG|O=^v`g#N}QbcsIun=aBv}1cf?C9;|dc$*aUqtGOPZPTcrGYDMuP zyU4lyrHmtXoRf>9pGdxSLX;clKLdu4c`+r&kAKGHXJ>HKHTmX|N!^~$WsoR!N>e*X zm?nG@pq@(}gn~G2p>Zana2eUNQ4?aQ?U25C-;2?F5rGqJmFA-NJ?=xrGPpYc8bpyG zyN~~dROu?k zvW@D-LUR_tx@kk>)PP8AAY1wLpxwk{Vv~3{pF4a$+xnAwj_==v!_Um(+fU|XIA`kv z(m&cHrOZ?7OV$J}BDx3FPE5@=v89JTldmzTW}OlEcxuWQ-?<%5wF;;0zU?-L&hYxpVZ>L-7y9Ij@w9K52+QF zj2{1tBoB)KTRWv7qiv6yPh-ms&j4L9+PF7uuUDdd-bZ$EuYn!5{oF@A!nqh;%XEBj zd2vqa&2?+-JQ;fM4a5*nBC{{Ltjy@lENRW+E=4C{nkD^?=Ig6MmVX6UszxaJq+4lj z@M(F#KomNvUjps$u*Yr4w$C%H3RrLjkGjRm9 z9fp5AMkWujt9Ks-!B*#{M8q;5&uukdAm8+>xz4}`&J44G-hw-(h^WBsL56r zj`9#kD%w$^xXnn-qxfA%3C@|@*2V(aYDwy>B^mHd#&r6>J}2CQr(+x?YrBFM1ZROa z?#R6IgSJjNoeSkw4sGgIM+*%Sd0#=A%~6wH;2B`Wl5NXdysGwjjNvXxp`Tq;C} zi?3?8*qo%_8{V{DsG|CHEiayb_1vku8AP2eS#|uwrz)T=1*h=!u1&jfw1u~p33{xS z?!8mY%Lh(0wsu`z)VjE(F1d+Ad02?MDvx+^&up61)gC{%g z;S%IY$UDO=kS+z}*-aB17Q^lPZ)9v|l+x!uje8=>8%|RY$#@49#w}u$j<3V8HVlC{ z9-CUIy;pYda9kYZ9Jk0lZ%kbJ>-3hJwv9TA-@fj3Rs1k4Yy1$3 z{^7i*)BO_N>yERW3|r=x30-7Yc(IuI2R=W+ITM;T=o5-pbJ-OS5O#YjPKdUfq}5WB zv2}G7XGTa9+bDCw4POvS5cC9A6nt^`PxG-y2w4j0 zK309rNjSFGqJL;G9f4cunvsm3&UL1?sv%N#1QmBlQ$h{BHGwKp$XGCj!{6+Zu4U48 zQVIx$Kr-l}N4d7c@r!@Xp$5<-MY6no06hXY%50E~q15J*#{c0QnWR^ko%xI^-`Spg zJoN73nIzw3B{AP-Q@^Tl`uS2{R4JQ_%_3lsjSXyi>i-q6uKGZp#>VfU5Na zzfM-c3<#_S$hQjzG)VIlQokitERDMMw6ax_7+*3Lp})J7P;-pPt3AG&b||Y60x{j( zZQr8LI1f79yoD4!_#5)6n2mu&)-3`glRzPaT+RpGzK4M=bi}6E&EaVNjG@zuHC1_1 zpFfIPC^k!;cjLr7#o?D5VjxzAlBfzwvuo7mj{(gIrNG$OY_OpX^2T@dA4~jsmC9VH zbfl>CAXVqxc^`2Pw9!-uSl{Jg7;;45c~?20ve|+S`VMONazPv{uI<|0Ca1;cH-?&hiI_~=>JwOZ4 zV#J8cGho7%?wv7wa2VB$M|1|<6aA16?|imrRN8+%W}?tT6B6##^l8j+AIFnJ@$u|v zR-kbL!6!Sb=aOdpQEk%iUr1R{-C(CThfw?cg4DY21}eahuju)}qj&lyJn*eFTd(yq zWu2R~Iemza2u;t?i4LNUan@44UsA(q;y}C(P1-X~w-ffeWHhB+WeJv@%JGO`&Er|% zsTY+7@aFMCeONroU^IgdCJOv~DmP7hgpiGjg)FSJVd25eblVwV{_lgc9nkeWVwS}! zE`^6?6qxSG5B$5KnbnNAQ`fBW%ZD6DHSJQMO9RT9>eh~MYB_D<)S>Q-EIcqe8m=(#1p@=4OnnG*#ABXjsAe&A;l zx`xjXoFG<-`H7 z0vIq1(t0@6QIfTNuDHfDBD4alceIGE7%eV|> z*qSH|pkq=&j$J#E^%UQg7Tkf84XaR={iT4Uw5C|?&Efr!y?)auCu*_G;w6j(T7!YC5>fbSQPB9Qw5^h)DXaU)OJY`s zC7&N*<9MAf6y$+G>RYDl@cOloOSr&rojzz4SJxLZ4>2IlEPFYsK`BxK!?>aQZ#tUz zlBIYW_&3a;Yhf;1yc=`Kxm~pmHHQKzf)dso^q54ACERSKZ<9CvATRi@mrbrP6n3w) z9g0-UDo?8YSv?^JDZV(NQvze(7UCuJ`Q3p6fAzq5Hg^8Ck^8@VgA4q{TYji+L~8~Xr$|2-C9<(*<* zM*);h-eB4*eHj-my&*y4t=}s_^Cv45uH(zMpX6wc6os42$$%xig1sud(XF5PLh(;q zPahNL(?X*d7|9-$!zx?7W2kNBX$cv);P&w+?48%1Vg%zpPQ|$1O*w)_RjkHz= z(fq7nrm12%J!j}I#pdS)x znp@NO)VWVuHZJX!JP##*$gC-$~6KZpNZNmo5 z{OQF$kN_zI?k4;$xsg&B0GLhcvdD^*$pFO~ZGaAh-HLz2DwzdSF8I62 zaF9^x`vQ}k`SSs$k&Y&n>rzm;(aGOyXK|AoVjE8HR7C)cj6vKKKkaq$n^biyH4=eB$JN_fV|U=$7pRE=NLB@8AVU3aCn-~c$=$IQ3IeG7Zqpi2Yh zvQ0Vj2UJW6j!gio%#H7_-hgXk-nYKbs+VjjH#(HmUcs~CQWp=%xL2I-Z>p#5)TC!+yOj$h&7NFOOwoTaOxS>7FG=G~X2Z*Nc8se(hYQ z+oC6$+u>|m-xLwMfk%8TJ+r67?szm^pyOW=KZob3JMkXoZpm$SAgAl~P9dFzGadoBYVB421&L~`62IV@WS z{>_yWe&&zn-U>T}?I4!Jj)t)%K}XzaM?L!0l0QM7+1RXi5sFe!CaFMssxsyQSb;<3 z>}^Y0-*UHxWfpmkl;@@vi`P9$?zax44lITOR^^2Q>%OG8Z*LqgI2ox5 z{%hMEbZ}*>mG2CfLhvr#5r1gX#dkIJCDp=XDrW6MXD4m|wA4XSgl3J2;de3_(Oz7_ zPq_Qt?~PfC7pq=};Hegr+Z44ye+0E!<}Ik6xP!FO>t4-rg@q{D15G|8EcDMa6MuyjNFBpGn6(+w9AJRCp+DBzc7?ZN*x>?Fg=0wE7 z$s_qJ<*Xb2rct5RNa)ExB#tie;~4wQQ$(T{v8r+7Ak^$P`O5-=pSZ}dHF3Y~pno}7 z(Ejb-oIh5lF!|BrPk(YY(M0eOUv6c~htZ8%4HbtEIM~(ye2wm|W&W`TG|>KS@*`)~7<6C<=LN@2^t~Sx0s=lnXh?8>k!P_Xd7tu>Ps|zszs*Lr z=CROHp5?wiUt|xc2hm{a+pNm4${ea}By>owS(+TlUu~@ifc1heYOCl2=5}Y4IhZ2M zgXcYqKANVh+VNwpKcRDXz1`IKgaLa&Ly*F7 zd>gb+W6d-xbMpMYkJ43t3T1r&a%wPgQwB7Tm=${p3ON7$${t30aI~O70obyrr_pq2 zb8A*KPL@Mgu|t^v`C^m@DJ&V?UMe(MsARA(0AqaXCx1cHUmz0_4PBJxA^iQu4%G)> zCT*{RBa{LEzYm7rcY=@S@;@WA2lXF1hc5l!>>)uw+RwiMd6^rt|6VlEd7xdPX&>=F D`tIgW literal 0 HcmV?d00001 diff --git a/demo/src/public/img/logo-stack.svg b/demo/src/public/img/logo-stack.svg new file mode 100644 index 0000000..d651733 --- /dev/null +++ b/demo/src/public/img/logo-stack.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/src/public/img/logo.svg b/demo/src/public/img/logo.svg new file mode 100644 index 0000000..4aa906e --- /dev/null +++ b/demo/src/public/img/logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/demo/src/public/img/ngb-logo.png b/demo/src/public/img/ngb-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..49997c96caca8b336dddf689fedcfcef4ce4ca66 GIT binary patch literal 6252 zcmZu#c|4Tg_rK5V24l}U86lH|EZK^&lqJj9LYqm}v=GS_&rp_1gvwfj7L`y`vdx4( z6pBv@MMl{pld>E08{hBu&)S!&Dm%{@9gl%oCTmV4u zy9j_o@|))SDsB7*6=3dQ4#3lN!L?&({ul~#v9Br<{Utcc0zNH(S_Qo7!O8c)zYPSngEQS=-=mnv z-%8si-+i0g8qsQ+`tQ-4Ps6n1d3??Ia2H2cu$Ra4li2<@y)mjVB@_T@OnOXRfs`_Tyn^G&BatXe?+{MxU^d<%Er z{@BMICyUCq=`HWYJadY>PCxa}c=+7Q!Zu3d+adj1?S6r3FWZ|!J}SqX9A3AU$e=p~ zHP3A2DO+LAI;>kyI8Ir5ZDlGU9Vi&zeKo@)2EGg_**2Z!w49remrSGi8aH;=a;I!j zPx7~2%U3)yy?tqq-+JHQ8lNZB))dnI_Dma`oyy1XP{elb!3G|Kg0<9nrR#KIHD-2d zmD9S4Y=-Rc{VeY(OFrP9WR+!!pO#yY@{W87zdLI#TW^jym5JCoJrP$_$n<{KQaPga zN`=n{-kNc@r8|1VN`5{ea|T>AaSJk+4?CJKmd6)+Z@PTXUgNEnpO0Spnt zHDu_7o}Cl9bt)-4RU(rata*s=zdzj^k(N!_%G;UW_UHJ;;SuJGcGax*a~@TzGRL%+ z(~b5y&KE__jmzd{r>ZVn>Qv6JWT?a+#_X!s&K`;P!hDO|M}BK&iP`0$h`&>FBsx|D zng2-SlGEsxhCOwre-9`s3*C9Qcg2W|oaNoq&s;OQ#aGi?F7osyWV>F_-jca?(eHbq z3g4Hi|224CX~W?Cd;@Ie_Fu5c^%Q|C-nzcBN#|UrXUf2{BWC*}5$=k`Xtv4n8I$d|x-Y5h*5N$g3}WKvvSw!T zw*g^a>&4?2eZiVjoIqrs-*@ii$3S-Z)uXhX@+63HVL zGDwkc(Y2{3Jw;YwDbtj4XO|qh54I5rer_hIN02@;ekZgLM&zW`L5uKrSu1}AVCkpS zliCTpZ8-uCXm}V?=os)y`xOi_JXLQi!EJ)i21t|J_9+41%~nS<{IGV9$Au|sPu`RN zBb9w1K!EiwK*V605_E2#^S?-{&12om32j1pGc4sRc+a{(y`iGy^`+BXCx6Vr2vWuT z>@(+xQzff^>HuX7?RC2ACbYDNdrr3=iB1HSKH+JmOkv<14kiwRni~y&FX@6j0`NaA zDg_Lb3>zU31JN9aGC7E@E{N43%wMVK=z%Jbp$mG$Ms%#;4ndy)?7v7(8oUYg)tKWz z_p@z6 z#K{iI=?IcIT>LW^Py#V$!x9;wG8ZQrZH@re2nv|^I7tZ~#sdk2o+`i@1m+3pfxZ17 z@4fbo7Ci_NK>v1DA$*nZS7Bo|QiSz<%jKKEVmk{18pjm?EdzHK#O9b>`^D1+gICGS zA6^K(S}9xue;G!~0k^}-n7C?gaLVA;#kt5f9BAI4dVoytw^y&6uFXvZ)lzK)p|EL| z0Tos|^w;Elt%FAbWD5(hRyJ0sSe2atSb|z{a$iPnfa-1J29gA<8@-UPgEibUUu$Nz zvg^WT+!f_}(!TxlGezKoD({F%rWkBTaS~+RGMYPe2>PWh zM>yH$_R5xw8D*pW1X)FXCKyp9=lV77K3oR`=q)FB5tLuDDEU}69mo;{2aJp|PckbFSXPXN(YL3J6lbQglN)ZU zD_irTsYus0t6&DiegdxW-q2*hN7TsCj8V+!Juti%1X`dl?8;7emTvrMo6}eKN-^&4 zLhdl3J}lTuf4z+oYIpi7`Uj(}lhRJ;*bgO~V|@*=(E>oX>lw1j1>UNIrjYruqch0J zdS?SQStkjrHv~)DL)zeiBhVYRwA{G^a1t~O$jYDz(UowJA_&}o)it3@$VzL#_ydt* zX@SXSAPcat&|@G7RyePqYEXF!I39T&ipTvQZ5Rm49dfL7a6 zq%I(>+z?~Ep<2(PM=_MO-Q6}s4KdIc!Ttx5DfU+K+#>iL-5?wIl`fAZjhd8Xe9}&l z4g5mS$FLc$jTFgR$)rg0tJ5rfNG9)Y1>ue6#psJttTw`7LD+Qp%5jrvYafgdI61%= zg(i}{h7`ZiRjGo2{yOSF^{KC&>tm52>xpGxUj)sSQnGi^3=ejl7Tv_(X-UrzD7bEI zQT@=yfkFX!i9TpnGN2f*gxSPr%eJ)%pP2g7h@d3ZZMzVb*oeQ*ROH)Pu?mV zw2m^rM#L2>YGZ3U!IU@V#Plg)6e~QDx^li@SB^fV(~nSU1i(PNJ(2OzU;OiemhWOA zcjzOc(qM)q7j{Vl8Sl%6e{olDKtMRePDlgoEyfwWJ5JYs!CS-3C1`*X1F&Z=MYmKB zn{dz}d|=Dh2gyJNB+0uCsg1Ps@=z2rk*;~n+I*n@($ep#h(Eatu9Tx0!XQu}vJM@; zr8Kn8dHGjk?FFCNQy`)m$eb2EYkLvH-inB5yfzzMQ}}!^Y;kc}6%Q9ez#moP-3?+Z z6825)&iyC#tNYSu8CD6KWE4U2A#h@@U>SjDX2-{c_k0_gZ5|uTp1FN%wg1EbbI>xE zev{}av3Q{t5nffIaYq(IIPMo6{w#l3;A^YXc}3o{BuwAXF|jvx ze$u;1ol)UU-Z@C!zuWZhedzPjh<}Y-`qZ%A9fdWGv~EKnWYAVnM{SM9bE>Q@|8*`d z)K9F+vy3{bUvp&nt83IR^Y`MDX!z06IQf_yI5C#?OnxL0lu4bnRXR!!{$`e3yaB$wh>2%Hl<_@9~Fdha6J6O77 zsc*AbMtH^=o@nOWbAC$?M(^DgyXD^H+cO8!A3t^Jdkk`<^$?UO*YN&3jb*8Tt@-}@ z;)(R6A+>>>V&s?)@@~g-%E}LH9`yK#|Bev_?Sz&$(OInDwUrKexbi&{Ud-kW);7=+ zy&b_@%Mpbm2VV(ve7R6k>!BqP<81_{OQFhX7K@-&!P`Wr0@Usp^|HF1h-eo6#79#Rn-Mb!&Wr*wt7~URncfl-_EIib$i&z{7RGUzy4+i(2Bq7Tg5Klu>pClb>(sK^&$DU zdHV^Fd((Y&zfB=IvDe=@WOpO4Tb&i7Az4nm9hp5ECTur`>+7mL4jQ9;r1q6JDC~`;}FKr|uEIuQM7J%Y0^#`w~OyjaNS?65-wY zO>6e}=R0%t$zS~03nf1g@E-H1-^RhEDS-Ov3K`CYdxIn8O1D1iUCIBI5?~hW0;vc0 zkwg6%!C1|YixERh|1!=deY8Sz3>J2ygsyizJqK4T7T8ioT@;fW&&Ff(=r4#9r#|0O z@^pa;#un2Mp8~TSihsA5HWFk6mJE4?#Ldh%slAb90`Sp{VC;eC(jx)ZbUkqzo_g!L zE$qR3EJh8LRbVFlUWF#n{Sk~kmF`+jIIh;N+vKffh)sEUM~6O^So>tbZIYxYnef_y z+>_>=lvcuDQQG?_<7@U9vNLpl$acbc_49==2aBCuTPGgA$w*A7Tcv8Xg(ngYaGMSe zcLb1L$!WVnTW#_qdz4ylAc?Dn+`S|Fr)T5O+a7!hF}&l1Y&tVl(4IBBuWJkb zRsSM0RGr{Gx?S>!mRu0pGlDiBy}MT}^(dSMiiD4KAw@+279_~iT@UK^gi+Oie&xnD~a`BHi*ygE6TV#;iOQn5Znd`0obWO`a zl+E8#PNMDY-ckU-yqP1^1pcaUz0LU-Heqpe2p-bIcPTZHu)w~j)RrctiT>vQ=BITk znpKJX0qPTcHchb^SC(R2MO4PQ1e$`sjJ1F%^neF+5lrs{8o}7cPYR52KxxK;LU}M; z(wrh*{Q4mDf|w%<@{nMMBFGy6DFqsj)G%yIK>C%;QLm(`tbQwkXlx7}q-Y>gOvO;U zkYO0oGaPAdpIw>sNFUt`63s=3jw1U6eU4!hK#DySc;D)>81Op^{-TqOMF%$M79i!e zZk`OtkYc?i@E;DId7^}PUrb)k?>k4vNCet7m4``Oo8=Xo#jgrNIbSn zfD^es?AX%X&ax#=6dz8&um)F)CU4Mx z7*qd6Y%1<=e!0(^*Ag@2JA6p-y9dPQbw(S$I(5X_}7 zJTn*Iyt%1!C{>JR;|Q(m#ap_LeJH7@PvNdrsCA*!eo{J*vk!&wO#Ou&@|hQnN7g>! zsC9-*x!_D}vt{qRC3k9?sSGpDYx0=|M6D5VDwI{ZcM@|io#W6Ap}l->?mLxnMJL{4 zT-~tUq3w{?C{7f*L4cC}oC%x167?W%#3{A@Au??{OFOsje9HyD0efO;jet=7`s1gD z%MFt6Sly3**l%a;BS?!#T%!!g9ih>8-XY+Yr^=H+d;QUKr8y8Am0!w=4%|(|i5korvuD*G9sK*b}0R zr&jR9kMoY~*Oj62zA50;;orO@D`~xlNC#2INnZEhFDp)Ki&7KzRO2ki1Q(c7#9Fpq5~8y^bV{(6d*? zLoY%FD(PS+HFYCqD%tpOqE#*F(gUPJah4LRl;G=^RO?&Ia3;1LZaVzn#(hh8;v8d7 zfsuBp8$WI`KFQiZCx{69pOmp57giqZ&jc4B3-kMea-ZnvpSoT5tl-DS)IU6RTSQwn zy~LR6qH!bv!^Y?8gt%c(Xx@}WGcqO<$_1zkWJ+nwyK}fo*$YQ4=!lCo7(RAy@7KYBRyCdSH{C;;&$$8j;Sbz05m+@eC+fnW_5B2q zSXR}_hx*z`W@otePvo`NQHCqtdJ$Pc$Ns?{T(@H7ugvP~=~!YJDPY<+b1&_RcUq!& z>9R&Y@swTBMXaWr7=vG@^97Fk>-LG_?^di|M2vk>ROBeGU4Cp<1gEj36>&xDoyMQ; zP+7GrKajV3;$KBbe2>VUU;qU5XXBStv z{`IU@({Qy1Gnc(MNG1Iasj=qX5lGG&Ts0`=rHOQ0LbR$lGuEq2wdL_DHS4ZX-;}B3 zjK06=*dE?OYUW^3B<)q6BK)&AWC_& z&Bx-$&+>k#Egex&Fpd6_ABCM_QVr|hkEDP35N>#NMT>|p6YPoGY(bxg*}YC=%1p9I znH$$=MN1rC_8O}``O@Uc5EdUg|6t+IH1bOkczq@%q~>7OitB0=J{fJ!2{x7_EeXpL zu3SQ(m@h1$o}p?H#`=9rD8c)yDP<BBE<~1hnDG*9E?ka59BcM^L129Zq2^FE#hX-M_%F;ChTA_n*Pc zGz_NlCChsz`Q@|C4XtGgQO(4*#wE|1CCvhwS23xxEnboZvHfM7w#0yBs@D3ORem zKj@?%DLg8~j}#OVejI?vf**52xX=S+@;j&Y4%<-|fK(RLJT54qEU`aJ5-CBlg7unx sk;+wWr1rj^ww8_-g%)f(Y{RhA1XI#t;d2)HH#RWX?sc>(x1c5bA7ldiK>z>% literal 0 HcmV?d00001 diff --git a/demo/src/public/img/ngb-logo.svg b/demo/src/public/img/ngb-logo.svg new file mode 100644 index 0000000..27478e0 --- /dev/null +++ b/demo/src/public/img/ngb-logo.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/demo/src/public/img/stackblitz-icon.svg b/demo/src/public/img/stackblitz-icon.svg new file mode 100644 index 0000000..8c32ca2 --- /dev/null +++ b/demo/src/public/img/stackblitz-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/demo/src/public/img/sunbird-logo.png b/demo/src/public/img/sunbird-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f29a234acc8f108b4e875c7ce17760a92fc4edd5 GIT binary patch literal 4410 zcmV-A5ykF_P)8NmJkdI9JLpaXyw015#Rz*+#y0Tcn43*ch_9|2eipwu~6 zh6D-Hgv*wZ0sO?wB-;Ym1;8NyjsWoMEUNzkz?<@!<(&H>Y5*ljkTpYVrXtx5z&QZ= z0q6~&evav!2VgpYvCg@-e1j=Lf?TK?El55>av8~T#*bAb-yyks()tOq)|1t7eUgJo zuHesyyGb?<>C*HiNRSCLcOyB5`GL<#9!9btVd@06VeFGhe$D(s8OfVTHcFT}LE6%p z*mKV>gnYnYOx?WI;YkYCWTn+>_)VNsc0UvM#i(c-V4{NhF)3nA(mf`Ar=$^ahd# zCM;a7{oAML_ilMgvy<=NlKfuLTS;5%ZL^cQcpQ_ETk#pTi zmdLp~@|2l7C{?wD=)R3qadkHEt%BgM4EQ z05>4;6fXhrjGU_{`~X?F=a2QG%F}y0faU-y0Nn1J>j~g}xz?f(KvM*Xt#P3Pf-MWm zqKI_Py-%_LWR|)>PQ4^_13)KK zg+r{#!@;-@fB{0|QUG_!rI7yeLI8WjOh*Hx$Szl%UJRgF9kff|+A_2-Lkj>G12_pl zYcX}zL$Jq48Gw}}Cjz+1IaiWTOs!}ciXi5u48Sz!T&Wc4uZn+?hvvDXM1TOm3(8a% zTCY}v^!a7rGw%eTv;17(oSQA@j{(q5&W{K1qmN0Iy46Ld{D}Y_2hb;G z#_kAUPesQxp`)qH>;y2g5I~n0+ix|1scCwht^_a^!Pa4McWP(nTu}yU#_j+{n@i&n z?CSItfD&0mbwm(Kd#aiSZzDOwIX5>?)Quw%cxfvDe4vVGtHu)m%Ay7#$?X8VY=q%i z06IG7mc;Z51J&^ib-b&zJpoJu$!}wt+ABzo3=QFFPU}5M&a4Bq<{p8%~iq_X;osg=IGtK*j*eOPH-G}Nqqtx}wNS2BTvISyv zKQAQNFkSso5#C2aQR77a3>#I`^Qn%pwmPLc*81MP`H($Bay!KsKZ*~cfpUGXCV4^{ z^&IEnn-wIR#WlN_XZ=pjJMm!zP89$J%I=tJR5<50q^mzF6W$a6j{>NSv2Pn@YTz)( z^w>u&w60we^ZTTWPVxbO+0MCg02cw+QO>>UoO>aSzIs{I!$o2~n=fkCx|rzR+x(s_ z-(8iJZdAzqaY^V!=~y=+q;ym90R5~ITG8{mx&MNwRlj_ns=QYlNuFw?O{C=@l~o~I zi-}P!ZXwxDLTgF#%XAWp&HHXmvWuA1;+$E|H|-yWCJ&U~YeO0e5bVz!iw!?h=`RZ~ zIOo;@xFW_jT9Yn40vk1WAIY0F%_Q5}~7 z*dUt{Hol6vx+%Lskw1hL}3aNq$VSw<&jsBshf9`EXBNN0Iza zY}Jo>^wW*9(3uoLOBa$wq6hah*KU^CaXQJ4>U=ZV-XX-U>Ubc@{YXAXa(I-aR?OK@ z0)#FPrKKrXKyn|FeLYfr2a=uAnQ7GX-Y{#k79?BAHgM%6chonHi-~F0XOsFQBe^D3Z?N-3ZBhsVQe?sA|!ha;`DSf0^0s`V_i(SCSW! ze9J@EbXj99k#)+kzUeP6d?ZHrY}Af+BJpyC3E2!VHRT>}ros^4hEmf==6fUPX{73H zWv*QnVS3ZpTcp`N9zVd<>c0LF&;NwMruAdE>Bd4k8R=PH26Kqz9tqow!5)K}p-(gQ zuGJYPrfr9k{GTejM#_hHh2%wYJUKKCTZV}fgL1IS9hpOVF3E+VUs+=YdYn_0B*!Y| zA03h2w%F8tyt#gG4VDT@Tyr;1-785Rr|$FZQ8kysoBF-ZNWtgL^G2HBrmh|xcBIK>Z$$D<#VDbb_?U zZ6DfqkpLh)8(wOj-Otpqzo+ev^z=U&>?EriUwdv+_CRddti=sNO zKFPacmeT#Q)LXaBk=hY5KswKC#$XqyGLoOADY1`@NGDrQ@&#c~otbm3sY?r%86?|z z2H8|q=55KX3$0518SG?5lCwn@$2?mP&eai%##=q_KUbvx7nY&b zf>G1ASX)mFL)*23w^If%w;A6)y>jbJncA#+yOMk|OFC{%H>Z$hPwWh&`h{j%UO@76 zxwflsK`7Unn87f{H?x^C>y%U*PjY852QoDIDZ2EZIu%DS<95cOWvw(iy!0twbe(4* zR~asD)nHo_stb}*b&F3;RnHyg(ZOA+&e%Ridvu6t^!yi!XX@@jm7Q&vy0NGq8*B0W z4J7+yFqoF==`VwrlwQWv_?CL_3k^-p)qMw>#m|i%W@)Na3YuwnPqBfouofnnZ8RoE z^h0-JQqKq@{9|hv^S%_ehnK*GE zfLDxX%qh=QUy@T1Y)~^zM)q)>@&sVLbM7$!hXPob;q#}d7=U!3a}SF$xUazWhdSO% z9WRm?58Xc6v%1(5C(;@I?4K8%bDsd%4pnJv%OgB(fMla+52YM;0T5bU#z?<3BKZ)2 zyGXH9I|SPzZxXsdmE$Rn zqsF#lo@Cu%dyAXYNp#0)%7{1 z&eLROdRg4)>x@aQ3(3R9g0d{)-G&<*Zm2R$RtxI!=AND&+7DF=*0+tl`A1{U`;tL| zu(qGyA36hhbuXj;&30%l7Nm!IbnpozPn3mBf#~$RFbMdc=5b8Ck-~}*E^dnD z(_0u@GtrkMmzxEajXBrWfD@H#zEm7rpL^P49LbYJuyi7Mr5T77X6>`D@nDvc?4cZU zQR}aO8>{85`cSH=XGWFRQVIi7p?bjq9@RVDnf_S3b{1qf06hWW`&J|R9(2?Ao)wjp8 z&J4{M`aLQ<1LYBtd#Lu9@9E28B}BfHStX^NI*kQ*r3kpWs_t@mf7=gMfK7=qJ-rzd zLq9?CIw7P|7-m$4b=)Kil|n;%YmWphVO8vGZS3DpZ1l@Apl>b7vGn|BqUd>FiPTvp zvsFjEKo;p)ES>x{$-h$enUnWkAWZlXJ^Rm*d{qsCHR47dpn`^85zE3dxxQW`-a?X( zQ6k|yJTW3-a4#0-Zzz&H#K)qFq|?slq!`HaN$x0?nkdzDCCR^s)+FBpdNB9f3Vb16 zq#`vEN3eD|gl~w7akiMN_7*0xQ}Wr_MEj}Z{X{}c7YSHlB+62f|B;UETSM@FV+Ltv zJv^Q|D0_9!2*&Cq0Ybff)48Y+capq;Y8wSpR}*Qs!J>B`%OH+BGKoM2(}N5)w4W+^ z`870g+u{V7Co^$c`J^H^ZWW%}C#6thz#!03^t(o$&wJ6-5@K{M#v48 z1Na6(R&>L^~8ln(nbr@X?tZQDL|0%-$vT> zTj$(jPx+1rvds$tEC4V!4DR>7mys&Bar4Af=5WXUOud`GL@mkjm^eWyxEwOF&uzOuf`IqE5CpfkM3B+Y z96^>s1%g2F)$;icz^4d;%HMR(m87aaK>~~a1MLe+0`63;jsO4v07*qoM6N<$f)bfT Ao&W#< literal 0 HcmV?d00001 diff --git a/demo/src/public/index.html b/demo/src/public/index.html new file mode 100644 index 0000000..6ce5b2f --- /dev/null +++ b/demo/src/public/index.html @@ -0,0 +1,33 @@ + + + + + + + Sunbird UI Components + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/demo/src/style/app.scss b/demo/src/style/app.scss new file mode 100644 index 0000000..ea736e7 --- /dev/null +++ b/demo/src/style/app.scss @@ -0,0 +1,405 @@ +// styles in src/style directory are applied to the whole page + +header.navbar { + background: #0d9474; + background: -moz-linear-gradient(45deg, #0d9474 0%, #2a6fa8 100%); + background: -o-linear-gradient(45deg, #0d9474 0%, #2a6fa8 100%); + background: -webkit-linear-gradient(45deg, #0d9474 0%, #2a6fa8 100%); + background: linear-gradient(45deg, #0d9474 0%, #2a6fa8 100%); +} + +.masthead-followup { + img { + width: 64px; + height: 64px; + } +} + +.footer { + padding: 3rem 0; + font-size: 0.85rem; + text-align: left; + + p { + margin-bottom: 0; + } + + a { + font-weight: 500; + color: #000; + text-decoration: underline; + } +} + +.social-buttons { + svg { + margin-left: 1rem; + fill: #fff; + } +} + +.sidebar-collapsed { + margin-left: -15px; + margin-right: -15px; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + cursor: pointer; +} + +.sidebar { + position: sticky; + top: 1rem; +} + +header.title { + margin-left: -15px; + margin-right: -15px; + position: relative; + + .content-tabset { + position: absolute; + bottom: 0; + left: 0; + right: 0; + + .nav-link { + border-style: solid; + border-width: 3px 1px 1px; + border-radius: 3px 3px 0 0; + } + + .nav-link:not(.active):hover { + border-color: transparent; + } + + .active { + border-top-color: #28a745; + } + + .navigation-dropdown { + cursor: pointer; + + .nav-link { + padding-right: 0.3rem; + line-height: 1em; + border-color: transparent; + background: none; + color: #007bff; + &:hover { + color: #0056b3; + } + } + + .dropdown-toggle:after { + display: none; + } + + .dropdown-menu { + border-color: #d9d9d9; + } + + svg { + width: 22px; + height: 22px; + } + + &.show { + .nav-link { + color: #0056b3; + } + } + } + } +} + +.toc { + margin-left: -15px; + margin-right: -15px; + font-size: 1.1rem; + + .toc-item { + .toc-link { + display: block; + padding: 0.25rem 1.5rem; + color: rgba(0, 0, 0, 0.65); + } + + .nav > li > a { + font-size: 0.85em; + + &:hover, &:focus { + text-decoration: none; + color:#0275d8; + } + } + + .nav > li.active > a { + font-weight: 500; + color: rgba(0, 0, 0, 0.85); + } + + &.active > .toc-link { + font-weight: 500; + color: rgba(0, 0, 0, 0.85); + } + } +} + +.contextual-nav .nav { + position: sticky; + top: 0; + + a { + color: inherit; + font-size: 90%; + padding: .25rem 1rem; + } + + a:hover { + color: #0275d8; + } +} + +.deprecated { + h3 { + text-decoration: line-through; + } + + h5 { + display: inline-block; + } + + td.label-cell code, + p.signature, + code.selector, + code.export-as { + text-decoration: line-through; + } + + .description, + .meta, + .lead { + opacity: 0.5; + } +} + + +div.api-doc-component, .overview { + margin-bottom: 3rem; + + h2, + h3 { + .github-link { + transition: opacity 0.5s; + opacity: 0.3; + margin-left: .5rem; + } + + &:hover { + .github-link { + opacity: 1; + } + & > .title-fragment { + opacity: 1; + } + } + } + + section, ngbd-overview-section { + margin-top: 3rem; + h4 { + margin-top: 2rem; + margin-bottom: 1rem; + } + + .meta { + font-size: 0.8rem; + margin-bottom: 1rem; + > div { + margin-bottom: 0.5rem; + } + } + } +} + +ngbd-page-header { + margin-top: 3rem; + + h2 { + &:hover { + & > .title-fragment { + opacity: 1; + } + } + } +} + +a.title-fragment { + opacity: 0; + transition: opacity 125ms ease; + line-height: inherit; + position: absolute; + margin-left: -1.2em; + padding-right: 0.5em; + + & > img { + width: 1em; + height: 1em; + } +} + +div.component-demo { + margin-bottom: 3rem; + h2 { + display: flex; + margin-bottom: 1rem; + + span { + flex-grow: 1; + } + + .stackblitz, .toggle-code { + display: flex; + align-items: center; + align-self: center; + } + + .toggle-code svg { + vertical-align: middle; + fill: #28a745; + } + + .stackblitz .stackblitz-icon { + height: 1.2rem; + margin-left: -0.5rem; + } + + &:hover { + & > .title-fragment { + opacity: 1; + } + } + } + + .tabset-code { + ngb-tabset { + .nav { + padding: 0.5rem 1.25rem 0; + font-size: 80%; + + .nav-link.active { + background-color: #f5f2f0; + border-bottom: 1px solid #f5f2f0; + } + + .nav-link:not(.active) { + color: #999; + &:hover { + color: #666; + } + } + } + + .nav.nav-pills { + border-right: 1px solid #dee2e6; + padding-left: .75rem; + padding-right: 0; + + .nav-link.active { + color: #666; + background-color: #f5f5f5; + border: 1px solid #dee2e6; + border-right-color: #f5f5f5; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + margin-right: -1px; + } + + & + .tab-content { + flex: 1; + overflow: hidden; + } + } + + pre { + margin: 0; + max-height: 500px; + overflow: auto; + } + } + } +} + +.examples-legend { + font-size: 80%; +} + +ngbd-api-docs, +ngbd-api-docs-class, +ngbd-api-docs-config { + display: block; + + &:not(:first-child) { + margin-top: 3rem; + border-top: 1px solid #999; + padding-top: 1rem; + } +} + +.overview { + .alert { + border-left-width: 5px; + border-radius: 0; + padding-left: 0.5rem; + padding-right: 0.5rem; + } +} + +// override prism theme background color to inline it with bootstrap colors +code[class*='language-'], +pre[class*='language-'], ngb-alert { + background-color: #f5f5f5; // same as bootstrap card header + border-radius: 3px; +} + +span.token.tag { + font-size: 1em; + padding: 0; +} + +// Right-To-Left layout for the Islamic Calendars +ngb-datepicker.rtl { + direction: rtl; +} + +ngb-datepicker.rtl ngb-datepicker-navigation-select select.custom-select { + background-position: left 0.25rem center; +} + +ngb-datepicker.rtl .ngb-dp-arrow.right .ngb-dp-navigation-chevron { + transform: rotate(-135deg); + margin: 0 0 0 0.25rem; +} + +ngb-datepicker.rtl .ngb-dp-navigation-chevron { + transform: rotate(45deg); + margin: 0 0.25rem 0 0; +} + +ngb-datepicker.hebrew { + + .ngb-dp-day { + width: 2.75rem; + height: 2.75rem; + line-height: 1rem; + } + + .ngb-dp-weekday { + width: 2.75rem; + } +} + +ngb-carousel { + .carousel-item img { + width: 100%; + } +} diff --git a/demo/src/style/demos.css b/demo/src/style/demos.css new file mode 100644 index 0000000..d8dc584 --- /dev/null +++ b/demo/src/style/demos.css @@ -0,0 +1,57 @@ +/* Datepicker popup icon */ + +button.calendar, button.calendar:active { + width: 2.75rem; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACIAAAAcCAYAAAAEN20fAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAOxAAADsQBlSsOGwAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAEUSURBVEiJ7ZQxToVAEIY/YCHGxN6XGOIpnpaEsBSeQC9ArZbm9TZ6ADyBNzAhQGGl8Riv4BLAWAgmkpBYkH1b8FWT2WK/zJ8ZJ4qiI6XUI3ANnGKWBnht2/ZBDRK3hgVGNsCd7/ui+JkEIrKtqurLpEWaphd933+IyI3LEIdpCYCiKD6HcuOa/nwOa0ScJEnk0BJg0UTUWJRl6RxCYEzEmomsIlPU3IPW+grIAbquy+q6fluy/28RIBeRMwDXdXMgXLj/B2uimRXpui4D9sBeRLKl+1N+L+t6RwbWrZliTTTr1oxYtzVWiTQAcRxvTX+eJMnlUDaO1vpZRO5NS0x48sIwfPc87xg4B04MCzQi8hIEwe4bl1DnFMCN2zsAAAAASUVORK5CYII=') !important; + background-repeat: no-repeat; + background-size: 23px; + background-position: center; +} + +/* Sortable table demo */ + +th[sortable] { + cursor: pointer; + user-select: none; + -webkit-user-select: none; +} + +th[sortable].desc:before, th[sortable].asc:before { + content: ''; + display: block; + background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAmxJREFUeAHtmksrRVEUx72fH8CIGQNJkpGUUmakDEiZSJRIZsRQmCkTJRmZmJgQE0kpX0D5DJKJgff7v+ru2u3O3vvc67TOvsdatdrnnP1Y///v7HvvubdbUiIhBISAEBACQkAICAEhIAQ4CXSh2DnyDfmCPEG2Iv9F9MPlM/LHyAecdyMzHYNwR3fdNK/OH9HXl1UCozD24TCvILxizEDWIEzA0FcM8woCgRrJCoS5PIwrANQSMAJX1LEI9bqpQo4JYNFFKRSvIgsxHDVnqZgIkPnNBM0rIGtYk9YOOsqgbgepRCfdbmFtqhFkVEDVPjJp0+Z6e6hRHhqBKgg6ZDCvYBygVmUoEGoh5JTRvIJwhJo1aUOoh4CLPMyvxxi7EWOMgnCGsXXI1GIXlZUYX7ucU+kbR8NW8lh3O7cue0Pk32MKndfUxQFAwxdirk3fHappAnc0oqDPzDfGTBrCfHP04dM4oTV8cxr0SVzH9FF07xD3ib6xCDE+M+aUcVygtWzzbtGX2rPBrEUYfecfQkaFzYi6HjVnGBdtL7epqAlc1+jRdAap74RrnPc4BCijttY2tRcdN0g17w7HqZrXhdJTYAuS3hd8z+vKgK3V1zWPae0mZDMykadBn1hTQBLnZNwVrJpSe/NwEeDsEwCctEOsJTsgxLvCqUl2ACftEGvJDgjxrnBqkh3ASTvEWrIDQrwrnJpkB3DSDrGW7IAQ7wqnJtkBnLRztejXXVu4+mxz/nQ9jR1w5VB86ejLTFcnnDwhzV+F6T+CHZlx6THSjn76eyyBIOPHyDakhBAQAkJACAgBISAEhIAQYCLwC8JxpAmsEGt6AAAAAElFTkSuQmCC') no-repeat; + background-size: 22px; + width: 22px; + height: 22px; + float: left; + margin-left: -22px; +} + +th[sortable].desc:before { + transform: rotate(180deg); + -ms-transform: rotate(180deg); +} + +/* Filtering table demo */ +ngbd-table-filtering span.ngb-highlight { + background-color: yellow; +} + +/* Complete table demo */ +ngbd-table-complete span.ngb-highlight { + background-color: yellow; +} + +ngb-carousel .picsum-img-wrapper { + position: relative; + height: 0; + padding-top: 55%; /* Keep ratio for 900x500 images */ +} + +ngb-carousel .picsum-img-wrapper>img { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; +} diff --git a/demo/tsconfig.json b/demo/tsconfig.json new file mode 100644 index 0000000..fb7a0ca --- /dev/null +++ b/demo/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "baseUrl": "./", + "outDir": "temp", + "lib": ["es2017", "dom"], + "paths": { + "@ng-bootstrap/ng-bootstrap": ["../src/index"] + } + }, + "include": [ + "./src/**/*.ts" + ] +} diff --git a/demo/tslint.json b/demo/tslint.json new file mode 100644 index 0000000..ec365f1 --- /dev/null +++ b/demo/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": "../tslint.json" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a24dfbd --- /dev/null +++ b/package.json @@ -0,0 +1,119 @@ +{ + "name": "sunbird-ui-components", + "version": "1.0.0", + "description": "Sunbird UI Components", + "author": "https://github.com/ng-bootstrap/ng-bootstrap/graphs/contributors", + "engines": { + "node": ">=10.9", + "yarn": ">=1.3.0 <2.0.0" + }, + "scripts": { + "preinstall": "", + "build": "yarn ngb:build && yarn demo:build", + "test": "yarn check-format && yarn ngb:lint && yarn ngb:test", + "tdd": "yarn ngb:tdd", + "e2e": "yarn e2e-app:lint && yarn ngb:e2e", + "demo": "yarn demo:docs && yarn demo:stackblitzes && yarn demo:serve", + "ssr": "yarn ssr-app:lint && yarn ssr-app:build && yarn ssr-app:e2e", + "changelog": "conventional-changelog --preset angular --infile CHANGELOG.md --same-file --release-count 1", + "saucelabs:ie": "ng test ng-bootstrap --configuration ie --karma-config src/karma-ie.sauce.conf.js --source-map false --progress false", + "scripts:tdd": "ts-node-dev --respawn --project misc/tsconfig.json node_modules/jasmine/bin/jasmine misc/*.spec.ts", + "scripts:test": "ts-node --project misc/tsconfig.json node_modules/jasmine/bin/jasmine misc/*.spec.ts", + "ngb:static": "ts-node --project misc/tsconfig.json misc/copy-static-files.ts", + "ngb:lint": "ng lint ng-bootstrap", + "ngb:test": "ng test ng-bootstrap --code-coverage --source-map true --progress false --watch false", + "ngb:tdd": "ng test ng-bootstrap --source-map false", + "ngb:e2e": "ng e2e e2e-app", + "ngb:e2e-noserve": "ng e2e e2e-app -c noserve", + "ngb:build": "ng build ng-bootstrap --prod && yarn ngb:static", + "demo:serve": "ng serve demo --host 0.0.0.0", + "demo:docs": "ts-node --project misc/tsconfig.json misc/generate-docs.ts", + "demo:stackblitzes": "ts-node --project misc/tsconfig.json misc/generate-stackblitzes.ts", + "demo:lint": "ng lint demo", + "demo:build": "yarn demo:lint && yarn demo:docs && yarn demo:stackblitzes && ng build demo --prod", + "demo:deploy": "yarn demo:build && yarn demo:push", + "demo:publish": "gh-pages --dist demo/dist --branch master --repo https://github.com/ng-bootstrap/ng-bootstrap.github.io.git", + "ssr-app:lint": "ng lint ssr-app", + "ssr-app:serve": "ng serve ssr-app --host 0.0.0.0", + "ssr-app:serve-express": "node ssr-app/dist/server", + "ssr-app:e2e": "concurrently --success first --kill-others --names \"express,protractor\" \"yarn ssr-app:serve-express\" \"ng e2e ssr-app\"", + "ssr-app:build": "ng build ssr-app --prod && ng run ssr-app:server:production && yarn ssr-app:build-server", + "ssr-app:build-server": "webpack --config ssr-app/webpack.server.config.js --colors", + "ci": "yarn test && yarn e2e && yarn build --progress false && yarn ssr" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Sunbird-Ed/sunbird-ui-components.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/Sunbird-Ed/sunbird-ui-components/issues" + }, + "homepage": "https://github.com/Sunbird-Ed/sunbird-ui-components#readme", + "dependencies": {}, + "devDependencies": { + "@angular-devkit/build-angular": "~0.800.0", + "@angular-devkit/build-ng-packagr": "~0.800.0", + "@angular/animations": "8.0.0", + "@angular/cli": "^8.0.0", + "@angular/common": "8.0.0", + "@angular/compiler": "8.0.0", + "@angular/compiler-cli": "8.0.0", + "@angular/core": "8.0.0", + "@angular/forms": "8.0.0", + "@angular/platform-browser": "8.0.0", + "@angular/platform-browser-dynamic": "8.0.0", + "@angular/platform-server": "8.0.0", + "@angular/router": "8.0.0", + "@nguniversal/express-engine": "8.0.0-rc.1", + "@nguniversal/module-map-ngfactory-loader": "8.0.0-rc.1", + "@types/express": "^4.16.1", + "@types/fs-extra": "^7.0.0", + "@types/glob": "^7.1.1", + "@types/he": "^1.1.0", + "@types/jasmine": "~3.3.8", + "@types/jasminewd2": "~2.0.3", + "@types/marked": "^0.6.1", + "@types/node": "~10.9.0", + "@types/prismjs": "1.16.0", + "bootstrap": "4.3.1", + "clang-format": "1.0.35", + "concurrently": "^4.1.0", + "conventional-changelog-cli": "^2.0.12", + "core-js": "^2", + "ejs": "2.6.1", + "express": "^4.16.4", + "fs-extra": "^8.0.0", + "gh-pages": "^2.0.1", + "glob": "^7.1.1", + "gulp": "^3.9.1", + "gulp-clang-format": "1.0.23", + "jasmine": "~3.4.0", + "jasmine-core": "~3.4.0", + "jasmine-spec-reporter": "~4.2.1", + "karma": "~4.1.0", + "karma-chrome-launcher": "~2.2.0", + "karma-coverage-istanbul-reporter": "~2.0.1", + "karma-firefox-launcher": "^1.0.0", + "karma-jasmine": "~2.0.1", + "karma-sauce-launcher": "^2.0.2", + "marked": "^0.6.1", + "ng-packagr": "^5.2.0", + "ngx-build-plus": "^8.0.0-rc.3.0.1", + "nyc": "14.1.1", + "prismjs": "1.16.0", + "protractor": "~5.4.0", + "rxjs": "6.4.0", + "ts-loader": "^6.0.1", + "ts-node": "^8.2.0", + "ts-node-dev": "^1.0.0-pre.30", + "tsickle": "^0.35.0", + "tslib": "^1.9.0", + "tslint": "^5.16.0", + "tslint-jasmine-rules": "^1.3.2", + "typescript": "~3.4.3", + "webpack": "^4.29.5", + "webpack-cli": "^3.2.3", + "zone.js": "~0.9.1" + } +} diff --git a/src/accordion/accordion-config.spec.ts b/src/accordion/accordion-config.spec.ts new file mode 100644 index 0000000..3245062 --- /dev/null +++ b/src/accordion/accordion-config.spec.ts @@ -0,0 +1,10 @@ +import {NgbAccordionConfig} from './accordion-config'; + +describe('ngb-accordion-config', () => { + it('should have sensible default values', () => { + const config = new NgbAccordionConfig(); + + expect(config.closeOthers).toBe(false); + expect(config.type).toBeUndefined(); + }); +}); diff --git a/src/accordion/accordion-config.ts b/src/accordion/accordion-config.ts new file mode 100644 index 0000000..ae9c244 --- /dev/null +++ b/src/accordion/accordion-config.ts @@ -0,0 +1,13 @@ +import {Injectable} from '@angular/core'; + +/** + * A configuration service for the [NgbAccordion](#/components/accordion/api#NgbAccordion) component. + * + * You can inject this service, typically in your root component, and customize its properties + * to provide default values for all accordions used in the application. + */ +@Injectable({providedIn: 'root'}) +export class NgbAccordionConfig { + closeOthers = false; + type: string; +} diff --git a/src/accordion/accordion.module.ts b/src/accordion/accordion.module.ts new file mode 100644 index 0000000..cb51d34 --- /dev/null +++ b/src/accordion/accordion.module.ts @@ -0,0 +1,23 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; + +import {NgbAccordion, NgbPanel, NgbPanelTitle, NgbPanelContent, NgbPanelHeader, NgbPanelToggle} from './accordion'; + +export { + NgbAccordion, + NgbPanel, + NgbPanelTitle, + NgbPanelContent, + NgbPanelChangeEvent, + NgbPanelHeader, + NgbPanelHeaderContext, + NgbPanelToggle +} from './accordion'; +export {NgbAccordionConfig} from './accordion-config'; + +const NGB_ACCORDION_DIRECTIVES = + [NgbAccordion, NgbPanel, NgbPanelTitle, NgbPanelContent, NgbPanelHeader, NgbPanelToggle]; + +@NgModule({declarations: NGB_ACCORDION_DIRECTIVES, exports: NGB_ACCORDION_DIRECTIVES, imports: [CommonModule]}) +export class NgbAccordionModule { +} diff --git a/src/accordion/accordion.spec.ts b/src/accordion/accordion.spec.ts new file mode 100644 index 0000000..0d6dc5c --- /dev/null +++ b/src/accordion/accordion.spec.ts @@ -0,0 +1,844 @@ +import {TestBed, ComponentFixture, inject} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {createGenericTestComponent} from '../test/common'; + +import {Component} from '@angular/core'; + +import {NgbAccordionModule, NgbPanelChangeEvent, NgbAccordionConfig, NgbAccordion} from './accordion.module'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +function getPanels(element: HTMLElement): HTMLDivElement[] { + return Array.from(element.querySelectorAll('.card > .card-header')); +} + +function getPanelsContent(element: HTMLElement): HTMLDivElement[] { + return Array.from(element.querySelectorAll('.card > .collapse')); +} + +function getPanelsTitle(element: HTMLElement): HTMLButtonElement[] { + return Array.from(element.querySelectorAll('.card > .card-header button')); +} + +function getButton(element: HTMLElement, index: number): HTMLButtonElement { + return element.querySelectorAll('button[type="button"]')[index]; +} + +function expectOpenPanels(nativeEl: HTMLElement, openPanelsDef: boolean[]) { + const noOfOpenPanels = openPanelsDef.reduce((soFar, def) => def ? soFar + 1 : soFar, 0); + const panels = getPanels(nativeEl); + expect(panels.length).toBe(openPanelsDef.length); + + const panelsTitles = getPanelsTitle(nativeEl); + const result = panelsTitles.map((titleEl: HTMLButtonElement) => { + const isAriaExpanded = titleEl.getAttribute('aria-expanded') === 'true'; + const isCSSCollapsed = titleEl.classList.contains('collapsed'); + return isAriaExpanded === !isCSSCollapsed ? isAriaExpanded : fail('inconsistent state'); + }); + + const panelContents = getPanelsContent(nativeEl); + panelContents.forEach( + (panelContent: HTMLDivElement) => { expect(panelContent.classList.contains('show')).toBeTruthy(); }); + + expect(panelContents.length).toBe(noOfOpenPanels); + expect(result).toEqual(openPanelsDef); +} + +describe('ngb-accordion', () => { + let html = ` + + + {{panel.title}} + {{panel.content}} + + + + `; + + beforeEach(() => { + TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbAccordionModule]}); + TestBed.overrideComponent(TestComponent, {set: {template: html}}); + }); + + it('should initialize inputs with default values', () => { + const defaultConfig = new NgbAccordionConfig(); + const accordionCmp = new NgbAccordion(defaultConfig); + expect(accordionCmp.type).toBe(defaultConfig.type); + expect(accordionCmp.closeOtherPanels).toBe(defaultConfig.closeOthers); + }); + + it('should have no open panels', () => { + const fixture = TestBed.createComponent(TestComponent); + const accordionEl = fixture.nativeElement.children[0]; + const el = fixture.nativeElement; + fixture.detectChanges(); + expectOpenPanels(el, [false, false, false]); + expect(accordionEl.getAttribute('role')).toBe('tablist'); + expect(accordionEl.getAttribute('aria-multiselectable')).toBe('true'); + }); + + it('should have proper css classes', () => { + const fixture = TestBed.createComponent(TestComponent); + const accordion = fixture.debugElement.query(By.directive(NgbAccordion)); + expect(accordion.nativeElement).toHaveCssClass('accordion'); + }); + + it('should toggle panels based on "activeIds" values', () => { + const fixture = TestBed.createComponent(TestComponent); + const tc = fixture.componentInstance; + const el = fixture.nativeElement; + // as array + tc.activeIds = ['one', 'two']; + fixture.detectChanges(); + expectOpenPanels(el, [true, true, false]); + + tc.activeIds = ['two', 'three']; + fixture.detectChanges(); + expectOpenPanels(el, [false, true, true]); + + tc.activeIds = []; + fixture.detectChanges(); + expectOpenPanels(el, [false, false, false]); + + tc.activeIds = ['wrong id', 'one']; + fixture.detectChanges(); + expectOpenPanels(el, [true, false, false]); + + // as string + tc.activeIds = 'one'; + fixture.detectChanges(); + expectOpenPanels(el, [true, false, false]); + + tc.activeIds = 'two, three'; + fixture.detectChanges(); + expectOpenPanels(el, [false, true, true]); + + tc.activeIds = ''; + fixture.detectChanges(); + expectOpenPanels(el, [false, false, false]); + + tc.activeIds = 'wrong id,one'; + fixture.detectChanges(); + expectOpenPanels(el, [true, false, false]); + }); + + + it('should toggle panels independently', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const el = fixture.nativeElement; + + getButton(el, 1).click(); + fixture.detectChanges(); + expectOpenPanels(el, [false, true, false]); + + getButton(el, 0).click(); + fixture.detectChanges(); + expectOpenPanels(el, [true, true, false]); + + getButton(el, 1).click(); + fixture.detectChanges(); + expectOpenPanels(el, [true, false, false]); + + getButton(el, 2).click(); + fixture.detectChanges(); + + expectOpenPanels(el, [true, false, true]); + + getButton(el, 0).click(); + fixture.detectChanges(); + expectOpenPanels(el, [false, false, true]); + + getButton(el, 2).click(); + fixture.detectChanges(); + expectOpenPanels(el, [false, false, false]); + }); + + it('should allow only one panel to be active with "closeOthers" flag', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const tc = fixture.componentInstance; + const el = fixture.nativeElement; + + tc.closeOthers = true; + fixture.detectChanges(); + expect(el.children[0].getAttribute('aria-multiselectable')).toBe('false'); + + getButton(el, 0).click(); + fixture.detectChanges(); + expectOpenPanels(el, [true, false, false]); + + getButton(el, 1).click(); + fixture.detectChanges(); + expectOpenPanels(el, [false, true, false]); + }); + + it('should update the activeIds after closeOthers is set to true', () => { + const fixture = TestBed.createComponent(TestComponent); + const tc = fixture.componentInstance; + const el = fixture.nativeElement; + + tc.activeIds = 'one,two,three'; + fixture.detectChanges(); + expectOpenPanels(el, [true, true, true]); + + tc.closeOthers = true; + fixture.detectChanges(); + expectOpenPanels(el, [true, false, false]); + + tc.closeOthers = false; + fixture.detectChanges(); + expectOpenPanels(el, [true, false, false]); + }); + + it('should have the appropriate heading', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + + const titles = getPanelsTitle(compiled); + expect(titles.length).not.toBe(0); + + titles.forEach((title: HTMLElement, idx: number) => { expect(title.textContent.trim()).toBe(`Panel ${idx + 1}`); }); + }); + + it('can use a title without template', () => { + const testHtml = ` + + + {{panels[0].content}} + + + `; + const fixture = createTestComponent(testHtml); + + fixture.detectChanges(); + + const title: HTMLElement = getPanelsTitle(fixture.nativeElement)[0]; + expect(title.textContent.trim()).toBe('Panel 1'); + }); + + it('can mix title and template', () => { + const testHtml = ` + + + {{panels[0].content}} + + + {{panels[1].title}} + {{panels[1].content}} + + + `; + const fixture = createTestComponent(testHtml); + + fixture.detectChanges(); + + const titles = getPanelsTitle(fixture.nativeElement); + + titles.forEach((title: HTMLElement, idx: number) => { expect(title.textContent.trim()).toBe(`Panel ${idx + 1}`); }); + }); + + it('can use header as a template', () => { + const testHtml = ` + + + + + + Content 1 + + + + + + Content 2 + + + `; + const fixture = createTestComponent(testHtml); + const titles = getPanelsTitle(fixture.nativeElement); + titles.forEach((title: HTMLElement, idx: number) => { expect(title.textContent.trim()).toBe(`Title ${idx + 1}`); }); + }); + + it('can should pass context to a header template', () => { + const testHtml = ` + + + + + + Content 1 + + + `; + const fixture = createTestComponent(testHtml); + const titleButton = getPanelsTitle(fixture.nativeElement)[0]; + + expectOpenPanels(fixture.nativeElement, [false]); + expect(titleButton.textContent.trim()).toBe(`closed`); + + fixture.componentInstance.activeIds = 'one'; + fixture.detectChanges(); + + expectOpenPanels(fixture.nativeElement, [true]); + expect(titleButton.textContent.trim()).toBe(`opened`); + }); + + it('can should prefer header as a template to other ways of providing a title', () => { + const testHtml = ` + + + + + + Content 1 + + + Panel Title 2 + + + + Content 2 + + + `; + const fixture = createTestComponent(testHtml); + const titles = getPanelsTitle(fixture.nativeElement); + titles.forEach( + (title: HTMLElement, idx: number) => { expect(title.textContent.trim()).toBe(`Header Title ${idx + 1}`); }); + }); + + it('should not pick up titles from nested accordions', () => { + const testHtml = ` + + + + + + child title + child content + + + + + + `; + const fixture = createTestComponent(testHtml); + // additional change detection is required to reproduce the problem in the test environment + fixture.detectChanges(); + + const titles = getPanelsTitle(fixture.nativeElement); + const parentTitle = titles[0].textContent.trim(); + const childTitle = titles[1].textContent.trim(); + + expect(parentTitle).toContain('parent title'); + expect(parentTitle).not.toContain('child title'); + expect(childTitle).toContain('child title'); + expect(childTitle).not.toContain('parent title'); + }); + + it('should not crash for an empty accordion', () => { + const fixture = createTestComponent(''); + expect(getPanels(fixture.nativeElement).length).toBe(0); + }); + + it('should not crash for panels without content', () => { + const fixture = + createTestComponent(''); + const panelsContent = getPanelsContent(fixture.nativeElement); + + expect(panelsContent.length).toBe(1); + expect(panelsContent[0].textContent.trim()).toBe(''); + }); + + it('should have the appropriate content', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + const originalContent = fixture.componentInstance.panels; + fixture.componentInstance.activeIds = 'one,two,three'; + + fixture.detectChanges(); + + const contents = getPanelsContent(compiled); + expect(contents.length).not.toBe(0); + + contents.forEach((content: HTMLElement, idx: number) => { + expect(content.getAttribute('aria-labelledby')).toBe(`${content.id}-header`); + expect(content.textContent.trim()).toBe(originalContent[idx].content); + }); + }); + + it('should have the appropriate CSS visibility classes', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + fixture.componentInstance.activeIds = 'one,two,three'; + + fixture.detectChanges(); + + const contents = getPanelsContent(compiled); + expect(contents.length).not.toBe(0); + + contents.forEach((content: HTMLElement) => { + expect(content).toHaveCssClass('collapse'); + expect(content).toHaveCssClass('show'); + }); + }); + + it('should only open one at a time', () => { + const fixture = TestBed.createComponent(TestComponent); + const tc = fixture.componentInstance; + tc.closeOthers = true; + fixture.detectChanges(); + + const headingLinks = getPanelsTitle(fixture.nativeElement); + + headingLinks[0].click(); + fixture.detectChanges(); + expectOpenPanels(fixture.nativeElement, [true, false, false]); + + headingLinks[2].click(); + fixture.detectChanges(); + expectOpenPanels(fixture.nativeElement, [false, false, true]); + + headingLinks[2].click(); + fixture.detectChanges(); + expectOpenPanels(fixture.nativeElement, [false, false, false]); + }); + + it('should have only one open panel even if binding says otherwise', () => { + const fixture = TestBed.createComponent(TestComponent); + const tc = fixture.componentInstance; + + tc.activeIds = ['one', 'two']; + tc.closeOthers = true; + fixture.detectChanges(); + + expectOpenPanels(fixture.nativeElement, [true, false, false]); + }); + + it('should not open disabled panels from click', () => { + const fixture = TestBed.createComponent(TestComponent); + const tc = fixture.componentInstance; + tc.panels[0].disabled = true; + fixture.detectChanges(); + + const headingLinks = getPanelsTitle(fixture.nativeElement); + + headingLinks[0].click(); + fixture.detectChanges(); + + expectOpenPanels(fixture.nativeElement, [false, false, false]); + }); + + it('should not update activeIds when trying to toggle a disabled panel', () => { + const fixture = TestBed.createComponent(TestComponent); + const tc = fixture.componentInstance; + const el = fixture.nativeElement; + + tc.panels[0].disabled = true; + fixture.detectChanges(); + expectOpenPanels(el, [false, false, false]); + + const headingLinks = getPanelsTitle(fixture.nativeElement); + + headingLinks[0].click(); + fixture.detectChanges(); + expectOpenPanels(el, [false, false, false]); + + tc.panels[0].disabled = false; + fixture.detectChanges(); + expectOpenPanels(el, [false, false, false]); + }); + + it('should open/collapse disabled panels', () => { + const fixture = TestBed.createComponent(TestComponent); + const tc = fixture.componentInstance; + + tc.activeIds = ['one']; + fixture.detectChanges(); + expectOpenPanels(fixture.nativeElement, [true, false, false]); + + tc.panels[0].disabled = true; + fixture.detectChanges(); + expectOpenPanels(fixture.nativeElement, [false, false, false]); + + tc.panels[0].disabled = false; + fixture.detectChanges(); + expectOpenPanels(fixture.nativeElement, [true, false, false]); + }); + + it('should have correct disabled state', () => { + const fixture = TestBed.createComponent(TestComponent); + const tc = fixture.componentInstance; + + tc.activeIds = ['one']; + fixture.detectChanges(); + const headingLinks = getPanelsTitle(fixture.nativeElement); + expectOpenPanels(fixture.nativeElement, [true, false, false]); + expect(headingLinks[0].disabled).toBeFalsy(); + + tc.panels[0].disabled = true; + fixture.detectChanges(); + expectOpenPanels(fixture.nativeElement, [false, false, false]); + expect(headingLinks[0].disabled).toBeTruthy(); + }); + + it('should remove collapsed panels content from DOM', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + expect(getPanelsContent(fixture.nativeElement).length).toBe(0); + + getButton(fixture.nativeElement, 0).click(); + fixture.detectChanges(); + expect(getPanelsContent(fixture.nativeElement).length).toBe(1); + }); + + it('should not remove collapsed panels content from DOM with `destroyOnHide` flag', () => { + const testHtml = ` + + + {{panel.title}} + {{panel.content}} + + + + `; + const fixture = createTestComponent(testHtml); + + fixture.detectChanges(); + + getButton(fixture.nativeElement, 1).click(); + fixture.detectChanges(); + let panelContents = getPanelsContent(fixture.nativeElement); + expect(panelContents[1]).toHaveCssClass('show'); + expect(panelContents.length).toBe(3); + }); + + it('should emit panel change event when toggling panels', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + fixture.componentInstance.changeCallback = () => {}; + + spyOn(fixture.componentInstance, 'changeCallback'); + + // Select the first tab -> change event + getButton(fixture.nativeElement, 0).click(); + fixture.detectChanges(); + expect(fixture.componentInstance.changeCallback) + .toHaveBeenCalledWith(jasmine.objectContaining({panelId: 'one', nextState: true})); + + // Select the first tab again -> change event + getButton(fixture.nativeElement, 0).click(); + fixture.detectChanges(); + expect(fixture.componentInstance.changeCallback) + .toHaveBeenCalledWith(jasmine.objectContaining({panelId: 'one', nextState: false})); + }); + + it('should cancel panel toggle when preventDefault() is called', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + let changeEvent = null; + fixture.componentInstance.changeCallback = event => { + changeEvent = event; + event.preventDefault(); + }; + + // Select the first tab -> toggle will be canceled + getButton(fixture.nativeElement, 0).click(); + fixture.detectChanges(); + expect(changeEvent).toEqual(jasmine.objectContaining({panelId: 'one', nextState: true})); + expectOpenPanels(fixture.nativeElement, [false, false, false]); + }); + + + it('should have specified type of accordion ', () => { + const testHtml = ` + + + {{panel.title}} + {{panel.content}} + + + + `; + const fixture = createTestComponent(testHtml); + + fixture.componentInstance.classType = 'warning'; + fixture.detectChanges(); + + let el = fixture.nativeElement.querySelectorAll('.card-header'); + expect(el[0]).toHaveCssClass('bg-warning'); + expect(el[1]).toHaveCssClass('bg-warning'); + expect(el[2]).toHaveCssClass('bg-warning'); + }); + + it('should override the type in accordion with type in panel', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + fixture.componentInstance.classType = 'warning'; + + const tc = fixture.componentInstance; + tc.panels[0].type = 'success'; + tc.panels[1].type = 'danger'; + fixture.detectChanges(); + + let el = fixture.nativeElement.querySelectorAll('.card-header'); + expect(el[0]).toHaveCssClass('bg-success'); + expect(el[1]).toHaveCssClass('bg-danger'); + expect(el[2]).toHaveCssClass('bg-warning'); + }); + + it('should have the proper roles', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.componentInstance.activeIds = 'one,two,three'; + fixture.detectChanges(); + + const headers = getPanels(fixture.nativeElement); + headers.forEach((header: HTMLElement) => expect(header.getAttribute('role')).toBe('tab')); + + const contents = getPanelsContent(fixture.nativeElement); + contents.forEach((content: HTMLElement) => expect(content.getAttribute('role')).toBe('tabpanel')); + }); + + describe('Custom config', () => { + let config: NgbAccordionConfig; + + beforeEach(() => { TestBed.configureTestingModule({imports: [NgbAccordionModule]}); }); + + beforeEach(inject([NgbAccordionConfig], (c: NgbAccordionConfig) => { + config = c; + config.closeOthers = true; + config.type = 'success'; + })); + + it('should initialize inputs with provided config', () => { + const fixture = TestBed.createComponent(NgbAccordion); + fixture.detectChanges(); + + let accordion = fixture.componentInstance; + expect(accordion.closeOtherPanels).toBe(config.closeOthers); + expect(accordion.type).toBe(config.type); + }); + }); + + describe('Custom config as provider', () => { + let config = new NgbAccordionConfig(); + config.closeOthers = true; + config.type = 'success'; + + beforeEach(() => { + TestBed.configureTestingModule( + {imports: [NgbAccordionModule], providers: [{provide: NgbAccordionConfig, useValue: config}]}); + }); + + it('should initialize inputs with provided config as provider', () => { + const fixture = TestBed.createComponent(NgbAccordion); + fixture.detectChanges(); + + let accordion = fixture.componentInstance; + expect(accordion.closeOtherPanels).toBe(config.closeOthers); + expect(accordion.type).toBe(config.type); + }); + }); + + describe('imperative API', () => { + + function createTestImperativeAccordion(testHtml: string) { + const fixture = createTestComponent(testHtml); + const accordion = fixture.debugElement.query(By.directive(NgbAccordion)).componentInstance; + const nativeElement = fixture.nativeElement; + return {fixture, accordion, nativeElement}; + } + + it('should check if a panel with a given id is expanded', () => { + const testHtml = ` + + + + `; + + const {accordion, nativeElement} = createTestImperativeAccordion(testHtml); + + expectOpenPanels(nativeElement, [true, false]); + expect(accordion.isExpanded('first')).toBe(true); + expect(accordion.isExpanded('second')).toBe(false); + }); + + it('should expanded and collapse individual panels', () => { + const testHtml = ` + + + + `; + + const {accordion, nativeElement, fixture} = createTestImperativeAccordion(testHtml); + + expectOpenPanels(nativeElement, [false, false]); + + accordion.expand('first'); + fixture.detectChanges(); + expectOpenPanels(nativeElement, [true, false]); + + accordion.expand('second'); + fixture.detectChanges(); + expectOpenPanels(nativeElement, [true, true]); + + accordion.collapse('second'); + fixture.detectChanges(); + expectOpenPanels(nativeElement, [true, false]); + }); + + it('should not expand / collapse if already expanded / collapsed', () => { + const testHtml = ` + + + + `; + + const {accordion, nativeElement, fixture} = createTestImperativeAccordion(testHtml); + + expectOpenPanels(nativeElement, [true, false]); + + spyOn(fixture.componentInstance, 'changeCallback'); + + accordion.expand('first'); + fixture.detectChanges(); + expectOpenPanels(nativeElement, [true, false]); + + accordion.collapse('second'); + fixture.detectChanges(); + expectOpenPanels(nativeElement, [true, false]); + + expect(fixture.componentInstance.changeCallback).not.toHaveBeenCalled(); + }); + + it('should not expand disabled panels', () => { + const testHtml = ` + + + `; + + const {accordion, nativeElement, fixture} = createTestImperativeAccordion(testHtml); + + expectOpenPanels(nativeElement, [false]); + + spyOn(fixture.componentInstance, 'changeCallback'); + + accordion.expand('first'); + fixture.detectChanges(); + expectOpenPanels(nativeElement, [false]); + expect(fixture.componentInstance.changeCallback).not.toHaveBeenCalled(); + }); + + it('should not expand / collapse when preventDefault called on the panelChange event', () => { + const testHtml = ` + + + + `; + + const {accordion, nativeElement, fixture} = createTestImperativeAccordion(testHtml); + + expectOpenPanels(nativeElement, [true, false]); + + accordion.collapse('first'); + fixture.detectChanges(); + expectOpenPanels(nativeElement, [true, false]); + + accordion.expand('second'); + fixture.detectChanges(); + expectOpenPanels(nativeElement, [true, false]); + }); + + it('should expandAll when closeOthers is false', () => { + + const testHtml = ` + + + + `; + + const {accordion, nativeElement, fixture} = createTestImperativeAccordion(testHtml); + + expectOpenPanels(nativeElement, [false, false]); + + accordion.expandAll(); + fixture.detectChanges(); + expectOpenPanels(nativeElement, [true, true]); + }); + + it('should expand first panel when closeOthers is true and none of panels is expanded', () => { + const testHtml = ` + + + + `; + + const {accordion, nativeElement, fixture} = createTestImperativeAccordion(testHtml); + + expectOpenPanels(nativeElement, [false, false]); + + accordion.expandAll(); + fixture.detectChanges(); + expectOpenPanels(nativeElement, [true, false]); + }); + + it('should do nothing if closeOthers is true and one panel is expanded', () => { + const testHtml = ` + + + + `; + + const {accordion, nativeElement, fixture} = createTestImperativeAccordion(testHtml); + + expectOpenPanels(nativeElement, [false, true]); + + accordion.expandAll(); + fixture.detectChanges(); + expectOpenPanels(nativeElement, [false, true]); + }); + + it('should collapse all panels', () => { + const testHtml = ` + + + + `; + + const {accordion, nativeElement, fixture} = createTestImperativeAccordion(testHtml); + + expectOpenPanels(nativeElement, [false, true]); + + accordion.collapseAll(); + fixture.detectChanges(); + expectOpenPanels(nativeElement, [false, false]); + }); + }); +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + activeIds: string | string[] = []; + classType; + closeOthers = false; + panels = [ + {id: 'one', disabled: false, title: 'Panel 1', content: 'foo', type: ''}, + {id: 'two', disabled: false, title: 'Panel 2', content: 'bar', type: ''}, + {id: 'three', disabled: false, title: 'Panel 3', content: 'baz', type: ''} + ]; + changeCallback = (event: NgbPanelChangeEvent) => {}; + preventDefaultCallback = (event: NgbPanelChangeEvent) => { event.preventDefault(); }; +} diff --git a/src/accordion/accordion.ts b/src/accordion/accordion.ts new file mode 100644 index 0000000..a1528c8 --- /dev/null +++ b/src/accordion/accordion.ts @@ -0,0 +1,344 @@ +import { + AfterContentChecked, + Component, + ContentChildren, + Directive, + EventEmitter, + Host, + Input, + Optional, + Output, + QueryList, + TemplateRef +} from '@angular/core'; + +import {isString} from '../util/util'; + +import {NgbAccordionConfig} from './accordion-config'; + +let nextId = 0; + +/** + * The context for the [NgbPanelHeader](#/components/accordion/api#NgbPanelHeader) template + * + * @since 4.1.0 + */ +export interface NgbPanelHeaderContext { + /** + * `True` if current panel is opened + */ + opened: boolean; +} + +/** + * A directive that wraps an accordion panel header with any HTML markup and a toggling button + * marked with [`NgbPanelToggle`](#/components/accordion/api#NgbPanelToggle). + * See the [header customization demo](#/components/accordion/examples#header) for more details. + * + * You can also use [`NgbPanelTitle`](#/components/accordion/api#NgbPanelTitle) to customize only the panel title. + * + * @since 4.1.0 + */ +@Directive({selector: 'ng-template[ngbPanelHeader]'}) +export class NgbPanelHeader { + constructor(public templateRef: TemplateRef) {} +} + +/** + * A directive that wraps only the panel title with HTML markup inside. + * + * You can also use [`NgbPanelHeader`](#/components/accordion/api#NgbPanelHeader) to customize the full panel header. + */ +@Directive({selector: 'ng-template[ngbPanelTitle]'}) +export class NgbPanelTitle { + constructor(public templateRef: TemplateRef) {} +} + +/** + * A directive that wraps the accordion panel content. + */ +@Directive({selector: 'ng-template[ngbPanelContent]'}) +export class NgbPanelContent { + constructor(public templateRef: TemplateRef) {} +} + +/** + * A directive that wraps an individual accordion panel with title and collapsible content. + */ +@Directive({selector: 'ngb-panel'}) +export class NgbPanel implements AfterContentChecked { + /** + * If `true`, the panel is disabled an can't be toggled. + */ + @Input() disabled = false; + + /** + * An optional id for the panel that must be unique on the page. + * + * If not provided, it will be auto-generated in the `ngb-panel-xxx` format. + */ + @Input() id = `ngb-panel-${nextId++}`; + + isOpen = false; + + /** + * The panel title. + * + * You can alternatively use [`NgbPanelTitle`](#/components/accordion/api#NgbPanelTitle) to set panel title. + */ + @Input() title: string; + + /** + * Type of the current panel. + * + * Bootstrap provides styles for the following types: `'success'`, `'info'`, `'warning'`, `'danger'`, `'primary'`, + * `'secondary'`, `'light'` and `'dark'`. + */ + @Input() type: string; + + titleTpl: NgbPanelTitle | null; + headerTpl: NgbPanelHeader | null; + contentTpl: NgbPanelContent | null; + + @ContentChildren(NgbPanelTitle, {descendants: false}) titleTpls: QueryList; + @ContentChildren(NgbPanelHeader, {descendants: false}) headerTpls: QueryList; + @ContentChildren(NgbPanelContent, {descendants: false}) contentTpls: QueryList; + + ngAfterContentChecked() { + // We are using @ContentChildren instead of @ContentChild as in the Angular version being used + // only @ContentChildren allows us to specify the {descendants: false} option. + // Without {descendants: false} we are hitting bugs described in: + // https://github.com/ng-bootstrap/ng-bootstrap/issues/2240 + this.titleTpl = this.titleTpls.first; + this.headerTpl = this.headerTpls.first; + this.contentTpl = this.contentTpls.first; + } +} + +/** + * An event emitted right before toggling an accordion panel. + */ +export interface NgbPanelChangeEvent { + /** + * The id of the accordion panel that is being toggled. + */ + panelId: string; + + /** + * The next state of the panel. + * + * `true` if it will be opened, `false` if closed. + */ + nextState: boolean; + + /** + * Calling this function will prevent panel toggling. + */ + preventDefault: () => void; +} + +/** + * Accordion is a collection of collapsible panels (bootstrap cards). + * + * It can ensure only one panel is opened at a time and allows to customize panel + * headers. + */ +@Component({ + selector: 'ngb-accordion', + exportAs: 'ngbAccordion', + host: {'class': 'accordion', 'role': 'tablist', '[attr.aria-multiselectable]': '!closeOtherPanels'}, + template: ` + + + + +
+ +
+
+ +
+
+
+
+ ` +}) +export class NgbAccordion implements AfterContentChecked { + @ContentChildren(NgbPanel) panels: QueryList; + + /** + * An array or comma separated strings of panel ids that should be opened **initially**. + * + * For subsequent changes use methods like `expand()`, `collapse()`, etc. and + * the `(panelChange)` event. + */ + @Input() activeIds: string | string[] = []; + + /** + * If `true`, only one panel could be opened at a time. + * + * Opening a new panel will close others. + */ + @Input('closeOthers') closeOtherPanels: boolean; + + /** + * If `true`, panel content will be detached from DOM and not simply hidden when the panel is collapsed. + */ + @Input() destroyOnHide = true; + + /** + * Type of panels. + * + * Bootstrap provides styles for the following types: `'success'`, `'info'`, `'warning'`, `'danger'`, `'primary'`, + * `'secondary'`, `'light'` and `'dark'`. + */ + @Input() type: string; + + /** + * Event emitted right before the panel toggle happens. + * + * See [NgbPanelChangeEvent](#/components/accordion/api#NgbPanelChangeEvent) for payload details. + */ + @Output() panelChange = new EventEmitter(); + + constructor(config: NgbAccordionConfig) { + this.type = config.type; + this.closeOtherPanels = config.closeOthers; + } + + /** + * Checks if a panel with a given id is expanded. + */ + isExpanded(panelId: string): boolean { return this.activeIds.indexOf(panelId) > -1; } + + /** + * Expands a panel with a given id. + * + * Has no effect if the panel is already expanded or disabled. + */ + expand(panelId: string): void { this._changeOpenState(this._findPanelById(panelId), true); } + + /** + * Expands all panels, if `[closeOthers]` is `false`. + * + * If `[closeOthers]` is `true`, it will expand the first panel, unless there is already a panel opened. + */ + expandAll(): void { + if (this.closeOtherPanels) { + if (this.activeIds.length === 0 && this.panels.length) { + this._changeOpenState(this.panels.first, true); + } + } else { + this.panels.forEach(panel => this._changeOpenState(panel, true)); + } + } + + /** + * Collapses a panel with the given id. + * + * Has no effect if the panel is already collapsed or disabled. + */ + collapse(panelId: string) { this._changeOpenState(this._findPanelById(panelId), false); } + + /** + * Collapses all opened panels. + */ + collapseAll() { + this.panels.forEach((panel) => { this._changeOpenState(panel, false); }); + } + + /** + * Toggles a panel with the given id. + * + * Has no effect if the panel is disabled. + */ + toggle(panelId: string) { + const panel = this._findPanelById(panelId); + if (panel) { + this._changeOpenState(panel, !panel.isOpen); + } + } + + ngAfterContentChecked() { + // active id updates + if (isString(this.activeIds)) { + this.activeIds = this.activeIds.split(/\s*,\s*/); + } + + // update panels open states + this.panels.forEach(panel => panel.isOpen = !panel.disabled && this.activeIds.indexOf(panel.id) > -1); + + // closeOthers updates + if (this.activeIds.length > 1 && this.closeOtherPanels) { + this._closeOthers(this.activeIds[0]); + this._updateActiveIds(); + } + } + + private _changeOpenState(panel: NgbPanel, nextState: boolean) { + if (panel && !panel.disabled && panel.isOpen !== nextState) { + let defaultPrevented = false; + + this.panelChange.emit( + {panelId: panel.id, nextState: nextState, preventDefault: () => { defaultPrevented = true; }}); + + if (!defaultPrevented) { + panel.isOpen = nextState; + + if (nextState && this.closeOtherPanels) { + this._closeOthers(panel.id); + } + this._updateActiveIds(); + } + } + } + + private _closeOthers(panelId: string) { + this.panels.forEach(panel => { + if (panel.id !== panelId) { + panel.isOpen = false; + } + }); + } + + private _findPanelById(panelId: string): NgbPanel | null { return this.panels.find(p => p.id === panelId); } + + private _updateActiveIds() { + this.activeIds = this.panels.filter(panel => panel.isOpen && !panel.disabled).map(panel => panel.id); + } +} + +/** + * A directive to put on a button that toggles panel opening and closing. + * + * To be used inside the [`NgbPanelHeader`](#/components/accordion/api#NgbPanelHeader) + * + * @since 4.1.0 + */ +@Directive({ + selector: 'button[ngbPanelToggle]', + host: { + 'type': 'button', + '[disabled]': 'panel.disabled', + '[class.collapsed]': '!panel.isOpen', + '[attr.aria-expanded]': 'panel.isOpen', + '[attr.aria-controls]': 'panel.id', + '(click)': 'accordion.toggle(panel.id)' + } +}) +export class NgbPanelToggle { + @Input() + set ngbPanelToggle(panel: NgbPanel) { + if (panel) { + this.panel = panel; + } + } + + constructor(public accordion: NgbAccordion, @Optional() @Host() public panel: NgbPanel) {} +} diff --git a/src/alert/alert-config.spec.ts b/src/alert/alert-config.spec.ts new file mode 100644 index 0000000..e0ecd09 --- /dev/null +++ b/src/alert/alert-config.spec.ts @@ -0,0 +1,10 @@ +import {NgbAlertConfig} from './alert-config'; + +describe('ngb-alert-config', () => { + it('should have sensible default values', () => { + const config = new NgbAlertConfig(); + + expect(config.dismissible).toBe(true); + expect(config.type).toBe('warning'); + }); +}); diff --git a/src/alert/alert-config.ts b/src/alert/alert-config.ts new file mode 100644 index 0000000..e5c0930 --- /dev/null +++ b/src/alert/alert-config.ts @@ -0,0 +1,13 @@ +import {Injectable} from '@angular/core'; + +/** + * A configuration service for the [NgbAlert](#/components/alert/api#NgbAlert) component. + * + * You can inject this service, typically in your root component, and customize its properties + * to provide default values for all alerts used in the application. + */ +@Injectable({providedIn: 'root'}) +export class NgbAlertConfig { + dismissible = true; + type = 'warning'; +} diff --git a/src/alert/alert.module.ts b/src/alert/alert.module.ts new file mode 100644 index 0000000..c13d8cf --- /dev/null +++ b/src/alert/alert.module.ts @@ -0,0 +1,11 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; + +import {NgbAlert} from './alert'; + +export {NgbAlert} from './alert'; +export {NgbAlertConfig} from './alert-config'; + +@NgModule({declarations: [NgbAlert], exports: [NgbAlert], imports: [CommonModule], entryComponents: [NgbAlert]}) +export class NgbAlertModule { +} diff --git a/src/alert/alert.scss b/src/alert/alert.scss new file mode 100644 index 0000000..95de00b --- /dev/null +++ b/src/alert/alert.scss @@ -0,0 +1,3 @@ +ngb-alert { + display: block; +} diff --git a/src/alert/alert.spec.ts b/src/alert/alert.spec.ts new file mode 100644 index 0000000..7bf106f --- /dev/null +++ b/src/alert/alert.spec.ts @@ -0,0 +1,170 @@ +import {TestBed, ComponentFixture, inject} from '@angular/core/testing'; +import {createGenericTestComponent} from '../test/common'; + +import {Component} from '@angular/core'; + +import {NgbAlertModule} from './alert.module'; +import {NgbAlert} from './alert'; +import {NgbAlertConfig} from './alert-config'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +function getAlertElement(element: HTMLElement): HTMLDivElement { + return element.querySelector('.alert'); +} + +function getCloseButton(element: HTMLElement): HTMLButtonElement { + return element.querySelector('button'); +} + +function getCloseButtonIcon(element: HTMLElement): HTMLSpanElement { + return element.querySelector('button > span'); +} + +describe('ngb-alert', () => { + + beforeEach(() => { TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbAlertModule]}); }); + + it('should initialize inputs with default values', () => { + const defaultConfig = new NgbAlertConfig(); + const alertCmp = TestBed.createComponent(NgbAlert).componentInstance; + expect(alertCmp.dismissible).toBe(defaultConfig.dismissible); + expect(alertCmp.type).toBe(defaultConfig.type); + }); + + it('should apply those default values to the template', () => { + const fixture = createTestComponent('Cool!'); + const alertEl = getAlertElement(fixture.nativeElement); + + expect(alertEl.getAttribute('role')).toEqual('alert'); + expect(alertEl).toHaveCssClass('alert-warning'); + expect(alertEl).toHaveCssClass('alert-dismissible'); + }); + + it('should allow specifying alert type', () => { + const fixture = createTestComponent('Cool!'); + const alertEl = getAlertElement(fixture.nativeElement); + + expect(alertEl.getAttribute('role')).toEqual('alert'); + expect(alertEl).toHaveCssClass('alert-success'); + }); + + it('should allow changing alert type', () => { + const fixture = createTestComponent('Cool!'); + const alertEl = getAlertElement(fixture.nativeElement); + + expect(alertEl).toHaveCssClass('alert-success'); + expect(alertEl).not.toHaveCssClass('alert-warning'); + + fixture.componentInstance.type = 'warning'; + fixture.detectChanges(); + expect(alertEl).not.toHaveCssClass('alert-success'); + expect(alertEl).toHaveCssClass('alert-warning'); + }); + + it('should allow adding custom CSS classes', () => { + const fixture = createTestComponent('Cool!'); + const alertEl = getAlertElement(fixture.nativeElement); + + expect(alertEl).toHaveCssClass('alert'); + expect(alertEl).toHaveCssClass('alert-success'); + expect(alertEl).toHaveCssClass('myClass'); + }); + + it('should render close button when dismissible', () => { + const fixture = createTestComponent('Watch out!'); + const alertEl = getAlertElement(fixture.nativeElement); + const buttonEl = getCloseButton(alertEl); + const buttonIconEl = getCloseButtonIcon(alertEl); + + expect(alertEl).toHaveCssClass('alert-dismissible'); + expect(buttonEl).toBeTruthy(); + expect(buttonEl.getAttribute('class')).toContain('close'); + expect(buttonEl.getAttribute('aria-label')).toBe('Close'); + expect(buttonIconEl.getAttribute('aria-hidden')).toBe('true'); + expect(buttonIconEl.textContent).toBe('×'); + }); + + it('should not render the close button if it is not dismissible', () => { + const fixture = createTestComponent(`Don't close!`); + const alertEl = getAlertElement(fixture.nativeElement); + + expect(alertEl).not.toHaveCssClass('alert-dismissible'); + expect(getCloseButton(alertEl)).toBeFalsy(); + }); + + it('should fire an event after closing a dismissible alert', () => { + const fixture = + createTestComponent('Watch out!'); + const alertEl = getAlertElement(fixture.nativeElement); + const buttonEl = getCloseButton(alertEl); + + expect(fixture.componentInstance.closed).toBe(false); + buttonEl.click(); + expect(fixture.componentInstance.closed).toBe(true); + }); + + it('should project the content given into the component', () => { + const fixture = createTestComponent('Cool!'); + const alertEl = getAlertElement(fixture.nativeElement); + + expect(alertEl.textContent).toContain('Cool!'); + }); + + it('should project content before the closing button for a11y/screen readers', () => { + const fixture = createTestComponent('Cool!'); + const alertEl = getAlertElement(fixture.nativeElement); + + const childElements = Array.from(alertEl.children).map(node => node.tagName.toLowerCase()); + expect(childElements).toEqual(['span', 'button']); + }); + + describe('Custom config', () => { + let config: NgbAlertConfig; + + beforeEach(() => { TestBed.configureTestingModule({imports: [NgbAlertModule]}); }); + + beforeEach(inject([NgbAlertConfig], (c: NgbAlertConfig) => { + config = c; + config.dismissible = false; + config.type = 'success'; + })); + + it('should initialize inputs with provided config', () => { + const fixture = TestBed.createComponent(NgbAlert); + fixture.detectChanges(); + + const alert = fixture.componentInstance; + expect(alert.dismissible).toBe(config.dismissible); + expect(alert.type).toBe(config.type); + }); + }); + + describe('Custom config as provider', () => { + let config = new NgbAlertConfig(); + config.dismissible = false; + config.type = 'success'; + + beforeEach(() => { + TestBed.configureTestingModule( + {imports: [NgbAlertModule], providers: [{provide: NgbAlertConfig, useValue: config}]}); + }); + + it('should initialize inputs with provided config as provider', () => { + const fixture = TestBed.createComponent(NgbAlert); + fixture.detectChanges(); + + const alert = fixture.componentInstance; + expect(alert.dismissible).toBe(config.dismissible); + expect(alert.type).toBe(config.type); + }); + }); +}); + +@Component({selector: 'test-cmp', template: '', entryComponents: [NgbAlert]}) +class TestComponent { + name = 'World'; + closed = false; + type = 'success'; +} diff --git a/src/alert/alert.ts b/src/alert/alert.ts new file mode 100644 index 0000000..4df4cc3 --- /dev/null +++ b/src/alert/alert.ts @@ -0,0 +1,73 @@ +import { + Component, + Input, + Output, + EventEmitter, + ChangeDetectionStrategy, + Renderer2, + ElementRef, + OnChanges, + OnInit, + SimpleChanges, + ViewEncapsulation +} from '@angular/core'; + +import {NgbAlertConfig} from './alert-config'; + +/** + * Alert is a component to provide contextual feedback messages for user. + * + * It supports several alert types and can be dismissed. + */ +@Component({ + selector: 'ngb-alert', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + host: {'role': 'alert', 'class': 'alert', '[class.alert-dismissible]': 'dismissible'}, + template: ` + + + `, + styleUrls: ['./alert.scss'] +}) +export class NgbAlert implements OnInit, + OnChanges { + /** + * If `true`, alert can be dismissed by the user. + * + * The close button (×) will be displayed and you can be notified + * of the event with the `(close)` output. + */ + @Input() dismissible: boolean; + /** + * Type of the alert. + * + * Bootstrap provides styles for the following types: `'success'`, `'info'`, `'warning'`, `'danger'`, `'primary'`, + * `'secondary'`, `'light'` and `'dark'`. + */ + @Input() type: string; + /** + * An event emitted when the close button is clicked. It has no payload and only relevant for dismissible alerts. + */ + @Output() close = new EventEmitter(); + + constructor(config: NgbAlertConfig, private _renderer: Renderer2, private _element: ElementRef) { + this.dismissible = config.dismissible; + this.type = config.type; + } + + closeHandler() { this.close.emit(null); } + + ngOnChanges(changes: SimpleChanges) { + const typeChange = changes['type']; + if (typeChange && !typeChange.firstChange) { + this._renderer.removeClass(this._element.nativeElement, `alert-${typeChange.previousValue}`); + this._renderer.addClass(this._element.nativeElement, `alert-${typeChange.currentValue}`); + } + } + + ngOnInit() { this._renderer.addClass(this._element.nativeElement, `alert-${this.type}`); } +} diff --git a/src/browserslist b/src/browserslist new file mode 100644 index 0000000..ba8fa84 --- /dev/null +++ b/src/browserslist @@ -0,0 +1,13 @@ +# Source: https://github.com/twbs/bootstrap/blob/v4.1.3/.browserslistrc + +>= 1% +last 1 major version +not dead +Chrome >= 45 +Firefox >= 38 +Edge >= 12 +Explorer >= 10 +iOS >= 9 +Safari >= 9 +Android >= 4.4 +Opera >= 30 diff --git a/src/buttons/buttons.module.ts b/src/buttons/buttons.module.ts new file mode 100644 index 0000000..05e6f4b --- /dev/null +++ b/src/buttons/buttons.module.ts @@ -0,0 +1,15 @@ +import {NgModule} from '@angular/core'; +import {NgbButtonLabel} from './label'; +import {NgbCheckBox} from './checkbox'; +import {NgbRadio, NgbRadioGroup} from './radio'; + +export {NgbButtonLabel} from './label'; +export {NgbCheckBox} from './checkbox'; +export {NgbRadio, NgbRadioGroup} from './radio'; + + +const NGB_BUTTON_DIRECTIVES = [NgbButtonLabel, NgbCheckBox, NgbRadioGroup, NgbRadio]; + +@NgModule({declarations: NGB_BUTTON_DIRECTIVES, exports: NGB_BUTTON_DIRECTIVES}) +export class NgbButtonsModule { +} diff --git a/src/buttons/checkbox.spec.ts b/src/buttons/checkbox.spec.ts new file mode 100644 index 0000000..7fc4995 --- /dev/null +++ b/src/buttons/checkbox.spec.ts @@ -0,0 +1,185 @@ +import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {createGenericTestComponent} from '../test/common'; + +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {FormsModule, ReactiveFormsModule} from '@angular/forms'; + +import {NgbButtonsModule} from './buttons.module'; +import {NgbCheckBox} from './checkbox'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +const createOnPushTestComponent = (html: string) => + createGenericTestComponent(html, TestComponentOnPush) as ComponentFixture; + +function getLabel(nativeEl: HTMLElement): HTMLElement { + return nativeEl.querySelector('label'); +} + +function getInput(nativeEl: HTMLElement): HTMLInputElement { + return nativeEl.querySelector('input'); +} + +describe('NgbCheckBox', () => { + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestComponent, TestComponentOnPush], + imports: [NgbButtonsModule, FormsModule, ReactiveFormsModule] + }); + }); + + describe('bindings', () => { + + it('should mark input as checked / unchecked based on model change (default values)', fakeAsync(() => { + const fixture = + createTestComponent(``); + + fixture.componentInstance.model = true; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + expect(getInput(fixture.debugElement.nativeElement).checked).toBeTruthy(); + expect(getLabel(fixture.debugElement.nativeElement)).toHaveCssClass('active'); + + fixture.componentInstance.model = false; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + expect(getInput(fixture.debugElement.nativeElement).checked).toBeFalsy(); + expect(getLabel(fixture.debugElement.nativeElement)).not.toHaveCssClass('active'); + })); + + + it('should mark input as checked / unchecked based on model change (custom values)', fakeAsync(() => { + const fixture = createTestComponent(` + + `); + + fixture.componentInstance.model = 'foo'; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + expect(getInput(fixture.debugElement.nativeElement).checked).toBeTruthy(); + expect(getLabel(fixture.debugElement.nativeElement)).toHaveCssClass('active'); + + fixture.componentInstance.model = 'sth else'; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + expect(getInput(fixture.debugElement.nativeElement).checked).toBeFalsy(); + expect(getLabel(fixture.debugElement.nativeElement)).not.toHaveCssClass('active'); + })); + + it('should mark input as disabled / enabled based on binding change', fakeAsync(() => { + const fixture = createTestComponent(` + + `); + + fixture.componentInstance.disabled = true; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + expect(getInput(fixture.debugElement.nativeElement).disabled).toBeTruthy(); + expect(getLabel(fixture.debugElement.nativeElement)).toHaveCssClass('disabled'); + + fixture.componentInstance.disabled = false; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + expect(getInput(fixture.debugElement.nativeElement).disabled).toBeFalsy(); + expect(getLabel(fixture.debugElement.nativeElement)).not.toHaveCssClass('disabled'); + })); + }); + + describe('user interactions', () => { + + it('should bind model value on change (default values)', fakeAsync(() => { + const fixture = createTestComponent(` + + `); + + const inputDebugEl = fixture.debugElement.query(By.directive(NgbCheckBox)); + + inputDebugEl.triggerEventHandler('change', {target: {checked: true}}); + tick(); + expect(fixture.componentInstance.model).toBe(true); + + inputDebugEl.triggerEventHandler('change', {target: {checked: false}}); + tick(); + expect(fixture.componentInstance.model).toBe(false); + })); + + it('should bind model value on change (custom values)', fakeAsync(() => { + const fixture = createTestComponent(` + + `); + + const inputDebugEl = fixture.debugElement.query(By.directive(NgbCheckBox)); + + inputDebugEl.triggerEventHandler('change', {target: {checked: true}}); + tick(); + expect(fixture.componentInstance.model).toBe('foo'); + + inputDebugEl.triggerEventHandler('change', {target: {checked: false}}); + tick(); + expect(fixture.componentInstance.model).toBe('bar'); + })); + + it('should mark label as focused based on input focus', () => { + const fixture = createTestComponent(` + + `); + + const inputDebugEl = fixture.debugElement.query(By.directive(NgbCheckBox)); + + inputDebugEl.triggerEventHandler('focus', {}); + fixture.detectChanges(); + expect(getLabel(fixture.debugElement.nativeElement)).toHaveCssClass('focus'); + + inputDebugEl.triggerEventHandler('blur', {}); + fixture.detectChanges(); + expect(getLabel(fixture.debugElement.nativeElement)).not.toHaveCssClass('focus'); + }); + + }); + + describe('on push', () => { + it('should set initial model value', fakeAsync(() => { + const fixture = createOnPushTestComponent(` + + `); + + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + expect(getInput(fixture.debugElement.nativeElement).checked).toBeTruthy(); + expect(getLabel(fixture.debugElement.nativeElement)).toHaveCssClass('active'); + })); + }); + +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + disabled; + model; +} + +@Component({selector: 'test-cmp-on-push', template: '', changeDetection: ChangeDetectionStrategy.OnPush}) +class TestComponentOnPush { +} diff --git a/src/buttons/checkbox.ts b/src/buttons/checkbox.ts new file mode 100644 index 0000000..227ef98 --- /dev/null +++ b/src/buttons/checkbox.ts @@ -0,0 +1,84 @@ +import {ChangeDetectorRef, Directive, forwardRef, Input} from '@angular/core'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; + +import {NgbButtonLabel} from './label'; + +const NGB_CHECKBOX_VALUE_ACCESSOR = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NgbCheckBox), + multi: true +}; + + +/** + * Allows to easily create Bootstrap-style checkbox buttons. + * + * Integrates with forms, so the value of a checked button is bound to the underlying form control + * either in a reactive or template-driven way. + */ +@Directive({ + selector: '[ngbButton][type=checkbox]', + host: { + 'autocomplete': 'off', + '[checked]': 'checked', + '[disabled]': 'disabled', + '(change)': 'onInputChange($event)', + '(focus)': 'focused = true', + '(blur)': 'focused = false' + }, + providers: [NGB_CHECKBOX_VALUE_ACCESSOR] +}) +export class NgbCheckBox implements ControlValueAccessor { + checked; + + /** + * If `true`, the checkbox button will be disabled + */ + @Input() disabled = false; + + /** + * The form control value when the checkbox is checked. + */ + @Input() valueChecked = true; + + /** + * The form control value when the checkbox is unchecked. + */ + @Input() valueUnChecked = false; + + onChange = (_: any) => {}; + onTouched = () => {}; + + set focused(isFocused: boolean) { + this._label.focused = isFocused; + if (!isFocused) { + this.onTouched(); + } + } + + constructor(private _label: NgbButtonLabel, private _cd: ChangeDetectorRef) {} + + onInputChange($event) { + const modelToPropagate = $event.target.checked ? this.valueChecked : this.valueUnChecked; + this.onChange(modelToPropagate); + this.onTouched(); + this.writeValue(modelToPropagate); + } + + registerOnChange(fn: (value: any) => any): void { this.onChange = fn; } + + registerOnTouched(fn: () => any): void { this.onTouched = fn; } + + setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + this._label.disabled = isDisabled; + } + + writeValue(value) { + this.checked = value === this.valueChecked; + this._label.active = this.checked; + + // label won't be updated, if it is inside the OnPush component when [ngModel] changes + this._cd.markForCheck(); + } +} diff --git a/src/buttons/label.ts b/src/buttons/label.ts new file mode 100644 index 0000000..85921c8 --- /dev/null +++ b/src/buttons/label.ts @@ -0,0 +1,12 @@ +import {Directive} from '@angular/core'; + +@Directive({ + selector: '[ngbButtonLabel]', + host: + {'[class.btn]': 'true', '[class.active]': 'active', '[class.disabled]': 'disabled', '[class.focus]': 'focused'} +}) +export class NgbButtonLabel { + active: boolean; + disabled: boolean; + focused: boolean; +} diff --git a/src/buttons/radio.spec.ts b/src/buttons/radio.spec.ts new file mode 100644 index 0000000..f5af4d7 --- /dev/null +++ b/src/buttons/radio.spec.ts @@ -0,0 +1,617 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {async, ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {FormControl, FormGroup, FormsModule, NgModel, ReactiveFormsModule, Validators} from '@angular/forms'; +import {By} from '@angular/platform-browser'; + +import {createGenericTestComponent} from '../test/common'; +import {NgbButtonsModule} from './buttons.module'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +function expectRadios(element: HTMLElement, states: number[]) { + const labels = element.querySelectorAll('label'); + expect(labels.length).toEqual(states.length); + + for (let i = 0; i < states.length; i++) { + let state = states[i]; + + if (state === 1) { + expect(labels[i]).toHaveCssClass('active'); + } else if (state === 0) { + expect(labels[i]).not.toHaveCssClass('active'); + } + } +} + +function expectNameOnAllInputs(element: HTMLElement, name: string) { + const inputs = element.querySelectorAll('input'); + for (let i = 0; i < inputs.length; i++) { + expect(inputs[i].getAttribute('name')).toBe(name); + } +} + +function getGroupElement(nativeEl: HTMLElement): HTMLDivElement { + return nativeEl.querySelector('div[ngbRadioGroup]'); +} + +function getInput(nativeEl: HTMLElement, idx: number): HTMLInputElement { + return nativeEl.querySelectorAll('input')[idx]; +} + +function getLabel(nativeEl: HTMLElement, idx: number): HTMLElement { + return nativeEl.querySelectorAll('label')[idx]; +} + +describe('ngbRadioGroup', () => { + const defaultHtml = `
+ + +
`; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestComponent, TestComponentOnPush], + imports: [NgbButtonsModule, FormsModule, ReactiveFormsModule] + }); + TestBed.overrideComponent(TestComponent, {set: {template: defaultHtml}}); + TestBed.overrideComponent(TestComponentOnPush, {set: {template: defaultHtml}}); + }); + + it('toggles radio inputs based on model changes', async(() => { + const fixture = TestBed.createComponent(TestComponent); + + let values = fixture.componentInstance.values; + + // checking initial values + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [0, 0]); + expect(getInput(fixture.nativeElement, 0).value).toEqual(values[0]); + expect(getInput(fixture.nativeElement, 1).value).toEqual(values[1]); + + // checking null + fixture.componentInstance.model = null; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [0, 0]); + + // checking first radio + fixture.componentInstance.model = values[0]; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [1, 0]); + + // checking second radio + fixture.componentInstance.model = values[1]; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [0, 1]); + + // checking non-matching value + fixture.componentInstance.model = values[3]; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [0, 0]); + }); + })); + + it('updates model based on radio input clicks', async(() => { + const fixture = TestBed.createComponent(TestComponent); + + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [0, 0]); + + fixture.whenStable() + .then(() => { + // clicking first radio + getInput(fixture.nativeElement, 0).click(); + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [1, 0]); + expect(fixture.componentInstance.model).toBe('one'); + return fixture.whenStable(); + }) + .then(() => { + // clicking second radio + getInput(fixture.nativeElement, 1).click(); + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [0, 1]); + expect(fixture.componentInstance.model).toBe('two'); + }); + })); + + it('can be used with objects as values', async(() => { + const fixture = TestBed.createComponent(TestComponent); + + let [one, two] = [{one: 'one'}, {two: 'two'}]; + + fixture.componentInstance.values[0] = one; + fixture.componentInstance.values[1] = two; + + // checking initial values + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [0, 0]); + expect(getInput(fixture.nativeElement, 0).value).toEqual({}.toString()); + expect(getInput(fixture.nativeElement, 1).value).toEqual({}.toString()); + + // checking model -> radio input + fixture.componentInstance.model = one; + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [1, 0]); + + // checking radio click -> model + getInput(fixture.nativeElement, 1).click(); + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [0, 1]); + expect(fixture.componentInstance.model).toBe(two); + }); + })); + + it('updates radio input values dynamically', async(() => { + const fixture = TestBed.createComponent(TestComponent); + + let values = fixture.componentInstance.values; + + // checking first radio + fixture.componentInstance.model = values[0]; + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [1, 0]); + expect(fixture.componentInstance.model).toEqual(values[0]); + + // updating first radio value -> expecting none selected + let initialValue = values[0]; + values[0] = 'ten'; + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [0, 0]); + expect(getInput(fixture.nativeElement, 0).value).toEqual('ten'); + expect(fixture.componentInstance.model).toEqual(initialValue); + + // updating values back -> expecting initial state + values[0] = initialValue; + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [1, 0]); + expect(getInput(fixture.nativeElement, 0).value).toEqual(values[0]); + expect(fixture.componentInstance.model).toEqual(values[0]); + }); + })); + + it('can be used with ngFor', async(() => { + + const forHtml = `
+ +
`; + + const fixture = createTestComponent(forHtml); + let values = fixture.componentInstance.values; + + expectRadios(fixture.nativeElement, [0, 0, 0]); + + fixture.componentInstance.model = values[1]; + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [0, 1, 0]); + }); + })); + + it('cleans up the model when radio inputs are added / removed', async(() => { + + const ifHtml = `
+ + +
`; + const fixture = createTestComponent(ifHtml); + + let values = fixture.componentInstance.values; + + // hiding/showing non-selected radio -> expecting initial model value + expectRadios(fixture.nativeElement, [0, 0]); + + fixture.componentInstance.shown = false; + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [0]); + expect(fixture.componentInstance.model).toBeUndefined(); + + fixture.componentInstance.shown = true; + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [0, 0]); + expect(fixture.componentInstance.model).toBeUndefined(); + + // hiding/showing selected radio -> expecting model to unchange, but none selected + fixture.componentInstance.model = values[1]; + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [0, 1]); + + fixture.componentInstance.shown = false; + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [0]); + expect(fixture.componentInstance.model).toEqual(values[1]); + + fixture.componentInstance.shown = true; + fixture.detectChanges(); + expectRadios(fixture.nativeElement, [0, 1]); + expect(fixture.componentInstance.model).toEqual(values[1]); + }); + })); + + it('should work with template-driven form validation', async(() => { + const html = ` +
+
+ +
+
`; + + const fixture = createTestComponent(html); + + fixture.whenStable().then(() => { + fixture.detectChanges(); + expect(getGroupElement(fixture.nativeElement)).toHaveCssClass('ng-invalid'); + expect(getGroupElement(fixture.nativeElement)).not.toHaveCssClass('ng-valid'); + + getInput(fixture.nativeElement, 0).click(); + fixture.detectChanges(); + expect(getGroupElement(fixture.nativeElement)).toHaveCssClass('ng-valid'); + expect(getGroupElement(fixture.nativeElement)).not.toHaveCssClass('ng-invalid'); + }); + })); + + it('should work with model-driven form validation', () => { + const html = ` +
+
+ +
+
`; + + const fixture = createTestComponent(html); + + expect(getGroupElement(fixture.nativeElement)).toHaveCssClass('ng-invalid'); + expect(getGroupElement(fixture.nativeElement)).not.toHaveCssClass('ng-valid'); + + getInput(fixture.nativeElement, 0).click(); + fixture.detectChanges(); + expect(getGroupElement(fixture.nativeElement)).toHaveCssClass('ng-valid'); + expect(getGroupElement(fixture.nativeElement)).not.toHaveCssClass('ng-invalid'); + }); + + it('should disable label and input when it is disabled using reactive forms', () => { + const html = ` +
+
+ +
+
`; + + const fixture = createTestComponent(html); + + expect(getLabel(fixture.nativeElement, 0)).toHaveCssClass('disabled'); + expect(getInput(fixture.nativeElement, 0).hasAttribute('disabled')).toBeTruthy(); + + fixture.componentInstance.disabledControl.enable(); + fixture.detectChanges(); + expect(getLabel(fixture.nativeElement, 0)).not.toHaveCssClass('disabled'); + expect(getInput(fixture.nativeElement, 0).hasAttribute('disabled')).toBeFalsy(); + }); + + it('should disable label and input when added dynamically in reactive forms', () => { + const forHtml = ` +
+
+ +
+
+ `; + + const fixture = createTestComponent(forHtml); + fixture.componentInstance.shown = false; + fixture.componentInstance.disabledForm.disable(); + fixture.detectChanges(); + + fixture.componentInstance.shown = true; + fixture.detectChanges(); + expect(getLabel(fixture.nativeElement, 0)).toHaveCssClass('disabled'); + expect(getInput(fixture.nativeElement, 0).hasAttribute('disabled')).toBeTruthy(); + }); + + it('should disable label and input when it is disabled using template-driven forms', async(() => { + const html = ` +
+
+ +
+
`; + + const fixture = createTestComponent(html); + + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + expect(getLabel(fixture.nativeElement, 0)).toHaveCssClass('disabled'); + expect(getInput(fixture.nativeElement, 0).hasAttribute('disabled')).toBeTruthy(); + + fixture.componentInstance.disabled = false; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + fixture.detectChanges(); + expect(getLabel(fixture.nativeElement, 0)).not.toHaveCssClass('disabled'); + expect(getInput(fixture.nativeElement, 0).hasAttribute('disabled')).toBeFalsy(); + }); + })); + + it('should disable individual label and input using template-driven forms', async(() => { + const html = ` +
+
+ +
+
`; + + const fixture = createTestComponent(html); + + fixture.whenStable() + .then(() => { + fixture.componentInstance.disabled = true; + fixture.detectChanges(); + expect(getLabel(fixture.nativeElement, 0)).toHaveCssClass('disabled'); + expect(getInput(fixture.nativeElement, 0).hasAttribute('disabled')).toBeTruthy(); + + fixture.componentInstance.disabled = false; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + fixture.detectChanges(); + expect(getLabel(fixture.nativeElement, 0)).not.toHaveCssClass('disabled'); + expect(getInput(fixture.nativeElement, 0).hasAttribute('disabled')).toBeFalsy(); + }); + })); + + it('disable all radio buttons when group is disabled regardless of button disabled state', async(() => { + const html = ` +
+
+ +
+
`; + + const fixture = createTestComponent(html); + + fixture.whenStable() + .then(() => { + expect(getLabel(fixture.nativeElement, 0)).toHaveCssClass('disabled'); + expect(getInput(fixture.nativeElement, 0).hasAttribute('disabled')).toBeTruthy(); + + fixture.componentInstance.disabled = false; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + fixture.detectChanges(); + expect(getLabel(fixture.nativeElement, 0)).toHaveCssClass('disabled'); + expect(getInput(fixture.nativeElement, 0).hasAttribute('disabled')).toBeTruthy(); + }); + })); + + it('button stays disabled when group is enabled', async(() => { + const html = ` +
+
+ +
+
`; + + const fixture = createTestComponent(html); + + fixture.whenStable() + .then(() => { + expect(getLabel(fixture.nativeElement, 0)).toHaveCssClass('disabled'); + expect(getInput(fixture.nativeElement, 0).hasAttribute('disabled')).toBeTruthy(); + + fixture.componentInstance.groupDisabled = false; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + fixture.detectChanges(); + expect(getLabel(fixture.nativeElement, 0)).toHaveCssClass('disabled'); + expect(getInput(fixture.nativeElement, 0).hasAttribute('disabled')).toBeTruthy(); + }); + })); + + + it('should add / remove "focus" class on labels', () => { + const fixture = createTestComponent(` +
+ + +
+ `); + fixture.detectChanges(); + + const inputDebugEls = fixture.debugElement.queryAll(By.css('Input')); + + inputDebugEls[0].triggerEventHandler('focus', {}); + fixture.detectChanges(); + expect(inputDebugEls[0].nativeElement.parentNode).toHaveCssClass('focus'); + expect(inputDebugEls[1].nativeElement.parentNode).not.toHaveCssClass('focus'); + + inputDebugEls[0].triggerEventHandler('blur', {}); + inputDebugEls[1].triggerEventHandler('focus', {}); + fixture.detectChanges(); + expect(inputDebugEls[0].nativeElement.parentNode).not.toHaveCssClass('focus'); + expect(inputDebugEls[1].nativeElement.parentNode).toHaveCssClass('focus'); + }); + + it('should mark form control as touched when label loses focus', () => { + const fixture = createTestComponent(` +
+ + +
+ `); + fixture.detectChanges(); + + const inputDebugEls = fixture.debugElement.queryAll(By.css('Input')); + const ngModel = fixture.debugElement.query(By.directive(NgModel)).injector.get(NgModel); + + inputDebugEls[0].triggerEventHandler('focus', {}); + fixture.detectChanges(); + expect(ngModel.touched).toBe(false); + + inputDebugEls[0].triggerEventHandler('blur', {}); + fixture.detectChanges(); + expect(ngModel.touched).toBe(true); + }); + + it('should generate input names automatically if no name specified anywhere', () => { + const fixture = createTestComponent(` +
+ + +
+ `); + fixture.detectChanges(); + + const inputs = fixture.nativeElement.querySelectorAll('input'); + const distinctNames = new Set(); + for (let i = 0; i < inputs.length; i++) { + distinctNames.add(inputs[i].getAttribute('name')); + } + expect(distinctNames.size).toBe(1); + expect(distinctNames.values().next().value).toMatch(/ngb-radio-\d+/); + }); + + it('should set input names from group name if inputs don\'t have a name', () => { + const fixture = createTestComponent(` +
+ + +
+ `); + fixture.detectChanges(); + + const inputs = fixture.nativeElement.querySelectorAll('input'); + expectNameOnAllInputs(fixture.nativeElement, 'foo'); + }); + + it('should honor the input names if specified', () => { + const fixture = createTestComponent(` +
+ + +
+ `); + fixture.detectChanges(); + + const inputs = fixture.nativeElement.querySelectorAll('input'); + expectNameOnAllInputs(fixture.nativeElement, 'bar'); + }); + + describe('accessibility', () => { + it('should have "group" role', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + expect(getGroupElement(fixture.nativeElement).getAttribute('role')).toBe('radiogroup'); + }); + }); + + describe('on push', () => { + it('should set initial model value', fakeAsync(() => { + const fixture = TestBed.createComponent(TestComponentOnPush); + const {values} = fixture.componentInstance; + + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + expect(getInput(fixture.nativeElement, 0).value).toEqual(values[0]); + expect(getInput(fixture.nativeElement, 1).value).toEqual(values[1]); + expectRadios(fixture.nativeElement, [1, 0]); + })); + }); +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + form = new FormGroup({control: new FormControl('', Validators.required)}); + disabledControl = new FormControl({value: '', disabled: true}); + disabledForm = new FormGroup({control: this.disabledControl}); + + model; + values: any = ['one', 'two', 'three']; + shown = true; + disabled = true; + groupDisabled = true; + checked: any; +} + +@Component({selector: 'test-cmp-on-push', template: '', changeDetection: ChangeDetectionStrategy.OnPush}) +class TestComponentOnPush { + model = 'one'; + values = ['one', 'two', 'three']; +} diff --git a/src/buttons/radio.ts b/src/buttons/radio.ts new file mode 100644 index 0000000..efdd9a7 --- /dev/null +++ b/src/buttons/radio.ts @@ -0,0 +1,159 @@ +import {ChangeDetectorRef, Directive, ElementRef, forwardRef, Input, OnDestroy, Renderer2} from '@angular/core'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; + +import {NgbButtonLabel} from './label'; + +const NGB_RADIO_VALUE_ACCESSOR = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NgbRadioGroup), + multi: true +}; + +let nextId = 0; + +/** + * Allows to easily create Bootstrap-style radio buttons. + * + * Integrates with forms, so the value of a checked button is bound to the underlying form control + * either in a reactive or template-driven way. + */ +@Directive({selector: '[ngbRadioGroup]', host: {'role': 'radiogroup'}, providers: [NGB_RADIO_VALUE_ACCESSOR]}) +export class NgbRadioGroup implements ControlValueAccessor { + private _radios: Set = new Set(); + private _value = null; + private _disabled: boolean; + + get disabled() { return this._disabled; } + set disabled(isDisabled: boolean) { this.setDisabledState(isDisabled); } + + /** + * Name of the radio group applied to radio input elements. + * + * Will be applied to all radio input elements inside the group, + * unless [`NgbRadio`](#/components/buttons/api#NgbRadio)'s specify names themselves. + * + * If not provided, will be generated in the `ngb-radio-xx` format. + */ + @Input() name = `ngb-radio-${nextId++}`; + + onChange = (_: any) => {}; + onTouched = () => {}; + + onRadioChange(radio: NgbRadio) { + this.writeValue(radio.value); + this.onChange(radio.value); + } + + onRadioValueUpdate() { this._updateRadiosValue(); } + + register(radio: NgbRadio) { this._radios.add(radio); } + + registerOnChange(fn: (value: any) => any): void { this.onChange = fn; } + + registerOnTouched(fn: () => any): void { this.onTouched = fn; } + + setDisabledState(isDisabled: boolean): void { + this._disabled = isDisabled; + this._updateRadiosDisabled(); + } + + unregister(radio: NgbRadio) { this._radios.delete(radio); } + + writeValue(value) { + this._value = value; + this._updateRadiosValue(); + } + + private _updateRadiosValue() { this._radios.forEach((radio) => radio.updateValue(this._value)); } + private _updateRadiosDisabled() { this._radios.forEach((radio) => radio.updateDisabled()); } +} + + +/** + * A directive that marks an input of type "radio" as a part of the + * [`NgbRadioGroup`](#/components/buttons/api#NgbRadioGroup). + */ +@Directive({ + selector: '[ngbButton][type=radio]', + host: { + '[checked]': 'checked', + '[disabled]': 'disabled', + '[name]': 'nameAttr', + '(change)': 'onChange()', + '(focus)': 'focused = true', + '(blur)': 'focused = false' + } +}) +export class NgbRadio implements OnDestroy { + private _checked: boolean; + private _disabled: boolean; + private _value: any = null; + + /** + * The value for the 'name' property of the input element. + * + * All inputs of the radio group should have the same name. If not specified, + * the name of the enclosing group is used. + */ + @Input() name: string; + + /** + * The form control value when current radio button is checked. + */ + @Input('value') + set value(value: any) { + this._value = value; + const stringValue = value ? value.toString() : ''; + this._renderer.setProperty(this._element.nativeElement, 'value', stringValue); + this._group.onRadioValueUpdate(); + } + + /** + * If `true`, current radio button will be disabled. + */ + @Input('disabled') + set disabled(isDisabled: boolean) { + this._disabled = isDisabled !== false; + this.updateDisabled(); + } + + set focused(isFocused: boolean) { + if (this._label) { + this._label.focused = isFocused; + } + if (!isFocused) { + this._group.onTouched(); + } + } + + get checked() { return this._checked; } + + get disabled() { return this._group.disabled || this._disabled; } + + get value() { return this._value; } + + get nameAttr() { return this.name || this._group.name; } + + constructor( + private _group: NgbRadioGroup, private _label: NgbButtonLabel, private _renderer: Renderer2, + private _element: ElementRef, private _cd: ChangeDetectorRef) { + this._group.register(this); + this.updateDisabled(); + } + + ngOnDestroy() { this._group.unregister(this); } + + onChange() { this._group.onRadioChange(this); } + + updateValue(value) { + // label won't be updated, if it is inside the OnPush component when [ngModel] changes + if (this.value !== value) { + this._cd.markForCheck(); + } + + this._checked = this.value === value; + this._label.active = this._checked; + } + + updateDisabled() { this._label.disabled = this.disabled; } +} diff --git a/src/carousel/carousel-config.spec.ts b/src/carousel/carousel-config.spec.ts new file mode 100644 index 0000000..a13334a --- /dev/null +++ b/src/carousel/carousel-config.spec.ts @@ -0,0 +1,14 @@ +import {NgbCarouselConfig} from './carousel-config'; + +describe('ngb-carousel-config', () => { + it('should have sensible default values', () => { + const config = new NgbCarouselConfig(); + + expect(config.interval).toBe(5000); + expect(config.keyboard).toBe(true); + expect(config.wrap).toBe(true); + expect(config.pauseOnHover).toBe(true); + expect(config.showNavigationIndicators).toBe(true); + expect(config.showNavigationArrows).toBe(true); + }); +}); diff --git a/src/carousel/carousel-config.ts b/src/carousel/carousel-config.ts new file mode 100644 index 0000000..c21a453 --- /dev/null +++ b/src/carousel/carousel-config.ts @@ -0,0 +1,17 @@ +import {Injectable} from '@angular/core'; + +/** + * A configuration service for the [NgbCarousel](#/components/carousel/api#NgbCarousel) component. + * + * You can inject this service, typically in your root component, and customize its properties + * to provide default values for all carousels used in the application. + */ +@Injectable({providedIn: 'root'}) +export class NgbCarouselConfig { + interval = 5000; + wrap = true; + keyboard = true; + pauseOnHover = true; + showNavigationArrows = true; + showNavigationIndicators = true; +} diff --git a/src/carousel/carousel.module.ts b/src/carousel/carousel.module.ts new file mode 100644 index 0000000..51905c9 --- /dev/null +++ b/src/carousel/carousel.module.ts @@ -0,0 +1,11 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; + +import {NGB_CAROUSEL_DIRECTIVES} from './carousel'; + +export {NgbCarousel, NgbSlide, NgbSlideEvent, NgbSlideEventDirection, NgbSlideEventSource} from './carousel'; +export {NgbCarouselConfig} from './carousel-config'; + +@NgModule({declarations: NGB_CAROUSEL_DIRECTIVES, exports: NGB_CAROUSEL_DIRECTIVES, imports: [CommonModule]}) +export class NgbCarouselModule { +} diff --git a/src/carousel/carousel.spec.ts b/src/carousel/carousel.spec.ts new file mode 100644 index 0000000..e3ad7d6 --- /dev/null +++ b/src/carousel/carousel.spec.ts @@ -0,0 +1,909 @@ +import {fakeAsync, discardPeriodicTasks, tick, TestBed, ComponentFixture, inject} from '@angular/core/testing'; +import {createGenericTestComponent} from '../test/common'; + +import {By} from '@angular/platform-browser'; +import {ChangeDetectionStrategy, Component} from '@angular/core'; + +import {NgbCarouselModule} from './carousel.module'; +import {NgbCarousel, NgbSlideEvent, NgbSlideEventDirection, NgbSlideEventSource} from './carousel'; +import {NgbCarouselConfig} from './carousel-config'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +function expectActiveSlides(nativeEl: HTMLDivElement, active: boolean[]) { + const slideElms = nativeEl.querySelectorAll('.carousel-item'); + const indicatorElms = nativeEl.querySelectorAll('ol.carousel-indicators > li'); + + expect(slideElms.length).toBe(active.length); + expect(indicatorElms.length).toBe(active.length); + + for (let i = 0; i < active.length; i++) { + if (active[i]) { + expect(slideElms[i]).toHaveCssClass('active'); + expect(indicatorElms[i]).toHaveCssClass('active'); + } else { + expect(slideElms[i]).not.toHaveCssClass('active'); + expect(indicatorElms[i]).not.toHaveCssClass('active'); + } + } +} + +describe('ngb-carousel', () => { + beforeEach(() => { + TestBed.configureTestingModule({declarations: [TestComponent, TestComponentOnPush], imports: [NgbCarouselModule]}); + }); + + it('should initialize inputs with default values', () => { + const defaultConfig = new NgbCarouselConfig(); + const carousel = new NgbCarousel(new NgbCarouselConfig(), null, null, null); + + expect(carousel.interval).toBe(defaultConfig.interval); + expect(carousel.wrap).toBe(defaultConfig.wrap); + expect(carousel.keyboard).toBe(defaultConfig.keyboard); + expect(carousel.pauseOnHover).toBe(defaultConfig.pauseOnHover); + expect(carousel.showNavigationIndicators).toBe(defaultConfig.showNavigationIndicators); + expect(carousel.showNavigationArrows).toBe(defaultConfig.showNavigationArrows); + }); + + it('should render slides and navigation indicators', fakeAsync(() => { + const html = ` + + foo + bar + + `; + const fixture = createTestComponent(html); + + const slideElms = fixture.nativeElement.querySelectorAll('.carousel-item'); + expect(slideElms.length).toBe(2); + expect(slideElms[0].textContent).toMatch(/foo/); + expect(slideElms[1].textContent).toMatch(/bar/); + + expect(fixture.nativeElement.querySelectorAll('ol.carousel-indicators > li').length).toBe(2); + expect(fixture.nativeElement.querySelectorAll('[role="button"]').length).toBe(2); + + discardPeriodicTasks(); + })); + + + it('should mark the first slide as active by default', fakeAsync(() => { + const html = ` + + foo + bar + + `; + + const fixture = createTestComponent(html); + expectActiveSlides(fixture.nativeElement, [true, false]); + + discardPeriodicTasks(); + })); + + it('should work without any slides', fakeAsync(() => { + const fixture = createTestComponent(``); + + tick(1001); + fixture.detectChanges(); + + const carousel = fixture.nativeElement.querySelector('ngb-carousel'); + const slides = fixture.nativeElement.querySelectorAll('.carousel-item'); + + expect(carousel).toBeTruthy(); + expect(slides.length).toBe(0); + + discardPeriodicTasks(); + })); + + + it('should mark the requested slide as active', fakeAsync(() => { + const html = ` + + foo + bar + + `; + + const fixture = createTestComponent(html); + + fixture.componentInstance.activeSlideId = 1; + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + + discardPeriodicTasks(); + })); + + it('should auto-correct when slide index is undefined', fakeAsync(() => { + const html = ` + + foo + bar + + `; + + const fixture = createTestComponent(html); + expectActiveSlides(fixture.nativeElement, [true, false]); + + discardPeriodicTasks(); + })); + + it('should change slide on prev/next API calls', fakeAsync(() => { + const html = ` + + foo + bar + baz + + + + + `; + + const fixture = createTestComponent(html); + const next = fixture.nativeElement.querySelector('#next'); + const prev = fixture.nativeElement.querySelector('#prev'); + const select = fixture.nativeElement.querySelector('#select'); + + expectActiveSlides(fixture.nativeElement, [true, false, false]); + + next.click(); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true, false]); + + prev.click(); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false, false]); + + select.click(); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, false, true]); + })); + + it('should pause/resume slide change on API calls', fakeAsync(() => { + const html = ` + + foo + bar + + + + `; + + const fixture = createTestComponent(html); + const pause = fixture.nativeElement.querySelector('#pause'); + const cycle = fixture.nativeElement.querySelector('#cycle'); + + expectActiveSlides(fixture.nativeElement, [true, false]); + + tick(1000); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true]); + + pause.click(); + tick(1000); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true]); + + cycle.click(); + tick(1000); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + + discardPeriodicTasks(); + })); + + it('should not resume without call to cycle()', fakeAsync(() => { + const html = ` + + foo + bar + third + + + + + `; + + const fixture = createTestComponent(html); + const spyCallBack = spyOn(fixture.componentInstance, 'carouselSlideCallBack'); + const carouselDebugEl = fixture.debugElement.query(By.directive(NgbCarousel)); + const indicatorElms = fixture.nativeElement.querySelectorAll('ol.carousel-indicators > li'); + const prevControlElm = fixture.nativeElement.querySelector('.carousel-control-prev'); + const nextControlElm = fixture.nativeElement.querySelector('.carousel-control-next'); + const next = fixture.nativeElement.querySelector('#next'); + const pause = fixture.nativeElement.querySelector('#pause'); + const cycle = fixture.nativeElement.querySelector('#cycle'); + + expectActiveSlides(fixture.nativeElement, [true, false, false]); + + tick(1000); + fixture.detectChanges(); + expect(spyCallBack) + .toHaveBeenCalledWith(jasmine.objectContaining({paused: false, source: NgbSlideEventSource.TIMER})); + spyCallBack.calls.reset(); + expectActiveSlides(fixture.nativeElement, [false, true, false]); + + pause.click(); + tick(1000); + fixture.detectChanges(); + expect(spyCallBack).not.toHaveBeenCalled(); + expectActiveSlides(fixture.nativeElement, [false, true, false]); + + indicatorElms[0].click(); + fixture.detectChanges(); + expect(spyCallBack) + .toHaveBeenCalledWith(jasmine.objectContaining({paused: true, source: NgbSlideEventSource.INDICATOR})); + spyCallBack.calls.reset(); + expectActiveSlides(fixture.nativeElement, [true, false, false]); + tick(1000); + fixture.detectChanges(); + expect(spyCallBack).not.toHaveBeenCalled(); + expectActiveSlides(fixture.nativeElement, [true, false, false]); + + nextControlElm.click(); + fixture.detectChanges(); + expect(spyCallBack) + .toHaveBeenCalledWith(jasmine.objectContaining({paused: true, source: NgbSlideEventSource.ARROW_RIGHT})); + spyCallBack.calls.reset(); + expectActiveSlides(fixture.nativeElement, [false, true, false]); + tick(1000); + fixture.detectChanges(); + expect(spyCallBack).not.toHaveBeenCalled(); + expectActiveSlides(fixture.nativeElement, [false, true, false]); + + prevControlElm.click(); + fixture.detectChanges(); + expect(spyCallBack) + .toHaveBeenCalledWith(jasmine.objectContaining({paused: true, source: NgbSlideEventSource.ARROW_LEFT})); + spyCallBack.calls.reset(); + expectActiveSlides(fixture.nativeElement, [true, false, false]); + tick(1000); + fixture.detectChanges(); + expect(spyCallBack).not.toHaveBeenCalled(); + expectActiveSlides(fixture.nativeElement, [true, false, false]); + + next.click(); + fixture.detectChanges(); + expect(spyCallBack).toHaveBeenCalledWith(jasmine.objectContaining({paused: true})); + spyCallBack.calls.reset(); + expectActiveSlides(fixture.nativeElement, [false, true, false]); + tick(1000); + fixture.detectChanges(); + expect(spyCallBack).not.toHaveBeenCalled(); + expectActiveSlides(fixture.nativeElement, [false, true, false]); + + carouselDebugEl.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + carouselDebugEl.triggerEventHandler('mouseleave', {}); + fixture.detectChanges(); + tick(1000); + fixture.detectChanges(); + expect(spyCallBack).not.toHaveBeenCalled(); + expectActiveSlides(fixture.nativeElement, [false, true, false]); + + cycle.click(); + tick(1000); + fixture.detectChanges(); + expect(spyCallBack) + .toHaveBeenCalledWith(jasmine.objectContaining({paused: false, source: NgbSlideEventSource.TIMER})); + expectActiveSlides(fixture.nativeElement, [false, false, true]); + + discardPeriodicTasks(); + })); + + + it('should mark component for check for API calls', () => { + const html = ` + + foo + bar + baz + + + `; + + const fixture = createTestComponent(html); + const next = fixture.nativeElement.querySelector('#next'); + + expectActiveSlides(fixture.nativeElement, [true, false]); + + next.click(); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true, false]); + }); + + it('should mark component for check when slides change', () => { + const html = ` + + +
{{ s }}
+
+
+ `; + + function getSlidesText(element: HTMLElement): string[] { + return Array.from(element.querySelectorAll('.carousel-item .slide')).map((el: HTMLElement) => el.innerHTML); + } + + const fixture = createTestComponent(html); + expect(getSlidesText(fixture.nativeElement)).toEqual(['a', 'b']); + + fixture.componentInstance.slides = ['c', 'd']; + fixture.detectChanges(); + expect(getSlidesText(fixture.nativeElement)).toEqual(['c', 'd']); + }); + + it('should change slide on indicator click', fakeAsync(() => { + const html = ` + + foo + bar + + `; + + const fixture = createTestComponent(html); + const indicatorElms = fixture.nativeElement.querySelectorAll('ol.carousel-indicators > li'); + + expectActiveSlides(fixture.nativeElement, [true, false]); + + indicatorElms[1].click(); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true]); + + discardPeriodicTasks(); + })); + + it('should fire a slide event with correct direction and source on indicator click', fakeAsync(() => { + const html = ` + + foo + bar + pluto + + `; + + const fixture = createTestComponent(html); + const indicatorElms = fixture.nativeElement.querySelectorAll('ol.carousel-indicators > li'); + const spyCallBack = spyOn(fixture.componentInstance, 'carouselSlideCallBack'); + + indicatorElms[1].click(); + fixture.detectChanges(); + expect(fixture.componentInstance.carouselSlideCallBack).toHaveBeenCalledWith(jasmine.objectContaining({ + direction: NgbSlideEventDirection.LEFT, + source: NgbSlideEventSource.INDICATOR + })); + + spyCallBack.calls.reset(); + indicatorElms[0].click(); + fixture.detectChanges(); + expect(fixture.componentInstance.carouselSlideCallBack).toHaveBeenCalledWith(jasmine.objectContaining({ + direction: NgbSlideEventDirection.RIGHT, + source: NgbSlideEventSource.INDICATOR + })); + + spyCallBack.calls.reset(); + indicatorElms[2].click(); + fixture.detectChanges(); + expect(fixture.componentInstance.carouselSlideCallBack).toHaveBeenCalledWith(jasmine.objectContaining({ + direction: NgbSlideEventDirection.LEFT, + source: NgbSlideEventSource.INDICATOR + })); + + discardPeriodicTasks(); + })); + + it('should change slide on carousel control click', fakeAsync(() => { + const html = ` + + foo + bar + + `; + + const fixture = createTestComponent(html); + + const prevControlElm = fixture.nativeElement.querySelector('.carousel-control-prev'); + const nextControlElm = fixture.nativeElement.querySelector('.carousel-control-next'); + + expectActiveSlides(fixture.nativeElement, [true, false]); + + nextControlElm.click(); // next + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true]); + + prevControlElm.click(); // prev + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + + discardPeriodicTasks(); + })); + + it('should fire a slide event with correct direction and source on carousel control click', fakeAsync(() => { + const html = ` + + foo + bar + + `; + + const fixture = createTestComponent(html); + const prevControlElm = fixture.nativeElement.querySelector('.carousel-control-prev'); + const nextControlElm = fixture.nativeElement.querySelector('.carousel-control-next'); + const spyCallBack = spyOn(fixture.componentInstance, 'carouselSlideCallBack'); + + prevControlElm.click(); + fixture.detectChanges(); + expect(fixture.componentInstance.carouselSlideCallBack).toHaveBeenCalledWith(jasmine.objectContaining({ + direction: NgbSlideEventDirection.RIGHT, + source: NgbSlideEventSource.ARROW_LEFT + })); + spyCallBack.calls.reset(); + nextControlElm.click(); + fixture.detectChanges(); + expect(fixture.componentInstance.carouselSlideCallBack).toHaveBeenCalledWith(jasmine.objectContaining({ + direction: NgbSlideEventDirection.LEFT, + source: NgbSlideEventSource.ARROW_RIGHT + })); + + spyCallBack.calls.reset(); + prevControlElm.click(); + fixture.detectChanges(); + expect(fixture.componentInstance.carouselSlideCallBack).toHaveBeenCalledWith(jasmine.objectContaining({ + direction: NgbSlideEventDirection.RIGHT, + source: NgbSlideEventSource.ARROW_LEFT + })); + + discardPeriodicTasks(); + })); + + it('should change slide on time passage (default interval value)', fakeAsync(() => { + const html = ` + + foo + bar + + `; + + const fixture = createTestComponent(html); + + expectActiveSlides(fixture.nativeElement, [true, false]); + + tick(6000); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true]); + + discardPeriodicTasks(); + })); + + it('should fire a slide event with correct direction and source on time passage', fakeAsync(() => { + const html = ` + + foo + bar + + `; + + const fixture = createTestComponent(html); + const spyCallBack = spyOn(fixture.componentInstance, 'carouselSlideCallBack'); + + tick(1999); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + expect(spyCallBack).not.toHaveBeenCalled(); + + tick(1); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true]); + expect(spyCallBack).toHaveBeenCalledWith(jasmine.objectContaining({ + direction: NgbSlideEventDirection.LEFT, + source: NgbSlideEventSource.TIMER + })); + + discardPeriodicTasks(); + })); + + it('should change slide on time passage in OnPush component (default interval value)', fakeAsync(() => { + const fixture = createTestComponent(''); + + expectActiveSlides(fixture.nativeElement, [true, false]); + + tick(6000); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true]); + + discardPeriodicTasks(); + })); + + it('should change slide on time passage (custom interval value)', fakeAsync(() => { + const html = ` + + foo + bar + + `; + + const fixture = createTestComponent(html); + + expectActiveSlides(fixture.nativeElement, [true, false]); + + tick(1000); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + + tick(1200); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true]); + + discardPeriodicTasks(); + })); + + it('should not change slide on time passage (custom interval value is zero)', fakeAsync(() => { + const html = ` + + foo + bar + + `; + + const fixture = createTestComponent(html); + + expectActiveSlides(fixture.nativeElement, [true, false]); + + tick(1000); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + + tick(1200); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + + discardPeriodicTasks(); + })); + + it('should change slide with different rate when interval value changed', fakeAsync(() => { + const html = ` + + foo + bar + zoo + + `; + + const fixture = createTestComponent(html); + fixture.componentInstance.interval = 5000; + fixture.detectChanges(); + + expectActiveSlides(fixture.nativeElement, [true, false, false]); + + tick(5001); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true, false]); + + fixture.componentInstance.interval = 1000; + fixture.detectChanges(); + + tick(1001); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, false, true]); + + discardPeriodicTasks(); + })); + + it('should listen to mouse events based on pauseOnHover attribute', fakeAsync(() => { + + const html = ` + + foo + bar + + `; + + const fixture = createTestComponent(html); + + const carouselDebugEl = fixture.debugElement.query(By.directive(NgbCarousel)); + + expectActiveSlides(fixture.nativeElement, [true, false]); + + carouselDebugEl.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + + tick(6000); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + + carouselDebugEl.triggerEventHandler('mouseleave', {}); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + + tick(6000); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true]); + + fixture.componentInstance.pauseOnHover = false; + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true]); + + carouselDebugEl.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + + tick(6000); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + discardPeriodicTasks(); + })); + + it('should pause / resume slide change with time passage on mouse enter / leave', fakeAsync(() => { + const html = ` + + foo + bar + + `; + + const fixture = createTestComponent(html); + + const carouselDebugEl = fixture.debugElement.query(By.directive(NgbCarousel)); + + expectActiveSlides(fixture.nativeElement, [true, false]); + + carouselDebugEl.triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + + tick(6000); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + + carouselDebugEl.triggerEventHandler('mouseleave', {}); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + + tick(6000); + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true]); + + discardPeriodicTasks(); + })); + + it('should wrap slide changes by default', fakeAsync(() => { + const html = ` + + foo + bar + + `; + + const fixture = createTestComponent(html); + + const prevControlElm = fixture.nativeElement.querySelector('.carousel-control-prev'); + const nextControlElm = fixture.nativeElement.querySelector('.carousel-control-next'); + + expectActiveSlides(fixture.nativeElement, [true, false]); + + nextControlElm.click(); // next + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true]); + + nextControlElm.click(); // next + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + + prevControlElm.click(); // prev + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true]); + + discardPeriodicTasks(); + })); + + it('should not wrap slide changes by when requested', fakeAsync(() => { + const html = ` + + foo + bar + + `; + + const fixture = createTestComponent(html); + + const prevControlElm = fixture.nativeElement.querySelector('.carousel-control-prev'); + const nextControlElm = fixture.nativeElement.querySelector('.carousel-control-next'); + + expectActiveSlides(fixture.nativeElement, [true, false]); + + prevControlElm.click(); // prev + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + + nextControlElm.click(); // next + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true]); + + nextControlElm.click(); // next + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true]); + + discardPeriodicTasks(); + })); + + it('should change on key arrowRight and arrowLeft', fakeAsync(() => { + const html = ` + + foo + bar + + `; + + const fixture = createTestComponent(html); + expectActiveSlides(fixture.nativeElement, [true, false]); + + fixture.debugElement.query(By.directive(NgbCarousel)).triggerEventHandler('keydown.arrowRight', {}); // next() + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true]); + + fixture.debugElement.query(By.directive(NgbCarousel)).triggerEventHandler('keydown.arrowLeft', {}); // prev() + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + + fixture.componentInstance.keyboard = false; + fixture.detectChanges(); + fixture.debugElement.query(By.directive(NgbCarousel)).triggerEventHandler('keydown.arrowRight', {}); // prev() + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + + + discardPeriodicTasks(); + + })); + + it('should listen to keyevents based on keyboard attribute', fakeAsync(() => { + const html = ` + + foo + bar + + `; + + const fixture = createTestComponent(html); + expectActiveSlides(fixture.nativeElement, [true, false]); + + fixture.componentInstance.keyboard = false; + fixture.detectChanges(); + fixture.debugElement.query(By.directive(NgbCarousel)).triggerEventHandler('keydown.arrowRight', {}); // prev() + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [true, false]); + + fixture.componentInstance.keyboard = true; + fixture.detectChanges(); + fixture.debugElement.query(By.directive(NgbCarousel)).triggerEventHandler('keydown.arrowRight', {}); // next() + fixture.detectChanges(); + expectActiveSlides(fixture.nativeElement, [false, true]); + + discardPeriodicTasks(); + + })); + + it('should render navigation indicators according to the flags', fakeAsync(() => { + const html = ` + + foo + + `; + const fixture = createTestComponent(html); + + const slideElms = fixture.nativeElement.querySelectorAll('.carousel-item'); + expect(slideElms.length).toBe(1); + expect(slideElms[0].textContent).toMatch(/foo/); + expect(fixture.nativeElement.querySelectorAll('ol.carousel-indicators > li').length).toBe(1); + + fixture.componentInstance.showNavigationIndicators = false; + fixture.detectChanges(); + expect(fixture.nativeElement.querySelectorAll('ol.carousel-indicators > li').length).toBe(0); + + discardPeriodicTasks(); + })); + + it('should render navigation buttons according to the flags', fakeAsync(() => { + const html = ` + + foo + + `; + const fixture = createTestComponent(html); + + const slideElms = fixture.nativeElement.querySelectorAll('.carousel-item'); + expect(slideElms.length).toBe(1); + expect(fixture.nativeElement.querySelectorAll('[role="button"]').length).toBe(2); + + fixture.componentInstance.showNavigationArrows = false; + fixture.detectChanges(); + expect(fixture.nativeElement.querySelectorAll('[role="button"]').length).toBe(0); + + discardPeriodicTasks(); + })); + + describe('Custom config', () => { + let config: NgbCarouselConfig; + + beforeEach(() => { TestBed.configureTestingModule({imports: [NgbCarouselModule]}); }); + + beforeEach(inject([NgbCarouselConfig], (c: NgbCarouselConfig) => { + config = c; + config.interval = 1000; + config.wrap = false; + config.keyboard = false; + config.pauseOnHover = false; + config.showNavigationIndicators = true; + config.showNavigationArrows = true; + })); + + it('should initialize inputs with provided config', () => { + const fixture = TestBed.createComponent(NgbCarousel); + fixture.detectChanges(); + + const carousel = fixture.componentInstance; + expect(carousel.interval).toBe(config.interval); + expect(carousel.wrap).toBe(config.wrap); + expect(carousel.keyboard).toBe(config.keyboard); + expect(carousel.pauseOnHover).toBe(config.pauseOnHover); + expect(carousel.showNavigationIndicators).toBe(config.showNavigationIndicators); + expect(carousel.showNavigationArrows).toBe(config.showNavigationArrows); + }); + }); + + describe('Custom config as provider', () => { + const config = new NgbCarouselConfig(); + config.interval = 1000; + config.wrap = false; + config.keyboard = false; + config.pauseOnHover = false; + config.showNavigationIndicators = true; + config.showNavigationArrows = true; + + beforeEach(() => { + TestBed.configureTestingModule( + {imports: [NgbCarouselModule], providers: [{provide: NgbCarouselConfig, useValue: config}]}); + }); + + it('should initialize inputs with provided config as provider', () => { + const fixture = TestBed.createComponent(NgbCarousel); + fixture.detectChanges(); + + const carousel = fixture.componentInstance; + expect(carousel.interval).toBe(config.interval); + expect(carousel.wrap).toBe(config.wrap); + expect(carousel.keyboard).toBe(config.keyboard); + expect(carousel.pauseOnHover).toBe(config.pauseOnHover); + expect(carousel.showNavigationIndicators).toBe(config.showNavigationIndicators); + expect(carousel.showNavigationArrows).toBe(config.showNavigationArrows); + }); + }); + +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + addNewSlide = false; + interval; + activeSlideId; + keyboard = true; + pauseOnHover = true; + showNavigationArrows = true; + showNavigationIndicators = true; + slides = ['a', 'b']; + carouselSlideCallBack = (event: NgbSlideEvent) => {}; +} + +@Component({ + selector: 'test-cmp-on-push', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + foo + bar + + ` +}) +class TestComponentOnPush { +} diff --git a/src/carousel/carousel.ts b/src/carousel/carousel.ts new file mode 100644 index 0000000..531121f --- /dev/null +++ b/src/carousel/carousel.ts @@ -0,0 +1,342 @@ +import { + AfterContentChecked, + AfterContentInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ContentChildren, + Directive, + EventEmitter, + Inject, + Input, + NgZone, + OnDestroy, + Output, + PLATFORM_ID, + QueryList, + TemplateRef, + HostListener +} from '@angular/core'; +import {isPlatformBrowser} from '@angular/common'; + +import {NgbCarouselConfig} from './carousel-config'; + +import {Subject, timer, BehaviorSubject, combineLatest, NEVER} from 'rxjs'; +import {startWith, map, switchMap, takeUntil, distinctUntilChanged} from 'rxjs/operators'; + +let nextId = 0; + +/** + * A directive that wraps the individual carousel slide. + */ +@Directive({selector: 'ng-template[ngbSlide]'}) +export class NgbSlide { + /** + * Slide id that must be unique for the entire document. + * + * If not provided, will be generated in the `ngb-slide-xx` format. + */ + @Input() id = `ngb-slide-${nextId++}`; + constructor(public tplRef: TemplateRef) {} +} + +/** + * Carousel is a component to easily create and control slideshows. + * + * Allows to set intervals, change the way user interacts with the slides and provides a programmatic API. + */ +@Component({ + selector: 'ngb-carousel', + exportAs: 'ngbCarousel', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + 'class': 'carousel slide', + '[style.display]': '"block"', + 'tabIndex': '0', + '(keydown.arrowLeft)': 'keyboard && prev(NgbSlideEventSource.ARROW_LEFT)', + '(keydown.arrowRight)': 'keyboard && next(NgbSlideEventSource.ARROW_RIGHT)' + }, + template: ` + + + + + Previous + + + + Next + + ` +}) +export class NgbCarousel implements AfterContentChecked, + AfterContentInit, OnDestroy { + @ContentChildren(NgbSlide) slides: QueryList; + + public NgbSlideEventSource = NgbSlideEventSource; + + private _destroy$ = new Subject(); + private _interval$ = new BehaviorSubject(0); + private _mouseHover$ = new BehaviorSubject(false); + private _pauseOnHover$ = new BehaviorSubject(false); + private _pause$ = new BehaviorSubject(false); + private _wrap$ = new BehaviorSubject(false); + + /** + * The slide id that should be displayed **initially**. + * + * For subsequent interactions use methods `select()`, `next()`, etc. and the `(slide)` output. + */ + @Input() activeId: string; + + /** + * Time in milliseconds before the next slide is shown. + */ + @Input() + set interval(value: number) { + this._interval$.next(value); + } + + get interval() { return this._interval$.value; } + + /** + * If `true`, will 'wrap' the carousel by switching from the last slide back to the first. + */ + @Input() + set wrap(value: boolean) { + this._wrap$.next(value); + } + + get wrap() { return this._wrap$.value; } + + /** + * If `true`, allows to interact with carousel using keyboard 'arrow left' and 'arrow right'. + */ + @Input() keyboard: boolean; + + /** + * If `true`, will pause slide switching when mouse cursor hovers the slide. + * + * @since 2.2.0 + */ + @Input() + set pauseOnHover(value: boolean) { + this._pauseOnHover$.next(value); + } + + get pauseOnHover() { return this._pauseOnHover$.value; } + + /** + * If `true`, 'previous' and 'next' navigation arrows will be visible on the slide. + * + * @since 2.2.0 + */ + @Input() showNavigationArrows: boolean; + + /** + * If `true`, navigation indicators at the bottom of the slide will be visible. + * + * @since 2.2.0 + */ + @Input() showNavigationIndicators: boolean; + + /** + * An event emitted right after the slide transition is completed. + * + * See [`NgbSlideEvent`](#/components/carousel/api#NgbSlideEvent) for payload details. + */ + @Output() slide = new EventEmitter(); + + constructor( + config: NgbCarouselConfig, @Inject(PLATFORM_ID) private _platformId, private _ngZone: NgZone, + private _cd: ChangeDetectorRef) { + this.interval = config.interval; + this.wrap = config.wrap; + this.keyboard = config.keyboard; + this.pauseOnHover = config.pauseOnHover; + this.showNavigationArrows = config.showNavigationArrows; + this.showNavigationIndicators = config.showNavigationIndicators; + } + + @HostListener('mouseenter') + mouseEnter() { + this._mouseHover$.next(true); + } + + @HostListener('mouseleave') + mouseLeave() { + this._mouseHover$.next(false); + } + + ngAfterContentInit() { + // setInterval() doesn't play well with SSR and protractor, + // so we should run it in the browser and outside Angular + if (isPlatformBrowser(this._platformId)) { + this._ngZone.runOutsideAngular(() => { + const hasNextSlide$ = combineLatest( + this.slide.pipe(map(slideEvent => slideEvent.current), startWith(this.activeId)), + this._wrap$, this.slides.changes.pipe(startWith(null))) + .pipe( + map(([currentSlideId, wrap]) => { + const slideArr = this.slides.toArray(); + const currentSlideIdx = this._getSlideIdxById(currentSlideId); + return wrap ? slideArr.length > 1 : currentSlideIdx < slideArr.length - 1; + }), + distinctUntilChanged()); + combineLatest(this._pause$, this._pauseOnHover$, this._mouseHover$, this._interval$, hasNextSlide$) + .pipe( + map(([pause, pauseOnHover, mouseHover, interval, hasNextSlide]) => + ((pause || (pauseOnHover && mouseHover) || !hasNextSlide) ? 0 : interval)), + + distinctUntilChanged(), switchMap(interval => interval > 0 ? timer(interval, interval) : NEVER), + takeUntil(this._destroy$)) + .subscribe(() => this._ngZone.run(() => this.next(NgbSlideEventSource.TIMER))); + }); + } + + this.slides.changes.pipe(takeUntil(this._destroy$)).subscribe(() => this._cd.markForCheck()); + } + + ngAfterContentChecked() { + let activeSlide = this._getSlideById(this.activeId); + this.activeId = activeSlide ? activeSlide.id : (this.slides.length ? this.slides.first.id : null); + } + + ngOnDestroy() { this._destroy$.next(); } + + /** + * Navigates to a slide with the specified identifier. + */ + select(slideId: string, source?: NgbSlideEventSource) { + this._cycleToSelected(slideId, this._getSlideEventDirection(this.activeId, slideId), source); + } + + /** + * Navigates to the previous slide. + */ + prev(source?: NgbSlideEventSource) { + this._cycleToSelected(this._getPrevSlide(this.activeId), NgbSlideEventDirection.RIGHT, source); + } + + /** + * Navigates to the next slide. + */ + next(source?: NgbSlideEventSource) { + this._cycleToSelected(this._getNextSlide(this.activeId), NgbSlideEventDirection.LEFT, source); + } + + /** + * Pauses cycling through the slides. + */ + pause() { this._pause$.next(true); } + + /** + * Restarts cycling through the slides from left to right. + */ + cycle() { this._pause$.next(false); } + + private _cycleToSelected(slideIdx: string, direction: NgbSlideEventDirection, source?: NgbSlideEventSource) { + let selectedSlide = this._getSlideById(slideIdx); + if (selectedSlide && selectedSlide.id !== this.activeId) { + this.slide.emit( + {prev: this.activeId, current: selectedSlide.id, direction: direction, paused: this._pause$.value, source}); + this.activeId = selectedSlide.id; + } + + // we get here after the interval fires or any external API call like next(), prev() or select() + this._cd.markForCheck(); + } + + private _getSlideEventDirection(currentActiveSlideId: string, nextActiveSlideId: string): NgbSlideEventDirection { + const currentActiveSlideIdx = this._getSlideIdxById(currentActiveSlideId); + const nextActiveSlideIdx = this._getSlideIdxById(nextActiveSlideId); + + return currentActiveSlideIdx > nextActiveSlideIdx ? NgbSlideEventDirection.RIGHT : NgbSlideEventDirection.LEFT; + } + + private _getSlideById(slideId: string): NgbSlide { return this.slides.find(slide => slide.id === slideId); } + + private _getSlideIdxById(slideId: string): number { + return this.slides.toArray().indexOf(this._getSlideById(slideId)); + } + + private _getNextSlide(currentSlideId: string): string { + const slideArr = this.slides.toArray(); + const currentSlideIdx = this._getSlideIdxById(currentSlideId); + const isLastSlide = currentSlideIdx === slideArr.length - 1; + + return isLastSlide ? (this.wrap ? slideArr[0].id : slideArr[slideArr.length - 1].id) : + slideArr[currentSlideIdx + 1].id; + } + + private _getPrevSlide(currentSlideId: string): string { + const slideArr = this.slides.toArray(); + const currentSlideIdx = this._getSlideIdxById(currentSlideId); + const isFirstSlide = currentSlideIdx === 0; + + return isFirstSlide ? (this.wrap ? slideArr[slideArr.length - 1].id : slideArr[0].id) : + slideArr[currentSlideIdx - 1].id; + } +} + +/** + * A slide change event emitted right after the slide transition is completed. + */ +export interface NgbSlideEvent { + /** + * The previous slide id. + */ + prev: string; + + /** + * The current slide id. + */ + current: string; + + /** + * The slide event direction. + * + * Possible values are `'left' | 'right'`. + */ + direction: NgbSlideEventDirection; + + /** + * Whether the pause() method was called (and no cycle() call was done afterwards). + * + * @since 5.1.0 + */ + paused: boolean; + + /** + * Source triggering the slide change event. + * + * Possible values are `'timer' | 'arrowLeft' | 'arrowRight' | 'indicator'` + * + * @since 5.1.0 + */ + source?: NgbSlideEventSource; +} + +/** + * Defines the carousel slide transition direction. + */ +export enum NgbSlideEventDirection { + LEFT = 'left', + RIGHT = 'right' +} + +export enum NgbSlideEventSource { + TIMER = 'timer', + ARROW_LEFT = 'arrowLeft', + ARROW_RIGHT = 'arrowRight', + INDICATOR = 'indicator' +} + +export const NGB_CAROUSEL_DIRECTIVES = [NgbCarousel, NgbSlide]; diff --git a/src/collapse/collapse.module.ts b/src/collapse/collapse.module.ts new file mode 100644 index 0000000..c5722ff --- /dev/null +++ b/src/collapse/collapse.module.ts @@ -0,0 +1,8 @@ +import {NgModule} from '@angular/core'; +import {NgbCollapse} from './collapse'; + +export {NgbCollapse} from './collapse'; + +@NgModule({declarations: [NgbCollapse], exports: [NgbCollapse]}) +export class NgbCollapseModule { +} diff --git a/src/collapse/collapse.spec.ts b/src/collapse/collapse.spec.ts new file mode 100644 index 0000000..a48a519 --- /dev/null +++ b/src/collapse/collapse.spec.ts @@ -0,0 +1,75 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {createGenericTestComponent} from '../test/common'; + +import {Component} from '@angular/core'; + +import {NgbCollapseModule} from './collapse.module'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +function getCollapsibleContent(element: HTMLElement): HTMLDivElement { + return element.querySelector('.collapse'); +} + +describe('ngb-collapse', () => { + beforeEach(() => { TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbCollapseModule]}); }); + + it('should have content open', () => { + const fixture = createTestComponent(`
Some content
`); + + const collapseEl = getCollapsibleContent(fixture.nativeElement); + + expect(collapseEl).toHaveCssClass('show'); + }); + + it('should have content closed', () => { + const fixture = createTestComponent(`
Some content
`); + const tc = fixture.componentInstance; + tc.collapsed = true; + fixture.detectChanges(); + + const collapseEl = getCollapsibleContent(fixture.nativeElement); + + expect(collapseEl).not.toHaveCssClass('show'); + }); + + it('should toggle collapsed content based on bound model change', () => { + const fixture = createTestComponent(`
Some content
`); + + const tc = fixture.componentInstance; + const collapseEl = getCollapsibleContent(fixture.nativeElement); + expect(collapseEl).toHaveCssClass('show'); + + tc.collapsed = true; + fixture.detectChanges(); + expect(collapseEl).not.toHaveCssClass('show'); + + tc.collapsed = false; + fixture.detectChanges(); + expect(collapseEl).toHaveCssClass('show'); + }); + + it('should allow toggling collapse from outside', () => { + const fixture = createTestComponent(` + +
`); + + const compiled = fixture.nativeElement; + const collapseEl = getCollapsibleContent(compiled); + const buttonEl = compiled.querySelector('button'); + + buttonEl.click(); + fixture.detectChanges(); + expect(collapseEl).not.toHaveCssClass('show'); + + buttonEl.click(); + fixture.detectChanges(); + expect(collapseEl).toHaveCssClass('show'); + }); +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + collapsed = false; +} diff --git a/src/collapse/collapse.ts b/src/collapse/collapse.ts new file mode 100644 index 0000000..2661e4f --- /dev/null +++ b/src/collapse/collapse.ts @@ -0,0 +1,16 @@ +import {Directive, Input} from '@angular/core'; + +/** + * A directive to provide a simple way of hiding and showing elements on the page. + */ +@Directive({ + selector: '[ngbCollapse]', + exportAs: 'ngbCollapse', + host: {'[class.collapse]': 'true', '[class.show]': '!collapsed'} +}) +export class NgbCollapse { + /** + * If `true`, will collapse the element or show it otherwise. + */ + @Input('ngbCollapse') collapsed = false; +} diff --git a/src/datepicker/adapters/ngb-date-adapter.spec.ts b/src/datepicker/adapters/ngb-date-adapter.spec.ts new file mode 100644 index 0000000..8c9dd24 --- /dev/null +++ b/src/datepicker/adapters/ngb-date-adapter.spec.ts @@ -0,0 +1,56 @@ +import {NgbDateStructAdapter} from './ngb-date-adapter'; + +describe('ngb-date model adapter', () => { + let adapter: NgbDateStructAdapter; + + beforeEach(() => { adapter = new NgbDateStructAdapter(); }); + + describe('fromModel', () => { + + it('should convert invalid and incomplete values to null', () => { + expect(adapter.fromModel(null)).toBeNull(); + expect(adapter.fromModel(undefined)).toBeNull(); + expect(adapter.fromModel('')).toBeNull(); + expect(adapter.fromModel('s')).toBeNull(); + expect(adapter.fromModel(2)).toBeNull(); + expect(adapter.fromModel({})).toBeNull(); + expect(adapter.fromModel(new Date())).toBeNull(); + expect(adapter.fromModel({year: 2017, month: 10})).toBeNull(); + expect(adapter.fromModel({month: 10, day: 10})).toBeNull(); + expect(adapter.fromModel({year: 2017, day: 10})).toBeNull(); + expect(adapter.fromModel({year: '2017', month: 10, day: 10})).toBeNull(); + expect(adapter.fromModel({year: 2017, month: '10', day: 10})).toBeNull(); + expect(adapter.fromModel({year: 2017, month: 10, day: '10'})).toBeNull(); + }); + + it('should bypass numeric date', () => { + expect(adapter.fromModel({year: 0, month: 0, day: 0})).toEqual({year: 0, month: 0, day: 0}); + expect(adapter.fromModel({year: 2016, month: 5, day: 1})).toEqual({year: 2016, month: 5, day: 1}); + }); + }); + + describe('toModel', () => { + + it('should convert invalid and incomplete values to null', () => { + expect(adapter.toModel(null)).toBeNull(); + expect(adapter.toModel(undefined)).toBeNull(); + expect(adapter.toModel('')).toBeNull(); + expect(adapter.toModel('s')).toBeNull(); + expect(adapter.toModel(2)).toBeNull(); + expect(adapter.toModel({})).toBeNull(); + expect(adapter.toModel(new Date())).toBeNull(); + expect(adapter.toModel({year: 2017, month: 10})).toBeNull(); + expect(adapter.toModel({month: 10, day: 10})).toBeNull(); + expect(adapter.toModel({year: 2017, day: 10})).toBeNull(); + expect(adapter.toModel({year: '2017', month: 10, day: 10})).toBeNull(); + expect(adapter.toModel({year: 2017, month: '10', day: 10})).toBeNull(); + expect(adapter.toModel({year: 2017, month: 10, day: '10'})).toBeNull(); + }); + + it('should bypass numeric date', () => { + expect(adapter.toModel({year: 0, month: 0, day: 0})).toEqual({year: 0, month: 0, day: 0}); + expect(adapter.toModel({year: 2016, month: 10, day: 15})).toEqual({year: 2016, month: 10, day: 15}); + }); + }); + +}); diff --git a/src/datepicker/adapters/ngb-date-adapter.ts b/src/datepicker/adapters/ngb-date-adapter.ts new file mode 100644 index 0000000..464314c --- /dev/null +++ b/src/datepicker/adapters/ngb-date-adapter.ts @@ -0,0 +1,53 @@ +import {Injectable} from '@angular/core'; +import {NgbDateStruct} from '../ngb-date-struct'; +import {isInteger} from '../../util/util'; + +export function NGB_DATEPICKER_DATE_ADAPTER_FACTORY() { + return new NgbDateStructAdapter(); +} + +/** + * An abstract service that does the conversion between the internal datepicker `NgbDateStruct` model and + * any provided user date model `D`, ex. a string, a native date, etc. + * + * The adapter is used **only** for conversion when binding datepicker to a form control, + * ex. `[(ngModel)]="userDateModel"`. Here `userDateModel` can be of any type. + * + * The default datepicker implementation assumes we use `NgbDateStruct` as a user model. + * + * See the [date format overview](#/components/datepicker/overview#date-model) for more details + * and the [custom adapter demo](#/components/datepicker/examples#adapter) for an example. + */ +@Injectable({providedIn: 'root', useFactory: NGB_DATEPICKER_DATE_ADAPTER_FACTORY}) +export abstract class NgbDateAdapter { + /** + * Converts a user-model date of type `D` to an `NgbDateStruct` for internal use. + */ + abstract fromModel(value: D): NgbDateStruct; + + /** + * Converts an internal `NgbDateStruct` date to a user-model date of type `D`. + */ + abstract toModel(date: NgbDateStruct): D; +} + +@Injectable() +export class NgbDateStructAdapter extends NgbDateAdapter { + /** + * Converts a NgbDateStruct value into NgbDateStruct value + */ + fromModel(date: NgbDateStruct): NgbDateStruct { + return (date && isInteger(date.year) && isInteger(date.month) && isInteger(date.day)) ? + {year: date.year, month: date.month, day: date.day} : + null; + } + + /** + * Converts a NgbDateStruct value into NgbDateStruct value + */ + toModel(date: NgbDateStruct): NgbDateStruct { + return (date && isInteger(date.year) && isInteger(date.month) && isInteger(date.day)) ? + {year: date.year, month: date.month, day: date.day} : + null; + } +} diff --git a/src/datepicker/adapters/ngb-date-native-adapter.spec.ts b/src/datepicker/adapters/ngb-date-native-adapter.spec.ts new file mode 100644 index 0000000..55c2f93 --- /dev/null +++ b/src/datepicker/adapters/ngb-date-native-adapter.spec.ts @@ -0,0 +1,57 @@ +import {NgbDateNativeAdapter} from './ngb-date-native-adapter'; + +describe('ngb-date-native model adapter', () => { + let adapter: NgbDateNativeAdapter; + + beforeEach(() => { adapter = new NgbDateNativeAdapter(); }); + + describe('fromModel', () => { + + it('should convert invalid and incomplete values to null', () => { + expect(adapter.fromModel(null)).toBeNull(); + expect(adapter.fromModel(undefined)).toBeNull(); + expect(adapter.fromModel('')).toBeNull(); + expect(adapter.fromModel('s')).toBeNull(); + expect(adapter.fromModel(2)).toBeNull(); + expect(adapter.fromModel({})).toBeNull(); + expect(adapter.fromModel({year: 2017, month: 10})).toBeNull(); + expect(adapter.fromModel(new Date('boom'))).toBeNull(); + }); + + it('should convert valid date', + () => { expect(adapter.fromModel(new Date(2016, 4, 1))).toEqual({year: 2016, month: 5, day: 1}); }); + }); + + describe('toModel', () => { + + it('should convert invalid and incomplete values to null', () => { + expect(adapter.toModel(null)).toBeNull(); + expect(adapter.toModel(undefined)).toBeNull(); + expect(adapter.toModel('')).toBeNull(); + expect(adapter.toModel('s')).toBeNull(); + expect(adapter.toModel(2)).toBeNull(); + expect(adapter.toModel({})).toBeNull(); + expect(adapter.toModel(new Date())).toBeNull(); + }); + + it('should convert a valid date', + () => { expect(adapter.toModel({year: 2016, month: 10, day: 15})).toEqual(new Date(2016, 9, 15, 12)); }); + + it('should convert years between 0 and 99 correctly', () => { + + function jsDate(jsYear: number, jsMonth: number, jsDay: number): Date { + const date = new Date(jsYear, jsMonth, jsDay, 12); + if (jsYear >= 0 && jsYear <= 99) { + date.setFullYear(jsYear); + } + return date; + } + + expect(adapter.toModel({year: 0, month: 1, day: 1})).toEqual(jsDate(0, 0, 1)); + expect(adapter.toModel({year: 1, month: 1, day: 1})).toEqual(jsDate(1, 0, 1)); + expect(adapter.toModel({year: 99, month: 1, day: 1})).toEqual(jsDate(99, 0, 1)); + expect(adapter.toModel({year: 1900, month: 1, day: 1})).toEqual(jsDate(1900, 0, 1)); + }); + }); + +}); diff --git a/src/datepicker/adapters/ngb-date-native-adapter.ts b/src/datepicker/adapters/ngb-date-native-adapter.ts new file mode 100644 index 0000000..1c9bff5 --- /dev/null +++ b/src/datepicker/adapters/ngb-date-native-adapter.ts @@ -0,0 +1,37 @@ +import {Injectable} from '@angular/core'; +import {NgbDateAdapter} from './ngb-date-adapter'; +import {NgbDateStruct} from '../ngb-date-struct'; +import {isInteger} from '../../util/util'; + +/** + * [`NgbDateAdapter`](#/components/datepicker/api#NgbDateAdapter) implementation that uses + * native javascript dates as a user date model. + */ +@Injectable() +export class NgbDateNativeAdapter extends NgbDateAdapter { + /** + * Converts a native `Date` to a `NgbDateStruct`. + */ + fromModel(date: Date): NgbDateStruct { + return (date instanceof Date && !isNaN(date.getTime())) ? this._fromNativeDate(date) : null; + } + + /** + * Converts a `NgbDateStruct` to a native `Date`. + */ + toModel(date: NgbDateStruct): Date { + return date && isInteger(date.year) && isInteger(date.month) && isInteger(date.day) ? this._toNativeDate(date) : + null; + } + + protected _fromNativeDate(date: Date): NgbDateStruct { + return {year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate()}; + } + + protected _toNativeDate(date: NgbDateStruct): Date { + const jsDate = new Date(date.year, date.month - 1, date.day, 12); + // avoid 30 -> 1930 conversion + jsDate.setFullYear(date.year); + return jsDate; + } +} diff --git a/src/datepicker/adapters/ngb-date-native-utc-adapter.spec.ts b/src/datepicker/adapters/ngb-date-native-utc-adapter.spec.ts new file mode 100644 index 0000000..014ab76 --- /dev/null +++ b/src/datepicker/adapters/ngb-date-native-utc-adapter.spec.ts @@ -0,0 +1,57 @@ +import {NgbDateNativeUTCAdapter} from './ngb-date-native-utc-adapter'; + +describe('ngb-date-native-utc model adapter', () => { + let adapter: NgbDateNativeUTCAdapter; + + beforeEach(() => { adapter = new NgbDateNativeUTCAdapter(); }); + + describe('fromModel', () => { + + it('should convert invalid and incomplete values to null', () => { + expect(adapter.fromModel(null)).toBeNull(); + expect(adapter.fromModel(undefined)).toBeNull(); + expect(adapter.fromModel('')).toBeNull(); + expect(adapter.fromModel('s')).toBeNull(); + expect(adapter.fromModel(2)).toBeNull(); + expect(adapter.fromModel({})).toBeNull(); + expect(adapter.fromModel({year: 2017, month: 10})).toBeNull(); + expect(adapter.fromModel(new Date('boom'))).toBeNull(); + }); + + it('should convert valid date', + () => { expect(adapter.fromModel(new Date(Date.UTC(2016, 4, 1)))).toEqual({year: 2016, month: 5, day: 1}); }); + }); + + describe('toModel', () => { + + it('should convert invalid and incomplete values to null', () => { + expect(adapter.toModel(null)).toBeNull(); + expect(adapter.toModel(undefined)).toBeNull(); + expect(adapter.toModel('')).toBeNull(); + expect(adapter.toModel('s')).toBeNull(); + expect(adapter.toModel(2)).toBeNull(); + expect(adapter.toModel({})).toBeNull(); + expect(adapter.toModel(new Date())).toBeNull(); + }); + + it('should convert a valid date', + () => { expect(adapter.toModel({year: 2016, month: 10, day: 15})).toEqual(new Date(Date.UTC(2016, 9, 15))); }); + + it('should convert years between 0 and 99 correctly', () => { + + function jsDate(jsYear: number, jsMonth: number, jsDay: number): Date { + const date = new Date(Date.UTC(jsYear, jsMonth, jsDay)); + if (jsYear >= 0 && jsYear <= 99) { + date.setUTCFullYear(jsYear); + } + return date; + } + + expect(adapter.toModel({year: 0, month: 1, day: 1})).toEqual(jsDate(0, 0, 1)); + expect(adapter.toModel({year: 1, month: 1, day: 1})).toEqual(jsDate(1, 0, 1)); + expect(adapter.toModel({year: 99, month: 1, day: 1})).toEqual(jsDate(99, 0, 1)); + expect(adapter.toModel({year: 1900, month: 1, day: 1})).toEqual(jsDate(1900, 0, 1)); + }); + }); + +}); diff --git a/src/datepicker/adapters/ngb-date-native-utc-adapter.ts b/src/datepicker/adapters/ngb-date-native-utc-adapter.ts new file mode 100644 index 0000000..9c7e98b --- /dev/null +++ b/src/datepicker/adapters/ngb-date-native-utc-adapter.ts @@ -0,0 +1,22 @@ +import {Injectable} from '@angular/core'; +import {NgbDateStruct} from '../ngb-date-struct'; +import {NgbDateNativeAdapter} from './ngb-date-native-adapter'; + +/** + * Same as [`NgbDateNativeAdapter`](#/components/datepicker/api#NgbDateNativeAdapter), but with UTC dates. + * + * @since 3.2.0 + */ +@Injectable() +export class NgbDateNativeUTCAdapter extends NgbDateNativeAdapter { + protected _fromNativeDate(date: Date): NgbDateStruct { + return {year: date.getUTCFullYear(), month: date.getUTCMonth() + 1, day: date.getUTCDate()}; + } + + protected _toNativeDate(date: NgbDateStruct): Date { + const jsDate = new Date(Date.UTC(date.year, date.month - 1, date.day)); + // avoid 30 -> 1930 conversion + jsDate.setUTCFullYear(date.year); + return jsDate; + } +} diff --git a/src/datepicker/datepicker-config.spec.ts b/src/datepicker/datepicker-config.spec.ts new file mode 100644 index 0000000..e14860d --- /dev/null +++ b/src/datepicker/datepicker-config.spec.ts @@ -0,0 +1,19 @@ +import {NgbDatepickerConfig} from './datepicker-config'; + +describe('ngb-datepicker-config', () => { + it('should have sensible default values', () => { + const config = new NgbDatepickerConfig(); + + expect(config.dayTemplate).toBeUndefined(); + expect(config.displayMonths).toBe(1); + expect(config.firstDayOfWeek).toBe(1); + expect(config.markDisabled).toBeUndefined(); + expect(config.minDate).toBeUndefined(); + expect(config.maxDate).toBeUndefined(); + expect(config.navigation).toBe('select'); + expect(config.outsideDays).toBe('visible'); + expect(config.showWeekdays).toBe(true); + expect(config.showWeekNumbers).toBe(false); + expect(config.startDate).toBeUndefined(); + }); +}); diff --git a/src/datepicker/datepicker-config.ts b/src/datepicker/datepicker-config.ts new file mode 100644 index 0000000..fd23204 --- /dev/null +++ b/src/datepicker/datepicker-config.ts @@ -0,0 +1,26 @@ +import {Injectable, TemplateRef} from '@angular/core'; +import {DayTemplateContext} from './datepicker-day-template-context'; +import {NgbDateStruct} from './ngb-date-struct'; + +/** + * A configuration service for the [`NgbDatepicker`](#/components/datepicker/api#NgbDatepicker) component. + * + * You can inject this service, typically in your root component, and customize the values of its properties in + * order to provide default values for all the datepickers used in the application. + */ +@Injectable({providedIn: 'root'}) +export class NgbDatepickerConfig { + dayTemplate: TemplateRef; + dayTemplateData: (date: NgbDateStruct, current: {year: number, month: number}) => any; + footerTemplate: TemplateRef; + displayMonths = 1; + firstDayOfWeek = 1; + markDisabled: (date: NgbDateStruct, current: {year: number, month: number}) => boolean; + minDate: NgbDateStruct; + maxDate: NgbDateStruct; + navigation: 'select' | 'arrows' | 'none' = 'select'; + outsideDays: 'visible' | 'collapsed' | 'hidden' = 'visible'; + showWeekdays = true; + showWeekNumbers = false; + startDate: {year: number, month: number}; +} diff --git a/src/datepicker/datepicker-day-template-context.ts b/src/datepicker/datepicker-day-template-context.ts new file mode 100644 index 0000000..1172ac7 --- /dev/null +++ b/src/datepicker/datepicker-day-template-context.ts @@ -0,0 +1,55 @@ +import {NgbDate} from './ngb-date'; +/** + * The context for the datepicker 'day' template. + * + * You can override the way dates are displayed in the datepicker via the `[dayTemplate]` input. + */ +export interface DayTemplateContext { + /** + * The date that corresponds to the template. Same as the `date` parameter. + * + * Can be used for convenience as a default template key, ex. `let-d`. + * + * @since 3.3.0 + */ + $implicit: NgbDate; + + /** + * The month currently displayed by the datepicker. + */ + currentMonth: number; + + /** + * Any data you pass using the `[dayTemplateData]` input in the datepicker. + * + * @since 3.3.0 + */ + data?: any; + + /** + * The date that corresponds to the template. + */ + date: NgbDate; + + /** + * `True` if the current date is disabled. + */ + disabled: boolean; + + /** + * `True` if the current date is focused. + */ + focused: boolean; + + /** + * `True` if the current date is selected. + */ + selected: boolean; + + /** + * `True` if the current date is today (equal to `NgbCalendar.getToday()`). + * + * @since 4.1.0 + */ + today: boolean; +} diff --git a/src/datepicker/datepicker-day-view.scss b/src/datepicker/datepicker-day-view.scss new file mode 100644 index 0000000..2eba44a --- /dev/null +++ b/src/datepicker/datepicker-day-view.scss @@ -0,0 +1,12 @@ +[ngbDatepickerDayView] { + text-align: center; + width: 2rem; + height: 2rem; + line-height: 2rem; + border-radius: 0.25rem; + background: transparent; + + &.outside { + opacity: 0.5; + } +} diff --git a/src/datepicker/datepicker-day-view.spec.ts b/src/datepicker/datepicker-day-view.spec.ts new file mode 100644 index 0000000..fb6274a --- /dev/null +++ b/src/datepicker/datepicker-day-view.spec.ts @@ -0,0 +1,95 @@ +import {TestBed} from '@angular/core/testing'; + +import {Component} from '@angular/core'; +import {NgbDatepickerDayView} from './datepicker-day-view'; +import {NgbDate} from './ngb-date'; +import {NgbDatepickerI18n, NgbDatepickerI18nDefault} from './datepicker-i18n'; + +function getElement(element: HTMLElement): HTMLElement { + return element.querySelector('[ngbDatepickerDayView]'); +} + +describe('ngbDatepickerDayView', () => { + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestComponent, NgbDatepickerDayView], + providers: [{provide: NgbDatepickerI18n, useClass: NgbDatepickerI18nDefault}] + }); + }); + + it('should display date', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const el = getElement(fixture.nativeElement); + expect(el.innerText).toBe('22'); + + fixture.componentInstance.date = new NgbDate(2016, 7, 25); + fixture.detectChanges(); + expect(el.innerText).toBe('25'); + }); + + it('should apply text-muted style for disabled days', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const el = getElement(fixture.nativeElement); + expect(el).not.toHaveCssClass('text-muted'); + + fixture.componentInstance.disabled = true; + fixture.detectChanges(); + expect(el).toHaveCssClass('text-muted'); + }); + + it('should apply text-muted and outside classes for days of a different month', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const el = getElement(fixture.nativeElement); + expect(el).not.toHaveCssClass('text-muted'); + expect(el).not.toHaveCssClass('outside'); + + fixture.componentInstance.date = new NgbDate(2016, 8, 22); + fixture.detectChanges(); + expect(el).toHaveCssClass('text-muted'); + expect(el).toHaveCssClass('outside'); + }); + + it('should apply selected style', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const el = getElement(fixture.nativeElement); + expect(el).not.toHaveCssClass('text-white'); + expect(el).not.toHaveCssClass('bg-primary'); + + fixture.componentInstance.selected = true; + fixture.detectChanges(); + expect(el).toHaveCssClass('text-white'); + expect(el).toHaveCssClass('bg-primary'); + }); + + it('should not apply muted style if disabled but selected', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.componentInstance.disabled = true; + fixture.componentInstance.selected = true; + fixture.detectChanges(); + + const el = getElement(fixture.nativeElement); + expect(el).toHaveCssClass('bg-primary'); + expect(el).not.toHaveCssClass('text-muted'); + }); +}); + +@Component({ + selector: 'test-cmp', + template: + '
' +}) +class TestComponent { + currentMonth = 7; + date: NgbDate = new NgbDate(2016, 7, 22); + disabled = false; + selected = false; +} diff --git a/src/datepicker/datepicker-day-view.ts b/src/datepicker/datepicker-day-view.ts new file mode 100644 index 0000000..39aa234 --- /dev/null +++ b/src/datepicker/datepicker-day-view.ts @@ -0,0 +1,30 @@ +import {ChangeDetectionStrategy, Component, Input, ViewEncapsulation} from '@angular/core'; +import {NgbDate} from './ngb-date'; +import {NgbDatepickerI18n} from './datepicker-i18n'; + +@Component({ + selector: '[ngbDatepickerDayView]', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + styleUrls: ['./datepicker-day-view.scss'], + host: { + 'class': 'btn-light', + '[class.bg-primary]': 'selected', + '[class.text-white]': 'selected', + '[class.text-muted]': 'isMuted()', + '[class.outside]': 'isMuted()', + '[class.active]': 'focused' + }, + template: `{{ i18n.getDayNumerals(date) }}` +}) +export class NgbDatepickerDayView { + @Input() currentMonth: number; + @Input() date: NgbDate; + @Input() disabled: boolean; + @Input() focused: boolean; + @Input() selected: boolean; + + constructor(public i18n: NgbDatepickerI18n) {} + + isMuted() { return !this.selected && (this.date.month !== this.currentMonth || this.disabled); } +} diff --git a/src/datepicker/datepicker-i18n.spec.ts b/src/datepicker/datepicker-i18n.spec.ts new file mode 100644 index 0000000..ed44aaa --- /dev/null +++ b/src/datepicker/datepicker-i18n.spec.ts @@ -0,0 +1,52 @@ +import {NgbDatepickerI18nDefault} from './datepicker-i18n'; +import {TestBed} from '@angular/core/testing'; +import {NgbDate} from './ngb-date'; + +describe('ngb-datepicker-i18n-default', () => { + + let i18n: NgbDatepickerI18nDefault; + + beforeEach(() => { + TestBed.configureTestingModule({providers: [NgbDatepickerI18nDefault]}); + i18n = TestBed.get(NgbDatepickerI18nDefault); + }); + + it('should return abbreviated month name', () => { + expect(i18n.getMonthShortName(0)).toBe(undefined); + expect(i18n.getMonthShortName(1)).toBe('Jan'); + expect(i18n.getMonthShortName(12)).toBe('Dec'); + expect(i18n.getMonthShortName(13)).toBe(undefined); + }); + + it('should return wide month name', () => { + expect(i18n.getMonthFullName(0)).toBe(undefined); + expect(i18n.getMonthFullName(1)).toBe('January'); + expect(i18n.getMonthFullName(12)).toBe('December'); + expect(i18n.getMonthFullName(13)).toBe(undefined); + }); + + it('should return weekday name', () => { + expect(i18n.getWeekdayShortName(0)).toBe(undefined); + expect(i18n.getWeekdayShortName(1)).toBe('Mo'); + expect(i18n.getWeekdayShortName(7)).toBe('Su'); + expect(i18n.getWeekdayShortName(8)).toBe(undefined); + }); + + it('should generate aria label for a date', + () => { expect(i18n.getDayAriaLabel(new NgbDate(2010, 10, 8))).toBe('Friday, October 8, 2010'); }); + + it('should generate week number numerals', () => { + expect(i18n.getWeekNumerals(1)).toBe('1'); + expect(i18n.getWeekNumerals(55)).toBe('55'); + }); + + it('should generate day numerals', () => { + expect(i18n.getDayNumerals(new NgbDate(2010, 10, 1))).toBe('1'); + expect(i18n.getDayNumerals(new NgbDate(2010, 10, 31))).toBe('31'); + }); + + it('should generate year numerals', () => { + expect(i18n.getYearNumerals(0)).toBe('0'); + expect(i18n.getYearNumerals(2000)).toBe('2000'); + }); +}); diff --git a/src/datepicker/datepicker-i18n.ts b/src/datepicker/datepicker-i18n.ts new file mode 100644 index 0000000..d6315e0 --- /dev/null +++ b/src/datepicker/datepicker-i18n.ts @@ -0,0 +1,100 @@ +import {Inject, Injectable, LOCALE_ID} from '@angular/core'; +import {FormStyle, getLocaleDayNames, getLocaleMonthNames, TranslationWidth, formatDate} from '@angular/common'; +import {NgbDateStruct} from './ngb-date-struct'; + +export function NGB_DATEPICKER_18N_FACTORY(locale) { + return new NgbDatepickerI18nDefault(locale); +} + +/** + * A service supplying i18n data to the datepicker component. + * + * The default implementation of this service uses the Angular locale and registered locale data for + * weekdays and month names (as explained in the Angular i18n guide). + * + * It also provides a way to i18n data that depends on calendar calculations, like aria labels, day, week and year + * numerals. For other static labels the datepicker uses the default Angular i18n. + * + * See the [i18n demo](#/components/datepicker/examples#i18n) and + * [Hebrew calendar demo](#/components/datepicker/calendars#hebrew) on how to extend this class and define + * a custom provider for i18n. + */ +@Injectable({providedIn: 'root', useFactory: NGB_DATEPICKER_18N_FACTORY, deps: [LOCALE_ID]}) +export abstract class NgbDatepickerI18n { + /** + * Returns the short weekday name to display in the heading of the month view. + * + * With default calendar we use ISO 8601: 'weekday' is 1=Mon ... 7=Sun. + */ + abstract getWeekdayShortName(weekday: number): string; + + /** + * Returns the short month name to display in the date picker navigation. + * + * With default calendar we use ISO 8601: 'month' is 1=Jan ... 12=Dec. + */ + abstract getMonthShortName(month: number, year?: number): string; + + /** + * Returns the full month name to display in the date picker navigation. + * + * With default calendar we use ISO 8601: 'month' is 1=Jan ... 12=Dec. + */ + abstract getMonthFullName(month: number, year?: number): string; + + /** + * Returns the value of the `aria-label` attribute for a specific date. + * + * @since 2.0.0 + */ + abstract getDayAriaLabel(date: NgbDateStruct): string; + + /** + * Returns the textual representation of a day that is rendered in a day cell. + * + * @since 3.0.0 + */ + getDayNumerals(date: NgbDateStruct): string { return `${date.day}`; } + + /** + * Returns the textual representation of a week number rendered by datepicker. + * + * @since 3.0.0 + */ + getWeekNumerals(weekNumber: number): string { return `${weekNumber}`; } + + /** + * Returns the textual representation of a year that is rendered in the datepicker year select box. + * + * @since 3.0.0 + */ + getYearNumerals(year: number): string { return `${year}`; } +} + +@Injectable() +export class NgbDatepickerI18nDefault extends NgbDatepickerI18n { + private _weekdaysShort: Array; + private _monthsShort: Array; + private _monthsFull: Array; + + constructor(@Inject(LOCALE_ID) private _locale: string) { + super(); + + const weekdaysStartingOnSunday = getLocaleDayNames(_locale, FormStyle.Standalone, TranslationWidth.Short); + this._weekdaysShort = weekdaysStartingOnSunday.map((day, index) => weekdaysStartingOnSunday[(index + 1) % 7]); + + this._monthsShort = getLocaleMonthNames(_locale, FormStyle.Standalone, TranslationWidth.Abbreviated); + this._monthsFull = getLocaleMonthNames(_locale, FormStyle.Standalone, TranslationWidth.Wide); + } + + getWeekdayShortName(weekday: number): string { return this._weekdaysShort[weekday - 1]; } + + getMonthShortName(month: number): string { return this._monthsShort[month - 1]; } + + getMonthFullName(month: number): string { return this._monthsFull[month - 1]; } + + getDayAriaLabel(date: NgbDateStruct): string { + const jsDate = new Date(date.year, date.month - 1, date.day); + return formatDate(jsDate, 'fullDate', this._locale); + } +} diff --git a/src/datepicker/datepicker-input.spec.ts b/src/datepicker/datepicker-input.spec.ts new file mode 100644 index 0000000..910bd1d --- /dev/null +++ b/src/datepicker/datepicker-input.spec.ts @@ -0,0 +1,931 @@ +import {TestBed, ComponentFixture, fakeAsync, tick} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {createGenericTestComponent} from '../test/common'; + +import {Component, Injectable} from '@angular/core'; +import {FormsModule, NgForm} from '@angular/forms'; + +import {NgbDateAdapter, NgbDatepickerModule} from './datepicker.module'; +import {NgbInputDatepicker} from './datepicker-input'; +import {NgbDatepicker} from './datepicker'; +import {NgbDateStruct} from './ngb-date-struct'; +import {NgbDate} from './ngb-date'; +import * as positioning from 'src/util/positioning'; + +const createTestCmpt = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +const createTestNativeCmpt = (html: string) => + createGenericTestComponent(html, TestNativeComponent) as ComponentFixture; + +describe('NgbInputDatepicker', () => { + + beforeEach(() => { + TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbDatepickerModule, FormsModule]}); + }); + + describe('open, close and toggle', () => { + + it('should allow controlling datepicker popup from outside', () => { + const fixture = createTestCmpt(` + + + + `); + + const buttons = fixture.nativeElement.querySelectorAll('button'); + + buttons[0].click(); // open + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('ngb-datepicker')).not.toBeNull(); + + buttons[1].click(); // close + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('ngb-datepicker')).toBeNull(); + + buttons[2].click(); // toggle + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('ngb-datepicker')).not.toBeNull(); + + buttons[2].click(); // toggle + fixture.detectChanges(); + expect(fixture.nativeElement.querySelector('ngb-datepicker')).toBeNull(); + }); + + it('should support the "position" option', + () => { createTestCmpt(``); }); + }); + + describe('ngModel interactions', () => { + it('should not change again the value in the model on a change coming from the model (popup closed)', + fakeAsync(() => { + const fixture = createTestCmpt(``); + fixture.detectChanges(); + + const input = fixture.nativeElement.querySelector('input'); + + const value = new NgbDate(2018, 8, 29); + fixture.componentInstance.date = value; + + fixture.detectChanges(); + tick(); + expect(fixture.componentInstance.date).toBe(value); + expect(input.value).toBe('2018-08-29'); + })); + + it('should not change again the value in the model on a change coming from the model (popup opened)', + fakeAsync(() => { + const fixture = createTestCmpt(` + `); + fixture.detectChanges(); + + const button = fixture.nativeElement.querySelector('button'); + const input = fixture.nativeElement.querySelector('input'); + + button.click(); // open + tick(); + fixture.detectChanges(); + + const value = new NgbDate(2018, 8, 29); + fixture.componentInstance.date = value; + fixture.detectChanges(); + tick(); + expect(fixture.componentInstance.date).toBe(value); + expect(input.value).toBe('2018-08-29'); + })); + + + it('should format bound date as ISO (by default) in the input field', fakeAsync(() => { + const fixture = createTestCmpt(``); + const input = fixture.nativeElement.querySelector('input'); + + fixture.componentInstance.date = {year: 2016, month: 10, day: 10}; + fixture.detectChanges(); + tick(); + expect(input.value).toBe('2016-10-10'); + + fixture.componentInstance.date = {year: 2016, month: 10, day: 15}; + fixture.detectChanges(); + tick(); + expect(input.value).toBe('2016-10-15'); + })); + + it('should parse user-entered date as ISO (by default)', () => { + const fixture = createTestCmpt(``); + const inputDebugEl = fixture.debugElement.query(By.css('input')); + + inputDebugEl.triggerEventHandler('input', {target: {value: '2016-09-10'}}); + expect(fixture.componentInstance.date).toEqual({year: 2016, month: 9, day: 10}); + }); + + it('should not update the model twice with the same value on input and on change', fakeAsync(() => { + const fixture = + createTestCmpt(``); + const componentInstance = fixture.componentInstance; + const inputDebugEl = fixture.debugElement.query(By.css('input')); + spyOn(componentInstance, 'onModelChange'); + + tick(); + fixture.detectChanges(); + + inputDebugEl.triggerEventHandler('input', {target: {value: '2018-08-29'}}); + tick(); + fixture.detectChanges(); + + const value = componentInstance.date; + expect(value).toEqual({year: 2018, month: 8, day: 29}); + expect(componentInstance.onModelChange).toHaveBeenCalledTimes(1); + expect(componentInstance.onModelChange).toHaveBeenCalledWith(value); + + inputDebugEl.triggerEventHandler('change', {target: {value: '2018-08-29'}}); + + tick(); + fixture.detectChanges(); + + expect(fixture.componentInstance.date).toBe(value); + + // the value is still the same, there should not be new calls of onModelChange: + expect(componentInstance.onModelChange).toHaveBeenCalledTimes(1); + })); + + it('should set only valid dates', fakeAsync(() => { + const fixture = createTestCmpt(``); + const input = fixture.nativeElement.querySelector('input'); + + fixture.componentInstance.date = {}; + fixture.detectChanges(); + tick(); + expect(input.value).toBe(''); + + fixture.componentInstance.date = null; + fixture.detectChanges(); + tick(); + expect(input.value).toBe(''); + + fixture.componentInstance.date = new Date(); + fixture.detectChanges(); + tick(); + expect(input.value).toBe(''); + + fixture.componentInstance.date = undefined; + fixture.detectChanges(); + tick(); + expect(input.value).toBe(''); + + fixture.componentInstance.date = new NgbDate(300000, 1, 1); + fixture.detectChanges(); + tick(); + expect(input.value).toBe(''); + + fixture.componentInstance.date = new NgbDate(2017, 2, null); + fixture.detectChanges(); + tick(); + expect(input.value).toBe(''); + + fixture.componentInstance.date = new NgbDate(2017, null, 5); + fixture.detectChanges(); + tick(); + expect(input.value).toBe(''); + + fixture.componentInstance.date = new NgbDate(null, 2, 5); + fixture.detectChanges(); + tick(); + expect(input.value).toBe(''); + + fixture.componentInstance.date = new NgbDate('2017', '03', '10'); + fixture.detectChanges(); + tick(); + expect(input.value).toBe(''); + })); + + it('should propagate disabled state', fakeAsync(() => { + const fixture = createTestCmpt(` + + `); + fixture.componentInstance.isDisabled = true; + fixture.detectChanges(); + + const button = fixture.nativeElement.querySelector('button'); + const input = fixture.nativeElement.querySelector('input'); + + button.click(); // open + tick(); + fixture.detectChanges(); + const buttonInDatePicker = fixture.nativeElement.querySelector('ngb-datepicker button'); + + expect(fixture.nativeElement.querySelector('ngb-datepicker')).not.toBeNull(); + expect(input.disabled).toBeTruthy(); + expect(buttonInDatePicker.disabled).toBeTruthy(); + + const dayElements = fixture.nativeElement.querySelectorAll('ngb-datepicker-month-view .ngb-dp-day'); + expect(dayElements[1]).toHaveCssClass('disabled'); + expect(dayElements[11]).toHaveCssClass('disabled'); + expect(dayElements[21]).toHaveCssClass('disabled'); + + fixture.componentInstance.isDisabled = false; + fixture.detectChanges(); + tick(); + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('ngb-datepicker')).not.toBeNull(); + expect(input.disabled).toBeFalsy(); + expect(buttonInDatePicker.disabled).toBeFalsy(); + + const dayElements2 = fixture.nativeElement.querySelectorAll('ngb-datepicker-month-view .ngb-dp-day'); + expect(dayElements2[1]).not.toHaveCssClass('disabled'); + expect(dayElements2[11]).not.toHaveCssClass('disabled'); + expect(dayElements2[21]).not.toHaveCssClass('disabled'); + })); + + it('should propagate disabled state without form control', () => { + const fixture = createTestCmpt(` + + `); + fixture.componentInstance.isDisabled = true; + fixture.detectChanges(); + + const button = fixture.nativeElement.querySelector('button'); + const input = fixture.nativeElement.querySelector('input'); + + expect(input.disabled).toBeTruthy(); + + button.click(); // open + fixture.detectChanges(); + const buttonInDatePicker = fixture.nativeElement.querySelector('ngb-datepicker button'); + + expect(fixture.nativeElement.querySelector('ngb-datepicker')).not.toBeNull(); + expect(input.disabled).toBeTruthy(); + expect(buttonInDatePicker.disabled).toBeTruthy(); + + const dayElements = fixture.nativeElement.querySelectorAll('ngb-datepicker-month-view .ngb-dp-day'); + expect(dayElements[1]).toHaveCssClass('disabled'); + expect(dayElements[11]).toHaveCssClass('disabled'); + expect(dayElements[21]).toHaveCssClass('disabled'); + + fixture.componentInstance.isDisabled = false; + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('ngb-datepicker')).not.toBeNull(); + expect(input.disabled).toBeFalsy(); + expect(buttonInDatePicker.disabled).toBeFalsy(); + + const dayElements2 = fixture.nativeElement.querySelectorAll('ngb-datepicker-month-view .ngb-dp-day'); + expect(dayElements2[1]).not.toHaveCssClass('disabled'); + expect(dayElements2[11]).not.toHaveCssClass('disabled'); + expect(dayElements2[21]).not.toHaveCssClass('disabled'); + }); + + it('should propagate touched state on (blur)', fakeAsync(() => { + const fixture = createTestCmpt(``); + const inputDebugEl = fixture.debugElement.query(By.css('input')); + + expect(inputDebugEl.classes['ng-touched']).toBeFalsy(); + + inputDebugEl.triggerEventHandler('blur', {}); + tick(); + fixture.detectChanges(); + + expect(inputDebugEl.classes['ng-touched']).toBeTruthy(); + })); + + it('should propagate touched state when setting a date', fakeAsync(() => { + const fixture = createTestCmpt(` + + `); + + const buttonDebugEl = fixture.debugElement.query(By.css('button')); + const inputDebugEl = fixture.debugElement.query(By.css('input')); + + expect(inputDebugEl.classes['ng-touched']).toBeFalsy(); + + buttonDebugEl.triggerEventHandler('click', {}); // open + inputDebugEl.triggerEventHandler('change', {target: {value: '2016-09-10'}}); + tick(); + fixture.detectChanges(); + + expect(inputDebugEl.classes['ng-touched']).toBeTruthy(); + })); + + it('should update model with updateOnBlur when selecting a date', fakeAsync(() => { + const fixture = createTestCmpt(` + `); + + const inputDebugEl = fixture.debugElement.query(By.css('input')); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + // open + dpInput.open(); + fixture.detectChanges(); + expect(inputDebugEl.classes['ng-touched']).toBeFalsy(); + expect(fixture.componentInstance.date).toBeUndefined(); + + // select date + fixture.nativeElement.querySelectorAll('.ngb-dp-day')[3].click(); // 1 MAR 2018 + fixture.detectChanges(); + expect(fixture.componentInstance.date).toEqual({year: 2018, month: 3, day: 1}); + expect(inputDebugEl.nativeElement.value).toBe('2018-03-01'); + expect(inputDebugEl.classes['ng-touched']).toBeTruthy(); + })); + }); + + describe('manual data entry', () => { + + it('should reformat value entered by a user when it is valid', fakeAsync(() => { + const fixture = createTestCmpt(``); + const inputDebugEl = fixture.debugElement.query(By.css('input')); + + inputDebugEl.triggerEventHandler('change', {target: {value: '2016-9-1'}}); + tick(); + fixture.detectChanges(); + + expect(inputDebugEl.nativeElement.value).toBe('2016-09-01'); + })); + + it('should retain value entered by a user if it is not valid', fakeAsync(() => { + const fixture = createTestCmpt(``); + const inputDebugEl = fixture.debugElement.query(By.css('input')); + + inputDebugEl.nativeElement.value = '2016-09-aa'; + inputDebugEl.triggerEventHandler('change', {target: {value: inputDebugEl.nativeElement.value}}); + tick(); + fixture.detectChanges(); + + expect(inputDebugEl.nativeElement.value).toBe('2016-09-aa'); + })); + + }); + + describe('validation', () => { + + describe('values set from model', () => { + + it('should not return errors for valid model', fakeAsync(() => { + const fixture = createTestCmpt( + `
`); + const form = fixture.debugElement.query(By.directive(NgForm)).injector.get(NgForm); + + fixture.detectChanges(); + tick(); + expect(form.control.valid).toBeTruthy(); + expect(form.control.hasError('ngbDate', ['dp'])).toBeFalsy(); + })); + + it('should not return errors for empty model', fakeAsync(() => { + const fixture = createTestCmpt(`
`); + const form = fixture.debugElement.query(By.directive(NgForm)).injector.get(NgForm); + + fixture.detectChanges(); + tick(); + expect(form.control.valid).toBeTruthy(); + })); + + it('should return "invalid" errors for invalid model', fakeAsync(() => { + const fixture = createTestCmpt(`
`); + const form = fixture.debugElement.query(By.directive(NgForm)).injector.get(NgForm); + + fixture.detectChanges(); + tick(); + expect(form.control.invalid).toBeTruthy(); + expect(form.control.getError('ngbDate', ['dp']).invalid).toBe(5); + })); + + it('should return "requiredBefore" errors for dates before minimal date', fakeAsync(() => { + const fixture = createTestCmpt(`
+ +
`); + const form = fixture.debugElement.query(By.directive(NgForm)).injector.get(NgForm); + + fixture.detectChanges(); + tick(); + expect(form.control.invalid).toBeTruthy(); + expect(form.control.getError('ngbDate', ['dp']).requiredBefore).toEqual({year: 2017, month: 6, day: 4}); + })); + + it('should return "requiredAfter" errors for dates after maximal date', fakeAsync(() => { + const fixture = createTestCmpt(`
+ +
`); + const form = fixture.debugElement.query(By.directive(NgForm)).injector.get(NgForm); + + fixture.detectChanges(); + tick(); + expect(form.control.invalid).toBeTruthy(); + expect(form.control.getError('ngbDate', ['dp']).requiredAfter).toEqual({year: 2017, month: 2, day: 4}); + })); + + it('should update validity status when model changes', fakeAsync(() => { + const fixture = createTestCmpt(`
`); + const form = fixture.debugElement.query(By.directive(NgForm)).injector.get(NgForm); + + fixture.componentRef.instance.date = 'invalid'; + fixture.detectChanges(); + tick(); + expect(form.control.invalid).toBeTruthy(); + + fixture.componentRef.instance.date = {year: 2015, month: 7, day: 3}; + fixture.detectChanges(); + tick(); + expect(form.control.valid).toBeTruthy(); + })); + + it('should update validity status when minDate changes', fakeAsync(() => { + const fixture = createTestCmpt(`
+ +
`); + const form = fixture.debugElement.query(By.directive(NgForm)).injector.get(NgForm); + + fixture.detectChanges(); + tick(); + expect(form.control.valid).toBeTruthy(); + + fixture.componentRef.instance.date = {year: 2018, month: 7, day: 3}; + fixture.detectChanges(); + tick(); + expect(form.control.invalid).toBeTruthy(); + })); + + it('should update validity status when maxDate changes', fakeAsync(() => { + const fixture = createTestCmpt(`
+ +
`); + const form = fixture.debugElement.query(By.directive(NgForm)).injector.get(NgForm); + + fixture.detectChanges(); + tick(); + expect(form.control.valid).toBeTruthy(); + + fixture.componentRef.instance.date = {year: 2015, month: 7, day: 3}; + fixture.detectChanges(); + tick(); + expect(form.control.invalid).toBeTruthy(); + })); + + it('should update validity for manually entered dates', fakeAsync(() => { + const fixture = createTestCmpt(`
`); + const inputDebugEl = fixture.debugElement.query(By.css('input')); + const form = fixture.debugElement.query(By.directive(NgForm)).injector.get(NgForm); + + inputDebugEl.triggerEventHandler('input', {target: {value: '2016-09-10'}}); + fixture.detectChanges(); + tick(); + expect(form.control.valid).toBeTruthy(); + + inputDebugEl.triggerEventHandler('input', {target: {value: 'invalid'}}); + fixture.detectChanges(); + tick(); + expect(form.control.invalid).toBeTruthy(); + })); + + it('should consider empty strings as valid', fakeAsync(() => { + const fixture = createTestCmpt(`
`); + const inputDebugEl = fixture.debugElement.query(By.css('input')); + const form = fixture.debugElement.query(By.directive(NgForm)).injector.get(NgForm); + + inputDebugEl.triggerEventHandler('change', {target: {value: '2016-09-10'}}); + fixture.detectChanges(); + tick(); + expect(form.control.valid).toBeTruthy(); + + inputDebugEl.triggerEventHandler('change', {target: {value: ''}}); + fixture.detectChanges(); + tick(); + expect(form.control.valid).toBeTruthy(); + })); + }); + + }); + + describe('options', () => { + + it('should propagate the "dayTemplate" option', () => { + const fixture = createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + dpInput.open(); + fixture.detectChanges(); + + const dp = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker); + expect(dp.dayTemplate).toBeDefined(); + }); + + it('should propagate the "dayTemplateData" option', () => { + const fixture = createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + dpInput.open(); + fixture.detectChanges(); + + const dp = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker); + expect(dp.dayTemplateData).toBeDefined(); + }); + + it('should propagate the "displayMonths" option', () => { + const fixture = createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + dpInput.open(); + fixture.detectChanges(); + + const dp = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker); + expect(dp.displayMonths).toBe(3); + }); + + it('should propagate the "firstDayOfWeek" option', () => { + const fixture = createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + dpInput.open(); + fixture.detectChanges(); + + const dp = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker); + expect(dp.firstDayOfWeek).toBe(5); + }); + + it('should propagate the "footerTemplate" option', () => { + const fixture = createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + dpInput.open(); + fixture.detectChanges(); + + const dp = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker); + expect(dp.footerTemplate).toBeDefined(); + }); + + it('should propagate the "markDisabled" option', () => { + const fixture = createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + dpInput.open(); + fixture.detectChanges(); + + const dp = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker); + expect(dp.markDisabled).toBeDefined(); + }); + + it('should propagate the "minDate" option', () => { + const fixture = createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + dpInput.open(); + fixture.detectChanges(); + + const dp = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker); + expect(dp.minDate).toEqual({year: 2016, month: 9, day: 13}); + }); + + it('should propagate the "maxDate" option', () => { + const fixture = createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + dpInput.open(); + fixture.detectChanges(); + + const dp = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker); + expect(dp.maxDate).toEqual({year: 2016, month: 9, day: 13}); + }); + + it('should propagate the "outsideDays" option', () => { + const fixture = createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + dpInput.open(); + fixture.detectChanges(); + + const dp = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker); + expect(dp.outsideDays).toEqual('collapsed'); + }); + + it('should propagate the "navigation" option', () => { + const fixture = createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + dpInput.open(); + fixture.detectChanges(); + + const dp = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker); + expect(dp.navigation).toBe('none'); + }); + + it('should propagate the "showWeekdays" option', () => { + const fixture = createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + dpInput.open(); + fixture.detectChanges(); + + const dp = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker); + expect(dp.showWeekdays).toBeTruthy(); + }); + + it('should propagate the "showWeekNumbers" option', () => { + const fixture = createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + dpInput.open(); + fixture.detectChanges(); + + const dp = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker); + expect(dp.showWeekNumbers).toBeTruthy(); + }); + + it('should propagate the "startDate" option', () => { + const fixture = createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + dpInput.open(); + fixture.detectChanges(); + + const dp = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker); + expect(dp.startDate).toEqual({year: 2016, month: 9}); + }); + + it('should propagate model as "startDate" option when "startDate" not provided', fakeAsync(() => { + const fixture = createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + tick(); + fixture.detectChanges(); + dpInput.open(); + fixture.detectChanges(); + + const dp = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker); + expect(dp.startDate).toEqual(new NgbDate(2016, 9, 13)); + })); + + it('should relay the "navigate" event', () => { + const fixture = + createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + spyOn(fixture.componentInstance, 'onNavigate'); + + dpInput.open(); + fixture.detectChanges(); + expect(fixture.componentInstance.onNavigate) + .toHaveBeenCalledWith({current: null, next: {year: 2016, month: 9}, preventDefault: jasmine.any(Function)}); + + const dp = fixture.debugElement.query(By.css('ngb-datepicker')).injector.get(NgbDatepicker); + dp.navigateTo({year: 2018, month: 4}); + expect(fixture.componentInstance.onNavigate).toHaveBeenCalledWith({ + current: {year: 2016, month: 9}, + next: {year: 2018, month: 4}, + preventDefault: jasmine.any(Function) + }); + }); + + it('should relay the "closed" event', () => { + const fixture = createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + spyOn(fixture.componentInstance, 'onClose'); + + // open + dpInput.open(); + fixture.detectChanges(); + + // close + dpInput.close(); + expect(fixture.componentInstance.onClose).toHaveBeenCalledTimes(1); + }); + + it('should emit both "dateSelect" and "onModelChange" events', () => { + const fixture = createTestCmpt(` + `); + + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + spyOn(fixture.componentInstance, 'onDateSelect'); + spyOn(fixture.componentInstance, 'onModelChange'); + + // open + dpInput.open(); + fixture.detectChanges(); + + // click on a date + fixture.nativeElement.querySelectorAll('.ngb-dp-day')[3].click(); // 1 MAR 2018 + fixture.detectChanges(); + expect(fixture.componentInstance.onDateSelect).toHaveBeenCalledTimes(1); + expect(fixture.componentInstance.onModelChange).toHaveBeenCalledTimes(1); + + // open again + dpInput.open(); + fixture.detectChanges(); + + // click the same date + fixture.nativeElement.querySelectorAll('.ngb-dp-day')[3].click(); // 1 MAR 2018 + fixture.detectChanges(); + expect(fixture.componentInstance.onDateSelect).toHaveBeenCalledTimes(2); + expect(fixture.componentInstance.onModelChange).toHaveBeenCalledTimes(1); + + expect(fixture.componentInstance.onDateSelect).toHaveBeenCalledWith(new NgbDate(2018, 3, 1)); + expect(fixture.componentInstance.onModelChange).toHaveBeenCalledWith({year: 2018, month: 3, day: 1}); + }); + }); + + describe('container', () => { + + it('should be appended to the element matching the selector passed to "container"', () => { + const selector = 'body'; + const fixture = createTestCmpt(` + + + `); + + // open date-picker + const button = fixture.nativeElement.querySelector('button'); + button.click(); + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('ngb-datepicker')).toBeNull(); + expect(document.querySelector(selector).querySelector('ngb-datepicker')).not.toBeNull(); + }); + + it('should properly destroy datepicker window when the "container" option is used', () => { + const selector = 'body'; + const fixture = createTestCmpt(` + + + + `); + + // open date-picker + const buttons = fixture.nativeElement.querySelectorAll('button'); + buttons[0].click(); // open button + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('ngb-datepicker')).toBeNull(); + expect(document.querySelector(selector).querySelector('ngb-datepicker')).not.toBeNull(); + + // close date-picker + buttons[1].click(); // close button + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('ngb-datepicker')).toBeNull(); + expect(document.querySelector(selector).querySelector('ngb-datepicker')).toBeNull(); + }); + + it('should add .ngb-dp-body class when attached to body', () => { + const fixture = createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + // No container specified + dpInput.open(); + + let element = document.querySelector('ngb-datepicker') as HTMLElement; + expect(element).not.toBeNull(); + expect(element).not.toHaveCssClass('ngb-dp-body'); + + // Body + dpInput.close(); + fixture.componentInstance.container = 'body'; + fixture.detectChanges(); + dpInput.open(); + + element = document.querySelector('ngb-datepicker') as HTMLElement; + expect(element).not.toBeNull(); + expect(element).toHaveCssClass('ngb-dp-body'); + }); + }); + + describe('positionTarget', () => { + + let positionElementsSpy: jasmine.Spy; + + beforeEach(() => { + positionElementsSpy = jasmine.createSpy('positionElementsSpy'); + spyOnProperty(positioning, 'positionElements').and.returnValue(positionElementsSpy); + }); + + it('should position popup by input if no target provided (default)', () => { + const fixture = createTestCmpt(` + + + `); + const input = fixture.nativeElement.querySelector('input'); + + // open date-picker + const button = fixture.nativeElement.querySelector('button'); + button.click(); + fixture.detectChanges(); + + expect(positionElementsSpy).toHaveBeenCalled(); + expect(positionElementsSpy.calls.argsFor(0)[0]).toBe(input); + }); + + it('should position popup by html element', () => { + const fixture = createTestCmpt(` + + + `); + + // open date-picker + const button = fixture.nativeElement.querySelector('button'); + button.click(); + fixture.detectChanges(); + + expect(positionElementsSpy).toHaveBeenCalled(); + expect(positionElementsSpy.calls.argsFor(0)[0]).toBe(button); + }); + + it('should position popup by css selector', () => { + const selector = '#myButton'; + const fixture = createTestCmpt(` + + + `); + + // open date-picker + const button = fixture.nativeElement.querySelector(selector); + button.click(); + fixture.detectChanges(); + + expect(positionElementsSpy).toHaveBeenCalled(); + expect(positionElementsSpy.calls.argsFor(0)[0]).toBe(button); + }); + + it('should throw error if target element does not exists', fakeAsync(() => { + const fixture = createTestCmpt(``); + const dpInput = fixture.debugElement.query(By.directive(NgbInputDatepicker)).injector.get(NgbInputDatepicker); + + dpInput.open(); + fixture.detectChanges(); + + expect(() => tick()) + .toThrowError('ngbDatepicker could not find element declared in [positionTarget] to position against.'); + })); + }); + + describe('Native adapter', () => { + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestNativeComponent], + imports: [NgbDatepickerModule, FormsModule], + providers: [{provide: NgbDateAdapter, useClass: NgbDateNativeAdapter}] + }); + }); + + it('should format bound date as ISO (by default) in the input field', fakeAsync(() => { + const fixture = createTestNativeCmpt(``); + const input = fixture.nativeElement.querySelector('input'); + + fixture.componentInstance.date = new Date(2018, 0, 3); + fixture.detectChanges(); + tick(); + expect(input.value).toBe('2018-01-03'); + + fixture.componentInstance.date = new Date(2018, 10, 13); + fixture.detectChanges(); + tick(); + expect(input.value).toBe('2018-11-13'); + })); + + it('should parse user-entered date as ISO (by default)', () => { + const fixture = createTestNativeCmpt(``); + const inputDebugEl = fixture.debugElement.query(By.css('input')); + + inputDebugEl.triggerEventHandler('input', {target: {value: '2018-01-03'}}); + expect(fixture.componentInstance.date).toEqual(new Date(2018, 0, 3)); + }); + }); +}); + +@Injectable() +class NgbDateNativeAdapter extends NgbDateAdapter { + fromModel(date: Date): NgbDateStruct { + return (date && date.getFullYear) ? {year: date.getFullYear(), month: date.getMonth() + 1, day: date.getDate()} : + null; + } + + toModel(date: NgbDateStruct): Date { return date ? new Date(date.year, date.month - 1, date.day) : null; } +} + +@Component({selector: 'test-native-cmp', template: ''}) +class TestNativeComponent { + date: Date; +} + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + container; + date: NgbDateStruct; + isDisabled; + + onNavigate() {} + + onDateSelect() {} + + onModelChange() {} + + onClose() {} + + open(d: NgbInputDatepicker) { d.open(); } + + close(d: NgbInputDatepicker) { d.close(); } + + toggle(d: NgbInputDatepicker) { d.toggle(); } + + noop() {} +} diff --git a/src/datepicker/datepicker-input.ts b/src/datepicker/datepicker-input.ts new file mode 100644 index 0000000..c7c2561 --- /dev/null +++ b/src/datepicker/datepicker-input.ts @@ -0,0 +1,472 @@ +import { + ChangeDetectorRef, + ComponentFactoryResolver, + ComponentRef, + Directive, + ElementRef, + EventEmitter, + forwardRef, + Inject, + Input, + NgZone, + OnChanges, + OnDestroy, + Output, + Renderer2, + SimpleChanges, + TemplateRef, + ViewContainerRef +} from '@angular/core'; +import {DOCUMENT} from '@angular/common'; +import {AbstractControl, ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, Validator} from '@angular/forms'; + +import {ngbAutoClose} from '../util/autoclose'; +import {ngbFocusTrap} from '../util/focus-trap'; +import {PlacementArray, positionElements} from '../util/positioning'; + +import {NgbDateAdapter} from './adapters/ngb-date-adapter'; +import {NgbDatepicker, NgbDatepickerNavigateEvent} from './datepicker'; +import {DayTemplateContext} from './datepicker-day-template-context'; +import {NgbDatepickerService} from './datepicker-service'; +import {NgbCalendar} from './ngb-calendar'; +import {NgbDate} from './ngb-date'; +import {NgbDateParserFormatter} from './ngb-date-parser-formatter'; +import {NgbDateStruct} from './ngb-date-struct'; + +const NGB_DATEPICKER_VALUE_ACCESSOR = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NgbInputDatepicker), + multi: true +}; + +const NGB_DATEPICKER_VALIDATOR = { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => NgbInputDatepicker), + multi: true +}; + +/** + * A directive that allows to stick a datepicker popup to an input field. + * + * Manages interaction with the input field itself, does value formatting and provides forms integration. + */ +@Directive({ + selector: 'input[ngbDatepicker]', + exportAs: 'ngbDatepicker', + host: { + '(input)': 'manualDateChange($event.target.value)', + '(change)': 'manualDateChange($event.target.value, true)', + '(blur)': 'onBlur()', + '[disabled]': 'disabled' + }, + providers: [NGB_DATEPICKER_VALUE_ACCESSOR, NGB_DATEPICKER_VALIDATOR, NgbDatepickerService] +}) +export class NgbInputDatepicker implements OnChanges, + OnDestroy, ControlValueAccessor, Validator { + private _cRef: ComponentRef = null; + private _disabled = false; + private _model: NgbDate; + private _inputValue: string; + private _zoneSubscription: any; + + /** + * Indicates whether the datepicker popup should be closed automatically after date selection / outside click or not. + * + * * `true` - the popup will close on both date selection and outside click. + * * `false` - the popup can only be closed manually via `close()` or `toggle()` methods. + * * `"inside"` - the popup will close on date selection, but not outside clicks. + * * `"outside"` - the popup will close only on the outside click and not on date selection/inside clicks. + * + * @since 3.0.0 + */ + @Input() autoClose: boolean | 'inside' | 'outside' = true; + + /** + * The reference to a custom template for the day. + * + * Allows to completely override the way a day 'cell' in the calendar is displayed. + * + * See [`DayTemplateContext`](#/components/datepicker/api#DayTemplateContext) for the data you get inside. + */ + @Input() dayTemplate: TemplateRef; + + /** + * The callback to pass any arbitrary data to the template cell via the + * [`DayTemplateContext`](#/components/datepicker/api#DayTemplateContext)'s `data` parameter. + * + * `current` is the month that is currently displayed by the datepicker. + * + * @since 3.3.0 + */ + @Input() dayTemplateData: (date: NgbDate, current: {year: number, month: number}) => any; + + /** + * The number of months to display. + */ + @Input() displayMonths: number; + + /** + * The first day of the week. + * + * With default calendar we use ISO 8601: 'weekday' is 1=Mon ... 7=Sun. + */ + @Input() firstDayOfWeek: number; + + /** + * The reference to the custom template for the datepicker footer. + * + * @since 3.3.0 + */ + @Input() footerTemplate: TemplateRef; + + /** + * The callback to mark some dates as disabled. + * + * It is called for each new date when navigating to a different month. + * + * `current` is the month that is currently displayed by the datepicker. + */ + @Input() markDisabled: (date: NgbDate, current: {year: number, month: number}) => boolean; + + /** + * The earliest date that can be displayed or selected. Also used for form validation. + * + * If not provided, 'year' select box will display 10 years before the current month. + */ + @Input() minDate: NgbDateStruct; + + /** + * The latest date that can be displayed or selected. Also used for form validation. + * + * If not provided, 'year' select box will display 10 years after the current month. + */ + @Input() maxDate: NgbDateStruct; + + /** + * Navigation type. + * + * * `"select"` - select boxes for month and navigation arrows + * * `"arrows"` - only navigation arrows + * * `"none"` - no navigation visible at all + */ + @Input() navigation: 'select' | 'arrows' | 'none'; + + /** + * The way of displaying days that don't belong to the current month. + * + * * `"visible"` - days are visible + * * `"hidden"` - days are hidden, white space preserved + * * `"collapsed"` - days are collapsed, so the datepicker height might change between months + * + * For the 2+ months view, days in between months are never shown. + */ + @Input() outsideDays: 'visible' | 'collapsed' | 'hidden'; + + /** + * The preferred placement of the datepicker popup. + * + * Possible values are `"top"`, `"top-left"`, `"top-right"`, `"bottom"`, `"bottom-left"`, + * `"bottom-right"`, `"left"`, `"left-top"`, `"left-bottom"`, `"right"`, `"right-top"`, + * `"right-bottom"` + * + * Accepts an array of strings or a string with space separated possible values. + * + * The default order of preference is `"bottom-left bottom-right top-left top-right"` + * + * Please see the [positioning overview](#/positioning) for more details. + */ + @Input() placement: PlacementArray = ['bottom-left', 'bottom-right', 'top-left', 'top-right']; + + /** + * If `true`, weekdays will be displayed. + */ + @Input() showWeekdays: boolean; + + /** + * If `true`, week numbers will be displayed. + */ + @Input() showWeekNumbers: boolean; + + /** + * The date to open calendar with. + * + * With the default calendar we use ISO 8601: 'month' is 1=Jan ... 12=Dec. + * If nothing or invalid date is provided, calendar will open with current month. + * + * You could use `navigateTo(date)` method as an alternative. + */ + @Input() startDate: {year: number, month: number, day?: number}; + + /** + * A selector specifying the element the datepicker popup should be appended to. + * + * Currently only supports `"body"`. + */ + @Input() container: string; + + /** + * A css selector or html element specifying the element the datepicker popup should be positioned against. + * + * By default the input is used as a target. + * + * @since 4.2.0 + */ + @Input() positionTarget: string | HTMLElement; + + /** + * An event emitted when user selects a date using keyboard or mouse. + * + * The payload of the event is currently selected `NgbDate`. + * + * @since 1.1.1 + */ + @Output() dateSelect = new EventEmitter(); + + /** + * Event emitted right after the navigation happens and displayed month changes. + * + * See [`NgbDatepickerNavigateEvent`](#/components/datepicker/api#NgbDatepickerNavigateEvent) for the payload info. + */ + @Output() navigate = new EventEmitter(); + + /** + * An event fired after closing datepicker window. + * + * @since 4.2.0 + */ + @Output() closed = new EventEmitter(); + + @Input() + get disabled() { + return this._disabled; + } + set disabled(value: any) { + this._disabled = value === '' || (value && value !== 'false'); + + if (this.isOpen()) { + this._cRef.instance.setDisabledState(this._disabled); + } + } + + private _onChange = (_: any) => {}; + private _onTouched = () => {}; + private _validatorChange = () => {}; + + + constructor( + private _parserFormatter: NgbDateParserFormatter, private _elRef: ElementRef, + private _vcRef: ViewContainerRef, private _renderer: Renderer2, private _cfr: ComponentFactoryResolver, + private _ngZone: NgZone, private _service: NgbDatepickerService, private _calendar: NgbCalendar, + private _dateAdapter: NgbDateAdapter, @Inject(DOCUMENT) private _document: any, + private _changeDetector: ChangeDetectorRef) { + this._zoneSubscription = _ngZone.onStable.subscribe(() => this._updatePopupPosition()); + } + + registerOnChange(fn: (value: any) => any): void { this._onChange = fn; } + + registerOnTouched(fn: () => any): void { this._onTouched = fn; } + + registerOnValidatorChange(fn: () => void): void { this._validatorChange = fn; } + + setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } + + validate(c: AbstractControl): {[key: string]: any} { + const value = c.value; + + if (value === null || value === undefined) { + return null; + } + + const ngbDate = this._fromDateStruct(this._dateAdapter.fromModel(value)); + + if (!this._calendar.isValid(ngbDate)) { + return {'ngbDate': {invalid: c.value}}; + } + + if (this.minDate && ngbDate.before(NgbDate.from(this.minDate))) { + return {'ngbDate': {requiredBefore: this.minDate}}; + } + + if (this.maxDate && ngbDate.after(NgbDate.from(this.maxDate))) { + return {'ngbDate': {requiredAfter: this.maxDate}}; + } + } + + writeValue(value) { + this._model = this._fromDateStruct(this._dateAdapter.fromModel(value)); + this._writeModelValue(this._model); + } + + manualDateChange(value: string, updateView = false) { + const inputValueChanged = value !== this._inputValue; + if (inputValueChanged) { + this._inputValue = value; + this._model = this._fromDateStruct(this._parserFormatter.parse(value)); + } + if (inputValueChanged || !updateView) { + this._onChange(this._model ? this._dateAdapter.toModel(this._model) : (value === '' ? null : value)); + } + if (updateView && this._model) { + this._writeModelValue(this._model); + } + } + + isOpen() { return !!this._cRef; } + + /** + * Opens the datepicker popup. + * + * If the related form control contains a valid date, the corresponding month will be opened. + */ + open() { + if (!this.isOpen()) { + const cf = this._cfr.resolveComponentFactory(NgbDatepicker); + this._cRef = this._vcRef.createComponent(cf); + + this._applyPopupStyling(this._cRef.location.nativeElement); + this._applyDatepickerInputs(this._cRef.instance); + this._subscribeForDatepickerOutputs(this._cRef.instance); + this._cRef.instance.ngOnInit(); + this._cRef.instance.writeValue(this._dateAdapter.toModel(this._model)); + + // date selection event handling + this._cRef.instance.registerOnChange((selectedDate) => { + this.writeValue(selectedDate); + this._onChange(selectedDate); + this._onTouched(); + }); + + this._cRef.changeDetectorRef.detectChanges(); + + this._cRef.instance.setDisabledState(this.disabled); + + if (this.container === 'body') { + window.document.querySelector(this.container).appendChild(this._cRef.location.nativeElement); + } + + // focus handling + ngbFocusTrap(this._cRef.location.nativeElement, this.closed, true); + this._cRef.instance.focus(); + + ngbAutoClose( + this._ngZone, this._document, this.autoClose, () => this.close(), this.closed, [], + [this._elRef.nativeElement, this._cRef.location.nativeElement]); + } + } + + /** + * Closes the datepicker popup. + */ + close() { + if (this.isOpen()) { + this._vcRef.remove(this._vcRef.indexOf(this._cRef.hostView)); + this._cRef = null; + this.closed.emit(); + this._changeDetector.markForCheck(); + } + } + + /** + * Toggles the datepicker popup. + */ + toggle() { + if (this.isOpen()) { + this.close(); + } else { + this.open(); + } + } + + /** + * Navigates to the provided date. + * + * With the default calendar we use ISO 8601: 'month' is 1=Jan ... 12=Dec. + * If nothing or invalid date provided calendar will open current month. + * + * Use the `[startDate]` input as an alternative. + */ + navigateTo(date?: {year: number, month: number, day?: number}) { + if (this.isOpen()) { + this._cRef.instance.navigateTo(date); + } + } + + onBlur() { this._onTouched(); } + + ngOnChanges(changes: SimpleChanges) { + if (changes['minDate'] || changes['maxDate']) { + this._validatorChange(); + } + } + + ngOnDestroy() { + this.close(); + this._zoneSubscription.unsubscribe(); + } + + private _applyDatepickerInputs(datepickerInstance: NgbDatepicker): void { + ['dayTemplate', 'dayTemplateData', 'displayMonths', 'firstDayOfWeek', 'footerTemplate', 'markDisabled', 'minDate', + 'maxDate', 'navigation', 'outsideDays', 'showNavigation', 'showWeekdays', 'showWeekNumbers'] + .forEach((optionName: string) => { + if (this[optionName] !== undefined) { + datepickerInstance[optionName] = this[optionName]; + } + }); + datepickerInstance.startDate = this.startDate || this._model; + } + + private _applyPopupStyling(nativeElement: any) { + this._renderer.addClass(nativeElement, 'dropdown-menu'); + this._renderer.addClass(nativeElement, 'show'); + + if (this.container === 'body') { + this._renderer.addClass(nativeElement, 'ngb-dp-body'); + } + } + + private _subscribeForDatepickerOutputs(datepickerInstance: NgbDatepicker) { + datepickerInstance.navigate.subscribe(navigateEvent => this.navigate.emit(navigateEvent)); + datepickerInstance.select.subscribe(date => { + this.dateSelect.emit(date); + if (this.autoClose === true || this.autoClose === 'inside') { + this.close(); + } + }); + } + + private _writeModelValue(model: NgbDate) { + const value = this._parserFormatter.format(model); + this._inputValue = value; + this._renderer.setProperty(this._elRef.nativeElement, 'value', value); + if (this.isOpen()) { + this._cRef.instance.writeValue(this._dateAdapter.toModel(model)); + this._onTouched(); + } + } + + private _fromDateStruct(date: NgbDateStruct): NgbDate { + const ngbDate = date ? new NgbDate(date.year, date.month, date.day) : null; + return this._calendar.isValid(ngbDate) ? ngbDate : null; + } + + private _updatePopupPosition() { + if (!this._cRef) { + return; + } + + let hostElement: HTMLElement; + if (typeof this.positionTarget === 'string') { + hostElement = window.document.querySelector(this.positionTarget); + } else if (this.positionTarget instanceof HTMLElement) { + hostElement = this.positionTarget; + } else { + hostElement = this._elRef.nativeElement; + } + + if (this.positionTarget && !hostElement) { + throw new Error('ngbDatepicker could not find element declared in [positionTarget] to position against.'); + } + + positionElements(hostElement, this._cRef.location.nativeElement, this.placement, this.container === 'body'); + } +} diff --git a/src/datepicker/datepicker-integration.spec.ts b/src/datepicker/datepicker-integration.spec.ts new file mode 100644 index 0000000..1773982 --- /dev/null +++ b/src/datepicker/datepicker-integration.spec.ts @@ -0,0 +1,113 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Component, Injectable} from '@angular/core'; +import {NgbDatepickerModule, NgbDateStruct} from './datepicker.module'; +import {NgbCalendar, NgbCalendarGregorian} from './ngb-calendar'; +import {NgbDate} from './ngb-date'; +import {getMonthSelect, getYearSelect} from '../test/datepicker/common'; +import {NgbDatepickerI18n, NgbDatepickerI18nDefault} from './datepicker-i18n'; + +describe('ngb-datepicker integration', () => { + + beforeEach( + () => { TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbDatepickerModule]}); }); + + it('should allow overriding datepicker calendar', () => { + + class FixedTodayCalendar extends NgbCalendarGregorian { + getToday() { return new NgbDate(2000, 7, 1); } + } + + TestBed.overrideComponent(TestComponent, { + set: { + template: ``, + providers: [{provide: NgbCalendar, useClass: FixedTodayCalendar}] + } + }); + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + expect(getMonthSelect(fixture.nativeElement).value).toBe('7'); + expect(getYearSelect(fixture.nativeElement).value).toBe('2000'); + }); + + describe('i18n', () => { + + const ALPHABET = 'ABCDEFGHIJKLMNOPRSTQUVWXYZ'; + + @Injectable() + class CustomI18n extends NgbDatepickerI18nDefault { + // alphabetic months: Jan -> A, Feb -> B, etc + getMonthShortName(month: number) { return ALPHABET[month - 1]; } + + // alphabetic months: Jan -> A, Feb -> B, etc + getMonthFullName(month: number) { return ALPHABET[month - 1]; } + + // alphabetic days: 1 -> A, 2 -> B, etc + getDayNumerals(date: NgbDateStruct) { return ALPHABET[date.day - 1]; } + + // alphabetic week numbers: 1 -> A, 2 -> B, etc + getWeekNumerals(week: number) { return ALPHABET[week - 1]; } + + // reversed years: 1998 -> 9881 + getYearNumerals(year: number) { return `${year}`.split('').reverse().join(''); } + } + + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.overrideComponent(TestComponent, { + set: { + template: ` + `, + providers: [{provide: NgbDatepickerI18n, useClass: CustomI18n}] + } + }); + + fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + }); + + it('should allow overriding month names', () => { + const monthOptions = getMonthSelect(fixture.nativeElement).querySelectorAll('option'); + const months = Array.from(monthOptions).map(o => o.innerHTML); + expect(months.join('')).toEqual(ALPHABET.slice(0, 12)); + }); + + it('should allow overriding week number numerals', () => { + // month view that displays JAN 2018 starts directly with week 01 + const weekNumberElements = fixture.nativeElement.querySelectorAll('.ngb-dp-week-number'); + const weekNumbers = Array.from(weekNumberElements).map((o: HTMLElement) => o.innerHTML); + expect(weekNumbers.slice(0, 6).join('')).toEqual(ALPHABET.slice(0, 6)); + }); + + it('should allow overriding day numerals', () => { + // month view that displays JAN 2018 starts directly with 01 JAN + const daysElements = fixture.nativeElement.querySelectorAll('.ngb-dp-day > div'); + const days = Array.from(daysElements).map((o: HTMLElement) => o.innerHTML); + expect(days.slice(0, 26).join('')).toEqual(ALPHABET); + }); + + it('should allow overriding year numerals', () => { + // we have only 2017, 2018 and 2019 in the select box + const yearOptions = getYearSelect(fixture.nativeElement).querySelectorAll('option'); + const years = Array.from(yearOptions).map(o => o.innerText); + expect(years).toEqual(['7102', '8102', '9102']); + }); + + it('should allow overriding year and month numerals for multiple months', () => { + // we have JAN 2018 and FEB 2018 -> A 8102 and B 8102 + const monthNameElements = fixture.nativeElement.querySelectorAll('.ngb-dp-month-name'); + const monthNames = Array.from(monthNameElements).map((o: HTMLElement) => o.innerText.trim()); + expect(monthNames).toEqual(['A 8102', 'B 8102']); + }); + }); +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { +} diff --git a/src/datepicker/datepicker-keymap-service.spec.ts b/src/datepicker/datepicker-keymap-service.spec.ts new file mode 100644 index 0000000..a3269ae --- /dev/null +++ b/src/datepicker/datepicker-keymap-service.spec.ts @@ -0,0 +1,133 @@ +import {NgbDatepickerKeyMapService} from './datepicker-keymap-service'; +import {NgbCalendar, NgbCalendarGregorian} from './ngb-calendar'; +import {NgbDatepickerService} from './datepicker-service'; +import {TestBed} from '@angular/core/testing'; +import {Subject} from 'rxjs'; +import {NgbDate} from './ngb-date'; +import {Key} from '../util/key'; +import {Type} from '@angular/core'; + +const event = (keyCode: number, shift = false) => + ({which: keyCode, shiftKey: shift, preventDefault: () => {}, stopPropagation: () => {}}); + +describe('ngb-datepicker-keymap-service', () => { + + let service: NgbDatepickerKeyMapService; + let calendar: NgbCalendar; + let mock: {focus, focusMove, focusSelect, model$}; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + NgbDatepickerKeyMapService, {provide: NgbCalendar, useClass: NgbCalendarGregorian}, { + provide: NgbDatepickerService, + useValue: {focus: () => {}, focusMove: () => {}, focusSelect: () => {}, model$: new Subject()} + } + ] + }); + + calendar = TestBed.get(NgbCalendar as Type); + service = TestBed.get(NgbDatepickerKeyMapService); + mock = TestBed.get(NgbDatepickerService); + + spyOn(mock, 'focus'); + spyOn(mock, 'focusMove'); + spyOn(mock, 'focusSelect'); + }); + + it('should be instantiated', () => { expect(service).toBeTruthy(); }); + + it('should move focus by 1 day or 1 week with "Arrow" keys', () => { + service.processKey(event(Key.ArrowUp)); + expect(mock.focusMove).toHaveBeenCalledWith('d', -7); + + service.processKey(event(Key.ArrowDown)); + expect(mock.focusMove).toHaveBeenCalledWith('d', 7); + + service.processKey(event(Key.ArrowLeft)); + expect(mock.focusMove).toHaveBeenCalledWith('d', -1); + + service.processKey(event(Key.ArrowRight)); + expect(mock.focusMove).toHaveBeenCalledWith('d', 1); + + expect(mock.focusMove).toHaveBeenCalledTimes(4); + }); + + it('should move focus by 1 month or year "PgUp" and "PageDown"', () => { + service.processKey(event(Key.PageUp)); + expect(mock.focusMove).toHaveBeenCalledWith('m', -1); + + service.processKey(event(Key.PageDown)); + expect(mock.focusMove).toHaveBeenCalledWith('m', 1); + + service.processKey(event(Key.PageUp, true)); + expect(mock.focusMove).toHaveBeenCalledWith('y', -1); + + service.processKey(event(Key.PageDown, true)); + expect(mock.focusMove).toHaveBeenCalledWith('y', 1); + + expect(mock.focusMove).toHaveBeenCalledTimes(4); + }); + + it('should select focused date with "Space" and "Enter"', () => { + service.processKey(event(Key.Enter)); + service.processKey(event(Key.Space)); + expect(mock.focusSelect).toHaveBeenCalledTimes(2); + }); + + it('should move focus to the first and last days in the view with "Home" and "End"', () => { + service.processKey(event(Key.Home)); + expect(mock.focus).toHaveBeenCalledWith(undefined); + + service.processKey(event(Key.End)); + expect(mock.focus).toHaveBeenCalledWith(undefined); + + mock.model$.next({firstDate: new NgbDate(2017, 1, 1), lastDate: new NgbDate(2017, 12, 1)}); + + service.processKey(event(Key.Home)); + expect(mock.focus).toHaveBeenCalledWith(new NgbDate(2017, 1, 1)); + + service.processKey(event(Key.End)); + expect(mock.focus).toHaveBeenCalledWith(new NgbDate(2017, 12, 1)); + + expect(mock.focus).toHaveBeenCalledTimes(4); + }); + + it('should move focus to the "min" and "max" dates with "Home" and "End"', () => { + service.processKey(event(Key.Home, true)); + expect(mock.focus).toHaveBeenCalledWith(undefined); + + service.processKey(event(Key.End, true)); + expect(mock.focus).toHaveBeenCalledWith(undefined); + + mock.model$.next({minDate: new NgbDate(2017, 1, 1), maxDate: new NgbDate(2017, 12, 1), months: []}); + + service.processKey(event(Key.Home, true)); + expect(mock.focus).toHaveBeenCalledWith(new NgbDate(2017, 1, 1)); + + service.processKey(event(Key.End, true)); + expect(mock.focus).toHaveBeenCalledWith(new NgbDate(2017, 12, 1)); + + expect(mock.focus).toHaveBeenCalledTimes(4); + }); + + it('should prevent default and stop propagation of the known key', () => { + let e = event(Key.ArrowUp); + spyOn(e, 'preventDefault'); + spyOn(e, 'stopPropagation'); + + service.processKey(e); + expect(e.preventDefault).toHaveBeenCalled(); + expect(e.stopPropagation).toHaveBeenCalled(); + + // unknown key + e = event(23); + spyOn(e, 'preventDefault'); + spyOn(e, 'stopPropagation'); + + service.processKey(e); + expect(e.preventDefault).not.toHaveBeenCalled(); + expect(e.stopPropagation).not.toHaveBeenCalled(); + }); + +}); diff --git a/src/datepicker/datepicker-keymap-service.ts b/src/datepicker/datepicker-keymap-service.ts new file mode 100644 index 0000000..0bf6769 --- /dev/null +++ b/src/datepicker/datepicker-keymap-service.ts @@ -0,0 +1,62 @@ +import {Injectable} from '@angular/core'; +import {NgbDatepickerService} from './datepicker-service'; +import {NgbCalendar} from './ngb-calendar'; +import {Key} from '../util/key'; +import {NgbDate} from './ngb-date'; + +@Injectable() +export class NgbDatepickerKeyMapService { + private _minDate: NgbDate; + private _maxDate: NgbDate; + private _firstViewDate: NgbDate; + private _lastViewDate: NgbDate; + + constructor(private _service: NgbDatepickerService, private _calendar: NgbCalendar) { + _service.model$.subscribe(model => { + this._minDate = model.minDate; + this._maxDate = model.maxDate; + this._firstViewDate = model.firstDate; + this._lastViewDate = model.lastDate; + }); + } + + processKey(event: KeyboardEvent) { + // tslint:disable-next-line:deprecation + switch (event.which) { + case Key.PageUp: + this._service.focusMove(event.shiftKey ? 'y' : 'm', -1); + break; + case Key.PageDown: + this._service.focusMove(event.shiftKey ? 'y' : 'm', 1); + break; + case Key.End: + this._service.focus(event.shiftKey ? this._maxDate : this._lastViewDate); + break; + case Key.Home: + this._service.focus(event.shiftKey ? this._minDate : this._firstViewDate); + break; + case Key.ArrowLeft: + this._service.focusMove('d', -1); + break; + case Key.ArrowUp: + this._service.focusMove('d', -this._calendar.getDaysPerWeek()); + break; + case Key.ArrowRight: + this._service.focusMove('d', 1); + break; + case Key.ArrowDown: + this._service.focusMove('d', this._calendar.getDaysPerWeek()); + break; + case Key.Enter: + case Key.Space: + this._service.focusSelect(); + break; + default: + return; + } + + // note 'return' in default case + event.preventDefault(); + event.stopPropagation(); + } +} diff --git a/src/datepicker/datepicker-month-view.scss b/src/datepicker/datepicker-month-view.scss new file mode 100644 index 0000000..62af26a --- /dev/null +++ b/src/datepicker/datepicker-month-view.scss @@ -0,0 +1,38 @@ +ngb-datepicker-month-view { + display: block; +} + +.ngb-dp { + &-weekday, + &-week-number { + line-height: 2rem; + text-align: center; + font-style: italic; + } + &-weekday { + color: #5bc0de; + color: var(--info); + } + &-week { + border-radius: 0.25rem; + display: flex; + } + &-weekdays { + border-bottom: 1px solid rgba(0, 0, 0, 0.125); + border-radius: 0; + } + &-day, + &-weekday, + &-week-number { + width: 2rem; + height: 2rem; + } + &-day { + cursor: pointer; + + &.disabled, + &.hidden { + cursor: default; + } + } +} \ No newline at end of file diff --git a/src/datepicker/datepicker-month-view.spec.ts b/src/datepicker/datepicker-month-view.spec.ts new file mode 100644 index 0000000..4a68969 --- /dev/null +++ b/src/datepicker/datepicker-month-view.spec.ts @@ -0,0 +1,338 @@ +import {TestBed, ComponentFixture} from '@angular/core/testing'; +import {createGenericTestComponent, isBrowser} from '../test/common'; + +import {Component} from '@angular/core'; + +import {NgbDatepickerModule} from './datepicker.module'; +import {NgbDatepickerMonthView} from './datepicker-month-view'; +import {MonthViewModel} from './datepicker-view-model'; +import {NgbDate} from './ngb-date'; +import {NgbDatepickerDayView} from './datepicker-day-view'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +function getWeekdays(element: HTMLElement): HTMLElement[] { + return Array.from(element.querySelectorAll('.ngb-dp-weekday')); +} + +function getWeekNumbers(element: HTMLElement): HTMLElement[] { + return Array.from(element.querySelectorAll('.ngb-dp-week-number')); +} + +function getDates(element: HTMLElement): HTMLElement[] { + return Array.from(element.querySelectorAll('.ngb-dp-day')); +} + +function expectWeekdays(element: HTMLElement, weekdays: string[]) { + const result = getWeekdays(element).map(td => td.innerText.trim()); + expect(result).toEqual(weekdays); +} + +function expectWeekNumbers(element: HTMLElement, weeknumbers: string[]) { + const result = getWeekNumbers(element).map(td => td.innerText.trim()); + expect(result).toEqual(weeknumbers); +} + +function expectDates(element: HTMLElement, dates: string[]) { + const result = getDates(element).map(td => td.innerText.trim()); + expect(result).toEqual(dates); +} + +describe('ngb-datepicker-month-view', () => { + + beforeEach(() => { + TestBed.overrideModule(NgbDatepickerModule, {set: {exports: [NgbDatepickerMonthView, NgbDatepickerDayView]}}); + TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbDatepickerModule]}); + }); + + it('should show/hide weekdays', () => { + const fixture = createTestComponent( + ''); + + expectWeekdays(fixture.nativeElement, ['Mo', 'Tu']); + + fixture.componentInstance.showWeekdays = false; + fixture.detectChanges(); + expectWeekdays(fixture.nativeElement, []); + }); + + it('should show/hide week numbers', () => { + const fixture = createTestComponent( + ''); + + expectWeekNumbers(fixture.nativeElement, ['1', '2', '3']); + + fixture.componentInstance.showWeekNumbers = false; + fixture.detectChanges(); + expectWeekNumbers(fixture.nativeElement, []); + }); + + it('should use custom template to display dates', () => { + const fixture = createTestComponent(` + {{ date.day }} + + `); + expectDates(fixture.nativeElement, ['', '1', '2', '3', '4', '']); + }); + + it('should use "date" as an implicit value for the template', () => { + const fixture = createTestComponent(` + {{ d.day }} + + `); + expectDates(fixture.nativeElement, ['', '1', '2', '3', '4', '']); + }); + + it('should send date selection events', () => { + const fixture = createTestComponent(` + {{ date.day }} + + `); + + spyOn(fixture.componentInstance, 'onClick'); + + const dates = getDates(fixture.nativeElement); + dates[1].click(); + + expect(fixture.componentInstance.onClick).toHaveBeenCalledWith(new NgbDate(2016, 8, 1)); + }); + + it('should not send date selection events for hidden and disabled dates', () => { + const fixture = createTestComponent(` + {{ date.day }} + + `); + + spyOn(fixture.componentInstance, 'onClick'); + + const dates = getDates(fixture.nativeElement); + dates[0].click(); // hidden + dates[2].click(); // disabled + + expect(fixture.componentInstance.onClick).not.toHaveBeenCalled(); + }); + + it('should set cursor to pointer or default', () => { + const fixture = createTestComponent(` + {{ date.day }} + + `); + + const dates = getDates(fixture.nativeElement); + // hidden + expect(window.getComputedStyle(dates[0]).getPropertyValue('cursor')).toBe('default'); + // normal + expect(window.getComputedStyle(dates[1]).getPropertyValue('cursor')).toBe('pointer'); + // disabled + expect(window.getComputedStyle(dates[2]).getPropertyValue('cursor')).toBe('default'); + }); + + it('should apply correct CSS classes to days', () => { + const fixture = createTestComponent(` + {{ date.day }} + + `); + + let dates = getDates(fixture.nativeElement); + // hidden + expect(dates[0]).toHaveCssClass('hidden'); + expect(dates[0]).not.toHaveCssClass('disabled'); + expect(dates[0]).not.toHaveCssClass('ngb-dp-today'); + // normal + expect(dates[1]).not.toHaveCssClass('hidden'); + expect(dates[1]).not.toHaveCssClass('disabled'); + expect(dates[1]).not.toHaveCssClass('ngb-dp-today'); + // disabled + expect(dates[2]).not.toHaveCssClass('hidden'); + expect(dates[2]).toHaveCssClass('disabled'); + expect(dates[2]).toHaveCssClass('ngb-dp-today'); + }); + + it('should not display collapsed weeks', () => { + const fixture = createTestComponent(` + {{ date.day }} + + + `); + + expectDates(fixture.nativeElement, ['', '1', '2', '3', '4', '']); + }); + + it('should add correct aria-label attribute', () => { + const fixture = createTestComponent(` + {{ date.day }} + + `); + + let dates = getDates(fixture.nativeElement); + expect(dates[0].getAttribute('aria-label')).toBe('Monday'); + }); +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + month: MonthViewModel = { + firstDate: new NgbDate(2016, 8, 1), + lastDate: new NgbDate(2016, 8, 31), + year: 2016, + number: 8, + weekdays: [1, 2], + weeks: [ + // month: 7, 8 + { + number: 1, + days: [ + { + date: new NgbDate(2016, 7, 4), + context: { + currentMonth: 8, + $implicit: new NgbDate(2016, 7, 4), + date: new NgbDate(2016, 7, 4), + disabled: false, + focused: false, + selected: false, + today: false + }, + tabindex: -1, + ariaLabel: 'Monday', + hidden: true + }, + { + date: new NgbDate(2016, 8, 1), + context: { + currentMonth: 8, + $implicit: new NgbDate(2016, 8, 1), + date: new NgbDate(2016, 8, 1), + disabled: false, + focused: false, + selected: false, + today: false + }, + tabindex: -1, + ariaLabel: 'Monday', + hidden: false + } + ], + collapsed: false + }, + // month: 8, 8 + { + number: 2, + days: [ + { + date: new NgbDate(2016, 8, 2), + context: { + currentMonth: 8, + $implicit: new NgbDate(2016, 8, 2), + date: new NgbDate(2016, 8, 2), + disabled: true, + focused: false, + selected: false, + today: true + }, + tabindex: -1, + ariaLabel: 'Friday', + hidden: false + }, + { + date: new NgbDate(2016, 8, 3), + context: { + currentMonth: 8, + $implicit: new NgbDate(2016, 8, 3), + date: new NgbDate(2016, 8, 3), + disabled: false, + focused: false, + selected: false, + today: false + }, + tabindex: -1, + ariaLabel: 'Saturday', + hidden: false + } + ], + collapsed: false + }, + // month: 8, 9 + { + number: 3, + days: [ + { + date: new NgbDate(2016, 8, 4), + context: { + currentMonth: 8, + $implicit: new NgbDate(2016, 8, 4), + date: new NgbDate(2016, 8, 4), + disabled: false, + focused: false, + selected: false, + today: false + }, + tabindex: -1, + ariaLabel: 'Sunday', + hidden: false + }, + { + date: new NgbDate(2016, 9, 1), + context: { + currentMonth: 8, + $implicit: new NgbDate(2016, 9, 1), + date: new NgbDate(2016, 9, 1), + disabled: false, + focused: false, + selected: false, + today: false + }, + tabindex: -1, + ariaLabel: 'Saturday', + hidden: true + } + ], + collapsed: false + }, + // month: 9, 9 -> to collapse + { + number: 4, + days: [ + { + date: new NgbDate(2016, 9, 2), + context: { + currentMonth: 8, + $implicit: new NgbDate(2016, 9, 2), + date: new NgbDate(2016, 9, 2), + disabled: false, + focused: false, + selected: false, + today: false + }, + tabindex: -1, + ariaLabel: 'Sunday', + hidden: true + }, + { + date: new NgbDate(2016, 9, 3), + context: { + currentMonth: 8, + $implicit: new NgbDate(2016, 9, 3), + date: new NgbDate(2016, 9, 3), + disabled: false, + focused: false, + selected: false, + today: false + }, + tabindex: -1, + ariaLabel: 'Monday', + hidden: true + } + ], + collapsed: true + } + ] + }; + + showWeekdays = true; + showWeekNumbers = true; + outsideDays = 'visible'; + + onClick = () => {}; +} diff --git a/src/datepicker/datepicker-month-view.ts b/src/datepicker/datepicker-month-view.ts new file mode 100644 index 0000000..d2411e8 --- /dev/null +++ b/src/datepicker/datepicker-month-view.ts @@ -0,0 +1,51 @@ +import {Component, Input, TemplateRef, Output, EventEmitter, ViewEncapsulation} from '@angular/core'; +import {MonthViewModel, DayViewModel} from './datepicker-view-model'; +import {NgbDate} from './ngb-date'; +import {NgbDatepickerI18n} from './datepicker-i18n'; +import {DayTemplateContext} from './datepicker-day-template-context'; + +@Component({ + selector: 'ngb-datepicker-month-view', + host: {'role': 'grid'}, + encapsulation: ViewEncapsulation.None, + styleUrls: ['./datepicker-month-view.scss'], + template: ` +
+
+
+ {{ i18n.getWeekdayShortName(w) }} +
+
+ +
+
{{ i18n.getWeekNumerals(week.number) }}
+
+ + + +
+
+
+ ` +}) +export class NgbDatepickerMonthView { + @Input() dayTemplate: TemplateRef; + @Input() month: MonthViewModel; + @Input() showWeekdays; + @Input() showWeekNumbers; + + @Output() select = new EventEmitter(); + + constructor(public i18n: NgbDatepickerI18n) {} + + doSelect(day: DayViewModel) { + if (!day.context.disabled && !day.hidden) { + this.select.emit(day.date); + } + } +} diff --git a/src/datepicker/datepicker-navigation-select.scss b/src/datepicker/datepicker-navigation-select.scss new file mode 100644 index 0000000..df6f8d9 --- /dev/null +++ b/src/datepicker/datepicker-navigation-select.scss @@ -0,0 +1,6 @@ +ngb-datepicker-navigation-select > .custom-select { + flex: 1 1 auto; + padding: 0 0.5rem; + font-size: 0.875rem; + height: 1.85rem; +} diff --git a/src/datepicker/datepicker-navigation-select.spec.ts b/src/datepicker/datepicker-navigation-select.spec.ts new file mode 100644 index 0000000..96484c6 --- /dev/null +++ b/src/datepicker/datepicker-navigation-select.spec.ts @@ -0,0 +1,141 @@ +import {TestBed, ComponentFixture} from '@angular/core/testing'; +import {createGenericTestComponent} from '../test/common'; +import {getMonthSelect, getYearSelect} from '../test/datepicker/common'; + +import {Component} from '@angular/core'; + +import {NgbDatepickerModule} from './datepicker.module'; +import {NgbDatepickerNavigationSelect} from './datepicker-navigation-select'; +import {NgbDate} from './ngb-date'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +const getOptions = (element: HTMLSelectElement): HTMLOptionElement[] => Array.from(element.options); +const getOptionValues = (element: HTMLSelectElement): string[] => getOptions(element).map(x => x.value); + +function changeSelect(element: HTMLSelectElement, value: string) { + element.value = value; + const evt = document.createEvent('HTMLEvents'); + evt.initEvent('change', true, true); + element.dispatchEvent(evt); +} + +describe('ngb-datepicker-navigation-select', () => { + + beforeEach(() => { + TestBed.overrideModule(NgbDatepickerModule, {set: {exports: [NgbDatepickerNavigationSelect]}}); + TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbDatepickerModule]}); + }); + + it('should generate month options correctly', () => { + const fixture = + createTestComponent(``); + expect(getOptionValues(getMonthSelect(fixture.nativeElement))).toEqual([ + '1', '2', '3', '4', '5', '6', '7', '8', '9', '10' + ]); + + fixture.componentInstance.months = [1, 2, 3]; + fixture.detectChanges(); + expect(getOptionValues(getMonthSelect(fixture.nativeElement))).toEqual(['1', '2', '3']); + }); + + it('should generate year options correctly', () => { + const fixture = + createTestComponent(``); + + const yearSelect = getYearSelect(fixture.nativeElement); + expect(getOptionValues(yearSelect)).toEqual(['2015', '2016', '2017']); + + fixture.componentInstance.years = [2001, 2002, 2003]; + fixture.detectChanges(); + expect(getOptionValues(yearSelect)).toEqual(['2001', '2002', '2003']); + }); + + it('should send date selection events', () => { + const fixture = createTestComponent( + ``); + + const monthSelect = getMonthSelect(fixture.nativeElement); + const yearSelect = getYearSelect(fixture.nativeElement); + spyOn(fixture.componentInstance, 'onSelect'); + + changeSelect(monthSelect, '2'); + expect(fixture.componentInstance.onSelect).toHaveBeenCalledWith(new NgbDate(2016, 2, 1)); + + changeSelect(monthSelect, '10'); + expect(fixture.componentInstance.onSelect).toHaveBeenCalledWith(new NgbDate(2016, 10, 1)); + + changeSelect(yearSelect, '2017'); + expect(fixture.componentInstance.onSelect).toHaveBeenCalledWith(new NgbDate(2017, 8, 1)); + + // out of range + changeSelect(yearSelect, '2000'); + expect(fixture.componentInstance.onSelect).toHaveBeenCalledWith(new NgbDate(NaN, 8, 1)); + }); + + it('should select months and years when date changes', () => { + const fixture = + createTestComponent(``); + + expect(getMonthSelect(fixture.nativeElement).value).toBe('8'); + expect(getYearSelect(fixture.nativeElement).value).toBe('2016'); + + fixture.componentInstance.date = new NgbDate(2017, 9, 22); + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe('9'); + expect(getYearSelect(fixture.nativeElement).value).toBe('2017'); + + // out of range + fixture.componentInstance.date = new NgbDate(2222, 22, 22); + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe(''); + expect(getYearSelect(fixture.nativeElement).value).toBe(''); + }); + + it('should have disabled select boxes when disabled', () => { + const fixture = createTestComponent( + ``); + + expect(getMonthSelect(fixture.nativeElement).disabled).toBe(true); + expect(getYearSelect(fixture.nativeElement).disabled).toBe(true); + }); + + it('should have correct aria attributes on select options', () => { + const fixture = + createTestComponent(``); + + getOptions(getMonthSelect(fixture.nativeElement)).forEach((option, index) => { + expect(option.getAttribute('aria-label')).toBe(fixture.componentInstance.ariaMonths[index]); + }); + }); + + it('should have correct aria attributes on select elements', () => { + const fixture = + createTestComponent(``); + + expect(getMonthSelect(fixture.nativeElement).getAttribute('aria-label')).toBe('Select month'); + expect(getYearSelect(fixture.nativeElement).getAttribute('aria-label')).toBe('Select year'); + + }); + + it('should have correct title attributes on select elements', () => { + const fixture = + createTestComponent(``); + + expect(getMonthSelect(fixture.nativeElement).getAttribute('title')).toBe('Select month'); + expect(getYearSelect(fixture.nativeElement).getAttribute('title')).toBe('Select year'); + + }); + +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + date = new NgbDate(2016, 8, 22); + months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + ariaMonths = ['July', 'August', 'September']; + years = [2015, 2016, 2017]; + + onSelect = () => {}; +} diff --git a/src/datepicker/datepicker-navigation-select.ts b/src/datepicker/datepicker-navigation-select.ts new file mode 100644 index 0000000..9573288 --- /dev/null +++ b/src/datepicker/datepicker-navigation-select.ts @@ -0,0 +1,45 @@ +import {Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core'; +import {NgbDate} from './ngb-date'; +import {toInteger} from '../util/util'; +import {NgbDatepickerI18n} from './datepicker-i18n'; + +@Component({ + selector: 'ngb-datepicker-navigation-select', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + styleUrls: ['./datepicker-navigation-select.scss'], + template: ` + + ` +}) +export class NgbDatepickerNavigationSelect { + @Input() date: NgbDate; + @Input() disabled: boolean; + @Input() months: number[]; + @Input() years: number[]; + + @Output() select = new EventEmitter(); + + constructor(public i18n: NgbDatepickerI18n) {} + + changeMonth(month: string) { this.select.emit(new NgbDate(this.date.year, toInteger(month), 1)); } + + changeYear(year: string) { this.select.emit(new NgbDate(toInteger(year), this.date.month, 1)); } +} diff --git a/src/datepicker/datepicker-navigation.scss b/src/datepicker/datepicker-navigation.scss new file mode 100644 index 0000000..d62475b --- /dev/null +++ b/src/datepicker/datepicker-navigation.scss @@ -0,0 +1,70 @@ +ngb-datepicker-navigation { + display: flex; + align-items: center; +} + +.ngb-dp { + &-navigation-chevron { + border-style: solid; + border-width: 0.2em 0.2em 0 0; + display: inline-block; + width: 0.75em; + height: 0.75em; + margin-left: 0.25em; + margin-right: 0.15em; + transform: rotate(-135deg); + } + + .right &-navigation-chevron { + transform: rotate(45deg); + margin-left: 0.15em; + margin-right: 0.25em; + } + + &-arrow { + display: flex; + flex: 1 1 auto; + padding-right: 0; + padding-left: 0; + margin: 0; + width: 2rem; + height: 2rem; + + &.right { + justify-content: flex-end; + } + + } + + &-arrow-btn { + padding: 0 0.25rem; + margin: 0 0.5rem; + border: none; + background-color: transparent; + z-index: 1; + + &:focus { + outline-width: 1px; + outline-style: auto; + } + + // IE workaround, as outline-style: auto doesn't work + @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { + &:focus { + outline-style: solid; + } + } + } + + &-month-name { + font-size: larger; + height: 2rem; + line-height: 2rem; + text-align: center; + } + + &-navigation-select { + display: flex; + flex: 1 1 9rem; + } +} \ No newline at end of file diff --git a/src/datepicker/datepicker-navigation.spec.ts b/src/datepicker/datepicker-navigation.spec.ts new file mode 100644 index 0000000..de50888 --- /dev/null +++ b/src/datepicker/datepicker-navigation.spec.ts @@ -0,0 +1,137 @@ +import {TestBed, ComponentFixture} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {createGenericTestComponent} from '../test/common'; +import {getMonthSelect, getYearSelect, getNavigationLinks} from '../test/datepicker/common'; + +import {Component} from '@angular/core'; + +import {NgbDatepickerModule} from './datepicker.module'; +import {NavigationEvent} from './datepicker-view-model'; +import {NgbDatepickerNavigation} from './datepicker-navigation'; +import {NgbDate} from './ngb-date'; +import {NgbDatepickerNavigationSelect} from './datepicker-navigation-select'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +function changeSelect(element: HTMLSelectElement, value: string) { + element.value = value; + const evt = document.createEvent('HTMLEvents'); + evt.initEvent('change', true, true); + element.dispatchEvent(evt); +} + +describe('ngb-datepicker-navigation', () => { + + beforeEach(() => { + TestBed.overrideModule( + NgbDatepickerModule, {set: {exports: [NgbDatepickerNavigation, NgbDatepickerNavigationSelect]}}); + TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbDatepickerModule]}); + }); + + it('should toggle navigation select component', () => { + const fixture = createTestComponent(``); + + expect(fixture.debugElement.query(By.directive(NgbDatepickerNavigationSelect))).not.toBeNull(); + expect(getMonthSelect(fixture.nativeElement).value).toBe('8'); + expect(getYearSelect(fixture.nativeElement).value).toBe('2016'); + + fixture.componentInstance.showSelect = false; + fixture.detectChanges(); + expect(fixture.debugElement.query(By.directive(NgbDatepickerNavigationSelect))).toBeNull(); + }); + + it('should send date selection event', () => { + const fixture = createTestComponent(``); + + const monthSelect = getMonthSelect(fixture.nativeElement); + const yearSelect = getYearSelect(fixture.nativeElement); + spyOn(fixture.componentInstance, 'onSelect'); + + changeSelect(monthSelect, '2'); + expect(fixture.componentInstance.onSelect).toHaveBeenCalledWith(new NgbDate(2016, 2, 1)); + + changeSelect(yearSelect, '2020'); + expect(fixture.componentInstance.onSelect).toHaveBeenCalledWith(new NgbDate(2020, 8, 1)); + }); + + it('should make prev navigation button disabled', () => { + const fixture = + createTestComponent(``); + + const links = getNavigationLinks(fixture.nativeElement); + expect(links[0].hasAttribute('disabled')).toBeFalsy(); + + fixture.componentInstance.prevDisabled = true; + fixture.detectChanges(); + expect(links[0].hasAttribute('disabled')).toBeTruthy(); + }); + + it('should make next navigation button disabled', () => { + const fixture = + createTestComponent(``); + + const links = getNavigationLinks(fixture.nativeElement); + expect(links[1].hasAttribute('disabled')).toBeFalsy(); + + fixture.componentInstance.nextDisabled = true; + fixture.detectChanges(); + expect(links[1].hasAttribute('disabled')).toBeTruthy(); + }); + + it('should make year and month select boxes disabled', () => { + const fixture = createTestComponent(``); + + expect(getYearSelect(fixture.nativeElement).disabled).toBeTruthy(); + expect(getMonthSelect(fixture.nativeElement).disabled).toBeTruthy(); + }); + + it('should send navigation events', () => { + const fixture = + createTestComponent(``); + + const links = getNavigationLinks(fixture.nativeElement); + spyOn(fixture.componentInstance, 'onNavigate'); + + // prev + links[0].click(); + expect(fixture.componentInstance.onNavigate).toHaveBeenCalledWith(NavigationEvent.PREV); + + // next + links[1].click(); + expect(fixture.componentInstance.onNavigate).toHaveBeenCalledWith(NavigationEvent.NEXT); + }); + + it('should have buttons of type button', () => { + const fixture = createTestComponent(``); + + const links = getNavigationLinks(fixture.nativeElement); + links.forEach((link) => { expect(link.getAttribute('type')).toBe('button'); }); + }); + + it('should have correct titles and aria attributes on buttons', () => { + const fixture = createTestComponent(``); + + const links = getNavigationLinks(fixture.nativeElement); + expect(links[0].getAttribute('aria-label')).toBe('Previous month'); + expect(links[1].getAttribute('aria-label')).toBe('Next month'); + expect(links[0].getAttribute('title')).toBe('Previous month'); + expect(links[1].getAttribute('title')).toBe('Next month'); + }); + +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + date = new NgbDate(2016, 8, 1); + prevDisabled = false; + nextDisabled = false; + showSelect = true; + selectBoxes = {months: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], years: [2015, 2016, 2017, 2018, 2019, 2020]}; + + onNavigate = () => {}; + onSelect = () => {}; +} diff --git a/src/datepicker/datepicker-navigation.ts b/src/datepicker/datepicker-navigation.ts new file mode 100644 index 0000000..4d033fe --- /dev/null +++ b/src/datepicker/datepicker-navigation.ts @@ -0,0 +1,58 @@ +import {Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core'; +import {NavigationEvent, MonthViewModel} from './datepicker-view-model'; +import {NgbDate} from './ngb-date'; +import {NgbDatepickerI18n} from './datepicker-i18n'; + +@Component({ + selector: 'ngb-datepicker-navigation', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + styleUrls: ['./datepicker-navigation.scss'], + template: ` +
+ +
+ + + + +
+
+ {{ i18n.getMonthFullName(month.number, month.year) }} {{ i18n.getYearNumerals(month.year) }} +
+
+
+
+ +
+ ` +}) +export class NgbDatepickerNavigation { + navigation = NavigationEvent; + + @Input() date: NgbDate; + @Input() disabled: boolean; + @Input() months: MonthViewModel[] = []; + @Input() showSelect: boolean; + @Input() prevDisabled: boolean; + @Input() nextDisabled: boolean; + @Input() selectBoxes: {years: number[], months: number[]}; + + @Output() navigate = new EventEmitter(); + @Output() select = new EventEmitter(); + + constructor(public i18n: NgbDatepickerI18n) {} +} diff --git a/src/datepicker/datepicker-service.spec.ts b/src/datepicker/datepicker-service.spec.ts new file mode 100644 index 0000000..0538876 --- /dev/null +++ b/src/datepicker/datepicker-service.spec.ts @@ -0,0 +1,1431 @@ +import {TestBed} from '@angular/core/testing'; +import {NgbDatepickerService} from './datepicker-service'; +import {NgbCalendar, NgbCalendarGregorian} from './ngb-calendar'; +import {NgbDate} from './ngb-date'; +import {Subscription} from 'rxjs'; +import {DatepickerViewModel} from './datepicker-view-model'; +import {NgbDatepickerI18n, NgbDatepickerI18nDefault} from './datepicker-i18n'; +import {Type} from '@angular/core'; + +describe('ngb-datepicker-service', () => { + + let service: NgbDatepickerService; + let calendar: NgbCalendar; + let model: DatepickerViewModel; + let mock: {onNext}; + let selectDate: NgbDate; + let mockSelect: {onNext}; + + let subscriptions: Subscription[]; + + const getWeek = (week: number, month = 0) => model.months[month].weeks[week]; + const getDay = (day: number, week = 0, month = 0) => getWeek(week, month).days[day]; + const getDayCtx = (day: number) => getDay(day).context; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + NgbDatepickerService, {provide: NgbCalendar, useClass: NgbCalendarGregorian}, + {provide: NgbDatepickerI18n, useClass: NgbDatepickerI18nDefault} + ] + }); + + calendar = TestBed.get(NgbCalendar as Type); + service = TestBed.get(NgbDatepickerService); + subscriptions = []; + model = undefined; + selectDate = null; + + mock = {onNext: () => {}}; + spyOn(mock, 'onNext'); + + mockSelect = {onNext: () => {}}; + spyOn(mockSelect, 'onNext'); + + // subscribing + subscriptions.push( + service.model$.subscribe(mock.onNext), service.model$.subscribe(m => model = m), + service.select$.subscribe(mockSelect.onNext), service.select$.subscribe(d => selectDate = d)); + }); + + afterEach(() => { subscriptions.forEach(s => s.unsubscribe()); }); + + it(`should be possible to instantiate`, () => { expect(service).toBeTruthy(); }); + + it(`should not return anything upon subscription`, () => { + expect(model).toBeUndefined(); + expect(mock.onNext).not.toHaveBeenCalled(); + }); + + describe(`min/max dates`, () => { + + it(`should emit null and valid 'minDate' values`, () => { + // valid + const minDate = new NgbDate(2017, 5, 1); + service.minDate = minDate; + service.focus(new NgbDate(2017, 5, 1)); + expect(model.minDate).toEqual(minDate); + + // null + service.minDate = null; + expect(model.minDate).toBeNull(); + + // undefined -> ignore + service.minDate = undefined; + expect(model.minDate).toBeNull(); + + // invalid -> ignore + service.minDate = new NgbDate(-2, 0, null); + expect(model.minDate).toBeNull(); + + expect(mock.onNext).toHaveBeenCalledTimes(2); + }); + + it(`should emit null and valid 'maxDate' values`, () => { + // valid + const maxDate = new NgbDate(2017, 5, 1); + service.maxDate = maxDate; + service.focus(new NgbDate(2017, 5, 1)); + expect(model.maxDate).toEqual(maxDate); + + // null + service.maxDate = null; + expect(model.maxDate).toBeNull(); + + // undefined -> ignore + service.maxDate = undefined; + expect(model.maxDate).toBeNull(); + + // invalid -> ignore + service.maxDate = new NgbDate(-2, 0, null); + expect(model.maxDate).toBeNull(); + + expect(mock.onNext).toHaveBeenCalledTimes(2); + }); + + it(`should not emit the same 'minDate' value twice`, () => { + service.minDate = new NgbDate(2017, 5, 1); + service.focus(new NgbDate(2015, 5, 1)); + + service.minDate = new NgbDate(2017, 5, 1); + + expect(mock.onNext).toHaveBeenCalledTimes(1); + }); + + it(`should not emit the same 'maxDate' value twice`, () => { + service.maxDate = new NgbDate(2017, 5, 1); + service.focus(new NgbDate(2015, 5, 1)); + + service.maxDate = new NgbDate(2017, 5, 1); + + expect(mock.onNext).toHaveBeenCalledTimes(1); + }); + + it(`should throw if 'min' date is after 'max' date`, () => { + const minDate = new NgbDate(2017, 5, 1); + service.focus(new NgbDate(2015, 5, 1)); + + expect(() => { + service.minDate = minDate; + service.maxDate = new NgbDate(2017, 4, 1); + }).toThrowError(); + }); + + it(`should align 'date' with 'maxDate'`, () => { + service.maxDate = new NgbDate(2017, 5, 1); + service.focus(new NgbDate(2017, 5, 5)); + expect(model.focusDate).toEqual(new NgbDate(2017, 5, 1)); + }); + + it(`should align 'date' with 'minDate'`, () => { + service.minDate = new NgbDate(2017, 5, 10); + service.focus(new NgbDate(2017, 5, 5)); + expect(model.focusDate).toEqual(new NgbDate(2017, 5, 10)); + }); + + it(`should mark dates outside 'min/maxDates' as disabled`, () => { + // MAY 2017 + service.focus(new NgbDate(2017, 5, 1)); + expect(model.minDate).toBeUndefined(); + expect(model.maxDate).toBeUndefined(); + expect(getDayCtx(0).disabled).toBe(false); // 1 MAY + expect(getDayCtx(5).disabled).toBe(false); // 6 MAY + + service.minDate = new NgbDate(2017, 5, 2); + service.maxDate = new NgbDate(2017, 5, 5); + expect(getDayCtx(0).disabled).toBe(true); // 1 MAY + expect(getDayCtx(5).disabled).toBe(true); // 6 MAY + }); + + it(`should update month when 'min/maxDates' change and visible`, () => { + // MAY 2017 + service.focus(new NgbDate(2017, 5, 5)); + expect(model.months.length).toBe(1); + expect(model.minDate).toBeUndefined(); + expect(model.maxDate).toBeUndefined(); + + // MIN -> 5 MAY, 2017 + service.minDate = new NgbDate(2017, 5, 5); + expect(model.months.length).toBe(1); + expect(getDayCtx(0).disabled).toBe(true); + + // MAX -> 10 MAY, 2017 + service.maxDate = new NgbDate(2017, 5, 10); + expect(model.months.length).toBe(1); + expect(model.months[0].weeks[4].days[0].context.disabled).toBe(true); + }); + }); + + describe(`firstDayOfWeek`, () => { + + it(`should emit only positive numeric 'firstDayOfWeek' values`, () => { + // valid + service.firstDayOfWeek = 2; + service.focus(new NgbDate(2015, 5, 1)); + expect(model.firstDayOfWeek).toEqual(2); + + // -1 -> ignore + service.firstDayOfWeek = -1; + expect(model.firstDayOfWeek).toEqual(2); + + // null -> ignore + service.firstDayOfWeek = null; + expect(model.firstDayOfWeek).toEqual(2); + + // undefined -> ignore + service.firstDayOfWeek = null; + expect(model.firstDayOfWeek).toEqual(2); + + expect(mock.onNext).toHaveBeenCalledTimes(1); + }); + + it(`should not emit the same 'firstDayOfWeek' value twice`, () => { + service.firstDayOfWeek = 2; + service.focus(new NgbDate(2015, 5, 1)); + + service.firstDayOfWeek = 2; + + expect(mock.onNext).toHaveBeenCalledTimes(1); + }); + + it(`should generate a month with firstDayOfWeek=1 by default`, () => { + service.focus(new NgbDate(2017, 5, 5)); + expect(model.months.length).toBe(1); + expect(model.months[0].weekdays[0]).toBe(1); + }); + + it(`should generate weeks starting with 'firstDayOfWeek'`, () => { + service.firstDayOfWeek = 2; + service.focus(new NgbDate(2017, 5, 5)); + expect(model.months.length).toBe(1); + expect(model.months[0].weekdays[0]).toBe(2); + + service.firstDayOfWeek = 4; + expect(model.months.length).toBe(1); + expect(model.months[0].weekdays[0]).toBe(4); + }); + + it(`should update months when 'firstDayOfWeek' changes`, () => { + service.focus(new NgbDate(2017, 5, 5)); + expect(model.months.length).toBe(1); + expect(model.firstDayOfWeek).toBe(1); + + const oldFirstDate = getDay(0).date; + expect(oldFirstDate).toEqual(new NgbDate(2017, 5, 1)); + + service.firstDayOfWeek = 3; + expect(model.months.length).toBe(1); + expect(model.firstDayOfWeek).toBe(3); + const newFirstDate = getDay(0).date; + expect(newFirstDate).toEqual(new NgbDate(2017, 4, 26)); + }); + }); + + describe(`displayMonths`, () => { + + it(`should emit only positive numeric 'displayMonths' values`, () => { + // valid + service.displayMonths = 2; + service.focus(new NgbDate(2017, 5, 1)); + expect(model.displayMonths).toEqual(2); + + // -1 -> ignore + service.displayMonths = -1; + expect(model.displayMonths).toEqual(2); + + // null -> ignore + service.displayMonths = null; + expect(model.displayMonths).toEqual(2); + + // undefined -> ignore + service.displayMonths = null; + expect(model.displayMonths).toEqual(2); + + expect(mock.onNext).toHaveBeenCalledTimes(1); + }); + + it(`should not emit the same 'displayMonths' value twice`, () => { + service.displayMonths = 2; + service.focus(new NgbDate(2017, 5, 1)); + + service.displayMonths = 2; + + expect(mock.onNext).toHaveBeenCalledTimes(1); + }); + + it(`should generate 'displayMonths' number of months`, () => { + service.displayMonths = 2; + service.focus(new NgbDate(2017, 5, 5)); + expect(model.months.length).toBe(2); + + service.displayMonths = 4; + expect(model.months.length).toBe(4); + }); + + it(`should reuse existing months when 'displayMonths' changes`, () => { + service.focus(new NgbDate(2017, 5, 5)); + + // 1 month + expect(model.months.length).toBe(1); + const month = model.months[0]; + const date = month.weeks[0].days[0].date; + expect(date).toEqual(new NgbDate(2017, 5, 1)); + + // 2 months + service.displayMonths = 2; + expect(model.months.length).toBe(2); + expect(model.months[0]).toBe(month); + expect(getDay(0).date).toBe(date); + + // back to 1 month + service.displayMonths = 1; + expect(model.months.length).toBe(1); + expect(model.months[0]).toBe(month); + expect(getDay(0).date).toBe(date); + }); + + it(`should change the tabindex when changing the current month`, () => { + service.displayMonths = 2; + service.focus(new NgbDate(2018, 3, 31)); + + expect(getDay(5, 4, 0).tabindex).toEqual(0); // 31 march in the first month block + expect(getDay(5, 0, 1).tabindex).toEqual(-1); // 31 march in the second month block + + service.focusMove('d', 1); + expect(getDay(5, 4, 0).tabindex).toEqual(-1); // 31 march in the first month block + expect(getDay(5, 0, 1).tabindex).toEqual(-1); // 31 march in the second month block + expect(getDay(6, 4, 0).tabindex).toEqual(-1); // 1st april in the first month block + expect(getDay(6, 0, 1).tabindex).toEqual(0); // 1st april in the second month block + + }); + + it(`should set the aria-label when changing the current month`, () => { + service.displayMonths = 2; + service.focus(new NgbDate(2018, 3, 31)); + + expect(getDay(5, 4, 0).ariaLabel).toEqual('Saturday, March 31, 2018'); // 31 march in the first month block + expect(getDay(5, 0, 1).ariaLabel).toEqual('Saturday, March 31, 2018'); // 31 march in the second month block + + service.focusMove('d', 1); + expect(getDay(5, 4, 0).ariaLabel).toEqual('Saturday, March 31, 2018'); // 31 march in the first month block + expect(getDay(5, 0, 1).ariaLabel).toEqual('Saturday, March 31, 2018'); // 31 march in the second month block + expect(getDay(6, 4, 0).ariaLabel).toEqual('Sunday, April 1, 2018'); // 1st april in the first month block + expect(getDay(6, 0, 1).ariaLabel).toEqual('Sunday, April 1, 2018'); // 1st april in the second month block + + }); + }); + + describe(`disabled`, () => { + + it(`should emit 'disabled' values`, () => { + service.focus(new NgbDate(2017, 5, 1)); + expect(model.disabled).toEqual(false); + + service.disabled = true; + expect(model.disabled).toEqual(true); + }); + + it(`should not emit the same 'disabled' value twice`, () => { + service.focus(new NgbDate(2017, 5, 1)); // 1 + service.disabled = true; // 2 + + service.disabled = true; // ignored + + expect(mock.onNext).toHaveBeenCalledTimes(2); + }); + + it(`should not allow focusing when disabled`, () => { + const today = new NgbDate(2017, 5, 2); + service.focus(today); // 1 + service.disabled = true; // 2 + + // focus + service.focus(new NgbDate(2017, 5, 1)); // nope + expect(model.focusDate).toEqual(today); + + // focusMove + service.focusMove('d', 1); // nope + expect(model.focusDate).toEqual(today); + + expect(mock.onNext).toHaveBeenCalledTimes(2); + }); + + it(`should not allow selecting when disabled`, () => { + const today = new NgbDate(2017, 5, 2); + service.focus(today); // 1 + service.disabled = true; // 2 + + // select + service.select(new NgbDate(2017, 5, 2)); // nope + expect(model.selectedDate).toBeNull(); + + // focus select + service.focusSelect(); // nope + expect(model.selectedDate).toBeNull(); + + expect(mock.onNext).toHaveBeenCalledTimes(2); + }); + + it(`should not allow opening when disabled`, () => { + service.focus(new NgbDate(2017, 5, 2)); // 1 + service.disabled = true; // 2 + + // open + service.open(new NgbDate(2016, 5, 1)); // nope + expect(model.firstDate).toEqual(new NgbDate(2017, 5, 1)); + + expect(mock.onNext).toHaveBeenCalledTimes(2); + }); + + it(`should turn focus off when disabled`, () => { + service.focus(new NgbDate(2017, 5, 2)); + service.focusVisible = true; + expect(model.focusVisible).toBeTruthy(); + + service.disabled = true; + expect(model.focusVisible).toBeFalsy(); + }); + + it(`should not turn focus on when disabled`, () => { + service.focus(new NgbDate(2017, 5, 2)); + service.disabled = true; + expect(model.focusVisible).toBeFalsy(); + + service.focusVisible = true; + expect(model.focusVisible).toBeFalsy(); + }); + + it(`should disable navigation arrows`, () => { + service.focus(new NgbDate(2017, 5, 2)); + expect(model.prevDisabled).toBeFalsy(); + expect(model.nextDisabled).toBeFalsy(); + + service.disabled = true; + expect(model.prevDisabled).toBeTruthy(); + expect(model.nextDisabled).toBeTruthy(); + + service.disabled = false; + expect(model.prevDisabled).toBeFalsy(); + expect(model.nextDisabled).toBeFalsy(); + }); + + }); + + describe(`focusVisible`, () => { + + it(`should set focus visible or not`, () => { + service.focus(new NgbDate(2017, 5, 1)); + expect(model.focusVisible).toEqual(false); + + service.focusVisible = true; + expect(model.focusVisible).toEqual(true); + }); + + it(`should not emit the same 'focusVisible' value twice`, () => { + service.focusVisible = true; + service.focus(new NgbDate(2017, 5, 1)); + + service.focusVisible = true; // ignored + + expect(mock.onNext).toHaveBeenCalledTimes(1); + }); + + it(`should not rebuild months when focus visibility changes`, () => { + service.focus(new NgbDate(2017, 5, 1)); + expect(model.focusVisible).toEqual(false); + expect(model.months.length).toBe(1); + const month = model.months[0]; + const date = month.weeks[0].days[0].date; + + service.focusVisible = true; + expect(model.focusVisible).toEqual(true); + expect(model.months[0]).toBe(month); + expect(getDay(0).date).toBe(date); + }); + + }); + + describe(`navigation`, () => { + + it(`should emit navigation values`, () => { + // default = 'selected' + service.focus(new NgbDate(2015, 5, 1)); + expect(model.navigation).toEqual('select'); + + service.navigation = 'none'; + expect(model.navigation).toEqual('none'); + + service.navigation = 'arrows'; + expect(model.navigation).toEqual('arrows'); + }); + + it(`should not emit the same 'navigation' value twice`, () => { + service.focus(new NgbDate(2017, 5, 1)); + + service.navigation = 'select'; // ignored + expect(mock.onNext).toHaveBeenCalledTimes(1); + }); + + describe(`select`, () => { + + const range = (start, end) => Array.from({length: end - start + 1}, (e, i) => start + i); + + it(`should not generate 'months' and 'years' for non-select navigations`, () => { + service.minDate = new NgbDate(2010, 5, 1); + service.maxDate = new NgbDate(2012, 5, 1); + service.focus(new NgbDate(2011, 5, 1)); + expect(model.selectBoxes.years).not.toEqual([]); + expect(model.selectBoxes.months).not.toEqual([]); + + service.navigation = 'none'; + expect(model.selectBoxes.years).toEqual([]); + expect(model.selectBoxes.months).toEqual([]); + + service.navigation = 'arrows'; + expect(model.selectBoxes.years).toEqual([]); + expect(model.selectBoxes.months).toEqual([]); + }); + + it(`should generate 'months' and 'years' for given min/max dates`, () => { + service.minDate = new NgbDate(2010, 5, 1); + service.maxDate = new NgbDate(2012, 5, 1); + service.focus(new NgbDate(2011, 5, 1)); + + expect(model.selectBoxes.years).toEqual([2010, 2011, 2012]); + expect(model.selectBoxes.months).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + + service.focus(new NgbDate(2010, 5, 1)); + expect(model.selectBoxes.years).toEqual([2010, 2011, 2012]); + expect(model.selectBoxes.months).toEqual([5, 6, 7, 8, 9, 10, 11, 12]); + + service.focus(new NgbDate(2012, 5, 1)); + expect(model.selectBoxes.years).toEqual([2010, 2011, 2012]); + expect(model.selectBoxes.months).toEqual([1, 2, 3, 4, 5]); + }); + + it(`should update 'months' and 'years' when min/max dates change`, () => { + service.minDate = new NgbDate(2010, 5, 1); + service.maxDate = new NgbDate(2012, 5, 1); + service.focus(new NgbDate(2011, 5, 1)); + + expect(model.selectBoxes.years).toEqual([2010, 2011, 2012]); + expect(model.selectBoxes.months).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + + service.minDate = new NgbDate(2011, 2, 1); + expect(model.selectBoxes.years).toEqual([2011, 2012]); + expect(model.selectBoxes.months).toEqual([2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + + service.maxDate = new NgbDate(2011, 8, 1); + expect(model.selectBoxes.years).toEqual([2011]); + expect(model.selectBoxes.months).toEqual([2, 3, 4, 5, 6, 7, 8]); + }); + + it(`should generate [-10, +10] 'years' when min/max dates are missing`, () => { + const year = calendar.getToday().year; + service.open(null); + expect(model.selectBoxes.years).toEqual(range(year - 10, year + 10)); + + service.focus(new NgbDate(2011, 1, 1)); + expect(model.selectBoxes.years).toEqual(range(2001, 2021)); + + service.focus(new NgbDate(2020, 1, 1)); + expect(model.selectBoxes.years).toEqual(range(2010, 2030)); + }); + + it(`should generate [min, +10] 'years' when max date is missing`, () => { + service.minDate = new NgbDate(2010, 1, 1); + service.open(new NgbDate(2011, 1, 1)); + expect(model.selectBoxes.years).toEqual(range(2010, 2021)); + + service.minDate = new NgbDate(2015, 1, 1); + expect(model.selectBoxes.years).toEqual(range(2015, 2025)); + + service.minDate = new NgbDate(1000, 1, 1); + expect(model.selectBoxes.years).toEqual(range(1000, 2025)); + }); + + it(`should generate [min, +10] 'years' when min date is missing`, () => { + service.maxDate = new NgbDate(2010, 1, 1); + service.open(new NgbDate(2009, 1, 1)); + expect(model.selectBoxes.years).toEqual(range(1999, 2010)); + + service.maxDate = new NgbDate(2005, 1, 1); + expect(model.selectBoxes.years).toEqual(range(1995, 2005)); + + service.maxDate = new NgbDate(3000, 1, 1); + expect(model.selectBoxes.years).toEqual(range(1995, 3000)); + }); + + it(`should generate 'months' when min/max dates are missing`, () => { + service.open(null); + expect(model.selectBoxes.months).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + + service.focus(new NgbDate(2010, 1, 1)); + expect(model.selectBoxes.months).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + }); + + it(`should generate 'months' and 'years' when resetting min/max dates`, () => { + service.minDate = new NgbDate(2010, 3, 1); + service.maxDate = new NgbDate(2010, 8, 1); + service.open(new NgbDate(2010, 5, 10)); + expect(model.selectBoxes.months).toEqual([3, 4, 5, 6, 7, 8]); + expect(model.selectBoxes.years).toEqual([2010]); + + service.minDate = null; + expect(model.selectBoxes.months).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); + expect(model.selectBoxes.years).toEqual(range(2000, 2010)); + + service.maxDate = null; + expect(model.selectBoxes.months).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + expect(model.selectBoxes.years).toEqual(range(2000, 2020)); + }); + + it(`should generate 'months' when max date is missing`, () => { + service.minDate = new NgbDate(2010, 1, 1); + service.open(new NgbDate(2010, 5, 1)); + expect(model.selectBoxes.months).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + + service.minDate = new NgbDate(2010, 4, 1); + expect(model.selectBoxes.months).toEqual([4, 5, 6, 7, 8, 9, 10, 11, 12]); + }); + + it(`should generate 'months' when min date is missing`, () => { + service.maxDate = new NgbDate(2010, 12, 1); + service.open(new NgbDate(2010, 5, 1)); + expect(model.selectBoxes.months).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + + service.maxDate = new NgbDate(2010, 7, 1); + expect(model.selectBoxes.months).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); + + it(`should generate 'months' based on the first date, not the focus date`, () => { + service.displayMonths = 2; + service.maxDate = new NgbDate(2017, 1, 11); + service.open(new NgbDate(2017, 1, 1)); + expect(model.selectBoxes.months).toEqual([1]); + + service.open(new NgbDate(2016, 12, 1)); + expect(model.selectBoxes.months).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + }); + + it(`should rebuild 'months' and 'years' only when year change`, () => { + service.focus(new NgbDate(2010, 5, 1)); + let months = model.selectBoxes.months; + let years = model.selectBoxes.years; + + // focusing -> nothing + service.focus(new NgbDate(2010, 5, 10)); + expect(model.selectBoxes.months).toBe(months); + expect(model.selectBoxes.years).toBe(years); + + // month changes -> nothing + service.focus(new NgbDate(2010, 6, 1)); + expect(model.selectBoxes.months).toBe(months); + expect(model.selectBoxes.years).toBe(years); + + // year changes -> rebuilding both + service.focus(new NgbDate(2011, 6, 1)); + expect(model.selectBoxes.months).not.toBe(months); + expect(model.selectBoxes.years).not.toBe(years); + }); + }); + + describe(`arrows`, () => { + + it(`should be enabled by default`, () => { + service.focus(new NgbDate(2018, 3, 10)); + expect(model.prevDisabled).toBeFalsy(); + expect(model.nextDisabled).toBeFalsy(); + }); + + it(`should use initial 'minDate' and 'maxDate' values`, () => { + service.minDate = new NgbDate(2018, 3, 10); + service.maxDate = new NgbDate(2018, 3, 10); + service.focus(new NgbDate(2018, 3, 10)); + expect(model.prevDisabled).toBeTruthy(); + expect(model.nextDisabled).toBeTruthy(); + }); + + it(`should react to 'minDate' changes`, () => { + service.focus(new NgbDate(2018, 3, 10)); + service.minDate = new NgbDate(2018, 3, 1); + expect(model.prevDisabled).toBeTruthy(); + + service.minDate = new NgbDate(2018, 2, 1); + expect(model.prevDisabled).toBeFalsy(); + + service.minDate = new NgbDate(2018, 2, 28); + expect(model.prevDisabled).toBeFalsy(); + }); + + it(`should react to 'maxDate' changes`, () => { + service.focus(new NgbDate(2018, 3, 10)); + service.maxDate = new NgbDate(2018, 3, 31); + expect(model.nextDisabled).toBeTruthy(); + + service.maxDate = new NgbDate(2018, 4, 1); + expect(model.nextDisabled).toBeFalsy(); + + service.maxDate = new NgbDate(2018, 4, 30); + expect(model.nextDisabled).toBeFalsy(); + }); + + it(`should react to 'minDate' changes with multiple months`, () => { + service.displayMonths = 2; + service.minDate = new NgbDate(2018, 3, 1); + service.open(new NgbDate(2018, 3, 10)); // open: [MAR, APR], focus: MAR + expect(model.prevDisabled).toBeTruthy(); + + service.focus(new NgbDate(2018, 4, 10)); // open [MAR, APR], focus: APR + expect(model.prevDisabled).toBeTruthy(); + + service.open(new NgbDate(2018, 4, 10)); // open [APR, MAY], focus: APR + expect(model.prevDisabled).toBeFalsy(); + + service.focus(new NgbDate(2018, 5, 10)); // open [APR, MAY], focus: MAY + expect(model.prevDisabled).toBeFalsy(); + }); + + it(`should react to 'maxDate' changes with multiple months`, () => { + service.displayMonths = 2; + service.maxDate = new NgbDate(2018, 3, 10); + service.open(new NgbDate(2018, 3, 1)); // open: [MAR, APR], focus: MAR + expect(model.nextDisabled).toBeTruthy(); + + service.open(new NgbDate(2018, 2, 1)); // open: [FEB, MAR], focus: FEB + expect(model.nextDisabled).toBeTruthy(); + + service.focus(new NgbDate(2018, 3, 1)); // open: [FEB, MAR], focus: MAR + expect(model.nextDisabled).toBeTruthy(); + + service.open(new NgbDate(2018, 1, 1)); // open: [JAN, FEB], focus: JAN + expect(model.nextDisabled).toBeFalsy(); + + service.focus(new NgbDate(2018, 2, 1)); // open: [JAN, FEB], focus: FEB + expect(model.nextDisabled).toBeFalsy(); + }); + }); + }); + + describe(`outsideDays`, () => { + + it(`should emit 'outsideDays' values`, () => { + service.focus(new NgbDate(2015, 5, 1)); + expect(model.outsideDays).toEqual('visible'); + + service.outsideDays = 'hidden'; + expect(model.outsideDays).toEqual('hidden'); + + service.outsideDays = 'collapsed'; + expect(model.outsideDays).toEqual('collapsed'); + }); + + it(`should not emit the same 'outsideDays' value twice`, () => { + service.focus(new NgbDate(2017, 5, 1)); + + service.outsideDays = 'visible'; // ignored + expect(mock.onNext).toHaveBeenCalledTimes(1); + }); + + it(`should not hide days when 'outsideDays' is 'visible'`, () => { + // single month + service.outsideDays = 'visible'; + service.focus(new NgbDate(2018, 5, 1)); + + expect(getDay(0, 0).hidden).toBeFalsy(); // 30 APR + expect(getWeek(0).collapsed).toBeFalsy(); + + expect(getDay(0, 1).hidden).toBeFalsy(); // 7 MAY + expect(getWeek(1).collapsed).toBeFalsy(); + + expect(getDay(0, 5).hidden).toBeFalsy(); // 7 JUN + expect(getWeek(5).collapsed).toBeFalsy(); + + // multiple months + // days is between two month must stay hidden regardless of outside days value + service.displayMonths = 2; + + // MAY 2018 + expect(getDay(0, 0, 0).hidden).toBeFalsy(); // 30 APR + expect(getWeek(0, 0).collapsed).toBeFalsy(); + + expect(getDay(0, 1, 0).hidden).toBeFalsy(); // 7 MAY + expect(getWeek(1, 0).collapsed).toBeFalsy(); + + expect(getDay(0, 5, 0).hidden).toBeTruthy(); // 7 JUN + expect(getWeek(5, 0).collapsed).toBeFalsy(); + + // JUNE 2018 + expect(getDay(0, 0, 1).hidden).toBeTruthy(); // 28 MAY + expect(getWeek(0, 1).collapsed).toBeFalsy(); + + expect(getDay(0, 1).hidden).toBeFalsy(); // 4 JUN + expect(getWeek(1, 1).collapsed).toBeFalsy(); + + expect(getDay(0, 5, 1).hidden).toBeFalsy(); // 2 JUL + expect(getWeek(5, 1).collapsed).toBeFalsy(); + + // Edge case -> in between years + service.focus(new NgbDate(2018, 12, 1)); + + // DEC 2018 + expect(getDay(0, 0, 0).hidden).toBeFalsy(); // 26 NOV + expect(getWeek(0, 0).collapsed).toBeFalsy(); + + expect(getDay(0, 1, 0).hidden).toBeFalsy(); // 3 DEC + expect(getWeek(1, 0).collapsed).toBeFalsy(); + + expect(getDay(1, 5, 0).hidden).toBeTruthy(); // 1 JAN + expect(getWeek(5, 0).collapsed).toBeFalsy(); + + // JAN 2019 + expect(getDay(0, 0, 1).hidden).toBeTruthy(); // 31 DEC + expect(getWeek(0, 1).collapsed).toBeFalsy(); + + expect(getDay(0, 1).hidden).toBeFalsy(); // 7 JAN + expect(getWeek(1, 1).collapsed).toBeFalsy(); + + expect(getDay(0, 5, 1).hidden).toBeFalsy(); // 4 FEB + expect(getWeek(5, 1).collapsed).toBeFalsy(); + }); + + it(`should hide days when 'outsideDays' is 'hidden'`, () => { + // single month + service.outsideDays = 'hidden'; + service.focus(new NgbDate(2018, 5, 1)); + + expect(getDay(0, 0).hidden).toBeTruthy(); // 30, APR + expect(getWeek(0).collapsed).toBeFalsy(); + + expect(getDay(0, 1).hidden).toBeFalsy(); // 7, MAY + expect(getWeek(1).collapsed).toBeFalsy(); + + expect(getDay(0, 5).hidden).toBeTruthy(); // 7, JUN + expect(getWeek(5).collapsed).toBeFalsy(); + + // multiple months + service.displayMonths = 2; + + // MAY 2018 + expect(getDay(0, 0, 0).hidden).toBeTruthy(); // 30 APR + expect(getWeek(0, 0).collapsed).toBeFalsy(); + + expect(getDay(0, 1, 0).hidden).toBeFalsy(); // 7 MAY + expect(getWeek(1, 0).collapsed).toBeFalsy(); + + expect(getDay(0, 5, 0).hidden).toBeTruthy(); // 7 JUN + expect(getWeek(5, 0).collapsed).toBeFalsy(); + + // JUNE 2018 + expect(getDay(0, 0, 1).hidden).toBeTruthy(); // 28 MAY + expect(getWeek(0, 1).collapsed).toBeFalsy(); + + expect(getDay(0, 1).hidden).toBeFalsy(); // 4 JUN + expect(getWeek(1, 1).collapsed).toBeFalsy(); + + expect(getDay(0, 5, 1).hidden).toBeTruthy(); // 2 JUL + expect(getWeek(5, 1).collapsed).toBeFalsy(); + }); + + it(`should hide days when 'outsideDays' is 'collapsed'`, () => { + // single month + service.outsideDays = 'collapsed'; + service.focus(new NgbDate(2018, 5, 1)); + + expect(getDay(0, 0).hidden).toBeTruthy(); // 30, APR + expect(getWeek(0).collapsed).toBeFalsy(); + + expect(getDay(0, 1).hidden).toBeFalsy(); // 7, MAY + expect(getWeek(1).collapsed).toBeFalsy(); + + expect(getDay(0, 5).hidden).toBeTruthy(); // 7, JUN + expect(getWeek(5).collapsed).toBeTruthy(); + + // multiple months + service.displayMonths = 2; + + // MAY 2018 + expect(getDay(0, 0, 0).hidden).toBeTruthy(); // 30 APR + expect(getWeek(0, 0).collapsed).toBeFalsy(); + + expect(getDay(0, 1, 0).hidden).toBeFalsy(); // 7 MAY + expect(getWeek(1, 0).collapsed).toBeFalsy(); + + expect(getDay(0, 5, 0).hidden).toBeTruthy(); // 7 JUN + expect(getWeek(5, 0).collapsed).toBeTruthy(); + + // JUNE 2018 + expect(getDay(0, 0, 1).hidden).toBeTruthy(); // 28 MAY + expect(getWeek(0, 1).collapsed).toBeFalsy(); + + expect(getDay(0, 1).hidden).toBeFalsy(); // 4 JUN + expect(getWeek(1, 1).collapsed).toBeFalsy(); + + expect(getDay(0, 5, 1).hidden).toBeTruthy(); // 2 JUL + expect(getWeek(5, 1).collapsed).toBeTruthy(); + }); + + it(`should toggle days when 'outsideDays' changes`, () => { + service.outsideDays = 'visible'; + service.focus(new NgbDate(2018, 5, 1)); + expect(getDay(0).hidden).toBeFalsy(); // 30, APR + expect(getWeek(5).collapsed).toBeFalsy(); + + service.outsideDays = 'collapsed'; + expect(getDay(0).hidden).toBeTruthy(); // 30, APR + expect(getWeek(5).collapsed).toBeTruthy(); + }); + }); + + describe(`dayTemplateData`, () => { + + it(`should not pass anything to the template by default`, () => { + // MAY 2017 + service.focus(new NgbDate(2017, 5, 1)); + expect(getDay(0).context.data).toBeUndefined(); + }); + + it(`should pass arbitrary data to the template`, () => { + service.dayTemplateData = () => 'data'; + + // MAY 2017 + service.focus(new NgbDate(2017, 5, 1)); + expect(getDay(0).context.data).toBe('data'); + }); + + it(`should update months when 'dayTemplateData' changes`, () => { + // MAY 2017 + service.dayTemplateData = () => 'one'; + service.focus(new NgbDate(2017, 5, 1)); + + expect(getDay(0).context.data).toBe('one'); + + service.dayTemplateData = (_) => 'two'; + + expect(getDay(0).context.data).toBe('two'); + }); + }); + + describe(`markDisabled`, () => { + + it(`should mark dates as disabled by passing 'markDisabled'`, () => { + // marking 5th day of each month as disabled + service.markDisabled = (date) => date && date.day === 5; + + // MAY 2017 + service.focus(new NgbDate(2017, 5, 1)); + + const day = getDay(4); // 5th day; + expect(day.date).toEqual(new NgbDate(2017, 5, 5)); + expect(day.context.disabled).toBe(true); + }); + + it(`should update months when 'markDisabled changes'`, () => { + // MAY 2017 + service.markDisabled = (_) => true; + service.focus(new NgbDate(2017, 5, 1)); + + expect(getDay(0).context.disabled).toBe(true); + + service.markDisabled = (_) => false; + + expect(getDay(0).context.disabled).toBe(false); + }); + }); + + describe(`focus handling`, () => { + + it(`should generate 1 month on 'focus()' by default`, () => { + expect(model).toBeUndefined(); + + service.focus(new NgbDate(2017, 5, 5)); + expect(model.months).toBeTruthy(); + expect(model.months.length).toBe(1); + }); + + it(`should emit new date on 'focus()'`, () => { + const today = new NgbDate(2017, 5, 2); + service.focus(today); + expect(model.focusDate).toEqual(today); + + expect(mock.onNext).toHaveBeenCalledTimes(1); + }); + + it(`should ignore invalid 'focus()' values`, () => { + service.focus(null); + service.focus(undefined); + service.focus(new NgbDate(-2, 0, null)); + + expect(mock.onNext).not.toHaveBeenCalled(); + }); + + it(`should not emit the same date twice on 'focus()'`, () => { + service.focus(new NgbDate(2017, 5, 2)); + service.focus(new NgbDate(2017, 5, 2)); + + expect(mock.onNext).toHaveBeenCalledTimes(1); + }); + + it(`should update months when focused date updates`, () => { + service.focus(new NgbDate(2017, 5, 5)); + + expect(model.months.length).toBe(1); + expect(model.months[0].firstDate).toEqual(new NgbDate(2017, 5, 1)); + + // next month + service.focus(new NgbDate(2017, 6, 10)); + + expect(model.months.length).toBe(1); + expect(model.months[0].firstDate).toEqual(new NgbDate(2017, 6, 1)); + + // next year + service.focus(new NgbDate(2018, 6, 10)); + + expect(model.months.length).toBe(1); + expect(model.months[0].firstDate).toEqual(new NgbDate(2018, 6, 1)); + + expect(mock.onNext).toHaveBeenCalledTimes(3); + }); + + it(`should move focus with 'focusMove()'`, () => { + const date = new NgbDate(2017, 5, 5); + + // days + service.focus(date); + service.focusMove('d', 1); + expect(model.focusDate).toEqual(new NgbDate(2017, 5, 6)); + + service.focus(date); + service.focusMove('d', -1); + expect(model.focusDate).toEqual(new NgbDate(2017, 5, 4)); + + // months + service.focus(date); + service.focusMove('m', 1); + expect(model.focusDate).toEqual(new NgbDate(2017, 6, 1)); + + service.focus(date); + service.focusMove('m', -1); + expect(model.focusDate).toEqual(new NgbDate(2017, 4, 1)); + + // years + service.focus(date); + service.focusMove('y', 1); + expect(model.focusDate).toEqual(new NgbDate(2018, 1, 1)); + + service.focus(date); + service.focusMove('y', -1); + expect(model.focusDate).toEqual(new NgbDate(2016, 1, 1)); + }); + + it(`should move focus when 'minDate' changes`, () => { + service.focus(new NgbDate(2017, 5, 5)); + service.maxDate = new NgbDate(2017, 5, 1); + expect(model.focusDate).toEqual(new NgbDate(2017, 5, 1)); + }); + + it(`should move focus when 'maxDate' changes`, () => { + service.focus(new NgbDate(2017, 5, 5)); + service.minDate = new NgbDate(2017, 5, 10); + expect(model.focusDate).toEqual(new NgbDate(2017, 5, 10)); + }); + + it(`should not rebuild a single month if newly focused date is visible`, () => { + service.focus(new NgbDate(2017, 5, 5)); + + expect(model.months.length).toBe(1); + const month = model.months[0]; + const date = month.weeks[0].days[0].date; + expect(date).toEqual(new NgbDate(2017, 5, 1)); + + service.focus(new NgbDate(2017, 5, 10)); + expect(model.months[0]).toBe(month); + expect(getDay(0).date).toBe(date); + }); + + it(`should not rebuild multiple months if newly focused date is visible`, () => { + service.displayMonths = 2; + service.focus(new NgbDate(2017, 5, 5)); + + expect(model.months.length).toBe(2); + const months = model.months; + expect(months[0].firstDate).toEqual(new NgbDate(2017, 5, 1)); + expect(months[1].lastDate).toEqual(new NgbDate(2017, 6, 30)); + + service.focus(new NgbDate(2017, 6, 10)); + expect(model.months.length).toBe(2); + expect(model.months[0]).toBe(months[0]); + expect(model.months[1]).toBe(months[1]); + }); + }); + + describe(`view change handling`, () => { + + it(`should open current month if nothing is provided`, () => { + const today = calendar.getToday(); + service.open(null); + expect(model.months.length).toBe(1); + expect(model.firstDate).toEqual(new NgbDate(today.year, today.month, 1)); + expect(model.focusDate).toEqual(today); + }); + + it(`should open month and set up focus correctly`, () => { + service.open(new NgbDate(2017, 5, 5)); + expect(model.months.length).toBe(1); + expect(model.firstDate).toEqual(new NgbDate(2017, 5, 1)); + expect(model.focusDate).toEqual(new NgbDate(2017, 5, 5)); + }); + + it(`should open month and move the focus with it`, () => { + service.focus(new NgbDate(2017, 5, 5)); + expect(model.months.length).toBe(1); + expect(model.focusDate).toEqual(new NgbDate(2017, 5, 5)); + + // same month, same focus + service.open(new NgbDate(2017, 5, 1)); + expect(model.focusDate).toEqual(new NgbDate(2017, 5, 5)); + + // different month, moving focus along + service.open(new NgbDate(2017, 10, 10)); + expect(model.focusDate).toEqual(new NgbDate(2017, 10, 10)); + }); + + it(`should open multiple months and move focus with them`, () => { + // MAY-JUN + service.displayMonths = 2; + service.focus(new NgbDate(2017, 5, 5)); + expect(model.months.length).toBe(2); + expect(model.firstDate).toEqual(new NgbDate(2017, 5, 1)); + expect(model.focusDate).toEqual(new NgbDate(2017, 5, 5)); + + // moving view to JUL-AUG + service.open(new NgbDate(2017, 7, 10)); + expect(model.firstDate).toEqual(new NgbDate(2017, 7, 1)); + expect(model.focusDate).toEqual(new NgbDate(2017, 7, 10)); + + // moving view to MAY-JUN + service.open(new NgbDate(2017, 5, 10)); + expect(model.firstDate).toEqual(new NgbDate(2017, 5, 1)); + expect(model.focusDate).toEqual(new NgbDate(2017, 5, 10)); + }); + + it(`should open multiple months and do not touch focus if it is visible`, () => { + // MAY-JUN + service.displayMonths = 2; + service.focus(new NgbDate(2017, 5, 5)); + expect(model.months.length).toBe(2); + expect(model.firstDate).toEqual(new NgbDate(2017, 5, 1)); + expect(model.focusDate).toEqual(new NgbDate(2017, 5, 5)); + + // moving focus to JUN + service.focus(new NgbDate(2017, 6, 5)); + expect(model.focusDate).toEqual(new NgbDate(2017, 6, 5)); + + // moving view to JUN-JUL + service.open(new NgbDate(2017, 6, 10)); + expect(model.firstDate).toEqual(new NgbDate(2017, 6, 1)); + expect(model.focusDate).toEqual(new NgbDate(2017, 6, 5)); + + // moving view to MAY-JUN + service.open(new NgbDate(2017, 5, 10)); + expect(model.firstDate).toEqual(new NgbDate(2017, 5, 1)); + expect(model.focusDate).toEqual(new NgbDate(2017, 6, 5)); + }); + + it(`should reuse existing months when opening`, () => { + service.focus(new NgbDate(2017, 5, 5)); + expect(model.months.length).toBe(1); + const month = model.months[0]; + const date = month.weeks[0].days[0].date; + expect(date).toEqual(new NgbDate(2017, 5, 1)); + + service.open(new NgbDate(2017, 5, 10)); + expect(model.months.length).toBe(1); + expect(model.months[0]).toBe(month); + expect(getDay(0).date).toBe(date); + }); + + it(`should not rebuild anything when opening dates from the same month`, () => { + service.open(new NgbDate(2017, 5, 5)); + expect(model.months.length).toBe(1); + expect(model.firstDate).toEqual(new NgbDate(2017, 5, 1)); + expect(mock.onNext).toHaveBeenCalledTimes(1); + + service.open(new NgbDate(2017, 5, 5)); // new object, same date + service.open(new NgbDate(2017, 5, 1)); // another date + expect(mock.onNext).toHaveBeenCalledTimes(1); + }); + }); + + describe(`selection handling`, () => { + + it(`should generate months for initial selection`, () => { + const date = new NgbDate(2017, 5, 5); + service.select(date); + expect(model.months.length).toBe(1); + expect(model.selectedDate).toEqual(date); + }); + + it(`should select currently focused date with 'focusSelect()'`, () => { + const date = new NgbDate(2017, 5, 5); + service.focus(date); + expect(model.selectedDate).toBeNull(); + expect(selectDate).toBeNull(); + + service.focusSelect(); + expect(model.selectedDate).toEqual(date); + expect(selectDate).toEqual(date); + + expect(mockSelect.onNext).toHaveBeenCalledTimes(1); + }); + + it(`should not select disabled dates with 'focusSelect()'`, () => { + // marking 5th day of each month as disabled + service.markDisabled = (d) => d && d.day === 5; + + // focusing MAY, 5 + const date = new NgbDate(2017, 5, 5); + service.focus(date); + expect(model.focusDate).toEqual(date); + expect(model.selectedDate).toBeNull(); + expect(selectDate).toBeNull(); + + service.focusSelect(); + expect(model.selectedDate).toBeNull(); + expect(selectDate).toBeNull(); + + expect(mockSelect.onNext).not.toHaveBeenCalled(); + }); + + it(`should not emit selection event by default`, () => { + const date = new NgbDate(2017, 5, 5); + service.select(date); + expect(mockSelect.onNext).not.toHaveBeenCalled(); + }); + + it(`should not emit selection event for null values`, () => { + const date = new NgbDate(2017, 5, 5); + service.select(null, {emitEvent: true}); + + expect(mockSelect.onNext).not.toHaveBeenCalled(); + }); + + it(`should emit date selection event'`, () => { + const date = new NgbDate(2017, 5, 5); + service.focus(date); + expect(model.selectedDate).toBeNull(); + expect(selectDate).toBeNull(); + + service.select(date, {emitEvent: true}); + expect(model.selectedDate).toEqual(date); + expect(selectDate).toEqual(date); + + expect(mockSelect.onNext).toHaveBeenCalledTimes(1); + }); + + it(`should emit date selection event for non-visible dates'`, () => { + const date = new NgbDate(2017, 5, 5); + service.focus(date); + expect(model.selectedDate).toBeNull(); + expect(selectDate).toBeNull(); + + let invisibleDate = new NgbDate(2016, 5, 5); + service.select(invisibleDate, {emitEvent: true}); + expect(model.selectedDate).toEqual(invisibleDate); + expect(selectDate).toEqual(invisibleDate); + + expect(mockSelect.onNext).toHaveBeenCalledTimes(1); + }); + + it(`should not emit date selection event for disabled dates'`, () => { + // marking 5th day of each month as disabled + service.markDisabled = (d) => d && d.day === 5; + + // focusing MAY, 5 + const date = new NgbDate(2017, 5, 5); + service.focus(date); + expect(model.selectedDate).toBeNull(); + expect(selectDate).toBeNull(); + + service.select(date, {emitEvent: true}); + expect(model.selectedDate).toBeNull(); + expect(selectDate).toBeNull(); + + expect(mockSelect.onNext).not.toHaveBeenCalled(); + }); + + it(`should emit date selection event when focusing on the same date twice`, () => { + const date = new NgbDate(2017, 5, 5); + service.focus(date); + + service.focusSelect(); + service.focusSelect(); + + expect(mockSelect.onNext).toHaveBeenCalledTimes(2); + }); + + it(`should emit date selection event when selecting the same date twice`, () => { + const date = new NgbDate(2017, 5, 5); + service.focus(date); + + service.select(date, {emitEvent: true}); + service.select(date, {emitEvent: true}); + + expect(mockSelect.onNext).toHaveBeenCalledTimes(2); + }); + }); + + describe(`template context`, () => { + + it(`should generate 'date' for day template`, () => { + service.focus(new NgbDate(2017, 5, 1)); + expect(getDayCtx(0).date).toEqual(new NgbDate(2017, 5, 1)); + expect(getDayCtx(1).date).toEqual(new NgbDate(2017, 5, 2)); + + service.focus(new NgbDate(2017, 10, 1)); + expect(getDayCtx(0).date).toEqual(new NgbDate(2017, 9, 25)); + expect(getDayCtx(1).date).toEqual(new NgbDate(2017, 9, 26)); + }); + + it(`should generate date as $implicit value for day template`, () => { + service.focus(new NgbDate(2017, 5, 1)); + expect(getDayCtx(0).$implicit).toEqual(new NgbDate(2017, 5, 1)); + }); + + it(`should generate 'currentMonth' for day template`, () => { + service.focus(new NgbDate(2017, 5, 1)); + expect(getDayCtx(0).currentMonth).toBe(5); + + service.focus(new NgbDate(2017, 10, 1)); + expect(getDayCtx(0).currentMonth).toBe(10); + }); + + it(`should update 'focused' flag and tabindex for day template`, () => { + // off + service.focus(new NgbDate(2017, 5, 1)); + expect(getDayCtx(0).focused).toBeFalsy(); + expect(getDayCtx(1).focused).toBeFalsy(); + expect(getDay(0).tabindex).toEqual(0); + expect(getDay(1).tabindex).toEqual(-1); + + // on + service.focusVisible = true; + expect(getDayCtx(0).focused).toBeTruthy(); + expect(getDayCtx(1).focused).toBeFalsy(); + expect(getDay(0).tabindex).toEqual(0); + expect(getDay(1).tabindex).toEqual(-1); + + // move + service.focusMove('d', 1); + expect(getDayCtx(0).focused).toBeFalsy(); + expect(getDayCtx(1).focused).toBeTruthy(); + expect(getDay(0).tabindex).toEqual(-1); + expect(getDay(1).tabindex).toEqual(0); + + // off + service.focusVisible = false; + expect(getDayCtx(0).focused).toBeFalsy(); + expect(getDayCtx(1).focused).toBeFalsy(); + expect(getDay(0).tabindex).toEqual(-1); + expect(getDay(1).tabindex).toEqual(0); + }); + + it(`should update 'selected' flag for day template`, () => { + // off + service.focus(new NgbDate(2017, 5, 1)); + expect(getDayCtx(0).selected).toBeFalsy(); + expect(getDayCtx(1).selected).toBeFalsy(); + + // select + service.focusSelect(); + expect(getDayCtx(0).selected).toBeTruthy(); + expect(getDayCtx(1).selected).toBeFalsy(); + + // move + service.select(new NgbDate(2017, 5, 2)); + expect(getDayCtx(0).selected).toBeFalsy(); + expect(getDayCtx(1).selected).toBeTruthy(); + + // off + service.select(null); + expect(getDayCtx(0).selected).toBeFalsy(); + expect(getDayCtx(1).selected).toBeFalsy(); + }); + + it(`should update 'disabled' flag for day template`, () => { + // off + service.focus(new NgbDate(2017, 5, 1)); + expect(getDayCtx(0).disabled).toBeFalsy(); + expect(getDayCtx(1).disabled).toBeFalsy(); + + // marking 2nd day of each month as disabled + service.markDisabled = (date) => date && date.day === 2; + expect(getDayCtx(0).disabled).toBeFalsy(); + expect(getDayCtx(1).disabled).toBeTruthy(); + + // global disabled on + service.disabled = true; + expect(getDayCtx(0).disabled).toBeTruthy(); + expect(getDayCtx(1).disabled).toBeTruthy(); + + // global disabled on + service.disabled = false; + expect(getDayCtx(0).disabled).toBeFalsy(); + expect(getDayCtx(1).disabled).toBeTruthy(); + }); + + it(`should update 'today' flag for day template`, () => { + calendar.getToday = () => new NgbDate(2018, 12, 20); + const today = calendar.getToday(); + service.open(today); + + expect(getDay(2, 2, 0).context.today).toBeFalsy(); + expect(getDay(3, 3, 0).context.today).toBeTruthy(); + }); + }); + + describe('toValidDate()', () => { + + it('should convert a valid NgbDate', () => { + expect(service.toValidDate(new NgbDate(2016, 10, 5))).toEqual(new NgbDate(2016, 10, 5)); + expect(service.toValidDate({year: 2016, month: 10, day: 5})).toEqual(new NgbDate(2016, 10, 5)); + }); + + it('should return today for an invalid NgbDate', () => { + const today = calendar.getToday(); + expect(service.toValidDate(null)).toEqual(today); + expect(service.toValidDate({})).toEqual(today); + expect(service.toValidDate(undefined)).toEqual(today); + expect(service.toValidDate(new Date())).toEqual(today); + expect(service.toValidDate(new NgbDate(275760, 9, 14))).toEqual(today); + }); + + it('should return today if default value is undefined', + () => { expect(service.toValidDate(null, undefined)).toEqual(calendar.getToday()); }); + + it('should return default value for an invalid NgbDate if provided', () => { + expect(service.toValidDate(null, new NgbDate(1066, 6, 6))).toEqual(new NgbDate(1066, 6, 6)); + expect(service.toValidDate(null, null)).toEqual(null); + }); + }); +}); diff --git a/src/datepicker/datepicker-service.ts b/src/datepicker/datepicker-service.ts new file mode 100644 index 0000000..88b0665 --- /dev/null +++ b/src/datepicker/datepicker-service.ts @@ -0,0 +1,300 @@ +import {NgbCalendar, NgbPeriod} from './ngb-calendar'; +import {NgbDate} from './ngb-date'; +import {NgbDateStruct} from './ngb-date-struct'; +import {DatepickerViewModel, NgbDayTemplateData, NgbMarkDisabled} from './datepicker-view-model'; +import {Injectable} from '@angular/core'; +import {isInteger, toInteger} from '../util/util'; +import {Observable, Subject} from 'rxjs'; +import { + buildMonths, + checkDateInRange, + checkMinBeforeMax, + isChangedDate, + isChangedMonth, + isDateSelectable, + generateSelectBoxYears, + generateSelectBoxMonths, + prevMonthDisabled, + nextMonthDisabled +} from './datepicker-tools'; + +import {filter} from 'rxjs/operators'; +import {NgbDatepickerI18n} from './datepicker-i18n'; + +@Injectable() +export class NgbDatepickerService { + private _model$ = new Subject(); + + private _select$ = new Subject(); + + private _state: DatepickerViewModel = { + disabled: false, + displayMonths: 1, + firstDayOfWeek: 1, + focusVisible: false, + months: [], + navigation: 'select', + outsideDays: 'visible', + prevDisabled: false, + nextDisabled: false, + selectBoxes: {years: [], months: []}, + selectedDate: null + }; + + get model$(): Observable { return this._model$.pipe(filter(model => model.months.length > 0)); } + + get select$(): Observable { return this._select$.pipe(filter(date => date !== null)); } + + set dayTemplateData(dayTemplateData: NgbDayTemplateData) { + if (this._state.dayTemplateData !== dayTemplateData) { + this._nextState({dayTemplateData}); + } + } + + set disabled(disabled: boolean) { + if (this._state.disabled !== disabled) { + this._nextState({disabled}); + } + } + + set displayMonths(displayMonths: number) { + displayMonths = toInteger(displayMonths); + if (isInteger(displayMonths) && displayMonths > 0 && this._state.displayMonths !== displayMonths) { + this._nextState({displayMonths}); + } + } + + set firstDayOfWeek(firstDayOfWeek: number) { + firstDayOfWeek = toInteger(firstDayOfWeek); + if (isInteger(firstDayOfWeek) && firstDayOfWeek >= 0 && this._state.firstDayOfWeek !== firstDayOfWeek) { + this._nextState({firstDayOfWeek}); + } + } + + set focusVisible(focusVisible: boolean) { + if (this._state.focusVisible !== focusVisible && !this._state.disabled) { + this._nextState({focusVisible}); + } + } + + set maxDate(date: NgbDate) { + const maxDate = this.toValidDate(date, null); + if (isChangedDate(this._state.maxDate, maxDate)) { + this._nextState({maxDate}); + } + } + + set markDisabled(markDisabled: NgbMarkDisabled) { + if (this._state.markDisabled !== markDisabled) { + this._nextState({markDisabled}); + } + } + + set minDate(date: NgbDate) { + const minDate = this.toValidDate(date, null); + if (isChangedDate(this._state.minDate, minDate)) { + this._nextState({minDate}); + } + } + + set navigation(navigation: 'select' | 'arrows' | 'none') { + if (this._state.navigation !== navigation) { + this._nextState({navigation}); + } + } + + set outsideDays(outsideDays: 'visible' | 'collapsed' | 'hidden') { + if (this._state.outsideDays !== outsideDays) { + this._nextState({outsideDays}); + } + } + + constructor(private _calendar: NgbCalendar, private _i18n: NgbDatepickerI18n) {} + + focus(date: NgbDate) { + if (!this._state.disabled && this._calendar.isValid(date) && isChangedDate(this._state.focusDate, date)) { + this._nextState({focusDate: date}); + } + } + + focusMove(period?: NgbPeriod, number?: number) { + this.focus(this._calendar.getNext(this._state.focusDate, period, number)); + } + + focusSelect() { + if (isDateSelectable(this._state.focusDate, this._state)) { + this.select(this._state.focusDate, {emitEvent: true}); + } + } + + open(date: NgbDate) { + const firstDate = this.toValidDate(date, this._calendar.getToday()); + if (!this._state.disabled && (!this._state.firstDate || isChangedMonth(this._state.firstDate, date))) { + this._nextState({firstDate}); + } + } + + select(date: NgbDate, options: {emitEvent?: boolean} = {}) { + const selectedDate = this.toValidDate(date, null); + if (!this._state.disabled) { + if (isChangedDate(this._state.selectedDate, selectedDate)) { + this._nextState({selectedDate}); + } + + if (options.emitEvent && isDateSelectable(selectedDate, this._state)) { + this._select$.next(selectedDate); + } + } + } + + toValidDate(date: NgbDateStruct, defaultValue?: NgbDate): NgbDate { + const ngbDate = NgbDate.from(date); + if (defaultValue === undefined) { + defaultValue = this._calendar.getToday(); + } + return this._calendar.isValid(ngbDate) ? ngbDate : defaultValue; + } + + private _nextState(patch: Partial) { + const newState = this._updateState(patch); + this._patchContexts(newState); + this._state = newState; + this._model$.next(this._state); + } + + private _patchContexts(state: DatepickerViewModel) { + const {months, displayMonths, selectedDate, focusDate, focusVisible, disabled, outsideDays} = state; + state.months.forEach(month => { + month.weeks.forEach(week => { + week.days.forEach(day => { + + // patch focus flag + if (focusDate) { + day.context.focused = focusDate.equals(day.date) && focusVisible; + } + + // calculating tabindex + day.tabindex = !disabled && day.date.equals(focusDate) && focusDate.month === month.number ? 0 : -1; + + // override context disabled + if (disabled === true) { + day.context.disabled = true; + } + + // patch selection flag + if (selectedDate !== undefined) { + day.context.selected = selectedDate !== null && selectedDate.equals(day.date); + } + + // visibility + if (month.number !== day.date.month) { + day.hidden = outsideDays === 'hidden' || outsideDays === 'collapsed' || + (displayMonths > 1 && day.date.after(months[0].firstDate) && + day.date.before(months[displayMonths - 1].lastDate)); + } + }); + }); + }); + } + + private _updateState(patch: Partial): DatepickerViewModel { + // patching fields + const state = Object.assign({}, this._state, patch); + + let startDate = state.firstDate; + + // min/max dates changed + if ('minDate' in patch || 'maxDate' in patch) { + checkMinBeforeMax(state.minDate, state.maxDate); + state.focusDate = checkDateInRange(state.focusDate, state.minDate, state.maxDate); + state.firstDate = checkDateInRange(state.firstDate, state.minDate, state.maxDate); + startDate = state.focusDate; + } + + // disabled + if ('disabled' in patch) { + state.focusVisible = false; + } + + // initial rebuild via 'select()' + if ('selectedDate' in patch && this._state.months.length === 0) { + startDate = state.selectedDate; + } + + // terminate early if only focus visibility was changed + if ('focusVisible' in patch) { + return state; + } + + // focus date changed + if ('focusDate' in patch) { + state.focusDate = checkDateInRange(state.focusDate, state.minDate, state.maxDate); + startDate = state.focusDate; + + // nothing to rebuild if only focus changed and it is still visible + if (state.months.length !== 0 && !state.focusDate.before(state.firstDate) && + !state.focusDate.after(state.lastDate)) { + return state; + } + } + + // first date changed + if ('firstDate' in patch) { + state.firstDate = checkDateInRange(state.firstDate, state.minDate, state.maxDate); + startDate = state.firstDate; + } + + // rebuilding months + if (startDate) { + const forceRebuild = 'dayTemplateData' in patch || 'firstDayOfWeek' in patch || 'markDisabled' in patch || + 'minDate' in patch || 'maxDate' in patch || 'disabled' in patch || 'outsideDays' in patch; + + const months = buildMonths(this._calendar, startDate, state, this._i18n, forceRebuild); + + // updating months and boundary dates + state.months = months; + state.firstDate = months.length > 0 ? months[0].firstDate : undefined; + state.lastDate = months.length > 0 ? months[months.length - 1].lastDate : undefined; + + // reset selected date if 'markDisabled' returns true + if ('selectedDate' in patch && !isDateSelectable(state.selectedDate, state)) { + state.selectedDate = null; + } + + // adjusting focus after months were built + if ('firstDate' in patch) { + if (state.focusDate === undefined || state.focusDate.before(state.firstDate) || + state.focusDate.after(state.lastDate)) { + state.focusDate = startDate; + } + } + + // adjusting months/years for the select box navigation + const yearChanged = !this._state.firstDate || this._state.firstDate.year !== state.firstDate.year; + const monthChanged = !this._state.firstDate || this._state.firstDate.month !== state.firstDate.month; + if (state.navigation === 'select') { + // years -> boundaries (min/max were changed) + if ('minDate' in patch || 'maxDate' in patch || state.selectBoxes.years.length === 0 || yearChanged) { + state.selectBoxes.years = generateSelectBoxYears(state.firstDate, state.minDate, state.maxDate); + } + + // months -> when current year or boundaries change + if ('minDate' in patch || 'maxDate' in patch || state.selectBoxes.months.length === 0 || yearChanged) { + state.selectBoxes.months = + generateSelectBoxMonths(this._calendar, state.firstDate, state.minDate, state.maxDate); + } + } else { + state.selectBoxes = {years: [], months: []}; + } + + // updating navigation arrows -> boundaries change (min/max) or month/year changes + if ((state.navigation === 'arrows' || state.navigation === 'select') && + (monthChanged || yearChanged || 'minDate' in patch || 'maxDate' in patch || 'disabled' in patch)) { + state.prevDisabled = state.disabled || prevMonthDisabled(this._calendar, state.firstDate, state.minDate); + state.nextDisabled = state.disabled || nextMonthDisabled(this._calendar, state.lastDate, state.maxDate); + } + } + + return state; + } +} diff --git a/src/datepicker/datepicker-tools.spec.ts b/src/datepicker/datepicker-tools.spec.ts new file mode 100644 index 0000000..89916c5 --- /dev/null +++ b/src/datepicker/datepicker-tools.spec.ts @@ -0,0 +1,608 @@ +import { + buildMonth, + buildMonths, + checkDateInRange, + dateComparator, + generateSelectBoxMonths, + getFirstViewDate, + isChangedMonth, + isDateSelectable, + generateSelectBoxYears +} from './datepicker-tools'; +import {NgbDate} from './ngb-date'; +import {NgbCalendarGregorian} from './ngb-calendar'; +import {DatepickerViewModel, NgbMarkDisabled, MonthViewModel} from './datepicker-view-model'; +import {NgbDatepickerI18nDefault} from './datepicker-i18n'; + +describe(`datepicker-tools`, () => { + + const calendar = new NgbCalendarGregorian(); + const i18n = new NgbDatepickerI18nDefault('en'); + + describe(`dateComparator()`, () => { + + it(`should compare valid dates`, () => { + expect(dateComparator(new NgbDate(2017, 5, 2), new NgbDate(2017, 5, 2))).toBe(true); + + expect(dateComparator(new NgbDate(2017, 5, 2), new NgbDate(2017, 5, 1))).toBe(false); + expect(dateComparator(new NgbDate(2017, 5, 2), new NgbDate(2017, 1, 2))).toBe(false); + expect(dateComparator(new NgbDate(2017, 5, 2), new NgbDate(2001, 5, 2))).toBe(false); + }); + + it(`should compare invalid dates`, () => { + expect(dateComparator(undefined, undefined)).toBe(true); + expect(dateComparator(null, null)).toBe(true); + + expect(dateComparator(new NgbDate(2017, 5, 2), null)).toBe(false); + expect(dateComparator(new NgbDate(2017, 5, 2), undefined)).toBe(false); + expect(dateComparator(null, new NgbDate(2017, 5, 2))).toBe(false); + expect(dateComparator(undefined, new NgbDate(2017, 5, 2))).toBe(false); + }); + }); + + describe(`checkDateInRange()`, () => { + + it(`should throw adjust date to be in between of min and max dates`, () => { + const minDate = new NgbDate(2015, 5, 1); + const maxDate = new NgbDate(2015, 5, 10); + + expect(checkDateInRange(new NgbDate(2015, 5, 5), minDate, maxDate)).toEqual(new NgbDate(2015, 5, 5)); + expect(checkDateInRange(new NgbDate(2015, 4, 5), minDate, maxDate)).toEqual(minDate); + expect(checkDateInRange(new NgbDate(2015, 6, 5), minDate, maxDate)).toEqual(maxDate); + }); + + it(`should allow for undefined max and min dates`, () => { + const minDate = new NgbDate(2015, 5, 1); + const maxDate = new NgbDate(2015, 5, 10); + + expect(checkDateInRange(new NgbDate(2015, 5, 5), undefined, undefined)).toEqual(new NgbDate(2015, 5, 5)); + expect(checkDateInRange(new NgbDate(2015, 5, 5), minDate, undefined)).toEqual(new NgbDate(2015, 5, 5)); + expect(checkDateInRange(new NgbDate(2015, 5, 5), undefined, maxDate)).toEqual(new NgbDate(2015, 5, 5)); + + expect(checkDateInRange(new NgbDate(2015, 4, 5), minDate, undefined)).toEqual(minDate); + expect(checkDateInRange(new NgbDate(2015, 6, 5), undefined, maxDate)).toEqual(maxDate); + }); + + it(`should bypass invalid date values`, () => { + expect(checkDateInRange(undefined, undefined, undefined)).toBeUndefined(); + expect(checkDateInRange(null, undefined, undefined)).toBeNull(); + expect(checkDateInRange(new NgbDate(-2, 0, 0), undefined, undefined)).toEqual(new NgbDate(-2, 0, 0)); + }); + + it(`should not alter date object`, () => { + const date = new NgbDate(2017, 5, 1); + expect(checkDateInRange(date, undefined, undefined)).toBe(date); + }); + }); + + describe(`buildMonth()`, () => { + + // TODO: this should be automated somehow, ex. generate next 10 years or something + const months = [ + { + // MAY 2017 + date: new NgbDate(2017, 5, 5), + lastDay: 31, + firstWeek: {number: 18, date: new NgbDate(2017, 5, 1)}, + lastWeek: {number: 23, date: new NgbDate(2017, 6, 11)} + }, + { + // JUN 2017 + date: new NgbDate(2017, 6, 5), + lastDay: 30, + firstWeek: {number: 22, date: new NgbDate(2017, 5, 29)}, + lastWeek: {number: 27, date: new NgbDate(2017, 7, 9)} + }, + { + // FEB 2017 + date: new NgbDate(2017, 2, 1), + lastDay: 28, + firstWeek: {number: 5, date: new NgbDate(2017, 1, 30)}, + lastWeek: {number: 10, date: new NgbDate(2017, 3, 12)} + }, + { + // FEB 2016 + date: new NgbDate(2016, 2, 10), + lastDay: 29, + firstWeek: {number: 5, date: new NgbDate(2016, 2, 1)}, + lastWeek: {number: 10, date: new NgbDate(2016, 3, 13)} + } + ]; + + months.forEach(refMonth => { + it(`should build month (${refMonth.date.year} - ${refMonth.date.month}) correctly`, () => { + + let month = buildMonth(calendar, refMonth.date, { firstDayOfWeek: 1 } as DatepickerViewModel, i18n); + + expect(month).toBeTruthy(); + expect(month.year).toEqual(refMonth.date.year); + expect(month.number).toEqual(refMonth.date.month); + expect(month.firstDate).toEqual(new NgbDate(refMonth.date.year, refMonth.date.month, 1)); + expect(month.lastDate).toEqual(new NgbDate(refMonth.date.year, refMonth.date.month, refMonth.lastDay)); + expect(month.weekdays).toEqual([1, 2, 3, 4, 5, 6, 7]); + expect(month.weeks.length).toBe(6); + + // First week, first day + expect(month.weeks[0].number).toEqual(refMonth.firstWeek.number); + expect(month.weeks[0].days.length).toEqual(7); + expect(month.weeks[0].days[0].date).toEqual(refMonth.firstWeek.date); + expect(month.weeks[0].days[0].context.disabled).toBe(false); + + // Last week, last day + expect(month.weeks[5].number).toEqual(refMonth.lastWeek.number); + expect(month.weeks[5].days.length).toEqual(7); + expect(month.weeks[5].days[6].date).toEqual(refMonth.lastWeek.date); + expect(month.weeks[5].days[6].context.disabled).toBe(false); + }); + }); + + it(`should mark dates as disabled`, () => { + // disable the second day + const markDisabled: NgbMarkDisabled = (date) => date.day === 2; + + // MAY 2017 + let month = buildMonth( + calendar, new NgbDate(2017, 5, 5), { firstDayOfWeek: 1, markDisabled } as DatepickerViewModel, i18n); + + // 2 MAY - disabled + expect(month.weeks[0].days[0].context.disabled).toBe(false); + expect(month.weeks[0].days[1].context.disabled).toBe(true); + expect(month.weeks[0].days[2].context.disabled).toBe(false); + }); + + + it(`should call 'markDisabled' with correct arguments`, () => { + const mock: {markDisabled: NgbMarkDisabled} = {markDisabled: () => false}; + spyOn(mock, 'markDisabled').and.returnValue(false); + + // MAY 2017 + let state = { + firstDayOfWeek: 1, + minDate: new NgbDate(2017, 5, 10), + maxDate: new NgbDate(2017, 5, 10), + markDisabled: mock.markDisabled + } as DatepickerViewModel; + buildMonth(calendar, new NgbDate(2017, 5, 5), state, i18n); + + // called one time, because it should be used only inside min-max range + expect(mock.markDisabled).toHaveBeenCalledWith(new NgbDate(2017, 5, 10), {year: 2017, month: 5}); + expect(mock.markDisabled).toHaveBeenCalledTimes(1); + }); + + it(`should mark dates before 'minDate' as disabled and ignore 'markDisabled'`, () => { + const markDisabled: NgbMarkDisabled = (date) => date.day === 1; + + // MAY 2017 + let state = { firstDayOfWeek: 1, minDate: new NgbDate(2017, 5, 3), markDisabled } as DatepickerViewModel; + const month = buildMonth(calendar, new NgbDate(2017, 5, 5), state, i18n); + + // MIN = 2, so 1-2 MAY - disabled + expect(month.weeks[0].days[0].context.disabled).toBe(true); + expect(month.weeks[0].days[1].context.disabled).toBe(true); + expect(month.weeks[0].days[2].context.disabled).toBe(false); + expect(month.weeks[0].days[3].context.disabled).toBe(false); + }); + + it(`should mark dates after 'maxDate' as disabled and ignore 'markDisabled`, () => { + const markDisabled: NgbMarkDisabled = (date) => date.day === 3; + + // MAY 2017 + let state = { firstDayOfWeek: 1, maxDate: new NgbDate(2017, 5, 2), markDisabled } as DatepickerViewModel; + const month = buildMonth(calendar, new NgbDate(2017, 5, 5), state, i18n); + + // MAX = 2, so 3-4 MAY - disabled + expect(month.weeks[0].days[0].context.disabled).toBe(false); + expect(month.weeks[0].days[1].context.disabled).toBe(false); + expect(month.weeks[0].days[2].context.disabled).toBe(true); + expect(month.weeks[0].days[3].context.disabled).toBe(true); + }); + + it(`should rotate days of the week`, () => { + // SUN = 7 + let month = buildMonth(calendar, new NgbDate(2017, 5, 5), { firstDayOfWeek: 7 } as DatepickerViewModel, i18n); + expect(month.weekdays).toEqual([7, 1, 2, 3, 4, 5, 6]); + expect(month.weeks[0].days[0].date).toEqual(new NgbDate(2017, 4, 30)); + + // WED = 3 + month = buildMonth(calendar, new NgbDate(2017, 5, 5), { firstDayOfWeek: 3 } as DatepickerViewModel, i18n); + expect(month.weekdays).toEqual([3, 4, 5, 6, 7, 1, 2]); + expect(month.weeks[0].days[0].date).toEqual(new NgbDate(2017, 4, 26)); + }); + }); + + describe(`buildMonths()`, () => { + + it(`should generate 'displayMonths' number of months`, () => { + let state = { displayMonths: 1, firstDayOfWeek: 1, months: [] } as DatepickerViewModel; + let months = buildMonths(calendar, new NgbDate(2017, 5, 5), state, i18n, false); + expect(months.length).toBe(1); + + state.displayMonths = 2; + months = buildMonths(calendar, new NgbDate(2017, 5, 5), state, i18n, false); + expect(months.length).toBe(2); + }); + + const storeMonthsDataStructure = (months: MonthViewModel[]) => { + return months.map(month => { + const storage = {weeks: month.weeks, weekdays: month.weekdays}; + const weeks = month.weeks; + for (let weekIndex = 0, weeksLength = weeks.length; weekIndex < weeksLength; weekIndex++) { + const currentWeek = weeks[weekIndex]; + storage[`weeks[${weekIndex}]`] = currentWeek; + const days = currentWeek.days; + storage[`weeks[${weekIndex}].days`] = days; + for (let dayIndex = 0, daysLength = days.length; dayIndex < daysLength; dayIndex++) { + const currentDay = days[dayIndex]; + storage[`weeks[${weekIndex}].days[${dayIndex}]`] = currentDay; + } + } + return storage; + }); + }; + + const customMatchers: jasmine.CustomMatcherFactories = { + toHaveTheSameMonthDataStructureAs: function(util, customEqualityTesters) { + return { + compare(actualMonthsStorage, expectedMonthsStorage) { + try { + const monthsNumber = actualMonthsStorage.length; + if (expectedMonthsStorage.length !== monthsNumber) { + throw 'the number of months'; + } + for (let i = 0; i < monthsNumber; i++) { + const storage1 = actualMonthsStorage[i]; + const storage2 = expectedMonthsStorage[i]; + const keys1 = Object.keys(storage1); + const keys2 = Object.keys(storage2); + if (!util.equals(keys2, keys1, customEqualityTesters)) { + throw `the set of keys in months[${i}]: ${keys1} != ${keys2}`; + } + for (const key of keys1) { + if (storage1[key] !== storage2[key]) { + throw `months[${i}].${key}`; + } + } + } + return { + pass: true, + message: 'Expected different months data structures, but the same data structure was found.' + }; + } catch (e) { + return { + pass: false, + message: typeof e === 'string' ? + `Expected the same months data structure, but a difference was found in ${e}` : + `${e}` + }; + } + } + }; + } + }; + + beforeEach(function() { jasmine.addMatchers(customMatchers); }); + + it(`should reuse the same data structure (force = false)`, () => { + let state = { displayMonths: 1, firstDayOfWeek: 1, months: [] } as DatepickerViewModel; + let months = buildMonths(calendar, new NgbDate(2017, 5, 5), state, i18n, false); + expect(months).toBe(state.months); + expect(months.length).toBe(1); + let monthsStructure = storeMonthsDataStructure(months); + + months = buildMonths(calendar, new NgbDate(2018, 5, 5), state, i18n, false); + expect(months).toBe(state.months); + expect(months.length).toBe(1); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + + state.displayMonths = 2; + months = buildMonths(calendar, new NgbDate(2018, 5, 5), state, i18n, false); + expect(months).toBe(state.months); + expect(months.length).toBe(2); + monthsStructure.push(...storeMonthsDataStructure([months[1]])); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + + // next month + months = buildMonths(calendar, new NgbDate(2018, 6, 5), state, i18n, false); + expect(months).toBe(state.months); + expect(months.length).toBe(2); + // the structures should be swapped: + monthsStructure.push(monthsStructure.shift()); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + + // previous month + months = buildMonths(calendar, new NgbDate(2018, 5, 5), state, i18n, false); + expect(months).toBe(state.months); + expect(months.length).toBe(2); + // the structures should be swapped (again): + monthsStructure.push(monthsStructure.shift()); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + + state.displayMonths = 5; + months = buildMonths(calendar, new NgbDate(2018, 5, 5), state, i18n, false); + expect(months).toBe(state.months); + expect(months.length).toBe(5); + monthsStructure.push(...storeMonthsDataStructure(months.slice(2))); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + + // go to two months after, the 3 last months are reused as is + months = buildMonths(calendar, new NgbDate(2018, 7, 5), state, i18n, false); + expect(months).toBe(state.months); + expect(months.length).toBe(5); + monthsStructure.unshift(...monthsStructure.splice(2, 3)); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + + // go to two months before, the 3 first months are reused as is + months = buildMonths(calendar, new NgbDate(2018, 5, 5), state, i18n, false); + expect(months).toBe(state.months); + expect(months.length).toBe(5); + monthsStructure.push(...monthsStructure.splice(0, 3)); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + + // completely change the dates, nothing is shifted in monthsStructure + months = buildMonths(calendar, new NgbDate(2018, 10, 5), state, i18n, false); + expect(months).toBe(state.months); + expect(months.length).toBe(5); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + + // keep 2 months + state.displayMonths = 2; + months = buildMonths(calendar, new NgbDate(2018, 11, 5), state, i18n, false); + expect(months).toBe(state.months); + expect(months.length).toBe(2); + monthsStructure = monthsStructure.slice(1, 3); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + }); + + it(`should reuse the same data structure (force = true)`, () => { + let state = { displayMonths: 1, firstDayOfWeek: 1, months: [] } as DatepickerViewModel; + let months = buildMonths(calendar, new NgbDate(2017, 5, 5), state, i18n, true); + expect(months).toBe(state.months); + expect(months.length).toBe(1); + let monthsStructure = storeMonthsDataStructure(months); + + months = buildMonths(calendar, new NgbDate(2018, 5, 5), state, i18n, true); + expect(months).toBe(state.months); + expect(months.length).toBe(1); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + + state.displayMonths = 2; + months = buildMonths(calendar, new NgbDate(2018, 5, 5), state, i18n, true); + expect(months).toBe(state.months); + expect(months.length).toBe(2); + monthsStructure.push(...storeMonthsDataStructure([months[1]])); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + + // next month + months = buildMonths(calendar, new NgbDate(2018, 6, 5), state, i18n, true); + expect(months).toBe(state.months); + expect(months.length).toBe(2); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + + // previous month + months = buildMonths(calendar, new NgbDate(2018, 5, 5), state, i18n, true); + expect(months).toBe(state.months); + expect(months.length).toBe(2); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + + state.displayMonths = 5; + months = buildMonths(calendar, new NgbDate(2018, 5, 5), state, i18n, true); + expect(months).toBe(state.months); + expect(months.length).toBe(5); + monthsStructure.push(...storeMonthsDataStructure(months.slice(2))); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + + // go to two months after + months = buildMonths(calendar, new NgbDate(2018, 7, 5), state, i18n, true); + expect(months).toBe(state.months); + expect(months.length).toBe(5); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + + // go to two months before + months = buildMonths(calendar, new NgbDate(2018, 5, 5), state, i18n, true); + expect(months).toBe(state.months); + expect(months.length).toBe(5); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + + // completely change the dates + months = buildMonths(calendar, new NgbDate(2018, 10, 5), state, i18n, true); + expect(months).toBe(state.months); + expect(months.length).toBe(5); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + + // keep 2 months + state.displayMonths = 2; + months = buildMonths(calendar, new NgbDate(2018, 11, 5), state, i18n, true); + expect(months).toBe(state.months); + expect(months.length).toBe(2); + monthsStructure = monthsStructure.slice(0, 2); + expect(storeMonthsDataStructure(months))['toHaveTheSameMonthDataStructureAs'](monthsStructure); + }); + }); + + describe(`getFirstViewDate()`, () => { + + const months = [ + // Mon + {start: 1, date: new NgbDate(2017, 1, 10), first: new NgbDate(2016, 12, 26)}, + {start: 1, date: new NgbDate(2017, 2, 10), first: new NgbDate(2017, 1, 30)}, + {start: 1, date: new NgbDate(2017, 3, 10), first: new NgbDate(2017, 2, 27)}, + {start: 1, date: new NgbDate(2017, 4, 10), first: new NgbDate(2017, 3, 27)}, + {start: 1, date: new NgbDate(2017, 5, 10), first: new NgbDate(2017, 5, 1)}, + {start: 1, date: new NgbDate(2017, 6, 10), first: new NgbDate(2017, 5, 29)}, + {start: 1, date: new NgbDate(2017, 7, 10), first: new NgbDate(2017, 6, 26)}, + {start: 1, date: new NgbDate(2017, 8, 10), first: new NgbDate(2017, 7, 31)}, + {start: 1, date: new NgbDate(2017, 9, 10), first: new NgbDate(2017, 8, 28)}, + {start: 1, date: new NgbDate(2017, 10, 10), first: new NgbDate(2017, 9, 25)}, + {start: 1, date: new NgbDate(2017, 11, 10), first: new NgbDate(2017, 10, 30)}, + {start: 1, date: new NgbDate(2017, 12, 10), first: new NgbDate(2017, 11, 27)}, + // Sun + {start: 7, date: new NgbDate(2017, 1, 10), first: new NgbDate(2017, 1, 1)}, + {start: 7, date: new NgbDate(2017, 2, 10), first: new NgbDate(2017, 1, 29)}, + {start: 7, date: new NgbDate(2017, 3, 10), first: new NgbDate(2017, 2, 26)}, + {start: 7, date: new NgbDate(2017, 4, 10), first: new NgbDate(2017, 3, 26)}, + {start: 7, date: new NgbDate(2017, 5, 10), first: new NgbDate(2017, 4, 30)}, + {start: 7, date: new NgbDate(2017, 6, 10), first: new NgbDate(2017, 5, 28)}, + {start: 7, date: new NgbDate(2017, 7, 10), first: new NgbDate(2017, 6, 25)}, + {start: 7, date: new NgbDate(2017, 8, 10), first: new NgbDate(2017, 7, 30)}, + {start: 7, date: new NgbDate(2017, 9, 10), first: new NgbDate(2017, 8, 27)}, + {start: 7, date: new NgbDate(2017, 10, 10), first: new NgbDate(2017, 10, 1)}, + {start: 7, date: new NgbDate(2017, 11, 10), first: new NgbDate(2017, 10, 29)}, + {start: 7, date: new NgbDate(2017, 12, 10), first: new NgbDate(2017, 11, 26)}, + // Wed + {start: 3, date: new NgbDate(2017, 1, 10), first: new NgbDate(2016, 12, 28)}, + {start: 3, date: new NgbDate(2017, 2, 10), first: new NgbDate(2017, 2, 1)}, + {start: 3, date: new NgbDate(2017, 3, 10), first: new NgbDate(2017, 3, 1)}, + {start: 3, date: new NgbDate(2017, 4, 10), first: new NgbDate(2017, 3, 29)}, + {start: 3, date: new NgbDate(2017, 5, 10), first: new NgbDate(2017, 4, 26)}, + {start: 3, date: new NgbDate(2017, 6, 10), first: new NgbDate(2017, 5, 31)}, + {start: 3, date: new NgbDate(2017, 7, 10), first: new NgbDate(2017, 6, 28)}, + {start: 3, date: new NgbDate(2017, 8, 10), first: new NgbDate(2017, 7, 26)}, + {start: 3, date: new NgbDate(2017, 9, 10), first: new NgbDate(2017, 8, 30)}, + {start: 3, date: new NgbDate(2017, 10, 10), first: new NgbDate(2017, 9, 27)}, + {start: 3, date: new NgbDate(2017, 11, 10), first: new NgbDate(2017, 11, 1)}, + {start: 3, date: new NgbDate(2017, 12, 10), first: new NgbDate(2017, 11, 29)} + ]; + + months.forEach(month => { + it(`should return the correct first view date`, + () => { expect(getFirstViewDate(calendar, month.date, month.start)).toEqual(month.first); }); + }); + }); + + describe(`isDateSelectable()`, () => { + + // disabling 15th of any month + const markDisabled: NgbMarkDisabled = (date, month) => date.day === 15; + + it(`should return false if date is invalid`, () => { + let state = { disabled: false } as DatepickerViewModel; + expect(isDateSelectable(null, state)).toBeFalsy(); + expect(isDateSelectable(undefined, state)).toBeFalsy(); + }); + + it(`should return false if datepicker is disabled`, () => { + let state = { disabled: true } as DatepickerViewModel; + expect(isDateSelectable(new NgbDate(2016, 11, 10), state)).toBeFalsy(); + expect(isDateSelectable(new NgbDate(2017, 11, 10), state)).toBeFalsy(); + expect(isDateSelectable(new NgbDate(2018, 11, 10), state)).toBeFalsy(); + }); + + it(`should take into account markDisabled values`, () => { + let state = { disabled: false, markDisabled } as DatepickerViewModel; + expect(isDateSelectable(new NgbDate(2016, 11, 15), state)).toBeFalsy(); + expect(isDateSelectable(new NgbDate(2017, 11, 15), state)).toBeFalsy(); + expect(isDateSelectable(new NgbDate(2018, 11, 15), state)).toBeFalsy(); + }); + + it(`should take into account minDate values`, () => { + let state = { disabled: false, minDate: new NgbDate(2018, 11, 10) } as DatepickerViewModel; + expect(isDateSelectable(new NgbDate(2017, 11, 10), state)).toBeFalsy(); + }); + + it(`should take into account maxDate values`, () => { + let state = { disabled: false, maxDate: new NgbDate(2016, 11, 10) } as DatepickerViewModel; + expect(isDateSelectable(new NgbDate(2017, 11, 10), state)).toBeFalsy(); + }); + + it(`should return true for normal values`, () => { + let state = { disabled: false } as DatepickerViewModel; + expect(isDateSelectable(new NgbDate(2016, 11, 10), state)).toBeTruthy(); + expect(isDateSelectable(new NgbDate(2017, 11, 10), state)).toBeTruthy(); + expect(isDateSelectable(new NgbDate(2018, 11, 10), state)).toBeTruthy(); + }); + }); + + describe(`generateSelectBoxMonths`, () => { + + const test = (minDate, date, maxDate, result) => { + expect(generateSelectBoxMonths(calendar, date, minDate, maxDate)).toEqual(result); + }; + + it(`should handle edge cases`, () => { + test(new NgbDate(2018, 6, 1), null, new NgbDate(2018, 6, 10), []); + test(null, null, null, []); + }); + + it(`should generate months correctly`, () => { + // clang-format off + // different years + test(new NgbDate(2017, 1, 1), new NgbDate(2018, 1, 1), new NgbDate(2019, 1, 1), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + test(null, new NgbDate(2018, 6, 10), null, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + test(null, new NgbDate(2018, 1, 1), new NgbDate(2019, 1, 1), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + test(new NgbDate(2017, 1, 1), new NgbDate(2018, 1, 1), null, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + + // same 'min year' + test(new NgbDate(2018, 1, 1), new NgbDate(2018, 6, 10), new NgbDate(2020, 1, 2), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + test(new NgbDate(2018, 6, 1), new NgbDate(2018, 6, 10), new NgbDate(2020, 1, 2), [6, 7, 8, 9, 10, 11, 12]); + test(new NgbDate(2018, 6, 1), new NgbDate(2018, 6, 10), null, [6, 7, 8, 9, 10, 11, 12]); + + // same 'max' year + test(new NgbDate(2017, 1, 1), new NgbDate(2018, 6, 10), new NgbDate(2018, 12, 1), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + test(new NgbDate(2017, 1, 1), new NgbDate(2018, 6, 10), new NgbDate(2018, 6, 10), [1, 2, 3, 4, 5, 6]); + test(null, new NgbDate(2018, 6, 10), new NgbDate(2018, 6, 10), [1, 2, 3, 4, 5, 6]); + + // same 'min' and 'max years' + test(new NgbDate(2018, 1, 1), new NgbDate(2018, 6, 10), new NgbDate(2018, 12, 1), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + test(new NgbDate(2018, 3, 1), new NgbDate(2018, 6, 10), new NgbDate(2018, 12, 1), [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + test(new NgbDate(2018, 3, 1), new NgbDate(2018, 6, 10), null, [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + test(null, new NgbDate(2018, 6, 10), new NgbDate(2018, 8, 1), [1, 2, 3, 4, 5, 6, 7, 8]); + test(new NgbDate(2018, 3, 1), new NgbDate(2018, 6, 10), new NgbDate(2018, 8, 1), [3, 4, 5, 6, 7, 8] ); + test(new NgbDate(2018, 6, 1), new NgbDate(2018, 6, 10), new NgbDate(2018, 6, 10), [6]); + // clang-format on + }); + }); + + describe(`generateSelectBoxYears`, () => { + + const test = + (minDate, date, maxDate, result) => { expect(generateSelectBoxYears(date, minDate, maxDate)).toEqual(result); }; + const range = (start, end) => Array.from({length: end - start + 1}, (e, i) => start + i); + + it(`should handle edge cases`, () => { + test(new NgbDate(2018, 6, 1), null, new NgbDate(2018, 6, 10), []); + test(null, null, null, []); + }); + + it(`should generate years correctly`, () => { + // both 'min' and 'max' are set + test(new NgbDate(2017, 1, 1), new NgbDate(2018, 1, 1), new NgbDate(2019, 1, 1), range(2017, 2019)); + test(new NgbDate(2000, 1, 1), new NgbDate(2018, 1, 1), new NgbDate(3000, 1, 1), range(2000, 3000)); + test(new NgbDate(2018, 1, 1), new NgbDate(2018, 1, 1), new NgbDate(2018, 1, 1), [2018]); + + // 'min' is not set + test(null, new NgbDate(2018, 1, 1), new NgbDate(2019, 1, 1), range(2008, 2019)); + test(null, new NgbDate(2018, 1, 1), new NgbDate(3000, 1, 1), range(2008, 3000)); + test(null, new NgbDate(2018, 1, 1), new NgbDate(2018, 1, 1), range(2008, 2018)); + + // 'max' is not set + test(new NgbDate(2017, 1, 1), new NgbDate(2018, 1, 1), null, range(2017, 2028)); + test(new NgbDate(2000, 1, 1), new NgbDate(2018, 1, 1), null, range(2000, 2028)); + test(new NgbDate(2018, 1, 1), new NgbDate(2018, 1, 1), null, range(2018, 2028)); + + // both are not set + test(null, new NgbDate(2018, 1, 1), null, range(2008, 2028)); + test(null, new NgbDate(2000, 1, 1), null, range(1990, 2010)); + }); + }); + + describe(`isChangedMonth()`, () => { + + it(`should compare valid dates`, () => { + expect(isChangedMonth(new NgbDate(2017, 1, 1), new NgbDate(2017, 1, 1))).toBe(false); + expect(isChangedMonth(new NgbDate(2017, 1, 1), new NgbDate(2017, 1, 10))).toBe(false); + expect(isChangedMonth(new NgbDate(2017, 1, 1), new NgbDate(2017, 2, 1))).toBe(true); + expect(isChangedMonth(new NgbDate(2017, 1, 1), new NgbDate(2018, 1, 1))).toBe(true); + expect(isChangedMonth(new NgbDate(2017, 1, 1), new NgbDate(2018, 2, 1))).toBe(true); + }); + + it(`should compare invalid dates`, () => { + expect(isChangedMonth(undefined, undefined)).toBe(false); + expect(isChangedMonth(null, null)).toBe(false); + + expect(isChangedMonth(new NgbDate(2017, 5, 2), null)).toBe(true); + expect(isChangedMonth(new NgbDate(2017, 5, 2), undefined)).toBe(true); + expect(isChangedMonth(null, new NgbDate(2017, 5, 2))).toBe(true); + expect(isChangedMonth(undefined, new NgbDate(2017, 5, 2))).toBe(true); + }); + }); +}); diff --git a/src/datepicker/datepicker-tools.ts b/src/datepicker/datepicker-tools.ts new file mode 100644 index 0000000..29eb5d0 --- /dev/null +++ b/src/datepicker/datepicker-tools.ts @@ -0,0 +1,215 @@ +import {NgbDate} from './ngb-date'; +import {DatepickerViewModel, DayViewModel, MonthViewModel} from './datepicker-view-model'; +import {NgbCalendar} from './ngb-calendar'; +import {isDefined} from '../util/util'; +import {NgbDatepickerI18n} from './datepicker-i18n'; + +export function isChangedDate(prev: NgbDate, next: NgbDate) { + return !dateComparator(prev, next); +} + +export function isChangedMonth(prev: NgbDate, next: NgbDate) { + return !prev && !next ? false : !prev || !next ? true : prev.year !== next.year || prev.month !== next.month; +} + +export function dateComparator(prev: NgbDate, next: NgbDate) { + return (!prev && !next) || (!!prev && !!next && prev.equals(next)); +} + +export function checkMinBeforeMax(minDate: NgbDate, maxDate: NgbDate) { + if (maxDate && minDate && maxDate.before(minDate)) { + throw new Error(`'maxDate' ${maxDate} should be greater than 'minDate' ${minDate}`); + } +} + +export function checkDateInRange(date: NgbDate, minDate: NgbDate, maxDate: NgbDate): NgbDate { + if (date && minDate && date.before(minDate)) { + return minDate; + } + if (date && maxDate && date.after(maxDate)) { + return maxDate; + } + + return date; +} + +export function isDateSelectable(date: NgbDate, state: DatepickerViewModel) { + const {minDate, maxDate, disabled, markDisabled} = state; + // clang-format off + return !( + !isDefined(date) || + disabled || + (markDisabled && markDisabled(date, {year: date.year, month: date.month})) || + (minDate && date.before(minDate)) || + (maxDate && date.after(maxDate)) + ); + // clang-format on +} + +export function generateSelectBoxMonths(calendar: NgbCalendar, date: NgbDate, minDate: NgbDate, maxDate: NgbDate) { + if (!date) { + return []; + } + + let months = calendar.getMonths(date.year); + + if (minDate && date.year === minDate.year) { + const index = months.findIndex(month => month === minDate.month); + months = months.slice(index); + } + + if (maxDate && date.year === maxDate.year) { + const index = months.findIndex(month => month === maxDate.month); + months = months.slice(0, index + 1); + } + + return months; +} + +export function generateSelectBoxYears(date: NgbDate, minDate: NgbDate, maxDate: NgbDate) { + if (!date) { + return []; + } + + const start = minDate && minDate.year || date.year - 10; + const end = maxDate && maxDate.year || date.year + 10; + + return Array.from({length: end - start + 1}, (e, i) => start + i); +} + +export function nextMonthDisabled(calendar: NgbCalendar, date: NgbDate, maxDate: NgbDate) { + return maxDate && calendar.getNext(date, 'm').after(maxDate); +} + +export function prevMonthDisabled(calendar: NgbCalendar, date: NgbDate, minDate: NgbDate) { + const prevDate = calendar.getPrev(date, 'm'); + return minDate && (prevDate.year === minDate.year && prevDate.month < minDate.month || + prevDate.year < minDate.year && minDate.month === 1); +} + +export function buildMonths( + calendar: NgbCalendar, date: NgbDate, state: DatepickerViewModel, i18n: NgbDatepickerI18n, + force: boolean): MonthViewModel[] { + const {displayMonths, months} = state; + // move old months to a temporary array + const monthsToReuse = months.splice(0, months.length); + + // generate new first dates, nullify or reuse months + const firstDates = Array.from({length: displayMonths}, (_, i) => { + const firstDate = calendar.getNext(date, 'm', i); + months[i] = null; + + if (!force) { + const reusedIndex = monthsToReuse.findIndex(month => month.firstDate.equals(firstDate)); + // move reused month back to months + if (reusedIndex !== -1) { + months[i] = monthsToReuse.splice(reusedIndex, 1)[0]; + } + } + + return firstDate; + }); + + // rebuild nullified months + firstDates.forEach((firstDate, i) => { + if (months[i] === null) { + months[i] = buildMonth(calendar, firstDate, state, i18n, monthsToReuse.shift() || {} as MonthViewModel); + } + }); + + return months; +} + +export function buildMonth( + calendar: NgbCalendar, date: NgbDate, state: DatepickerViewModel, i18n: NgbDatepickerI18n, + month: MonthViewModel = {} as MonthViewModel): MonthViewModel { + const {dayTemplateData, minDate, maxDate, firstDayOfWeek, markDisabled, outsideDays} = state; + const calendarToday = calendar.getToday(); + + month.firstDate = null; + month.lastDate = null; + month.number = date.month; + month.year = date.year; + month.weeks = month.weeks || []; + month.weekdays = month.weekdays || []; + + date = getFirstViewDate(calendar, date, firstDayOfWeek); + + // month has weeks + for (let week = 0; week < calendar.getWeeksPerMonth(); week++) { + let weekObject = month.weeks[week]; + if (!weekObject) { + weekObject = month.weeks[week] = {number: 0, days: [], collapsed: true}; + } + const days = weekObject.days; + + // week has days + for (let day = 0; day < calendar.getDaysPerWeek(); day++) { + if (week === 0) { + month.weekdays[day] = calendar.getWeekday(date); + } + + const newDate = new NgbDate(date.year, date.month, date.day); + const nextDate = calendar.getNext(newDate); + + const ariaLabel = i18n.getDayAriaLabel(newDate); + + // marking date as disabled + let disabled = !!((minDate && newDate.before(minDate)) || (maxDate && newDate.after(maxDate))); + if (!disabled && markDisabled) { + disabled = markDisabled(newDate, {month: month.number, year: month.year}); + } + + // today + let today = newDate.equals(calendarToday); + + // adding user-provided data to the context + let contextUserData = + dayTemplateData ? dayTemplateData(newDate, {month: month.number, year: month.year}) : undefined; + + // saving first date of the month + if (month.firstDate === null && newDate.month === month.number) { + month.firstDate = newDate; + } + + // saving last date of the month + if (newDate.month === month.number && nextDate.month !== month.number) { + month.lastDate = newDate; + } + + let dayObject = days[day]; + if (!dayObject) { + dayObject = days[day] = {} as DayViewModel; + } + dayObject.date = newDate; + dayObject.context = Object.assign(dayObject.context || {}, { + $implicit: newDate, + date: newDate, + data: contextUserData, + currentMonth: month.number, disabled, + focused: false, + selected: false, today + }); + dayObject.tabindex = -1; + dayObject.ariaLabel = ariaLabel; + dayObject.hidden = false; + + date = nextDate; + } + + weekObject.number = calendar.getWeekNumber(days.map(day => day.date), firstDayOfWeek); + + // marking week as collapsed + weekObject.collapsed = outsideDays === 'collapsed' && days[0].date.month !== month.number && + days[days.length - 1].date.month !== month.number; + } + + return month; +} + +export function getFirstViewDate(calendar: NgbCalendar, date: NgbDate, firstDayOfWeek: number): NgbDate { + const daysPerWeek = calendar.getDaysPerWeek(); + const firstMonthDate = new NgbDate(date.year, date.month, 1); + const dayOfWeek = calendar.getWeekday(firstMonthDate) % daysPerWeek; + return calendar.getPrev(firstMonthDate, 'd', (daysPerWeek + dayOfWeek - firstDayOfWeek) % daysPerWeek); +} diff --git a/src/datepicker/datepicker-view-model.ts b/src/datepicker/datepicker-view-model.ts new file mode 100644 index 0000000..17c2b66 --- /dev/null +++ b/src/datepicker/datepicker-view-model.ts @@ -0,0 +1,60 @@ +import {NgbDate} from './ngb-date'; +import {NgbDateStruct} from './ngb-date-struct'; +import {DayTemplateContext} from './datepicker-day-template-context'; + +export type NgbMarkDisabled = (date: NgbDateStruct, current: {year: number, month: number}) => boolean; +export type NgbDayTemplateData = (date: NgbDateStruct, current: {year: number, month: number}) => any; + +export type DayViewModel = { + date: NgbDate, + context: DayTemplateContext, + tabindex: number, + ariaLabel: string, + hidden: boolean +}; + +export type WeekViewModel = { + number: number, + days: DayViewModel[], + collapsed: boolean +}; + +export type MonthViewModel = { + firstDate: NgbDate, + lastDate: NgbDate, + number: number, + year: number, + weeks: WeekViewModel[], + weekdays: number[] +}; + +// clang-format off +export type DatepickerViewModel = { + dayTemplateData?: NgbDayTemplateData, + disabled: boolean, + displayMonths: number, + firstDate?: NgbDate, + firstDayOfWeek: number, + focusDate?: NgbDate, + focusVisible: boolean, + lastDate?: NgbDate, + markDisabled?: NgbMarkDisabled, + maxDate?: NgbDate, + minDate?: NgbDate, + months: MonthViewModel[], + navigation: 'select' | 'arrows' | 'none', + outsideDays: 'visible' | 'collapsed' | 'hidden', + prevDisabled: boolean, + nextDisabled: boolean, + selectBoxes: { + years: number[], + months: number[] + }, + selectedDate: NgbDate +}; +// clang-format on + +export enum NavigationEvent { + PREV, + NEXT +} diff --git a/src/datepicker/datepicker.module.ts b/src/datepicker/datepicker.module.ts new file mode 100644 index 0000000..a4455b2 --- /dev/null +++ b/src/datepicker/datepicker.module.ts @@ -0,0 +1,42 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormsModule} from '@angular/forms'; +import {NgbDatepicker} from './datepicker'; +import {NgbDatepickerMonthView} from './datepicker-month-view'; +import {NgbDatepickerNavigation} from './datepicker-navigation'; +import {NgbInputDatepicker} from './datepicker-input'; +import {NgbDatepickerDayView} from './datepicker-day-view'; +import {NgbDatepickerNavigationSelect} from './datepicker-navigation-select'; + +export {NgbDatepicker, NgbDatepickerNavigateEvent} from './datepicker'; +export {NgbInputDatepicker} from './datepicker-input'; +export {NgbCalendar, NgbPeriod, NgbCalendarGregorian} from './ngb-calendar'; +export {NgbCalendarIslamicCivil} from './hijri/ngb-calendar-islamic-civil'; +export {NgbCalendarIslamicUmalqura} from './hijri/ngb-calendar-islamic-umalqura'; +export {NgbCalendarPersian} from './jalali/ngb-calendar-persian'; +export {NgbCalendarHebrew} from './hebrew/ngb-calendar-hebrew'; +export {NgbDatepickerI18nHebrew} from './hebrew/datepicker-i18n-hebrew'; +export {NgbDatepickerMonthView} from './datepicker-month-view'; +export {NgbDatepickerDayView} from './datepicker-day-view'; +export {NgbDatepickerNavigation} from './datepicker-navigation'; +export {NgbDatepickerNavigationSelect} from './datepicker-navigation-select'; +export {NgbDatepickerConfig} from './datepicker-config'; +export {NgbDatepickerI18n} from './datepicker-i18n'; +export {NgbDateStruct} from './ngb-date-struct'; +export {NgbDate} from './ngb-date'; +export {NgbDateAdapter} from './adapters/ngb-date-adapter'; +export {NgbDateNativeAdapter} from './adapters/ngb-date-native-adapter'; +export {NgbDateNativeUTCAdapter} from './adapters/ngb-date-native-utc-adapter'; +export {NgbDateParserFormatter} from './ngb-date-parser-formatter'; + +@NgModule({ + declarations: [ + NgbDatepicker, NgbDatepickerMonthView, NgbDatepickerNavigation, NgbDatepickerNavigationSelect, NgbDatepickerDayView, + NgbInputDatepicker + ], + exports: [NgbDatepicker, NgbInputDatepicker], + imports: [CommonModule, FormsModule], + entryComponents: [NgbDatepicker] +}) +export class NgbDatepickerModule { +} diff --git a/src/datepicker/datepicker.scss b/src/datepicker/datepicker.scss new file mode 100644 index 0000000..eecef69 --- /dev/null +++ b/src/datepicker/datepicker.scss @@ -0,0 +1,60 @@ +ngb-datepicker { + border: 1px solid #dfdfdf; + border-radius: .25rem; + display: inline-block; + + &-month-view { + pointer-events: auto; + } + + &.dropdown-menu { + padding: 0; + } +} + +.ngb-dp { + &-body { + z-index: 1050; + } + + &-header { + border-bottom: 0; + border-radius: .25rem .25rem 0 0; + padding-top: .25rem; + background-color: #f8f9fa; + } + + &-months { + display: flex; + } + + &-month { + pointer-events: none; + + &-name { + font-size: larger; + height: 2rem; + line-height: 2rem; + text-align: center; + background-color: #f8f9fa; + } + + & + & { + .ngb-dp-month-name, .ngb-dp-week { + padding-left: 1rem; + } + } + + &:last-child .ngb-dp-week { + padding-right: .25rem; + } + + &:first-child .ngb-dp-week { + padding-left: .25rem; + } + + .ngb-dp-week:last-child { + padding-bottom: .25rem; + } + } +} diff --git a/src/datepicker/datepicker.spec.ts b/src/datepicker/datepicker.spec.ts new file mode 100644 index 0000000..c6e36ba --- /dev/null +++ b/src/datepicker/datepicker.spec.ts @@ -0,0 +1,1217 @@ +import {TestBed, ComponentFixture, async, inject, fakeAsync, tick} from '@angular/core/testing'; +import {createGenericTestComponent} from '../test/common'; +import {getMonthSelect, getYearSelect, getNavigationLinks} from '../test/datepicker/common'; + +import {Component, TemplateRef, DebugElement} from '@angular/core'; +import {By} from '@angular/platform-browser'; +import {FormsModule, ReactiveFormsModule, FormGroup, FormControl, Validators} from '@angular/forms'; + +import {NgbDatepickerModule, NgbDatepickerNavigateEvent} from './datepicker.module'; +import {NgbDate} from './ngb-date'; +import {NgbDatepickerConfig} from './datepicker-config'; +import {NgbDatepicker} from './datepicker'; +import {DayTemplateContext} from './datepicker-day-template-context'; +import {NgbDateStruct} from './ngb-date-struct'; +import {NgbDatepickerMonthView} from './datepicker-month-view'; +import {NgbDatepickerDayView} from './datepicker-day-view'; +import {NgbDatepickerNavigationSelect} from './datepicker-navigation-select'; +import {NgbDatepickerNavigation} from './datepicker-navigation'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +function getDates(element: HTMLElement): HTMLElement[] { + return Array.from(element.querySelectorAll('.ngb-dp-day')); +} + +function getDay(element: HTMLElement, index: number): HTMLElement { + return getDates(element)[index].querySelector('div') as HTMLElement; +} + +function getDatepicker(element: HTMLElement): HTMLElement { + return element.querySelector('ngb-datepicker') as HTMLElement; +} + +function getFocusableDays(element: DebugElement): DebugElement[] { + return Array.from(element.queryAll(By.css('div.ngb-dp-day[tabindex="0"]'))); +} + +function getSelectedDays(element: DebugElement): DebugElement[] { + return Array.from(element.queryAll(By.css('div.ngb-dp-day > div.bg-primary'))); +} + +function focusDay() { + const element = document.querySelector('div.ngb-dp-day[tabindex="0"]') as HTMLElement; + const evt = document.createEvent('Event'); + evt.initEvent('focusin', true, false); + element.dispatchEvent(evt); + element.focus(); +} + +function triggerKeyDown(element: DebugElement, keyCode: number, shiftKey = false) { + let event = { + which: keyCode, + shiftKey: shiftKey, + defaultPrevented: false, + propagationStopped: false, + stopPropagation: function() { this.propagationStopped = true; }, + preventDefault: function() { this.defaultPrevented = true; } + }; + expect(document.activeElement.classList.contains('ngb-dp-day')) + .toBeTruthy('You must focus day before triggering key events'); + element.triggerEventHandler('keydown', event); + return event; +} + +function getMonthContainer(datepicker: DebugElement) { + return datepicker.query(By.css('div.ngb-dp-months')); +} + +function expectSelectedDate(element: DebugElement, selectedDate: NgbDate) { + // checking we have 1 day with .selected class + const days = getSelectedDays(element); + + if (selectedDate) { + expect(days.length).toBe(1); + + // checking it corresponds to our date + const day = days[0]; + const dayView = day.parent.query(By.directive(NgbDatepickerDayView)).componentInstance as NgbDatepickerDayView; + expect(NgbDate.from(dayView.date)).toEqual(selectedDate); + } else { + expect(days.length).toBe(0); + } +} + +function expectFocusedDate(element: DebugElement, focusableDate: NgbDate, isFocused = true) { + // checking we have 1 day with tabIndex 0 + const days = getFocusableDays(element); + expect(days.length).toBe(1); + + const day = days[0]; + + // checking it corresponds to our date + const dayView = day.query(By.directive(NgbDatepickerDayView)).componentInstance as NgbDatepickerDayView; + expect(NgbDate.from(dayView.date)).toEqual(focusableDate); + + // checking the active class + // Unable to test it because of unknown failure (works when tested manually) + // expect(day.queryAll(By.css('div.active')).length).toEqual(1, `The day must have a single element with the active + // class`); + + // checking it is focused by the browser + if (isFocused) { + expect(document.activeElement).toBe(day.nativeElement, `Date HTML element for ${focusableDate} is not focused`); + } else { + expect(document.activeElement) + .not.toBe(day.nativeElement, `Date HTML element for ${focusableDate} must not be focused`); + } +} + + +function expectSameValues(datepicker: NgbDatepicker, config: NgbDatepickerConfig) { + expect(datepicker.dayTemplate).toBe(config.dayTemplate); + expect(datepicker.dayTemplateData).toBe(config.dayTemplateData); + expect(datepicker.displayMonths).toBe(config.displayMonths); + expect(datepicker.firstDayOfWeek).toBe(config.firstDayOfWeek); + expect(datepicker.footerTemplate).toBe(config.footerTemplate); + expect(datepicker.markDisabled).toBe(config.markDisabled); + expect(datepicker.minDate).toEqual(config.minDate); + expect(datepicker.maxDate).toEqual(config.maxDate); + expect(datepicker.navigation).toBe(config.navigation); + expect(datepicker.outsideDays).toBe(config.outsideDays); + expect(datepicker.showWeekdays).toBe(config.showWeekdays); + expect(datepicker.showWeekNumbers).toBe(config.showWeekNumbers); + expect(datepicker.startDate).toEqual(config.startDate); +} + +function customizeConfig(config: NgbDatepickerConfig) { + config.dayTemplate = {} as TemplateRef; + config.dayTemplateData = (date, current) => 42; + config.firstDayOfWeek = 2; + config.footerTemplate = {} as TemplateRef; + config.markDisabled = (date, current) => false; + config.minDate = {year: 2000, month: 1, day: 1}; + config.maxDate = {year: 2030, month: 12, day: 31}; + config.navigation = 'none'; + config.outsideDays = 'collapsed'; + config.showWeekdays = false; + config.showWeekNumbers = true; + config.startDate = {year: 2015, month: 1}; +} + +describe('ngb-datepicker', () => { + + beforeEach(() => { + TestBed.configureTestingModule( + {declarations: [TestComponent], imports: [NgbDatepickerModule, FormsModule, ReactiveFormsModule]}); + }); + + it('should initialize inputs with provided config', () => { + const defaultConfig = new NgbDatepickerConfig(); + const datepicker = TestBed.createComponent(NgbDatepicker).componentInstance; + expectSameValues(datepicker, defaultConfig); + }); + + it('should display current month if no date provided', () => { + const fixture = createTestComponent(``); + + const today = new Date(); + expect(getMonthSelect(fixture.nativeElement).value).toBe(`${today.getMonth() + 1}`); + expect(getYearSelect(fixture.nativeElement).value).toBe(`${today.getFullYear()}`); + }); + + it('should throw if max date is before min date', () => { + expect(() => { + createTestComponent(''); + }).toThrowError(); + }); + + it('should handle incorrect startDate values', () => { + const fixture = createTestComponent(``); + const today = new Date(); + const currentMonth = `${today.getMonth() + 1}`; + const currentYear = `${today.getFullYear()}`; + + expect(getMonthSelect(fixture.nativeElement).value).toBe('8'); + expect(getYearSelect(fixture.nativeElement).value).toBe('2016'); + + fixture.componentInstance.date = null; + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe(currentMonth); + expect(getYearSelect(fixture.nativeElement).value).toBe(currentYear); + + fixture.componentInstance.date = undefined; + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe(currentMonth); + expect(getYearSelect(fixture.nativeElement).value).toBe(currentYear); + + fixture.componentInstance.date = {}; + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe(currentMonth); + expect(getYearSelect(fixture.nativeElement).value).toBe(currentYear); + + fixture.componentInstance.date = new Date(); + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe(currentMonth); + expect(getYearSelect(fixture.nativeElement).value).toBe(currentYear); + + fixture.componentInstance.date = new NgbDate(3000000, 1, 1); + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe(currentMonth); + expect(getYearSelect(fixture.nativeElement).value).toBe(currentYear); + }); + + it(`should allow navigation work when startDate value changes`, () => { + const fixture = createTestComponent(``); + + expect(getMonthSelect(fixture.nativeElement).value).toBe('8'); + expect(getYearSelect(fixture.nativeElement).value).toBe('2016'); + + const navigation = getNavigationLinks(fixture.nativeElement); + + // JUL 2016 + navigation[0].click(); + fixture.detectChanges(); + + expect(getMonthSelect(fixture.nativeElement).value).toBe('7'); + expect(getYearSelect(fixture.nativeElement).value).toBe('2016'); + }); + + it('should allow infinite navigation when min/max dates are not set', () => { + const fixture = createTestComponent(``); + + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe('8'); + expect(getYearSelect(fixture.nativeElement).value).toBe('2016'); + + fixture.componentInstance.date = {year: 1066, month: 2}; + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe('2'); + expect(getYearSelect(fixture.nativeElement).value).toBe('1066'); + + fixture.componentInstance.date = {year: 3066, month: 5}; + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe('5'); + expect(getYearSelect(fixture.nativeElement).value).toBe('3066'); + }); + + it('should allow setting minDate separately', () => { + const fixture = createTestComponent(``); + + fixture.componentInstance.minDate = {year: 2000, month: 5, day: 20}; + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe('8'); + expect(getYearSelect(fixture.nativeElement).value).toBe('2016'); + + fixture.componentInstance.date = {year: 1000, month: 2}; + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe('5'); + expect(getYearSelect(fixture.nativeElement).value).toBe('2000'); + + fixture.componentInstance.date = {year: 3000, month: 5}; + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe('5'); + expect(getYearSelect(fixture.nativeElement).value).toBe('3000'); + }); + + it('should allow setting maxDate separately', () => { + const fixture = createTestComponent(``); + + fixture.componentInstance.maxDate = {year: 2050, month: 5, day: 20}; + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe('8'); + expect(getYearSelect(fixture.nativeElement).value).toBe('2016'); + + fixture.componentInstance.date = {year: 3000, month: 2}; + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe('5'); + expect(getYearSelect(fixture.nativeElement).value).toBe('2050'); + + fixture.componentInstance.date = {year: 1000, month: 5}; + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe('5'); + expect(getYearSelect(fixture.nativeElement).value).toBe('1000'); + }); + + it('should handle minDate edge case values', () => { + const fixture = createTestComponent(``); + const datepicker = fixture.debugElement.query(By.directive(NgbDatepicker)).injector.get(NgbDatepicker); + + function expectMinDate(year: number, month: number) { + datepicker.navigateTo({year: 1000, month: 1}); + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe(`${month}`); + expect(getYearSelect(fixture.nativeElement).value).toBe(`${year}`); + } + + expectMinDate(2010, 1); + + // resetting + fixture.componentInstance.minDate = {}; + fixture.detectChanges(); + expectMinDate(1000, 1); + + // resetting + fixture.componentInstance.minDate = new Date(); + fixture.detectChanges(); + expectMinDate(1000, 1); + + // resetting + fixture.componentInstance.minDate = new NgbDate(3000000, 1, 1); + fixture.detectChanges(); + expectMinDate(1000, 1); + + // resetting + fixture.componentInstance.minDate = null; + fixture.detectChanges(); + expectMinDate(1000, 1); + + // resetting + fixture.componentInstance.minDate = undefined; + fixture.detectChanges(); + expectMinDate(1000, 1); + }); + + it('should handle maxDate edge case values', () => { + const fixture = createTestComponent(``); + const datepicker = fixture.debugElement.query(By.directive(NgbDatepicker)).injector.get(NgbDatepicker); + + function expectMaxDate(year: number, month: number) { + datepicker.navigateTo({year: 10000, month: 1}); + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe(`${month}`); + expect(getYearSelect(fixture.nativeElement).value).toBe(`${year}`); + } + + expectMaxDate(2020, 12); + + // resetting + fixture.componentInstance.maxDate = {}; + fixture.detectChanges(); + expectMaxDate(10000, 1); + + // resetting + fixture.componentInstance.maxDate = new Date(); + fixture.detectChanges(); + expectMaxDate(10000, 1); + + // resetting + fixture.componentInstance.maxDate = new NgbDate(3000000, 1, 1); + fixture.detectChanges(); + expectMaxDate(10000, 1); + + // resetting + fixture.componentInstance.maxDate = null; + fixture.detectChanges(); + expectMaxDate(10000, 1); + + // resetting + fixture.componentInstance.maxDate = undefined; + fixture.detectChanges(); + expectMaxDate(10000, 1); + }); + + it('should support disabling dates via min/max dates', () => { + const fixture = createTestComponent( + ``); + + fixture.componentInstance.minDate = {year: 2016, month: 8, day: 20}; + fixture.componentInstance.maxDate = {year: 2016, month: 8, day: 25}; + fixture.detectChanges(); + + // 19 AUG 2016 + expect(getDay(fixture.nativeElement, 18)).toHaveCssClass('text-muted'); + // 20 AUG 2016 + expect(getDay(fixture.nativeElement, 19)).not.toHaveCssClass('text-muted'); + // 25 AUG 2016 + expect(getDay(fixture.nativeElement, 24)).not.toHaveCssClass('text-muted'); + // 26 AUG 2016 + expect(getDay(fixture.nativeElement, 25)).toHaveCssClass('text-muted'); + }); + + it('should support disabling dates via callback', () => { + const fixture = createTestComponent( + ``); + + // 22 AUG 2016 + expect(getDay(fixture.nativeElement, 21)).toHaveCssClass('text-muted'); + }); + + it('should support passing custom data to the day template', () => { + const fixture = createTestComponent(` +
{{ date.day }}{{ data }}
+ + `); + + // 22 AUG 2016 + expect(getDay(fixture.nativeElement, 21).innerText).toBe('22!'); + }); + + it('should display multiple months', () => { + const fixture = createTestComponent(``); + + let months = fixture.debugElement.queryAll(By.directive(NgbDatepickerMonthView)); + expect(months.length).toBe(1); + + fixture.componentInstance.displayMonths = 3; + fixture.detectChanges(); + months = fixture.debugElement.queryAll(By.directive(NgbDatepickerMonthView)); + expect(months.length).toBe(3); + }); + + it('should reuse DOM elements when changing month (single month display)', () => { + const fixture = createTestComponent(``); + + // AUG 2016 + const oldDates = getDates(fixture.nativeElement); + const navigation = getNavigationLinks(fixture.nativeElement); + expect(oldDates[0].innerText.trim()).toBe('1'); + + // JUL 2016 + navigation[0].click(); + fixture.detectChanges(); + + const newDates = getDates(fixture.nativeElement); + expect(newDates[0].innerText.trim()).toBe('27'); + + expect(oldDates).toEqual(newDates); + }); + + it('should reuse DOM elements when changing month (multiple months display)', () => { + const fixture = createTestComponent(``); + + // AUG 2016 and SEP 2016 + const oldDates = getDates(fixture.nativeElement); + const oldAugDates = oldDates.slice(0, 42); + const oldSepDates = oldDates.slice(42); + + const navigation = getNavigationLinks(fixture.nativeElement); + expect(oldAugDates[0].innerText.trim()).toBe('1'); + expect(oldSepDates[3].innerText.trim()).toBe('1'); + + // JUL 2016 and AUG 2016 + navigation[0].click(); + fixture.detectChanges(); + + const newDates = getDates(fixture.nativeElement); + const newJulDates = newDates.slice(0, 42); + const newAugDates = newDates.slice(42); + + expect(newJulDates[0].innerText.trim()).toBe('27'); + expect(newAugDates[0].innerText.trim()).toBe('1'); + + // DOM elements were reused: + expect(newAugDates).toEqual(oldAugDates); + expect(newJulDates).toEqual(oldSepDates); + }); + + it('should switch navigation types', () => { + const fixture = createTestComponent(``); + + expect(fixture.debugElement.query(By.directive(NgbDatepickerNavigationSelect))).not.toBeNull(); + expect(fixture.debugElement.query(By.directive(NgbDatepickerNavigation))).not.toBeNull(); + + fixture.componentInstance.navigation = 'arrows'; + fixture.detectChanges(); + expect(fixture.debugElement.query(By.directive(NgbDatepickerNavigationSelect))).toBeNull(); + expect(fixture.debugElement.query(By.directive(NgbDatepickerNavigation))).not.toBeNull(); + + fixture.componentInstance.navigation = 'none'; + fixture.detectChanges(); + expect(fixture.debugElement.query(By.directive(NgbDatepickerNavigationSelect))).toBeNull(); + expect(fixture.debugElement.query(By.directive(NgbDatepickerNavigation))).toBeNull(); + }); + + it('should toggle month names display for a single month', () => { + const fixture = createTestComponent( + ``); + + let months = fixture.debugElement.queryAll(By.css('.ngb-dp-month-name')); + expect(months.length).toBe(0); + + fixture.componentInstance.navigation = 'arrows'; + fixture.detectChanges(); + months = fixture.debugElement.queryAll(By.css('.ngb-dp-month-name')); + expect(months.length).toBe(1); + expect(months.map(c => c.nativeElement.innerText.trim())).toEqual(['August 2016']); + + fixture.componentInstance.navigation = 'none'; + fixture.detectChanges(); + months = fixture.debugElement.queryAll(By.css('.ngb-dp-month-name')); + expect(months.length).toBe(1); + expect(months.map(c => c.nativeElement.innerText.trim())).toEqual(['August 2016']); + }); + + it('should always display month names for multiple months', () => { + const fixture = createTestComponent( + ``); + + let months = fixture.debugElement.queryAll(By.css('.ngb-dp-month-name')); + expect(months.length).toBe(3); + expect(months.map(c => c.nativeElement.innerText.trim())).toEqual([ + 'August 2016', 'September 2016', 'October 2016' + ]); + + fixture.componentInstance.navigation = 'arrows'; + fixture.detectChanges(); + months = fixture.debugElement.queryAll(By.css('.ngb-dp-month-name')); + expect(months.length).toBe(3); + expect(months.map(c => c.nativeElement.innerText.trim())).toEqual([ + 'August 2016', 'September 2016', 'October 2016' + ]); + }); + + it('should emit navigate event when startDate is defined', () => { + TestBed.overrideComponent( + TestComponent, + {set: {template: ``}}); + const fixture = TestBed.createComponent(TestComponent); + + spyOn(fixture.componentInstance, 'onNavigate'); + fixture.detectChanges(); + + expect(fixture.componentInstance.onNavigate) + .toHaveBeenCalledWith({current: null, next: {year: 2016, month: 8}, preventDefault: jasmine.any(Function)}); + }); + + it('should emit navigate event without startDate defined', () => { + TestBed.overrideComponent( + TestComponent, {set: {template: ``}}); + const fixture = TestBed.createComponent(TestComponent); + const now = new Date(); + + spyOn(fixture.componentInstance, 'onNavigate'); + fixture.detectChanges(); + + expect(fixture.componentInstance.onNavigate).toHaveBeenCalledWith({ + current: null, + next: {year: now.getFullYear(), month: now.getMonth() + 1}, + preventDefault: jasmine.any(Function) + }); + }); + + it('should emit navigate event using built-in navigation arrows', () => { + const fixture = + createTestComponent(``); + + spyOn(fixture.componentInstance, 'onNavigate'); + const navigation = getNavigationLinks(fixture.nativeElement); + + // JUL 2016 + navigation[0].click(); + fixture.detectChanges(); + expect(fixture.componentInstance.onNavigate).toHaveBeenCalledWith({ + current: {year: 2016, month: 8}, + next: {year: 2016, month: 7}, + preventDefault: jasmine.any(Function) + }); + }); + + it('should emit navigate event using navigateTo({date})', () => { + const fixture = + createTestComponent(` + `); + + spyOn(fixture.componentInstance, 'onNavigate'); + const button = fixture.nativeElement.querySelector('button#btn'); + button.click(); + + fixture.detectChanges(); + expect(fixture.componentInstance.onNavigate).toHaveBeenCalledWith({ + current: {year: 2016, month: 8}, + next: {year: 2015, month: 6}, + preventDefault: jasmine.any(Function) + }); + }); + + it('should prevent navigation when calling preventDefault()', () => { + const fixture = createTestComponent( + ` + `); + + expect(getMonthSelect(fixture.nativeElement).value).toBe('8'); + expect(getYearSelect(fixture.nativeElement).value).toBe('2016'); + expect(getDay(fixture.nativeElement, 0).innerText).toBe('1'); + + const button = fixture.nativeElement.querySelector('button#btn'); + button.click(); + fixture.detectChanges(); + + expect(getMonthSelect(fixture.nativeElement).value).toBe('8'); + expect(getYearSelect(fixture.nativeElement).value).toBe('2016'); + expect(getDay(fixture.nativeElement, 0).innerText).toBe('1'); + }); + + it('should not focus day initially', () => { + const fixture = createTestComponent(''); + const datepicker = fixture.debugElement.query(By.directive(NgbDatepicker)); + expectFocusedDate(datepicker, new NgbDate(2016, 8, 1), false); + }); + + it('should remove focus day on blur', () => { + const fixture = + createTestComponent(''); + const datepicker = fixture.debugElement.query(By.directive(NgbDatepicker)); + + // focus in + focusDay(); + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2016, 8, 1), true); + + // focus out + (document.querySelector('#focusout') as HTMLElement).focus(); + + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2016, 8, 1), false); + expectSelectedDate(datepicker, null); + + }); + + it('should emit select event when select date', () => { + const fixture = + createTestComponent(``); + + spyOn(fixture.componentInstance, 'onSelect'); + let dates = getDates(fixture.nativeElement); + dates[11].click(); + + fixture.detectChanges(); + expect(fixture.componentInstance.onSelect).toHaveBeenCalledTimes(1); + }); + + it('should emit select event twice when select same date twice', () => { + const fixture = + createTestComponent(``); + + spyOn(fixture.componentInstance, 'onSelect'); + let dates = getDates(fixture.nativeElement); + + dates[11].click(); + fixture.detectChanges(); + + dates[11].click(); + fixture.detectChanges(); + + expect(fixture.componentInstance.onSelect).toHaveBeenCalledTimes(2); + }); + + it('should emit select event twice when press enter key twice', () => { + const fixture = + createTestComponent(``); + const datepicker = fixture.debugElement.query(By.directive(NgbDatepicker)); + + spyOn(fixture.componentInstance, 'onSelect'); + + focusDay(); + fixture.detectChanges(); + + triggerKeyDown(getMonthContainer(datepicker), 13 /* enter */); + fixture.detectChanges(); + + triggerKeyDown(getMonthContainer(datepicker), 13 /* enter */); + fixture.detectChanges(); + expect(fixture.componentInstance.onSelect).toHaveBeenCalledTimes(2); + }); + + it('should emit select event twice when press space key twice', () => { + const fixture = + createTestComponent(``); + const datepicker = fixture.debugElement.query(By.directive(NgbDatepicker)); + + spyOn(fixture.componentInstance, 'onSelect'); + + focusDay(); + fixture.detectChanges(); + + triggerKeyDown(getMonthContainer(datepicker), 32 /* space */); + fixture.detectChanges(); + + triggerKeyDown(getMonthContainer(datepicker), 32 /* space */); + fixture.detectChanges(); + expect(fixture.componentInstance.onSelect).toHaveBeenCalledTimes(2); + }); + + it('should insert an embedded view for footer when `footerTemplate` provided', () => { + const fixture = createTestComponent(` + My footer`); + + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('#myDatepickerFooter')).not.toBeNull(); + }); + + describe('ngModel', () => { + + it('should update model based on calendar clicks', async(() => { + const fixture = createTestComponent( + ``); + + const dates = getDates(fixture.nativeElement); + dates[0].click(); // 1 AUG 2016 + expect(fixture.componentInstance.model).toEqual({year: 2016, month: 8, day: 1}); + + dates[1].click(); + expect(fixture.componentInstance.model).toEqual({year: 2016, month: 8, day: 2}); + })); + + it('should not update model based on calendar clicks when disabled', async(() => { + const fixture = createTestComponent( + ` + `); + + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + + const dates = getDates(fixture.nativeElement); + dates[0].click(); // 1 AUG 2016 + expect(fixture.componentInstance.model).toBeFalsy(); + + dates[1].click(); + expect(fixture.componentInstance.model).toBeFalsy(); + }); + })); + + it('select calendar date based on model updates', async(() => { + const fixture = createTestComponent( + ``); + + fixture.componentInstance.model = {year: 2016, month: 8, day: 1}; + + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expect(getDay(fixture.nativeElement, 0)).toHaveCssClass('bg-primary'); + + fixture.componentInstance.model = {year: 2016, month: 8, day: 2}; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expect(getDay(fixture.nativeElement, 0)).not.toHaveCssClass('bg-primary'); + expect(getDay(fixture.nativeElement, 1)).toHaveCssClass('bg-primary'); + }); + })); + + it('should switch month when clicked on the date outside of current month', async(() => { + const fixture = createTestComponent( + ``); + fixture.detectChanges(); + fixture.whenStable().then(() => { + + let dates = getDates(fixture.nativeElement); + + dates[31].click(); // 1 SEP 2016 + expect(fixture.componentInstance.model).toEqual({year: 2016, month: 9, day: 1}); + + // month changes to SEP + fixture.detectChanges(); + expect(getDay(fixture.nativeElement, 0).innerText).toBe('29'); // 29 AUG 2016 + expect(getDay(fixture.nativeElement, 3)).toHaveCssClass('bg-primary'); // 1 SEP still selected + }); + })); + + it('should switch month on prev/next navigation click', async(() => { + const fixture = createTestComponent( + ``); + + let dates = getDates(fixture.nativeElement); + const navigation = getNavigationLinks(fixture.nativeElement); + + dates[0].click(); // 1 AUG 2016 + expect(fixture.componentInstance.model).toEqual({year: 2016, month: 8, day: 1}); + + // PREV + navigation[0].click(); + fixture.detectChanges(); + dates = getDates(fixture.nativeElement); + dates[4].click(); // 1 JUL 2016 + expect(fixture.componentInstance.model).toEqual({year: 2016, month: 7, day: 1}); + + // NEXT + navigation[1].click(); + fixture.detectChanges(); + dates = getDates(fixture.nativeElement); + dates[0].click(); // 1 AUG 2016 + expect(fixture.componentInstance.model).toEqual({year: 2016, month: 8, day: 1}); + })); + + it('should switch month using navigateTo({date})', async(() => { + const fixture = createTestComponent( + ` + `); + + const button = fixture.nativeElement.querySelector('button#btn'); + button.click(); + + fixture.detectChanges(); + expect(getMonthSelect(fixture.nativeElement).value).toBe('6'); + expect(getYearSelect(fixture.nativeElement).value).toBe('2015'); + + const dates = getDates(fixture.nativeElement); + dates[0].click(); // 1 JUN 2015 + expect(fixture.componentInstance.model).toEqual({year: 2015, month: 6, day: 1}); + })); + + it('should switch to current month using navigateTo() without arguments', () => { + const fixture = createTestComponent( + ` + `); + + const button = fixture.nativeElement.querySelector('button#btn'); + button.click(); + + fixture.detectChanges(); + const today = new Date(); + expect(getMonthSelect(fixture.nativeElement).value).toBe(`${today.getMonth() + 1}`); + expect(getYearSelect(fixture.nativeElement).value).toBe(`${today.getFullYear()}`); + }); + + it('should support disabling all dates and navigation via the disabled attribute', async(() => { + const fixture = createTestComponent( + ``); + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + for (let index = 0; index < 31; index++) { + expect(getDay(fixture.nativeElement, index)).toHaveCssClass('text-muted'); + } + + const links = getNavigationLinks(fixture.nativeElement); + expect(links[0].hasAttribute('disabled')).toBeTruthy(); + expect(links[1].hasAttribute('disabled')).toBeTruthy(); + expect(getYearSelect(fixture.nativeElement).disabled).toBeTruthy(); + expect(getMonthSelect(fixture.nativeElement).disabled).toBeTruthy(); + }); + })); + }); + + describe('aria attributes', () => { + const template = ` + `; + + it('should contains aria-label on the days', () => { + const fixture = createTestComponent(template); + + const datepicker = fixture.debugElement.query(By.directive(NgbDatepicker)); + const dates = getDates(fixture.nativeElement); + + dates.forEach(function(date) { + expect(date.getAttribute('aria-label')).toBeDefined('Missing aria-label attribute on a day'); + }); + }); + }); + + describe('keyboard navigation', () => { + + const template = ` + + `; + + it('should move focus with arrow keys', () => { + const fixture = createTestComponent(template); + + const datepicker = fixture.debugElement.query(By.directive(NgbDatepicker)); + + // focus in + focusDay(); + + triggerKeyDown(getMonthContainer(datepicker), 40 /* down arrow */); + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2016, 8, 8)); + expectSelectedDate(datepicker, null); + + triggerKeyDown(getMonthContainer(datepicker), 39 /* right arrow */); + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2016, 8, 9)); + expectSelectedDate(datepicker, null); + + triggerKeyDown(getMonthContainer(datepicker), 38 /* up arrow */); + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2016, 8, 2)); + expectSelectedDate(datepicker, null); + + triggerKeyDown(getMonthContainer(datepicker), 37 /* left arrow */); + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2016, 8, 1)); + expectSelectedDate(datepicker, null); + }); + + it('should select focused date with enter or space', () => { + const fixture = createTestComponent(template); + + const datepicker = fixture.debugElement.query(By.directive(NgbDatepicker)); + + focusDay(); + + triggerKeyDown(getMonthContainer(datepicker), 32 /* space */); + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2016, 8, 1)); + expectSelectedDate(datepicker, new NgbDate(2016, 8, 1)); + + triggerKeyDown(getMonthContainer(datepicker), 40 /* down arrow */); + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2016, 8, 8)); + expectSelectedDate(datepicker, new NgbDate(2016, 8, 1)); + + triggerKeyDown(getMonthContainer(datepicker), 13 /* enter */); + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2016, 8, 8)); + expectSelectedDate(datepicker, new NgbDate(2016, 8, 8)); + }); + + it('should select first and last dates of the view with home/end', () => { + const fixture = createTestComponent(template); + + const datepicker = fixture.debugElement.query(By.directive(NgbDatepicker)); + + focusDay(); + + triggerKeyDown(getMonthContainer(datepicker), 35 /* end */); + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2016, 9, 30)); + expectSelectedDate(datepicker, null); + + triggerKeyDown(getMonthContainer(datepicker), 36 /* home */); + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2016, 8, 1)); + expectSelectedDate(datepicker, null); + }); + + it('should select min and max dates with shift+home/end', () => { + const fixture = createTestComponent(template); + + const datepicker = fixture.debugElement.query(By.directive(NgbDatepicker)); + + focusDay(); + + triggerKeyDown(getMonthContainer(datepicker), 35 /* end */, true /* shift */); + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2020, 12, 31)); + expectSelectedDate(datepicker, null); + + triggerKeyDown(getMonthContainer(datepicker), 40 /* down arrow */); + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2020, 12, 31)); + expectSelectedDate(datepicker, null); + + triggerKeyDown(getMonthContainer(datepicker), 36 /* home */, true /* shift */); + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2010, 1, 1)); + expectSelectedDate(datepicker, null); + + triggerKeyDown(getMonthContainer(datepicker), 38 /* up arrow */); + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2010, 1, 1)); + expectSelectedDate(datepicker, null); + }); + + it('should navigate between months with pageUp/Down', () => { + const fixture = createTestComponent(template); + + let datepicker = fixture.debugElement.query(By.directive(NgbDatepicker)); + + focusDay(); + + triggerKeyDown(getMonthContainer(datepicker), 39 /* right arrow */); + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2016, 8, 2)); + expectSelectedDate(datepicker, null); + + triggerKeyDown(getMonthContainer(datepicker), 33 /* page up */); + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2016, 7, 1)); + expectSelectedDate(datepicker, null); + + triggerKeyDown(getMonthContainer(datepicker), 34 /* page down */); + fixture.detectChanges(); + expectFocusedDate(datepicker, new NgbDate(2016, 8, 1)); + expectSelectedDate(datepicker, null); + + triggerKeyDown(getMonthContainer(datepicker), 34 /* page down */); + fixture.detectChanges(); + + expectFocusedDate(datepicker, new NgbDate(2016, 9, 1)); + expectSelectedDate(datepicker, null); + + triggerKeyDown(getMonthContainer(datepicker), 34 /* page down */); + fixture.detectChanges(); + datepicker = fixture.debugElement.query(By.directive(NgbDatepicker)); + expectFocusedDate(datepicker, new NgbDate(2016, 10, 1)); + expectSelectedDate(datepicker, null); + }); + + it('should navigate between years with shift+pageUp/Down', () => { + const fixture = createTestComponent(template); + + const datepicker = fixture.debugElement.query(By.directive(NgbDatepicker)); + focusDay(); + + getMonthContainer(datepicker).triggerEventHandler('focus', {}); + fixture.detectChanges(); + + expectFocusedDate(datepicker, new NgbDate(2016, 8, 1)); + expectSelectedDate(datepicker, null); + + triggerKeyDown(getMonthContainer(datepicker), 33 /* page up */, true /* shift */); + fixture.detectChanges(); + + expectFocusedDate(datepicker, new NgbDate(2015, 1, 1), true); + expectSelectedDate(datepicker, null); + + triggerKeyDown(getMonthContainer(datepicker), 34 /* page down */, true /* shift */); + fixture.detectChanges(); + + expectFocusedDate(datepicker, new NgbDate(2016, 1, 1)); + expectSelectedDate(datepicker, null); + }); + + it(`shouldn't be focusable when disabled`, fakeAsync(() => { + const fixture = + createTestComponent(``); + tick(); + fixture.detectChanges(); + + const datepicker = fixture.debugElement.query(By.directive(NgbDatepicker)); + + const days = getFocusableDays(datepicker); + + expect(days.length).toEqual(0, 'A focusable day has been found'); + + })); + + }); + + describe('forms', () => { + + it('should work with template-driven form validation', async(() => { + const fixture = createTestComponent(` +
+ + +
+ `); + + const compiled = fixture.nativeElement; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expect(getDatepicker(compiled)).toHaveCssClass('ng-invalid'); + expect(getDatepicker(compiled)).not.toHaveCssClass('ng-valid'); + + fixture.componentInstance.model = {year: 2016, month: 8, day: 1}; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expect(getDatepicker(compiled)).toHaveCssClass('ng-valid'); + expect(getDatepicker(compiled)).not.toHaveCssClass('ng-invalid'); + }); + })); + + it('should work with model-driven form validation', async(() => { + const html = ` +
+ +
`; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + const dates = getDates(fixture.nativeElement); + + expect(getDatepicker(compiled)).toHaveCssClass('ng-invalid'); + expect(getDatepicker(compiled)).not.toHaveCssClass('ng-valid'); + + dates[0].click(); + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expect(getDatepicker(compiled)).toHaveCssClass('ng-valid'); + expect(getDatepicker(compiled)).not.toHaveCssClass('ng-invalid'); + }); + })); + + it('should be disabled with reactive forms', async(() => { + const html = `
+ + +
`; + + const fixture = createTestComponent(html); + fixture.detectChanges(); + const dates = getDates(fixture.nativeElement); + dates[0].click(); // 1 AUG 2016 + expect(fixture.componentInstance.disabledForm.controls['control'].value).toBeFalsy(); + for (let index = 0; index < 31; index++) { + expect(getDay(fixture.nativeElement, index)).toHaveCssClass('text-muted'); + } + expect(fixture.nativeElement.querySelector('ngb-datepicker').getAttribute('tabindex')).toBeFalsy(); + })); + + it('should not change again the value in the model on a change coming from the model (template-driven form)', + async(() => { + const html = `
+ + +
`; + + const fixture = createTestComponent(html); + fixture.detectChanges(); + + const value = new NgbDate(2018, 7, 28); + fixture.componentInstance.model = value; + + fixture.detectChanges(); + fixture.whenStable().then(() => { expect(fixture.componentInstance.model).toBe(value); }); + })); + + it('should not change again the value in the model on a change coming from the model (reactive form)', async(() => { + const html = `
+ + +
`; + + const fixture = createTestComponent(html); + fixture.detectChanges(); + + const formChangeSpy = jasmine.createSpy('form change'); + const form = fixture.componentInstance.form; + form.valueChanges.subscribe(formChangeSpy); + const controlValue = new NgbDate(2018, 7, 28); + form.setValue({control: controlValue}); + + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(formChangeSpy).toHaveBeenCalledTimes(1); + expect(form.value.control).toBe(controlValue); + }); + })); + + }); + + describe('Custom config', () => { + let config: NgbDatepickerConfig; + + beforeEach(() => { TestBed.configureTestingModule({imports: [NgbDatepickerModule]}); }); + + beforeEach(inject([NgbDatepickerConfig], (c: NgbDatepickerConfig) => { + config = c; + customizeConfig(config); + })); + + it('should initialize inputs with provided config', () => { + const fixture = TestBed.createComponent(NgbDatepicker); + + const datepicker = fixture.componentInstance; + expectSameValues(datepicker, config); + }); + }); + + describe('Custom config as provider', () => { + const config = new NgbDatepickerConfig(); + customizeConfig(config); + + beforeEach(() => { + TestBed.configureTestingModule( + {imports: [NgbDatepickerModule], providers: [{provide: NgbDatepickerConfig, useValue: config}]}); + }); + + it('should initialize inputs with provided config as provider', () => { + const fixture = TestBed.createComponent(NgbDatepicker); + + const datepicker = fixture.componentInstance; + expectSameValues(datepicker, config); + }); + }); +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + date = {year: 2016, month: 8}; + displayMonths = 1; + navigation = 'select'; + minDate: NgbDateStruct = {year: 2010, month: 1, day: 1}; + maxDate: NgbDateStruct = {year: 2020, month: 12, day: 31}; + form = new FormGroup({control: new FormControl('', Validators.required)}); + disabledForm = new FormGroup({control: new FormControl({value: null, disabled: true})}); + model; + showWeekdays = true; + dayTemplateData = () => '!'; + markDisabled = (date: NgbDateStruct) => { return NgbDate.from(date).equals(new NgbDate(2016, 8, 22)); }; + onNavigate = () => {}; + onSelect = () => {}; + getDate = () => ({year: 2016, month: 8}); + onPreventableNavigate = (event: NgbDatepickerNavigateEvent) => event.preventDefault(); +} diff --git a/src/datepicker/datepicker.ts b/src/datepicker/datepicker.ts new file mode 100644 index 0000000..a17e658 --- /dev/null +++ b/src/datepicker/datepicker.ts @@ -0,0 +1,395 @@ +import {fromEvent, merge, Subject} from 'rxjs'; +import {filter, take, takeUntil} from 'rxjs/operators'; +import { + AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + forwardRef, + Input, + NgZone, + OnChanges, + OnDestroy, + OnInit, + Output, + SimpleChanges, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; +import {NgbCalendar} from './ngb-calendar'; +import {NgbDate} from './ngb-date'; +import {NgbDatepickerService} from './datepicker-service'; +import {NgbDatepickerKeyMapService} from './datepicker-keymap-service'; +import {DatepickerViewModel, NavigationEvent} from './datepicker-view-model'; +import {DayTemplateContext} from './datepicker-day-template-context'; +import {NgbDatepickerConfig} from './datepicker-config'; +import {NgbDateAdapter} from './adapters/ngb-date-adapter'; +import {NgbDateStruct} from './ngb-date-struct'; +import {NgbDatepickerI18n} from './datepicker-i18n'; +import {isChangedDate, isChangedMonth} from './datepicker-tools'; +import {hasClassName} from '../util/util'; + +const NGB_DATEPICKER_VALUE_ACCESSOR = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NgbDatepicker), + multi: true +}; + +/** + * An event emitted right before the navigation happens and the month displayed by the datepicker changes. + */ +export interface NgbDatepickerNavigateEvent { + /** + * The currently displayed month. + */ + current: {year: number, month: number}; + + /** + * The month we're navigating to. + */ + next: {year: number, month: number}; + + /** + * Calling this function will prevent navigation from happening. + * + * @since 4.1.0 + */ + preventDefault: () => void; +} + +/** + * A highly configurable component that helps you with selecting calendar dates. + * + * `NgbDatepicker` is meant to be displayed inline on a page or put inside a popup. + */ +@Component({ + exportAs: 'ngbDatepicker', + selector: 'ngb-datepicker', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + styleUrls: ['./datepicker.scss'], + template: ` + +
+
+
+ +
+ + +
+ +
+ +
+
+ {{ i18n.getMonthFullName(month.number, month.year) }} {{ i18n.getYearNumerals(month.year) }} +
+ + +
+
+
+ + + `, + providers: [NGB_DATEPICKER_VALUE_ACCESSOR, NgbDatepickerService, NgbDatepickerKeyMapService] +}) +export class NgbDatepicker implements OnDestroy, + OnChanges, OnInit, AfterViewInit, ControlValueAccessor { + model: DatepickerViewModel; + + @ViewChild('months', {static: true}) private _monthsEl: ElementRef; + private _controlValue: NgbDate; + private _destroyed$ = new Subject(); + + /** + * The reference to a custom template for the day. + * + * Allows to completely override the way a day 'cell' in the calendar is displayed. + * + * See [`DayTemplateContext`](#/components/datepicker/api#DayTemplateContext) for the data you get inside. + */ + @Input() dayTemplate: TemplateRef; + + /** + * The callback to pass any arbitrary data to the template cell via the + * [`DayTemplateContext`](#/components/datepicker/api#DayTemplateContext)'s `data` parameter. + * + * `current` is the month that is currently displayed by the datepicker. + * + * @since 3.3.0 + */ + @Input() dayTemplateData: (date: NgbDate, current: {year: number, month: number}) => any; + + /** + * The number of months to display. + */ + @Input() displayMonths: number; + + /** + * The first day of the week. + * + * With default calendar we use ISO 8601: 'weekday' is 1=Mon ... 7=Sun. + */ + @Input() firstDayOfWeek: number; + + /** + * The reference to the custom template for the datepicker footer. + * + * @since 3.3.0 + */ + @Input() footerTemplate: TemplateRef; + + /** + * The callback to mark some dates as disabled. + * + * It is called for each new date when navigating to a different month. + * + * `current` is the month that is currently displayed by the datepicker. + */ + @Input() markDisabled: (date: NgbDate, current: {year: number, month: number}) => boolean; + + /** + * The latest date that can be displayed or selected. + * + * If not provided, 'year' select box will display 10 years after the current month. + */ + @Input() maxDate: NgbDateStruct; + + /** + * The earliest date that can be displayed or selected. + * + * If not provided, 'year' select box will display 10 years before the current month. + */ + @Input() minDate: NgbDateStruct; + + /** + * Navigation type. + * + * * `"select"` - select boxes for month and navigation arrows + * * `"arrows"` - only navigation arrows + * * `"none"` - no navigation visible at all + */ + @Input() navigation: 'select' | 'arrows' | 'none'; + + /** + * The way of displaying days that don't belong to the current month. + * + * * `"visible"` - days are visible + * * `"hidden"` - days are hidden, white space preserved + * * `"collapsed"` - days are collapsed, so the datepicker height might change between months + * + * For the 2+ months view, days in between months are never shown. + */ + @Input() outsideDays: 'visible' | 'collapsed' | 'hidden'; + + /** + * If `true`, weekdays will be displayed. + */ + @Input() showWeekdays: boolean; + + /** + * If `true`, week numbers will be displayed. + */ + @Input() showWeekNumbers: boolean; + + /** + * The date to open calendar with. + * + * With the default calendar we use ISO 8601: 'month' is 1=Jan ... 12=Dec. + * If nothing or invalid date is provided, calendar will open with current month. + * + * You could use `navigateTo(date)` method as an alternative. + */ + @Input() startDate: {year: number, month: number, day?: number}; + + /** + * An event emitted right before the navigation happens and displayed month changes. + * + * See [`NgbDatepickerNavigateEvent`](#/components/datepicker/api#NgbDatepickerNavigateEvent) for the payload info. + */ + @Output() navigate = new EventEmitter(); + + /** + * An event emitted when user selects a date using keyboard or mouse. + * + * The payload of the event is currently selected `NgbDate`. + */ + @Output() select = new EventEmitter(); + + onChange = (_: any) => {}; + onTouched = () => {}; + + constructor( + private _keyMapService: NgbDatepickerKeyMapService, public _service: NgbDatepickerService, + private _calendar: NgbCalendar, public i18n: NgbDatepickerI18n, config: NgbDatepickerConfig, + private _cd: ChangeDetectorRef, private _elementRef: ElementRef, + private _ngbDateAdapter: NgbDateAdapter, private _ngZone: NgZone) { + ['dayTemplate', 'dayTemplateData', 'displayMonths', 'firstDayOfWeek', 'footerTemplate', 'markDisabled', 'minDate', + 'maxDate', 'navigation', 'outsideDays', 'showWeekdays', 'showWeekNumbers', 'startDate'] + .forEach(input => this[input] = config[input]); + + _service.select$.pipe(takeUntil(this._destroyed$)).subscribe(date => { this.select.emit(date); }); + + _service.model$.pipe(takeUntil(this._destroyed$)).subscribe(model => { + const newDate = model.firstDate; + const oldDate = this.model ? this.model.firstDate : null; + + let navigationPrevented = false; + // emitting navigation event if the first month changes + if (!newDate.equals(oldDate)) { + this.navigate.emit({ + current: oldDate ? {year: oldDate.year, month: oldDate.month} : null, + next: {year: newDate.year, month: newDate.month}, + preventDefault: () => navigationPrevented = true + }); + + // can't prevent the very first navigation + if (navigationPrevented && oldDate !== null) { + this._service.open(oldDate); + return; + } + } + + const newSelectedDate = model.selectedDate; + const newFocusedDate = model.focusDate; + const oldFocusedDate = this.model ? this.model.focusDate : null; + + this.model = model; + + // handling selection change + if (isChangedDate(newSelectedDate, this._controlValue)) { + this._controlValue = newSelectedDate; + this.onTouched(); + this.onChange(this._ngbDateAdapter.toModel(newSelectedDate)); + } + + // handling focus change + if (isChangedDate(newFocusedDate, oldFocusedDate) && oldFocusedDate && model.focusVisible) { + this.focus(); + } + + _cd.markForCheck(); + }); + } + + focus() { + this._ngZone.onStable.asObservable().pipe(take(1)).subscribe(() => { + const elementToFocus = + this._elementRef.nativeElement.querySelector('div.ngb-dp-day[tabindex="0"]'); + if (elementToFocus) { + elementToFocus.focus(); + } + }); + } + + /** + * Navigates to the provided date. + * + * With the default calendar we use ISO 8601: 'month' is 1=Jan ... 12=Dec. + * If nothing or invalid date provided calendar will open current month. + * + * Use the `[startDate]` input as an alternative. + */ + navigateTo(date?: {year: number, month: number, day?: number}) { + this._service.open(NgbDate.from(date ? date.day ? date as NgbDateStruct : {...date, day: 1} : null)); + } + + ngAfterViewInit() { + this._ngZone.runOutsideAngular(() => { + const focusIns$ = fromEvent(this._monthsEl.nativeElement, 'focusin'); + const focusOuts$ = fromEvent(this._monthsEl.nativeElement, 'focusout'); + + // we're changing 'focusVisible' only when entering or leaving months view + // and ignoring all focus events where both 'target' and 'related' target are day cells + merge(focusIns$, focusOuts$) + .pipe( + filter( + ({target, relatedTarget}) => + !(hasClassName(target, 'ngb-dp-day') && hasClassName(relatedTarget, 'ngb-dp-day'))), + takeUntil(this._destroyed$)) + .subscribe(({type}) => this._ngZone.run(() => this._service.focusVisible = type === 'focusin')); + }); + } + + ngOnDestroy() { this._destroyed$.next(); } + + ngOnInit() { + if (this.model === undefined) { + ['dayTemplateData', 'displayMonths', 'markDisabled', 'firstDayOfWeek', 'navigation', 'minDate', 'maxDate', + 'outsideDays'] + .forEach(input => this._service[input] = this[input]); + this.navigateTo(this.startDate); + } + } + + ngOnChanges(changes: SimpleChanges) { + ['dayTemplateData', 'displayMonths', 'markDisabled', 'firstDayOfWeek', 'navigation', 'minDate', 'maxDate', + 'outsideDays'] + .filter(input => input in changes) + .forEach(input => this._service[input] = this[input]); + + if ('startDate' in changes) { + const {currentValue, previousValue} = changes.startDate; + if (isChangedMonth(previousValue, currentValue)) { + this.navigateTo(this.startDate); + } + } + } + + onDateSelect(date: NgbDate) { + this._service.focus(date); + this._service.select(date, {emitEvent: true}); + } + + onKeyDown(event: KeyboardEvent) { this._keyMapService.processKey(event); } + + onNavigateDateSelect(date: NgbDate) { this._service.open(date); } + + onNavigateEvent(event: NavigationEvent) { + switch (event) { + case NavigationEvent.PREV: + this._service.open(this._calendar.getPrev(this.model.firstDate, 'm', 1)); + break; + case NavigationEvent.NEXT: + this._service.open(this._calendar.getNext(this.model.firstDate, 'm', 1)); + break; + } + } + + registerOnChange(fn: (value: any) => any): void { this.onChange = fn; } + + registerOnTouched(fn: () => any): void { this.onTouched = fn; } + + setDisabledState(isDisabled: boolean) { this._service.disabled = isDisabled; } + + writeValue(value) { + this._controlValue = NgbDate.from(this._ngbDateAdapter.fromModel(value)); + this._service.select(this._controlValue); + } +} diff --git a/src/datepicker/hebrew/datepicker-i18n-hebrew.spec.ts b/src/datepicker/hebrew/datepicker-i18n-hebrew.spec.ts new file mode 100644 index 0000000..e8c6bf6 --- /dev/null +++ b/src/datepicker/hebrew/datepicker-i18n-hebrew.spec.ts @@ -0,0 +1,76 @@ +import {TestBed} from '@angular/core/testing'; +import {NgbDate} from '../ngb-date'; +import {NgbDatepickerI18nHebrew} from './datepicker-i18n-hebrew'; + +describe('datepicker-i18n-hebrew', () => { + + let i18n: NgbDatepickerI18nHebrew; + + beforeEach(() => { + TestBed.configureTestingModule({providers: [NgbDatepickerI18nHebrew]}); + i18n = TestBed.get(NgbDatepickerI18nHebrew); + }); + + it('should return abbreviated month name', () => { + expect(i18n.getMonthShortName(0, 5778)).toBe(undefined); + expect(i18n.getMonthShortName(1, 5778)).toBe('תשרי'); + expect(i18n.getMonthShortName(6, 5778)).toBe('אדר'); + expect(i18n.getMonthShortName(7, 5778)).toBe('ניסן'); + expect(i18n.getMonthShortName(12, 5778)).toBe('אלול'); + expect(i18n.getMonthShortName(13, 5778)).toBe(undefined); + }); + + it('should return abbreviated month name (leap year)', () => { + expect(i18n.getMonthShortName(0, 5779)).toBe(undefined); + expect(i18n.getMonthShortName(1, 5779)).toBe('תשרי'); + expect(i18n.getMonthShortName(6, 5779)).toBe('אדר א׳'); + expect(i18n.getMonthShortName(7, 5779)).toBe('אדר ב׳'); + expect(i18n.getMonthShortName(12, 5779)).toBe('אב'); + expect(i18n.getMonthShortName(13, 5779)).toBe('אלול'); + expect(i18n.getMonthShortName(14, 5779)).toBe(undefined); + }); + + it('should return wide month name', () => { + expect(i18n.getMonthFullName(0, 5778)).toBe(undefined); + expect(i18n.getMonthFullName(1, 5778)).toBe('תשרי'); + expect(i18n.getMonthFullName(6, 5778)).toBe('אדר'); + expect(i18n.getMonthFullName(7, 5778)).toBe('ניסן'); + expect(i18n.getMonthFullName(12, 5778)).toBe('אלול'); + expect(i18n.getMonthFullName(13, 5778)).toBe(undefined); + }); + + it('should return wide month name (leap year)', () => { + expect(i18n.getMonthFullName(0, 5779)).toBe(undefined); + expect(i18n.getMonthFullName(1, 5779)).toBe('תשרי'); + expect(i18n.getMonthFullName(6, 5779)).toBe('אדר א׳'); + expect(i18n.getMonthFullName(7, 5779)).toBe('אדר ב׳'); + expect(i18n.getMonthFullName(12, 5779)).toBe('אב'); + expect(i18n.getMonthFullName(13, 5779)).toBe('אלול'); + expect(i18n.getMonthFullName(14, 5779)).toBe(undefined); + }); + + it('should return weekday name', () => { + expect(i18n.getWeekdayShortName(0)).toBe(undefined); + expect(i18n.getWeekdayShortName(1)).toBe('שני'); + expect(i18n.getWeekdayShortName(7)).toBe('ראשון'); + expect(i18n.getWeekdayShortName(8)).toBe(undefined); + }); + + it('should generate aria label for a date', + () => { expect(i18n.getDayAriaLabel(new NgbDate(5778, 10, 8))).toBe('ח׳ תמוז תשע״ח'); }); + + it('should generate week number numerals', () => { + expect(i18n.getWeekNumerals(1)).toBe('א׳'); + expect(i18n.getWeekNumerals(50)).toBe('נ׳'); + }); + + it('should generate day numerals', () => { + expect(i18n.getDayNumerals(new NgbDate(5778, 10, 1))).toBe('א׳'); + expect(i18n.getDayNumerals(new NgbDate(5778, 10, 29))).toBe('כ״ט'); + }); + + it('should generate year numerals', () => { + expect(i18n.getYearNumerals(0)).toBe(''); + expect(i18n.getYearNumerals(5778)).toBe('תשע״ח'); + }); +}); diff --git a/src/datepicker/hebrew/datepicker-i18n-hebrew.ts b/src/datepicker/hebrew/datepicker-i18n-hebrew.ts new file mode 100644 index 0000000..7267fe7 --- /dev/null +++ b/src/datepicker/hebrew/datepicker-i18n-hebrew.ts @@ -0,0 +1,34 @@ +import {NgbDatepickerI18n} from '../datepicker-i18n'; +import {NgbDateStruct} from '../../index'; +import {hebrewNumerals, isHebrewLeapYear} from './hebrew'; +import {Injectable} from '@angular/core'; + + +const WEEKDAYS = ['שני', 'שלישי', 'רביעי', 'חמישי', 'שישי', 'שבת', 'ראשון']; +const MONTHS = ['תשרי', 'חשון', 'כסלו', 'טבת', 'שבט', 'אדר', 'ניסן', 'אייר', 'סיון', 'תמוז', 'אב', 'אלול']; +const MONTHS_LEAP = + ['תשרי', 'חשון', 'כסלו', 'טבת', 'שבט', 'אדר א׳', 'אדר ב׳', 'ניסן', 'אייר', 'סיון', 'תמוז', 'אב', 'אלול']; + +/** + * @since 3.2.0 + */ +@Injectable() +export class NgbDatepickerI18nHebrew extends NgbDatepickerI18n { + getMonthShortName(month: number, year?: number): string { return this.getMonthFullName(month, year); } + + getMonthFullName(month: number, year?: number): string { + return isHebrewLeapYear(year) ? MONTHS_LEAP[month - 1] : MONTHS[month - 1]; + } + + getWeekdayShortName(weekday: number): string { return WEEKDAYS[weekday - 1]; } + + getDayAriaLabel(date: NgbDateStruct): string { + return `${hebrewNumerals(date.day)} ${this.getMonthFullName(date.month, date.year)} ${hebrewNumerals(date.year)}`; + } + + getDayNumerals(date: NgbDateStruct): string { return hebrewNumerals(date.day); } + + getWeekNumerals(weekNumber: number): string { return hebrewNumerals(weekNumber); } + + getYearNumerals(year: number): string { return hebrewNumerals(year); } +} diff --git a/src/datepicker/hebrew/hebrew.spec.ts b/src/datepicker/hebrew/hebrew.spec.ts new file mode 100644 index 0000000..e793423 --- /dev/null +++ b/src/datepicker/hebrew/hebrew.spec.ts @@ -0,0 +1,64 @@ +import {NgbDate} from '../ngb-date'; +import {fromGregorian, hebrewNumerals, toGregorian} from './hebrew'; + +const DATE_TABLE = [ + [5760, 3, 16, 1999, 11, 25], [5760, 7, 27, 2000, 4, 3], [5760, 12, 14, 2000, 8, 15], [5761, 1, 30, 2000, 10, 29], + [5761, 8, 1, 2001, 4, 24], [5761, 10, 17, 2001, 7, 8], [5762, 2, 29, 2001, 11, 15], [5762, 7, 2, 2002, 3, 15], + [5762, 9, 10, 2002, 5, 21], [5763, 5, 22, 2003, 1, 25], [5763, 7, 28, 2003, 4, 1], [5763, 13, 29, 2003, 9, 26], + [5764, 11, 14, 2004, 8, 1], [5764, 5, 13, 2004, 2, 5], [5764, 1, 1, 2003, 9, 27], [5765, 6, 3, 2005, 2, 12], + [5765, 3, 19, 2004, 12, 2], [5765, 12, 9, 2005, 8, 14], [5766, 4, 11, 2006, 1, 11], [5766, 5, 2, 2006, 1, 31], + [5766, 10, 22, 2006, 7, 18], [5767, 6, 27, 2007, 3, 17], [5767, 8, 4, 2007, 4, 22], [5767, 2, 30, 2006, 11, 21], + [5768, 13, 28, 2008, 9, 28], [5768, 6, 23, 2008, 2, 29], [5768, 3, 17, 2007, 11, 27], [5769, 2, 27, 2008, 11, 25], + [5769, 10, 5, 2009, 6, 27], [5769, 9, 9, 2009, 6, 1], [5770, 1, 18, 2009, 10, 6], [5770, 12, 2, 2010, 8, 12], + [5770, 7, 30, 2010, 4, 14], [5771, 7, 15, 2011, 3, 21], [5771, 6, 2, 2011, 2, 6], [5771, 12, 1, 2011, 8, 1], + [5772, 3, 30, 2011, 12, 26], [5772, 9, 26, 2012, 6, 16], [5772, 12, 29, 2012, 9, 16], [5773, 11, 1, 2013, 7, 8], + [5773, 4, 20, 2013, 1, 2], [5773, 2, 11, 2012, 10, 27], [5774, 1, 21, 2013, 9, 25], [5774, 11, 2, 2014, 6, 30], + [5774, 6, 30, 2014, 3, 2], [5775, 10, 27, 2015, 7, 14], [5775, 4, 2, 2014, 12, 24], [5775, 5, 23, 2015, 2, 12], + [5776, 12, 20, 2016, 8, 24], [5776, 10, 10, 2016, 6, 16], [5776, 5, 4, 2016, 1, 14], [5777, 3, 17, 2016, 12, 17], + [5777, 8, 29, 2017, 5, 25], [5777, 10, 7, 2017, 7, 1], [5778, 12, 11, 2018, 8, 22], [5778, 10, 19, 2018, 7, 2], + [5778, 6, 25, 2018, 3, 12], [5779, 2, 3, 2018, 10, 12], [5779, 13, 15, 2019, 9, 15], [5779, 8, 30, 2019, 5, 5], + [5780, 5, 14, 2020, 2, 9], [5780, 11, 12, 2020, 8, 2], [5780, 3, 30, 2019, 12, 28], [5781, 4, 20, 2021, 1, 4], + [5781, 9, 19, 2021, 5, 30], [5781, 10, 29, 2021, 7, 9], [5782, 12, 24, 2022, 8, 21], [5782, 1, 2, 2021, 9, 8], + [5782, 7, 26, 2022, 3, 29], [5783, 2, 16, 2022, 11, 10], [5783, 10, 19, 2023, 7, 8], [5783, 5, 5, 2023, 1, 27], + [5784, 7, 1, 2024, 3, 11], [5784, 13, 29, 2024, 10, 2], [5784, 3, 14, 2023, 11, 27], [5785, 3, 30, 2024, 12, 31], + [5785, 7, 4, 2025, 4, 2], [5785, 11, 11, 2025, 8, 5], [5786, 10, 1, 2026, 6, 16], [5786, 5, 28, 2026, 2, 15], + [5786, 2, 17, 2025, 11, 8], [5787, 10, 18, 2027, 6, 23], [5787, 6, 29, 2027, 3, 8], [5787, 5, 3, 2027, 1, 11], + [5788, 1, 30, 2027, 10, 31], [5788, 7, 15, 2028, 4, 11], [5788, 9, 2, 2028, 5, 27], [5789, 12, 16, 2029, 8, 27], + [5789, 2, 3, 2028, 10, 23], [5789, 8, 17, 2029, 5, 2], [5790, 3, 6, 2029, 11, 13], [5790, 10, 27, 2030, 6, 28], + [5790, 12, 15, 2030, 8, 14] +]; + +describe('hebrew', () => { + + describe('toGregorian', () => { + DATE_TABLE.forEach(element => { + let hDate = new NgbDate(element[0], element[1], element[2]); + let gDate = toGregorian(hDate); + it('should convert correctly from Hebrew to Gregorian', () => { + expect( + new NgbDate(gDate.getFullYear(), gDate.getMonth() + 1, gDate.getDate()) + .equals(new NgbDate(element[3], element[4], element[5]))) + .toBeTruthy(); + }); + }); + }); + + describe('fromGregorian', () => { + DATE_TABLE.forEach(element => { + const gDate = new Date(element[3], element[4] - 1, element[5]); + let hDate = fromGregorian(gDate); + it('should convert correctly from Gregorian to Hebrew', + () => { expect(new NgbDate(element[0], element[1], element[2]).equals(hDate)).toBeTruthy(); }); + }); + }); + + describe('hebrewNumerals', () => { + it('should return Hebrew numerals', () => { + expect(hebrewNumerals(3)).toEqual('ג׳'); + expect(hebrewNumerals(11)).toEqual('י״א'); + expect(hebrewNumerals(15)).toEqual('ט״ו'); + expect(hebrewNumerals(19)).toEqual('י״ט'); + expect(hebrewNumerals(5777)).toEqual('תשע״ז'); + }); + }); +}); diff --git a/src/datepicker/hebrew/hebrew.ts b/src/datepicker/hebrew/hebrew.ts new file mode 100644 index 0000000..05b37e0 --- /dev/null +++ b/src/datepicker/hebrew/hebrew.ts @@ -0,0 +1,295 @@ +import {NgbDate} from '../ngb-date'; +import {NgbDateStruct} from '../ngb-date-struct'; + +const PARTS_PER_HOUR = 1080; +const PARTS_PER_DAY = 24 * PARTS_PER_HOUR; +const PARTS_FRACTIONAL_MONTH = 12 * PARTS_PER_HOUR + 793; +const PARTS_PER_MONTH = 29 * PARTS_PER_DAY + PARTS_FRACTIONAL_MONTH; +const BAHARAD = 11 * PARTS_PER_HOUR + 204; +const HEBREW_DAY_ON_JAN_1_1970 = 2092591; +const GREGORIAN_EPOCH = 1721425.5; + +function isGregorianLeapYear(year: number): boolean { + return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0; +} + +function numberOfFirstDayInYear(year: number): number { + let monthsBeforeYear = Math.floor((235 * year - 234) / 19); + let fractionalMonthsBeforeYear = monthsBeforeYear * PARTS_FRACTIONAL_MONTH + BAHARAD; + let dayNumber = monthsBeforeYear * 29 + Math.floor(fractionalMonthsBeforeYear / PARTS_PER_DAY); + let timeOfDay = fractionalMonthsBeforeYear % PARTS_PER_DAY; + + let dayOfWeek = dayNumber % 7; // 0 == Monday + + if (dayOfWeek === 2 || dayOfWeek === 4 || dayOfWeek === 6) { + dayNumber++; + dayOfWeek = dayNumber % 7; + } + if (dayOfWeek === 1 && timeOfDay > 15 * PARTS_PER_HOUR + 204 && !isHebrewLeapYear(year)) { + dayNumber += 2; + } else if (dayOfWeek === 0 && timeOfDay > 21 * PARTS_PER_HOUR + 589 && isHebrewLeapYear(year - 1)) { + dayNumber++; + } + return dayNumber; +} + +function getDaysInGregorianMonth(month: number, year: number): number { + let days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + if (isGregorianLeapYear(year)) { + days[1]++; + } + return days[month - 1]; +} + +function getHebrewMonths(year: number): number { + return isHebrewLeapYear(year) ? 13 : 12; +} + +/** + * Returns the number of days in a specific Hebrew year. + * `year` is any Hebrew year. + */ +function getDaysInHebrewYear(year: number): number { + return numberOfFirstDayInYear(year + 1) - numberOfFirstDayInYear(year); +} + +export function isHebrewLeapYear(year: number): boolean { + let b = (year * 12 + 17) % 19; + return b >= ((b < 0) ? -7 : 12); +} + +/** + * Returns the number of days in a specific Hebrew month. + * `month` is 1 for Nisan, 2 for Iyar etc. Note: Hebrew leap year contains 13 months. + * `year` is any Hebrew year. + */ +export function getDaysInHebrewMonth(month: number, year: number): number { + let yearLength = numberOfFirstDayInYear(year + 1) - numberOfFirstDayInYear(year); + let yearType = (yearLength <= 380 ? yearLength : (yearLength - 30)) - 353; + let leapYear = isHebrewLeapYear(year); + let daysInMonth = leapYear ? [30, 29, 29, 29, 30, 30, 29, 30, 29, 30, 29, 30, 29] : + [30, 29, 29, 29, 30, 29, 30, 29, 30, 29, 30, 29]; + if (yearType > 0) { + daysInMonth[2]++; // Kislev gets an extra day in normal or complete years. + } + if (yearType > 1) { + daysInMonth[1]++; // Heshvan gets an extra day in complete years only. + } + return daysInMonth[month - 1]; +} + +export function getDayNumberInHebrewYear(date: NgbDate): number { + let numberOfDay = 0; + for (let i = 1; i < date.month; i++) { + numberOfDay += getDaysInHebrewMonth(i, date.year); + } + return numberOfDay + date.day; +} + +export function setHebrewMonth(date: NgbDate, val: number): NgbDate { + let after = val >= 0; + if (!after) { + val = -val; + } + while (val > 0) { + if (after) { + if (val > getHebrewMonths(date.year) - date.month) { + val -= getHebrewMonths(date.year) - date.month + 1; + date.year++; + date.month = 1; + } else { + date.month += val; + val = 0; + } + } else { + if (val >= date.month) { + date.year--; + val -= date.month; + date.month = getHebrewMonths(date.year); + } else { + date.month -= val; + val = 0; + } + } + } + return date; +} + +export function setHebrewDay(date: NgbDate, val: number): NgbDate { + let after = val >= 0; + if (!after) { + val = -val; + } + while (val > 0) { + if (after) { + if (val > getDaysInHebrewYear(date.year) - getDayNumberInHebrewYear(date)) { + val -= getDaysInHebrewYear(date.year) - getDayNumberInHebrewYear(date) + 1; + date.year++; + date.month = 1; + date.day = 1; + } else if (val > getDaysInHebrewMonth(date.month, date.year) - date.day) { + val -= getDaysInHebrewMonth(date.month, date.year) - date.day + 1; + date.month++; + date.day = 1; + } else { + date.day += val; + val = 0; + } + } else { + if (val >= date.day) { + val -= date.day; + date.month--; + if (date.month === 0) { + date.year--; + date.month = getHebrewMonths(date.year); + } + date.day = getDaysInHebrewMonth(date.month, date.year); + } else { + date.day -= val; + val = 0; + } + } + } + return date; +} + +/** + * Returns the equivalent Hebrew date value for a give input Gregorian date. + * `gdate` is a JS Date to be converted to Hebrew date. + */ +export function fromGregorian(gdate: Date): NgbDate { + const date = new Date(gdate); + const gYear = date.getFullYear(), gMonth = date.getMonth(), gDay = date.getDate(); + let julianDay = GREGORIAN_EPOCH - 1 + 365 * (gYear - 1) + Math.floor((gYear - 1) / 4) - + Math.floor((gYear - 1) / 100) + Math.floor((gYear - 1) / 400) + + Math.floor((367 * (gMonth + 1) - 362) / 12 + (gMonth + 1 <= 2 ? 0 : isGregorianLeapYear(gYear) ? -1 : -2) + gDay); + julianDay = Math.floor(julianDay + 0.5); + let daysSinceHebEpoch = julianDay - 347997; + let monthsSinceHebEpoch = Math.floor(daysSinceHebEpoch * PARTS_PER_DAY / PARTS_PER_MONTH); + let hYear = Math.floor((monthsSinceHebEpoch * 19 + 234) / 235) + 1; + let firstDayOfThisYear = numberOfFirstDayInYear(hYear); + let dayOfYear = daysSinceHebEpoch - firstDayOfThisYear; + while (dayOfYear < 1) { + hYear--; + firstDayOfThisYear = numberOfFirstDayInYear(hYear); + dayOfYear = daysSinceHebEpoch - firstDayOfThisYear; + } + let hMonth = 1; + let hDay = dayOfYear; + while (hDay > getDaysInHebrewMonth(hMonth, hYear)) { + hDay -= getDaysInHebrewMonth(hMonth, hYear); + hMonth++; + } + return new NgbDate(hYear, hMonth, hDay); +} + +/** + * Returns the equivalent JS date value for a given Hebrew date. + * `hebrewDate` is an Hebrew date to be converted to Gregorian. + */ +export function toGregorian(hebrewDate: NgbDateStruct | NgbDate): Date { + const hYear = hebrewDate.year; + const hMonth = hebrewDate.month; + const hDay = hebrewDate.day; + let days = numberOfFirstDayInYear(hYear); + for (let i = 1; i < hMonth; i++) { + days += getDaysInHebrewMonth(i, hYear); + } + days += hDay; + let diffDays = days - HEBREW_DAY_ON_JAN_1_1970; + let after = diffDays >= 0; + if (!after) { + diffDays = -diffDays; + } + let gYear = 1970; + let gMonth = 1; + let gDay = 1; + while (diffDays > 0) { + if (after) { + if (diffDays >= (isGregorianLeapYear(gYear) ? 366 : 365)) { + diffDays -= isGregorianLeapYear(gYear) ? 366 : 365; + gYear++; + } else if (diffDays >= getDaysInGregorianMonth(gMonth, gYear)) { + diffDays -= getDaysInGregorianMonth(gMonth, gYear); + gMonth++; + } else { + gDay += diffDays; + diffDays = 0; + } + } else { + if (diffDays >= (isGregorianLeapYear(gYear - 1) ? 366 : 365)) { + diffDays -= isGregorianLeapYear(gYear - 1) ? 366 : 365; + gYear--; + } else { + if (gMonth > 1) { + gMonth--; + } else { + gMonth = 12; + gYear--; + } + if (diffDays >= getDaysInGregorianMonth(gMonth, gYear)) { + diffDays -= getDaysInGregorianMonth(gMonth, gYear); + } else { + gDay = getDaysInGregorianMonth(gMonth, gYear) - diffDays + 1; + diffDays = 0; + } + } + } + } + return new Date(gYear, gMonth - 1, gDay); +} + +export function hebrewNumerals(numerals: number): string { + if (!numerals) { + return ''; + } + const hArray0_9 = ['', '\u05d0', '\u05d1', '\u05d2', '\u05d3', '\u05d4', '\u05d5', '\u05d6', '\u05d7', '\u05d8']; + const hArray10_19 = [ + '\u05d9', '\u05d9\u05d0', '\u05d9\u05d1', '\u05d9\u05d2', '\u05d9\u05d3', '\u05d8\u05d5', '\u05d8\u05d6', + '\u05d9\u05d6', '\u05d9\u05d7', '\u05d9\u05d8' + ]; + const hArray20_90 = ['', '', '\u05db', '\u05dc', '\u05de', '\u05e0', '\u05e1', '\u05e2', '\u05e4', '\u05e6']; + const hArray100_900 = [ + '', '\u05e7', '\u05e8', '\u05e9', '\u05ea', '\u05ea\u05e7', '\u05ea\u05e8', '\u05ea\u05e9', '\u05ea\u05ea', + '\u05ea\u05ea\u05e7' + ]; + const hArray1000_9000 = [ + '', '\u05d0', '\u05d1', '\u05d1\u05d0', '\u05d1\u05d1', '\u05d4', '\u05d4\u05d0', '\u05d4\u05d1', + '\u05d4\u05d1\u05d0', '\u05d4\u05d1\u05d1' + ]; + const geresh = '\u05f3', gershaim = '\u05f4'; + let mem = 0; + let result = []; + let step = 0; + while (numerals > 0) { + let m = numerals % 10; + if (step === 0) { + mem = m; + } else if (step === 1) { + if (m !== 1) { + result.unshift(hArray20_90[m], hArray0_9[mem]); + } else { + result.unshift(hArray10_19[mem]); + } + } else if (step === 2) { + result.unshift(hArray100_900[m]); + } else { + if (m !== 5) { + result.unshift(hArray1000_9000[m], geresh, ' '); + } + break; + } + numerals = Math.floor(numerals / 10); + if (step === 0 && numerals === 0) { + result.unshift(hArray0_9[m]); + } + step++; + } + result = result.join('').split(''); + if (result.length === 1) { + result.push(geresh); + } else if (result.length > 1) { + result.splice(result.length - 1, 0, gershaim); + } + return result.join(''); +} diff --git a/src/datepicker/hebrew/ngb-calendar-hebrew.spec.ts b/src/datepicker/hebrew/ngb-calendar-hebrew.spec.ts new file mode 100644 index 0000000..a25bb8b --- /dev/null +++ b/src/datepicker/hebrew/ngb-calendar-hebrew.spec.ts @@ -0,0 +1,90 @@ +import {NgbCalendarHebrew} from './ngb-calendar-hebrew'; +import {NgbDate} from '../ngb-date'; + +describe('ngb-calendar-hebrew', () => { + + let calendar: NgbCalendarHebrew; + + beforeEach(() => { calendar = new NgbCalendarHebrew(); }); + + it('should return number of days per week', () => { expect(calendar.getDaysPerWeek()).toBe(7); }); + + it('should return number of weeks per month', () => { expect(calendar.getWeeksPerMonth()).toBe(6); }); + + it('should return months of a year', () => { + expect(calendar.getMonths(5770)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + expect(calendar.getMonths(5771)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]); + expect(calendar.getMonths(5772)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + expect(calendar.getMonths(5773)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + expect(calendar.getMonths(5774)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]); + expect(calendar.getMonths(5775)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + expect(calendar.getMonths(5776)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]); + expect(calendar.getMonths(5777)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + expect(calendar.getMonths(5778)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + expect(calendar.getMonths(5779)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]); + expect(calendar.getMonths(5780)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + }); + + it('should return day of week', () => { + expect(calendar.getWeekday(new NgbDate(5777, 10, 8))).toEqual(7); + expect(calendar.getWeekday(new NgbDate(5771, 4, 6))).toEqual(1); + expect(calendar.getWeekday(new NgbDate(5779, 1, 30))).toEqual(2); + expect(calendar.getWeekday(new NgbDate(5774, 7, 17))).toEqual(3); + expect(calendar.getWeekday(new NgbDate(5778, 12, 5))).toEqual(4); + expect(calendar.getWeekday(new NgbDate(5775, 3, 27))).toEqual(5); + expect(calendar.getWeekday(new NgbDate(5774, 13, 18))).toEqual(6); + }); + + it('should add days to date', () => { + expect(calendar.getNext(new NgbDate(5776, 2, 29))).toEqual(new NgbDate(5776, 2, 30)); + expect(calendar.getNext(new NgbDate(5777, 3, 29))).toEqual(new NgbDate(5777, 4, 1)); + expect(calendar.getNext(new NgbDate(5779, 12, 30))).toEqual(new NgbDate(5779, 13, 1)); + }); + + it('should subtract days from date', () => { + expect(calendar.getPrev(new NgbDate(5766, 1, 1))).toEqual(new NgbDate(5765, 13, 29)); + expect(calendar.getPrev(new NgbDate(5781, 4, 1))).toEqual(new NgbDate(5781, 3, 29)); + expect(calendar.getPrev(new NgbDate(5780, 3, 1))).toEqual(new NgbDate(5780, 2, 30)); + }); + + it('should add months to date', () => { + expect(calendar.getNext(new NgbDate(5778, 12, 18), 'm')).toEqual(new NgbDate(5779, 1, 1)); + expect(calendar.getNext(new NgbDate(5771, 12, 2), 'm')).toEqual(new NgbDate(5771, 13, 1)); + expect(calendar.getNext(new NgbDate(5765, 5, 26), 'm')).toEqual(new NgbDate(5765, 6, 1)); + }); + + it('should subtract months from date', () => { + expect(calendar.getPrev(new NgbDate(5779, 1, 14), 'm')).toEqual(new NgbDate(5778, 12, 1)); + expect(calendar.getPrev(new NgbDate(5772, 1, 25), 'm')).toEqual(new NgbDate(5771, 13, 1)); + expect(calendar.getPrev(new NgbDate(5765, 6, 8), 'm')).toEqual(new NgbDate(5765, 5, 1)); + }); + + it('should add years to date', () => { + expect(calendar.getNext(new NgbDate(5770, 12, 24), 'y')).toEqual(new NgbDate(5771, 1, 1)); + expect(calendar.getNext(new NgbDate(5771, 4, 11), 'y')).toEqual(new NgbDate(5772, 1, 1)); + }); + + it('should subtract years from date', () => { + expect(calendar.getPrev(new NgbDate(5777, 12, 1), 'y')).toEqual(new NgbDate(5776, 1, 1)); + expect(calendar.getPrev(new NgbDate(5779, 2, 18), 'y')).toEqual(new NgbDate(5778, 1, 1)); + }); + + it('should return week number', () => { + let week = [ + new NgbDate(5776, 13, 29), new NgbDate(5777, 1, 1), new NgbDate(5777, 1, 2), new NgbDate(5777, 1, 3), + new NgbDate(5777, 1, 4), new NgbDate(5777, 1, 5), new NgbDate(5777, 1, 6) + ]; + expect(calendar.getWeekNumber(week, 7)).toEqual(1); + week = [ + new NgbDate(5777, 7, 13), new NgbDate(5777, 7, 14), new NgbDate(5777, 7, 15), new NgbDate(5777, 7, 16), + new NgbDate(5777, 7, 17), new NgbDate(5777, 7, 18), new NgbDate(5777, 7, 19) + ]; + expect(calendar.getWeekNumber(week, 7)).toEqual(28); + week = [ + new NgbDate(5777, 12, 26), new NgbDate(5777, 12, 27), new NgbDate(5777, 12, 28), new NgbDate(5777, 12, 29), + new NgbDate(5778, 1, 1), new NgbDate(5778, 1, 2), new NgbDate(5778, 1, 3) + ]; + expect(calendar.getWeekNumber(week, 7)).toEqual(1); + }); + +}); diff --git a/src/datepicker/hebrew/ngb-calendar-hebrew.ts b/src/datepicker/hebrew/ngb-calendar-hebrew.ts new file mode 100644 index 0000000..608b960 --- /dev/null +++ b/src/datepicker/hebrew/ngb-calendar-hebrew.ts @@ -0,0 +1,83 @@ +import {NgbDate} from '../ngb-date'; +import {fromJSDate, NgbCalendar, NgbPeriod, toJSDate} from '../ngb-calendar'; +import {Injectable} from '@angular/core'; +import {isNumber} from '../../util/util'; +import { + fromGregorian, + getDayNumberInHebrewYear, + getDaysInHebrewMonth, + isHebrewLeapYear, + toGregorian, + setHebrewDay, + setHebrewMonth +} from './hebrew'; + +/** + * @since 3.2.0 + */ +@Injectable() +export class NgbCalendarHebrew extends NgbCalendar { + getDaysPerWeek() { return 7; } + + getMonths(year?: number) { + if (year && isHebrewLeapYear(year)) { + return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; + } else { + return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; + } + } + + getWeeksPerMonth() { return 6; } + + isValid(date: NgbDate): boolean { + let b = date && isNumber(date.year) && isNumber(date.month) && isNumber(date.day); + b = b && date.month > 0 && date.month <= (isHebrewLeapYear(date.year) ? 13 : 12); + b = b && date.day > 0 && date.day <= getDaysInHebrewMonth(date.month, date.year); + return b && !isNaN(toGregorian(date).getTime()); + } + + getNext(date: NgbDate, period: NgbPeriod = 'd', number = 1) { + date = new NgbDate(date.year, date.month, date.day); + + switch (period) { + case 'y': + date.year += number; + date.month = 1; + date.day = 1; + return date; + case 'm': + date = setHebrewMonth(date, number); + date.day = 1; + return date; + case 'd': + return setHebrewDay(date, number); + default: + return date; + } + } + + getPrev(date: NgbDate, period: NgbPeriod = 'd', number = 1) { return this.getNext(date, period, -number); } + + getWeekday(date: NgbDate) { + const day = toGregorian(date).getDay(); + // in JS Date Sun=0, in ISO 8601 Sun=7 + return day === 0 ? 7 : day; + } + + getWeekNumber(week: NgbDate[], firstDayOfWeek: number) { + const date = week[week.length - 1]; + return Math.ceil(getDayNumberInHebrewYear(date) / 7); + } + + getToday(): NgbDate { return fromGregorian(new Date()); } + + /** + * @since 3.4.0 + */ + toGregorian(date: NgbDate): NgbDate { return fromJSDate(toGregorian(date)); } + + /** + * @since 3.4.0 + */ + fromGregorian(date: NgbDate): NgbDate { return fromGregorian(toJSDate(date)); } +} diff --git a/src/datepicker/hijri/ngb-calendar-hijri.ts b/src/datepicker/hijri/ngb-calendar-hijri.ts new file mode 100644 index 0000000..b365f03 --- /dev/null +++ b/src/datepicker/hijri/ngb-calendar-hijri.ts @@ -0,0 +1,115 @@ +import {NgbDate} from '../ngb-date'; +import {NgbPeriod, NgbCalendar} from '../ngb-calendar'; +import {Injectable} from '@angular/core'; +import {isNumber} from '../../util/util'; + +@Injectable() +export abstract class NgbCalendarHijri extends NgbCalendar { + /** + * Returns the number of days in a specific Hijri month. + * `month` is 1 for Muharram, 2 for Safar, etc. + * `year` is any Hijri year. + */ + abstract getDaysPerMonth(month: number, year: number): number; + + /** + * Returns the equivalent Hijri date value for a give input Gregorian date. + * `gDate` is s JS Date to be converted to Hijri. + */ + abstract fromGregorian(gDate: Date): NgbDate; + + /** + * Converts the current Hijri date to Gregorian. + */ + abstract toGregorian(hDate: NgbDate): Date; + + getDaysPerWeek() { return 7; } + + getMonths() { return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; } + + getWeeksPerMonth() { return 6; } + + getNext(date: NgbDate, period: NgbPeriod = 'd', number = 1) { + date = new NgbDate(date.year, date.month, date.day); + + switch (period) { + case 'y': + date = this._setYear(date, date.year + number); + date.month = 1; + date.day = 1; + return date; + case 'm': + date = this._setMonth(date, date.month + number); + date.day = 1; + return date; + case 'd': + return this._setDay(date, date.day + number); + default: + return date; + } + } + + getPrev(date: NgbDate, period: NgbPeriod = 'd', number = 1) { return this.getNext(date, period, -number); } + + getWeekday(date: NgbDate) { + const day = this.toGregorian(date).getDay(); + // in JS Date Sun=0, in ISO 8601 Sun=7 + return day === 0 ? 7 : day; + } + + getWeekNumber(week: NgbDate[], firstDayOfWeek: number) { + // in JS Date Sun=0, in ISO 8601 Sun=7 + if (firstDayOfWeek === 7) { + firstDayOfWeek = 0; + } + + const thursdayIndex = (4 + 7 - firstDayOfWeek) % 7; + const date = week[thursdayIndex]; + + const jsDate = this.toGregorian(date); + jsDate.setDate(jsDate.getDate() + 4 - (jsDate.getDay() || 7)); // Thursday + const time = jsDate.getTime(); + const MuhDate = this.toGregorian(new NgbDate(date.year, 1, 1)); // Compare with Muharram 1 + return Math.floor(Math.round((time - MuhDate.getTime()) / 86400000) / 7) + 1; + } + + getToday(): NgbDate { return this.fromGregorian(new Date()); } + + + isValid(date: NgbDate): boolean { + return date && isNumber(date.year) && isNumber(date.month) && isNumber(date.day) && + !isNaN(this.toGregorian(date).getTime()); + } + + private _setDay(date: NgbDate, day: number): NgbDate { + day = +day; + let mDays = this.getDaysPerMonth(date.month, date.year); + if (day <= 0) { + while (day <= 0) { + date = this._setMonth(date, date.month - 1); + mDays = this.getDaysPerMonth(date.month, date.year); + day += mDays; + } + } else if (day > mDays) { + while (day > mDays) { + day -= mDays; + date = this._setMonth(date, date.month + 1); + mDays = this.getDaysPerMonth(date.month, date.year); + } + } + date.day = day; + return date; + } + + private _setMonth(date: NgbDate, month: number): NgbDate { + month = +month; + date.year = date.year + Math.floor((month - 1) / 12); + date.month = Math.floor(((month - 1) % 12 + 12) % 12) + 1; + return date; + } + + private _setYear(date: NgbDate, year: number): NgbDate { + date.year = +year; + return date; + } +} diff --git a/src/datepicker/hijri/ngb-calendar-islamic-civil.spec.ts b/src/datepicker/hijri/ngb-calendar-islamic-civil.spec.ts new file mode 100644 index 0000000..cdf1990 --- /dev/null +++ b/src/datepicker/hijri/ngb-calendar-islamic-civil.spec.ts @@ -0,0 +1,434 @@ +import {NgbCalendarIslamicCivil} from './ngb-calendar-islamic-civil'; +import {NgbDate} from '../ngb-date'; + +describe('ngb-calendar-islamic-civil', () => { + const DATE_TABLE = [ + [1420, 1, 1, 1999, 3, 17], [1420, 1, 12, 1999, 3, 28], [1420, 1, 23, 1999, 4, 9], + [1420, 2, 4, 1999, 4, 20], [1420, 2, 15, 1999, 4, 31], [1420, 2, 26, 1999, 5, 11], + [1420, 3, 8, 1999, 5, 22], [1420, 3, 19, 1999, 6, 3], [1420, 3, 30, 1999, 6, 14], + [1420, 4, 11, 1999, 6, 25], [1420, 4, 22, 1999, 7, 5], [1420, 5, 4, 1999, 7, 16], + [1420, 5, 15, 1999, 7, 27], [1420, 5, 26, 1999, 8, 7], [1420, 6, 7, 1999, 8, 18], + [1420, 6, 18, 1999, 8, 29], [1420, 6, 29, 1999, 9, 10], [1420, 7, 11, 1999, 9, 21], + [1420, 7, 22, 1999, 10, 1], [1420, 8, 3, 1999, 10, 12], [1420, 8, 14, 1999, 10, 23], + [1420, 9, 29, 2000, 0, 6], [1420, 10, 10, 2000, 0, 17], [1420, 10, 21, 2000, 0, 28], + [1420, 11, 3, 2000, 1, 8], [1420, 11, 14, 2000, 1, 19], [1420, 11, 25, 2000, 2, 1], + [1420, 12, 6, 2000, 2, 12], [1420, 12, 17, 2000, 2, 23], [1420, 12, 28, 2000, 3, 3], + [1421, 1, 9, 2000, 3, 14], [1421, 1, 20, 2000, 3, 25], [1421, 2, 1, 2000, 4, 6], + [1421, 2, 12, 2000, 4, 17], [1421, 2, 23, 2000, 4, 28], [1421, 3, 5, 2000, 5, 8], + [1421, 3, 16, 2000, 5, 19], [1421, 3, 27, 2000, 5, 30], [1421, 4, 8, 2000, 6, 11], + [1421, 4, 19, 2000, 6, 22], [1421, 5, 1, 2000, 7, 2], [1421, 5, 12, 2000, 7, 13], + [1421, 5, 23, 2000, 7, 24], [1421, 6, 4, 2000, 8, 4], [1421, 6, 15, 2000, 8, 15], + [1421, 6, 26, 2000, 8, 26], [1421, 7, 8, 2000, 9, 7], [1421, 7, 19, 2000, 9, 18], + [1421, 7, 30, 2000, 9, 29], [1421, 8, 11, 2000, 10, 9], [1421, 8, 22, 2000, 10, 20], + [1421, 10, 7, 2001, 0, 3], [1421, 10, 18, 2001, 0, 14], [1421, 10, 7, 2001, 0, 3], + [1421, 10, 18, 2001, 0, 14], [1421, 10, 29, 2001, 0, 25], [1421, 11, 11, 2001, 1, 5], + [1421, 11, 22, 2001, 1, 16], [1421, 12, 3, 2001, 1, 27], [1421, 12, 14, 2001, 2, 10], + [1421, 12, 25, 2001, 2, 21], [1422, 1, 7, 2001, 3, 1], [1422, 1, 18, 2001, 3, 12], + [1422, 1, 29, 2001, 3, 23], [1422, 2, 10, 2001, 4, 4], [1422, 2, 21, 2001, 4, 15], + [1422, 3, 3, 2001, 4, 26], [1422, 3, 14, 2001, 5, 6], [1422, 3, 25, 2001, 5, 17], + [1422, 4, 6, 2001, 5, 28], [1422, 4, 17, 2001, 6, 9], [1422, 4, 28, 2001, 6, 20], + [1422, 5, 10, 2001, 6, 31], [1422, 5, 21, 2001, 7, 11], [1422, 6, 2, 2001, 7, 22], + [1422, 6, 13, 2001, 8, 2], [1422, 6, 24, 2001, 8, 13], [1422, 7, 6, 2001, 8, 24], + [1422, 7, 17, 2001, 9, 5], [1422, 7, 28, 2001, 9, 16], [1422, 8, 9, 2001, 9, 27], + [1422, 8, 20, 2001, 10, 7], [1422, 9, 2, 2001, 10, 18], [1422, 9, 13, 2001, 10, 29], + [1422, 9, 24, 2001, 11, 10], [1422, 10, 5, 2001, 11, 21], [1422, 10, 16, 2002, 0, 1], + [1422, 10, 27, 2002, 0, 12], [1422, 11, 9, 2002, 0, 23], [1422, 11, 20, 2002, 1, 3], + [1422, 12, 1, 2002, 1, 14], [1422, 12, 12, 2002, 1, 25], [1422, 12, 23, 2002, 2, 8], + [1423, 1, 5, 2002, 2, 19], [1423, 1, 16, 2002, 2, 30], [1423, 1, 27, 2002, 3, 10], + [1423, 2, 8, 2002, 3, 21], [1423, 2, 19, 2002, 4, 2], [1423, 3, 1, 2002, 4, 13], + [1423, 3, 12, 2002, 4, 24], [1423, 3, 23, 2002, 5, 4], [1423, 4, 4, 2002, 5, 15], + [1423, 4, 15, 2002, 5, 26], [1423, 4, 26, 2002, 6, 7], [1423, 5, 8, 2002, 6, 18], + [1423, 5, 19, 2002, 6, 29], [1423, 5, 30, 2002, 7, 9], [1423, 6, 11, 2002, 7, 20], + [1423, 6, 22, 2002, 7, 31], [1423, 7, 4, 2002, 8, 11], [1423, 7, 15, 2002, 8, 22], + [1423, 7, 26, 2002, 9, 3], [1423, 8, 7, 2002, 9, 14], [1423, 8, 18, 2002, 9, 25], + [1423, 8, 29, 2002, 10, 5], [1423, 9, 11, 2002, 10, 16], [1423, 9, 22, 2002, 10, 27], + [1423, 10, 3, 2002, 11, 8], [1423, 10, 14, 2002, 11, 19], [1423, 10, 25, 2002, 11, 30], + [1423, 11, 7, 2003, 0, 10], [1423, 11, 18, 2003, 0, 21], [1423, 11, 29, 2003, 1, 1], + [1423, 12, 10, 2003, 1, 12], [1423, 12, 21, 2003, 1, 23], [1424, 1, 2, 2003, 2, 6], + [1424, 1, 13, 2003, 2, 17], [1424, 1, 24, 2003, 2, 28], [1424, 2, 5, 2003, 3, 8], + [1424, 2, 16, 2003, 3, 19], [1424, 2, 27, 2003, 3, 30], [1424, 3, 9, 2003, 4, 11], + [1424, 3, 20, 2003, 4, 22], [1424, 4, 1, 2003, 5, 2], [1424, 4, 12, 2003, 5, 13], + [1424, 4, 23, 2003, 5, 24], [1424, 5, 5, 2003, 6, 5], [1424, 5, 16, 2003, 6, 16], + [1424, 5, 27, 2003, 6, 27], [1424, 6, 8, 2003, 7, 7], [1424, 6, 19, 2003, 7, 18], + [1424, 7, 1, 2003, 7, 29], [1424, 7, 12, 2003, 8, 9], [1424, 7, 23, 2003, 8, 20], + [1424, 8, 4, 2003, 9, 1], [1424, 8, 15, 2003, 9, 12], [1424, 8, 26, 2003, 9, 23], + [1424, 9, 8, 2003, 10, 3], [1424, 9, 19, 2003, 10, 14], [1424, 9, 30, 2003, 10, 25], + [1424, 11, 15, 2004, 0, 8], [1424, 11, 26, 2004, 0, 19], [1424, 12, 7, 2004, 0, 30], + [1424, 12, 18, 2004, 1, 10], [1424, 12, 29, 2004, 1, 21], [1425, 1, 11, 2004, 2, 3], + [1425, 1, 22, 2004, 2, 14], [1425, 2, 3, 2004, 2, 25], [1425, 2, 14, 2004, 3, 5], + [1425, 2, 25, 2004, 3, 16], [1425, 3, 7, 2004, 3, 27], [1425, 3, 18, 2004, 4, 8], + [1425, 3, 29, 2004, 4, 19], [1425, 4, 10, 2004, 4, 30], [1425, 4, 21, 2004, 5, 10], + [1425, 5, 3, 2004, 5, 21], [1425, 5, 14, 2004, 6, 2], [1425, 5, 25, 2004, 6, 13], + [1425, 6, 6, 2004, 6, 24], [1425, 6, 17, 2004, 7, 4], [1425, 6, 28, 2004, 7, 15], + [1425, 7, 10, 2004, 7, 26], [1425, 7, 21, 2004, 8, 6], [1425, 8, 2, 2004, 8, 17], + [1425, 8, 13, 2004, 8, 28], [1425, 8, 24, 2004, 9, 9], [1425, 9, 6, 2004, 9, 20], + [1425, 9, 17, 2004, 9, 31], [1425, 9, 28, 2004, 10, 11], [1425, 10, 9, 2004, 10, 22], + [1425, 11, 24, 2005, 0, 5], [1425, 12, 5, 2005, 0, 16], [1425, 12, 16, 2005, 0, 27], + [1425, 12, 27, 2005, 1, 7], [1426, 1, 9, 2005, 1, 18], [1426, 1, 20, 2005, 2, 1], + [1426, 2, 1, 2005, 2, 12], [1426, 2, 12, 2005, 2, 23], [1426, 2, 23, 2005, 3, 3], + [1426, 3, 5, 2005, 3, 14], [1426, 3, 16, 2005, 3, 25], [1426, 3, 27, 2005, 4, 6], + [1426, 4, 8, 2005, 4, 17], [1426, 4, 19, 2005, 4, 28], [1426, 5, 1, 2005, 5, 8], + [1426, 5, 12, 2005, 5, 19], [1426, 5, 23, 2005, 5, 30], [1426, 6, 4, 2005, 6, 11], + [1426, 6, 15, 2005, 6, 22], [1426, 6, 26, 2005, 7, 2], [1426, 7, 8, 2005, 7, 13], + [1426, 7, 19, 2005, 7, 24], [1426, 7, 30, 2005, 8, 4], [1426, 8, 11, 2005, 8, 15], + [1426, 8, 22, 2005, 8, 26], [1426, 9, 4, 2005, 9, 7], [1426, 9, 15, 2005, 9, 18], + [1426, 9, 26, 2005, 9, 29], [1426, 10, 7, 2005, 10, 9], [1426, 10, 18, 2005, 10, 20], + [1426, 10, 29, 2005, 11, 1], [1426, 11, 11, 2005, 11, 12], [1426, 11, 22, 2005, 11, 23], + [1426, 12, 3, 2006, 0, 3], [1426, 12, 14, 2006, 0, 14], [1426, 12, 25, 2006, 0, 25], + [1427, 1, 6, 2006, 1, 5], [1427, 1, 17, 2006, 1, 16], [1427, 1, 28, 2006, 1, 27], + [1427, 2, 9, 2006, 2, 10], [1427, 2, 20, 2006, 2, 21], [1427, 3, 2, 2006, 3, 1], + [1427, 3, 13, 2006, 3, 12], [1427, 3, 24, 2006, 3, 23], [1427, 4, 5, 2006, 4, 4], + [1427, 4, 16, 2006, 4, 15], [1427, 4, 27, 2006, 4, 26], [1427, 5, 9, 2006, 5, 6], + [1427, 5, 20, 2006, 5, 17], [1427, 6, 1, 2006, 5, 28], [1427, 6, 12, 2006, 6, 9], + [1427, 6, 23, 2006, 6, 20], [1427, 7, 5, 2006, 6, 31], [1427, 7, 16, 2006, 7, 11], + [1427, 7, 27, 2006, 7, 22], [1427, 8, 8, 2006, 8, 2], [1427, 8, 19, 2006, 8, 13], + [1427, 9, 1, 2006, 8, 24], [1427, 9, 12, 2006, 9, 5], [1427, 9, 23, 2006, 9, 16], + [1427, 10, 4, 2006, 9, 27], [1427, 10, 15, 2006, 10, 7], [1427, 10, 26, 2006, 10, 18], + [1427, 11, 8, 2006, 10, 29], [1427, 11, 19, 2006, 11, 10], [1427, 11, 30, 2006, 11, 21], + [1427, 12, 11, 2007, 0, 1], [1427, 12, 22, 2007, 0, 12], [1428, 1, 4, 2007, 0, 23], + [1428, 1, 15, 2007, 1, 3], [1428, 1, 26, 2007, 1, 14], [1428, 2, 7, 2007, 1, 25], + [1428, 2, 18, 2007, 2, 8], [1428, 2, 29, 2007, 2, 19], [1428, 3, 11, 2007, 2, 30], + [1428, 3, 22, 2007, 3, 10], [1428, 4, 3, 2007, 3, 21], [1428, 4, 14, 2007, 4, 2], + [1428, 4, 25, 2007, 4, 13], [1428, 5, 7, 2007, 4, 24], [1428, 5, 18, 2007, 5, 4], + [1428, 5, 29, 2007, 5, 15], [1428, 6, 10, 2007, 5, 26], [1428, 6, 21, 2007, 6, 7], + [1428, 7, 3, 2007, 6, 18], [1428, 7, 14, 2007, 6, 29], [1428, 7, 25, 2007, 7, 9], + [1428, 8, 6, 2007, 7, 20], [1428, 8, 17, 2007, 7, 31], [1428, 8, 28, 2007, 8, 11], + [1428, 9, 10, 2007, 8, 22], [1428, 9, 21, 2007, 9, 3], [1428, 10, 2, 2007, 9, 14], + [1428, 10, 13, 2007, 9, 25], [1428, 10, 24, 2007, 10, 5], [1428, 11, 6, 2007, 10, 16], + [1428, 11, 17, 2007, 10, 27], [1429, 1, 1, 2008, 0, 10], [1429, 1, 12, 2008, 0, 21], + [1429, 1, 23, 2008, 1, 1], [1429, 2, 4, 2008, 1, 12], [1429, 2, 15, 2008, 1, 23], + [1429, 2, 26, 2008, 2, 5], [1429, 3, 8, 2008, 2, 16], [1429, 3, 19, 2008, 2, 27], + [1429, 3, 30, 2008, 3, 7], [1429, 4, 11, 2008, 3, 18], [1429, 4, 22, 2008, 3, 29], + [1429, 5, 4, 2008, 4, 10], [1429, 5, 15, 2008, 4, 21], [1429, 5, 26, 2008, 5, 1], + [1429, 6, 7, 2008, 5, 12], [1429, 6, 18, 2008, 5, 23], [1429, 6, 29, 2008, 6, 4], + [1429, 7, 11, 2008, 6, 15], [1429, 7, 22, 2008, 6, 26], [1429, 8, 3, 2008, 7, 6], + [1429, 8, 14, 2008, 7, 17], [1429, 8, 25, 2008, 7, 28], [1429, 9, 7, 2008, 8, 8], + [1429, 9, 18, 2008, 8, 19], [1429, 9, 29, 2008, 8, 30], [1429, 10, 10, 2008, 9, 11], + [1429, 10, 21, 2008, 9, 22], [1429, 11, 3, 2008, 10, 2], [1429, 11, 14, 2008, 10, 13], + [1429, 11, 25, 2008, 10, 24], [1430, 1, 10, 2009, 0, 7], [1430, 1, 21, 2009, 0, 18], + [1430, 2, 2, 2009, 0, 29], [1430, 2, 13, 2009, 1, 9], [1430, 2, 24, 2009, 1, 20], + [1430, 3, 6, 2009, 2, 3], [1430, 3, 17, 2009, 2, 14], [1430, 3, 28, 2009, 2, 25], + [1430, 4, 9, 2009, 3, 5], [1430, 4, 20, 2009, 3, 16], [1430, 5, 2, 2009, 3, 27], + [1430, 5, 13, 2009, 4, 8], [1430, 5, 24, 2009, 4, 19], [1430, 6, 5, 2009, 4, 30], + [1430, 6, 16, 2009, 5, 10], [1430, 6, 27, 2009, 5, 21], [1430, 7, 9, 2009, 6, 2], + [1430, 7, 20, 2009, 6, 13], [1430, 8, 1, 2009, 6, 24], [1430, 8, 12, 2009, 7, 4], + [1430, 8, 23, 2009, 7, 15], [1430, 9, 5, 2009, 7, 26], [1430, 9, 16, 2009, 8, 6], + [1430, 9, 27, 2009, 8, 17], [1430, 10, 8, 2009, 8, 28], [1430, 10, 19, 2009, 9, 9], + [1430, 11, 1, 2009, 9, 20], [1430, 11, 12, 2009, 9, 31], [1430, 11, 23, 2009, 10, 11], + [1430, 12, 4, 2009, 10, 22], [1430, 12, 15, 2009, 11, 3], [1430, 12, 26, 2009, 11, 14], + [1431, 1, 8, 2009, 11, 25], [1431, 1, 19, 2010, 0, 5], [1431, 1, 30, 2010, 0, 16], + [1431, 2, 11, 2010, 0, 27], [1431, 2, 22, 2010, 1, 7], [1431, 3, 4, 2010, 1, 18], + [1431, 3, 15, 2010, 2, 1], [1431, 3, 26, 2010, 2, 12], [1431, 4, 7, 2010, 2, 23], + [1431, 4, 18, 2010, 3, 3], [1431, 4, 29, 2010, 3, 14], [1431, 5, 11, 2010, 3, 25], + [1431, 5, 22, 2010, 4, 6], [1431, 6, 3, 2010, 4, 17], [1431, 6, 14, 2010, 4, 28], + [1431, 6, 25, 2010, 5, 8], [1431, 7, 7, 2010, 5, 19], [1431, 7, 18, 2010, 5, 30], + [1431, 7, 29, 2010, 6, 11], [1431, 8, 10, 2010, 6, 22], [1431, 8, 21, 2010, 7, 2], + [1431, 9, 3, 2010, 7, 13], [1431, 9, 14, 2010, 7, 24], [1431, 9, 25, 2010, 8, 4], + [1431, 10, 6, 2010, 8, 15], [1431, 10, 17, 2010, 8, 26], [1431, 10, 28, 2010, 9, 7], + [1431, 11, 10, 2010, 9, 18], [1431, 11, 21, 2010, 9, 29], [1431, 12, 2, 2010, 10, 9], + [1431, 12, 13, 2010, 10, 20], [1431, 12, 24, 2010, 11, 1], [1432, 1, 5, 2010, 11, 12], + [1432, 1, 16, 2010, 11, 23], [1432, 1, 27, 2011, 0, 3], [1432, 2, 8, 2011, 0, 14], + [1432, 2, 19, 2011, 0, 25], [1432, 3, 1, 2011, 1, 5], [1432, 3, 12, 2011, 1, 16], + [1432, 3, 23, 2011, 1, 27], [1432, 4, 4, 2011, 2, 10], [1432, 4, 15, 2011, 2, 21], + [1432, 4, 26, 2011, 3, 1], [1432, 5, 8, 2011, 3, 12], [1432, 5, 19, 2011, 3, 23], + [1432, 5, 30, 2011, 4, 4], [1432, 6, 11, 2011, 4, 15], [1432, 6, 22, 2011, 4, 26], + [1432, 7, 4, 2011, 5, 6], [1432, 7, 15, 2011, 5, 17], [1432, 7, 26, 2011, 5, 28], + [1432, 8, 7, 2011, 6, 9], [1432, 8, 18, 2011, 6, 20], [1432, 8, 29, 2011, 6, 31], + [1432, 9, 11, 2011, 7, 11], [1432, 9, 22, 2011, 7, 22], [1432, 10, 3, 2011, 8, 2], + [1432, 10, 14, 2011, 8, 13], [1432, 10, 25, 2011, 8, 24], [1432, 11, 7, 2011, 9, 5], + [1432, 11, 18, 2011, 9, 16], [1432, 11, 29, 2011, 9, 27], [1432, 12, 10, 2011, 10, 7], + [1432, 12, 21, 2011, 10, 18], [1433, 1, 3, 2011, 10, 29], [1433, 2, 6, 2012, 0, 1], + [1433, 2, 17, 2012, 0, 12], [1433, 2, 28, 2012, 0, 23], [1433, 3, 10, 2012, 1, 3], + [1433, 3, 21, 2012, 1, 14], [1433, 4, 2, 2012, 1, 25], [1433, 4, 13, 2012, 2, 7], + [1433, 4, 24, 2012, 2, 18], [1433, 5, 6, 2012, 2, 29], [1433, 5, 17, 2012, 3, 9], + [1433, 5, 28, 2012, 3, 20], [1433, 6, 9, 2012, 4, 1], [1433, 6, 20, 2012, 4, 12], + [1433, 7, 2, 2012, 4, 23], [1433, 7, 13, 2012, 5, 3], [1433, 7, 24, 2012, 5, 14], + [1433, 8, 5, 2012, 5, 25], [1433, 8, 16, 2012, 6, 6], [1433, 8, 27, 2012, 6, 17], + [1433, 9, 9, 2012, 6, 28], [1433, 9, 20, 2012, 7, 8], [1433, 10, 1, 2012, 7, 19], + [1433, 10, 12, 2012, 7, 30], [1433, 10, 23, 2012, 8, 10], [1433, 11, 5, 2012, 8, 21], + [1433, 11, 16, 2012, 9, 2], [1433, 11, 27, 2012, 9, 13], [1433, 12, 8, 2012, 9, 24], + [1433, 12, 19, 2012, 10, 4], [1434, 1, 1, 2012, 10, 15], [1434, 1, 12, 2012, 10, 26], + [1434, 2, 26, 2013, 0, 9], [1434, 3, 8, 2013, 0, 20], [1434, 3, 19, 2013, 0, 31], + [1434, 3, 30, 2013, 1, 11], [1434, 4, 11, 2013, 1, 22], [1434, 4, 22, 2013, 2, 5], + [1434, 5, 4, 2013, 2, 16], [1434, 5, 15, 2013, 2, 27], [1434, 5, 26, 2013, 3, 7], + [1434, 6, 7, 2013, 3, 18], [1434, 6, 18, 2013, 3, 29], [1434, 6, 29, 2013, 4, 10], + [1434, 7, 11, 2013, 4, 21], [1434, 7, 22, 2013, 5, 1], [1434, 8, 3, 2013, 5, 12], + [1434, 8, 14, 2013, 5, 23], [1434, 8, 25, 2013, 6, 4], [1434, 9, 7, 2013, 6, 15], + [1434, 9, 18, 2013, 6, 26], [1434, 9, 29, 2013, 7, 6], [1434, 10, 10, 2013, 7, 17], + [1434, 10, 21, 2013, 7, 28], [1434, 11, 3, 2013, 8, 8], [1434, 11, 14, 2013, 8, 19], + [1434, 11, 25, 2013, 8, 30], [1434, 12, 6, 2013, 9, 11], [1434, 12, 17, 2013, 9, 22], + [1434, 12, 28, 2013, 10, 2], [1435, 1, 9, 2013, 10, 13], [1435, 1, 20, 2013, 10, 24], + [1435, 2, 1, 2013, 11, 5], [1435, 2, 12, 2013, 11, 16], [1435, 2, 23, 2013, 11, 27], + [1435, 3, 5, 2014, 0, 7], [1435, 3, 16, 2014, 0, 18], [1435, 3, 27, 2014, 0, 29], + [1435, 4, 8, 2014, 1, 9], [1435, 4, 19, 2014, 1, 20], [1435, 5, 1, 2014, 2, 3], + [1435, 5, 12, 2014, 2, 14], [1435, 5, 23, 2014, 2, 25], [1435, 6, 4, 2014, 3, 5], + [1435, 6, 15, 2014, 3, 16], [1435, 6, 26, 2014, 3, 27], [1435, 7, 8, 2014, 4, 8], + [1435, 7, 19, 2014, 4, 19], [1435, 7, 30, 2014, 4, 30], [1435, 8, 11, 2014, 5, 10], + [1435, 8, 22, 2014, 5, 21], [1435, 9, 4, 2014, 6, 2], [1435, 9, 15, 2014, 6, 13], + [1435, 9, 26, 2014, 6, 24], [1435, 10, 7, 2014, 7, 4], [1435, 10, 18, 2014, 7, 15], + [1435, 10, 29, 2014, 7, 26], [1435, 11, 11, 2014, 8, 6], [1435, 11, 22, 2014, 8, 17], + [1435, 12, 3, 2014, 8, 28], [1435, 12, 14, 2014, 9, 9], [1435, 12, 25, 2014, 9, 20], + [1436, 1, 7, 2014, 9, 31], [1436, 1, 18, 2014, 10, 11], [1436, 1, 29, 2014, 10, 22], + [1436, 2, 10, 2014, 11, 3], [1436, 2, 21, 2014, 11, 14], [1436, 3, 3, 2014, 11, 25], + [1436, 3, 14, 2015, 0, 5], [1436, 3, 25, 2015, 0, 16], [1436, 4, 6, 2015, 0, 27], + [1436, 4, 17, 2015, 1, 7], [1436, 4, 28, 2015, 1, 18], [1436, 5, 10, 2015, 2, 1], + [1436, 5, 21, 2015, 2, 12], [1436, 6, 2, 2015, 2, 23], [1436, 6, 13, 2015, 3, 3], + [1436, 6, 24, 2015, 3, 14], [1436, 7, 6, 2015, 3, 25], [1436, 7, 17, 2015, 4, 6], + [1436, 7, 28, 2015, 4, 17], [1436, 8, 9, 2015, 4, 28], [1436, 8, 20, 2015, 5, 8], + [1436, 9, 2, 2015, 5, 19], [1436, 9, 13, 2015, 5, 30], [1436, 9, 24, 2015, 6, 11], + [1436, 10, 5, 2015, 6, 22], [1436, 10, 16, 2015, 7, 2], [1436, 10, 27, 2015, 7, 13], + [1436, 11, 9, 2015, 7, 24], [1436, 11, 20, 2015, 8, 4], [1436, 12, 1, 2015, 8, 15], + [1436, 12, 12, 2015, 8, 26], [1436, 12, 23, 2015, 9, 7], [1437, 1, 4, 2015, 9, 18], + [1437, 1, 15, 2015, 9, 29], [1437, 1, 26, 2015, 10, 9], [1437, 2, 7, 2015, 10, 20], + [1437, 3, 22, 2016, 0, 3], [1437, 4, 3, 2016, 0, 14], [1437, 4, 14, 2016, 0, 25], + [1437, 4, 25, 2016, 1, 5], [1437, 5, 7, 2016, 1, 16], [1437, 5, 18, 2016, 1, 27], + [1437, 5, 29, 2016, 2, 9], [1437, 6, 10, 2016, 2, 20], [1437, 6, 21, 2016, 2, 31], + [1437, 7, 3, 2016, 3, 11], [1437, 7, 14, 2016, 3, 22], [1437, 7, 25, 2016, 4, 3], + [1437, 8, 6, 2016, 4, 14], [1437, 8, 17, 2016, 4, 25], [1437, 8, 28, 2016, 5, 5], + [1437, 9, 10, 2016, 5, 16], [1437, 9, 21, 2016, 5, 27], [1437, 10, 2, 2016, 6, 8], + [1437, 10, 13, 2016, 6, 19], [1437, 10, 24, 2016, 6, 30], [1437, 11, 6, 2016, 7, 10], + [1437, 11, 17, 2016, 7, 21], [1437, 11, 28, 2016, 8, 1], [1437, 12, 9, 2016, 8, 12], + [1437, 12, 20, 2016, 8, 23], [1438, 1, 2, 2016, 9, 4], [1438, 1, 13, 2016, 9, 15], + [1438, 1, 24, 2016, 9, 26], [1438, 2, 5, 2016, 10, 6], [1438, 2, 16, 2016, 10, 17], + [1438, 2, 27, 2016, 10, 28], [1438, 4, 12, 2017, 0, 11], [1438, 4, 23, 2017, 0, 22], + [1438, 5, 5, 2017, 1, 2], [1438, 5, 16, 2017, 1, 13], [1438, 5, 27, 2017, 1, 24], + [1438, 6, 8, 2017, 2, 7], [1438, 6, 19, 2017, 2, 18], [1438, 7, 1, 2017, 2, 29], + [1438, 7, 12, 2017, 3, 9], [1438, 7, 23, 2017, 3, 20], [1438, 8, 4, 2017, 4, 1], + [1438, 8, 15, 2017, 4, 12], [1438, 8, 26, 2017, 4, 23], [1438, 9, 8, 2017, 5, 3], + [1438, 9, 19, 2017, 5, 14], [1438, 9, 30, 2017, 5, 25], [1438, 10, 11, 2017, 6, 6], + [1438, 10, 22, 2017, 6, 17], [1438, 11, 4, 2017, 6, 28], [1438, 11, 15, 2017, 7, 8], + [1438, 11, 26, 2017, 7, 19], [1438, 12, 7, 2017, 7, 30], [1438, 12, 18, 2017, 8, 10], + [1438, 12, 29, 2017, 8, 21], [1439, 1, 11, 2017, 9, 2], [1439, 1, 22, 2017, 9, 13], + [1439, 2, 3, 2017, 9, 24], [1439, 2, 14, 2017, 10, 4], [1439, 2, 25, 2017, 10, 15], + [1439, 3, 7, 2017, 10, 26], [1439, 3, 18, 2017, 11, 7], [1439, 3, 29, 2017, 11, 18], + [1439, 4, 10, 2017, 11, 29], [1439, 4, 21, 2018, 0, 9], [1439, 5, 3, 2018, 0, 20], + [1439, 5, 14, 2018, 0, 31], [1439, 5, 25, 2018, 1, 11], [1439, 6, 6, 2018, 1, 22], + [1439, 6, 17, 2018, 2, 5], [1439, 6, 28, 2018, 2, 16], [1439, 7, 10, 2018, 2, 27], + [1439, 7, 21, 2018, 3, 7], [1439, 8, 2, 2018, 3, 18], [1439, 8, 13, 2018, 3, 29], + [1439, 8, 24, 2018, 4, 10], [1439, 9, 6, 2018, 4, 21], [1439, 9, 17, 2018, 5, 1], + [1439, 9, 28, 2018, 5, 12], [1439, 10, 9, 2018, 5, 23], [1439, 10, 20, 2018, 6, 4], + [1439, 11, 2, 2018, 6, 15], [1439, 11, 13, 2018, 6, 26], [1439, 11, 24, 2018, 7, 6], + [1439, 12, 5, 2018, 7, 17], [1439, 12, 16, 2018, 7, 28], [1439, 12, 27, 2018, 8, 8], + [1440, 1, 8, 2018, 8, 19], [1440, 1, 19, 2018, 8, 30], [1440, 1, 30, 2018, 9, 11], + [1440, 2, 11, 2018, 9, 22], [1440, 2, 22, 2018, 10, 2], [1440, 3, 4, 2018, 10, 13], + [1440, 3, 15, 2018, 10, 24], [1440, 3, 26, 2018, 11, 5], [1440, 4, 7, 2018, 11, 16], + [1440, 4, 18, 2018, 11, 27], [1440, 4, 29, 2019, 0, 7], [1440, 5, 11, 2019, 0, 18], + [1440, 5, 22, 2019, 0, 29], [1440, 6, 3, 2019, 1, 9], [1440, 6, 14, 2019, 1, 20], + [1440, 6, 25, 2019, 2, 3], [1440, 7, 7, 2019, 2, 14], [1440, 7, 18, 2019, 2, 25], + [1440, 7, 29, 2019, 3, 5], [1440, 8, 10, 2019, 3, 16], [1440, 8, 21, 2019, 3, 27], + [1440, 9, 3, 2019, 4, 8], [1440, 9, 14, 2019, 4, 19], [1440, 9, 25, 2019, 4, 30], + [1440, 10, 6, 2019, 5, 10], [1440, 10, 17, 2019, 5, 21], [1440, 10, 28, 2019, 6, 2], + [1440, 11, 10, 2019, 6, 13], [1440, 11, 21, 2019, 6, 24], [1440, 12, 2, 2019, 7, 4], + [1440, 12, 13, 2019, 7, 15], [1440, 12, 24, 2019, 7, 26], [1441, 1, 6, 2019, 8, 6], + [1441, 1, 17, 2019, 8, 17], [1441, 1, 28, 2019, 8, 28], [1441, 2, 9, 2019, 9, 9], + [1441, 2, 20, 2019, 9, 20], [1441, 3, 2, 2019, 9, 31], [1441, 3, 13, 2019, 10, 11], + [1441, 3, 24, 2019, 10, 22], [1441, 5, 9, 2020, 0, 5], [1441, 5, 20, 2020, 0, 16], + [1441, 6, 1, 2020, 0, 27], [1441, 6, 12, 2020, 1, 7], [1441, 6, 23, 2020, 1, 18], + [1441, 7, 5, 2020, 1, 29], [1441, 7, 16, 2020, 2, 11], [1441, 7, 27, 2020, 2, 22], + [1441, 8, 8, 2020, 3, 2], [1441, 8, 19, 2020, 3, 13], [1441, 9, 1, 2020, 3, 24], + [1441, 9, 12, 2020, 4, 5], [1441, 9, 23, 2020, 4, 16], [1441, 10, 4, 2020, 4, 27], + [1441, 10, 15, 2020, 5, 7], [1441, 10, 26, 2020, 5, 18], [1441, 11, 8, 2020, 5, 29], + [1441, 11, 19, 2020, 6, 10], [1441, 11, 30, 2020, 6, 21], [1441, 12, 11, 2020, 7, 1], + [1441, 12, 22, 2020, 7, 12], [1442, 1, 4, 2020, 7, 23], [1442, 1, 15, 2020, 8, 3], + [1442, 1, 26, 2020, 8, 14], [1442, 2, 7, 2020, 8, 25], [1442, 2, 18, 2020, 9, 6], + [1442, 2, 29, 2020, 9, 17], [1442, 3, 11, 2020, 9, 28], [1442, 3, 22, 2020, 10, 8], + [1442, 4, 3, 2020, 10, 19], [1442, 4, 14, 2020, 10, 30], [1442, 5, 18, 2021, 0, 2], + [1442, 5, 29, 2021, 0, 13], [1442, 6, 10, 2021, 0, 24], [1442, 6, 21, 2021, 1, 4], + [1442, 7, 3, 2021, 1, 15], [1442, 7, 14, 2021, 1, 26], [1442, 7, 25, 2021, 2, 9], + [1442, 8, 6, 2021, 2, 20], [1442, 8, 17, 2021, 2, 31], [1442, 8, 28, 2021, 3, 11], + [1442, 9, 10, 2021, 3, 22], [1442, 9, 21, 2021, 4, 3], [1442, 10, 2, 2021, 4, 14], + [1442, 10, 13, 2021, 4, 25], [1442, 10, 24, 2021, 5, 5], [1442, 11, 6, 2021, 5, 16], + [1442, 11, 17, 2021, 5, 27], [1442, 11, 28, 2021, 6, 8], [1442, 12, 9, 2021, 6, 19], + [1442, 12, 20, 2021, 6, 30], [1443, 1, 1, 2021, 7, 10], [1443, 1, 12, 2021, 7, 21], + [1443, 1, 23, 2021, 8, 1], [1443, 2, 4, 2021, 8, 12], [1443, 2, 15, 2021, 8, 23], + [1443, 2, 26, 2021, 9, 4], [1443, 3, 8, 2021, 9, 15], [1443, 3, 19, 2021, 9, 26], + [1443, 3, 30, 2021, 10, 6], [1443, 4, 11, 2021, 10, 17], [1443, 4, 22, 2021, 10, 28], + [1443, 6, 7, 2022, 0, 11], [1443, 6, 18, 2022, 0, 22], [1443, 6, 29, 2022, 1, 2], + [1443, 7, 11, 2022, 1, 13], [1443, 7, 22, 2022, 1, 24], [1443, 8, 3, 2022, 2, 7], + [1443, 8, 14, 2022, 2, 18], [1443, 8, 25, 2022, 2, 29], [1443, 9, 7, 2022, 3, 9], + [1443, 9, 18, 2022, 3, 20], [1443, 9, 29, 2022, 4, 1], [1443, 10, 10, 2022, 4, 12], + [1443, 10, 21, 2022, 4, 23], [1443, 11, 3, 2022, 5, 3], [1443, 11, 14, 2022, 5, 14], + [1443, 11, 25, 2022, 5, 25], [1443, 12, 6, 2022, 6, 6], [1443, 12, 17, 2022, 6, 17], + [1443, 12, 28, 2022, 6, 28], [1444, 1, 10, 2022, 7, 8], [1444, 1, 21, 2022, 7, 19], + [1444, 2, 2, 2022, 7, 30], [1444, 2, 13, 2022, 8, 10], [1444, 2, 24, 2022, 8, 21], + [1444, 3, 6, 2022, 9, 2], [1444, 3, 17, 2022, 9, 13], [1444, 3, 28, 2022, 9, 24], + [1444, 4, 9, 2022, 10, 4], [1444, 4, 20, 2022, 10, 15], [1444, 5, 2, 2022, 10, 26], + [1444, 6, 16, 2023, 0, 9], [1444, 6, 27, 2023, 0, 20], [1444, 7, 9, 2023, 0, 31], + [1444, 7, 20, 2023, 1, 11], [1444, 8, 1, 2023, 1, 22], [1444, 8, 12, 2023, 2, 5], + [1444, 8, 23, 2023, 2, 16], [1444, 9, 5, 2023, 2, 27], [1444, 9, 16, 2023, 3, 7], + [1444, 9, 27, 2023, 3, 18], [1444, 10, 8, 2023, 3, 29], [1444, 10, 19, 2023, 4, 10], + [1444, 11, 1, 2023, 4, 21], [1444, 11, 12, 2023, 5, 1], [1444, 11, 23, 2023, 5, 12], + [1444, 12, 4, 2023, 5, 23], [1444, 12, 15, 2023, 6, 4], [1444, 12, 26, 2023, 6, 15], + [1445, 1, 8, 2023, 6, 26], [1445, 1, 19, 2023, 7, 6], [1445, 1, 30, 2023, 7, 17], + [1445, 2, 11, 2023, 7, 28], [1445, 2, 22, 2023, 8, 8], [1445, 3, 4, 2023, 8, 19], + [1445, 3, 15, 2023, 8, 30], [1445, 3, 26, 2023, 9, 11], [1445, 4, 7, 2023, 9, 22], + [1445, 4, 18, 2023, 10, 2], [1445, 4, 29, 2023, 10, 13], [1445, 5, 11, 2023, 10, 24], + [1445, 6, 25, 2024, 0, 7], [1445, 7, 7, 2024, 0, 18], [1445, 7, 18, 2024, 0, 29], + [1445, 7, 29, 2024, 1, 9], [1445, 8, 10, 2024, 1, 20], [1445, 8, 21, 2024, 2, 2], + [1445, 9, 3, 2024, 2, 13], [1445, 9, 14, 2024, 2, 24], [1445, 9, 25, 2024, 3, 4], + [1445, 10, 6, 2024, 3, 15], [1445, 10, 17, 2024, 3, 26], [1445, 10, 28, 2024, 4, 7], + [1445, 11, 10, 2024, 4, 18], [1445, 11, 21, 2024, 4, 29], [1445, 12, 2, 2024, 5, 9], + [1445, 12, 13, 2024, 5, 20], [1445, 12, 24, 2024, 6, 1], [1446, 1, 5, 2024, 6, 12], + [1446, 1, 16, 2024, 6, 23], [1446, 1, 27, 2024, 7, 3], [1446, 2, 8, 2024, 7, 14], + [1446, 2, 19, 2024, 7, 25], [1446, 3, 1, 2024, 8, 5], [1446, 3, 12, 2024, 8, 16], + [1446, 3, 23, 2024, 8, 27], [1446, 4, 4, 2024, 9, 8], [1446, 4, 15, 2024, 9, 19], + [1446, 4, 26, 2024, 9, 30], [1446, 5, 8, 2024, 10, 10], [1446, 5, 19, 2024, 10, 21], + [1446, 7, 4, 2025, 0, 4], [1446, 7, 15, 2025, 0, 15], [1446, 7, 26, 2025, 0, 26], + [1446, 8, 7, 2025, 1, 6], [1446, 8, 18, 2025, 1, 17], [1446, 8, 29, 2025, 1, 28], + [1446, 9, 11, 2025, 2, 11], [1446, 9, 22, 2025, 2, 22], [1446, 10, 3, 2025, 3, 2], + [1446, 10, 14, 2025, 3, 13], [1446, 10, 25, 2025, 3, 24], [1446, 11, 7, 2025, 4, 5], + [1446, 11, 18, 2025, 4, 16], [1446, 11, 29, 2025, 4, 27], [1446, 12, 10, 2025, 5, 7], + [1446, 12, 21, 2025, 5, 18], [1447, 1, 3, 2025, 5, 29], [1447, 1, 14, 2025, 6, 10], + [1447, 1, 25, 2025, 6, 21], [1447, 2, 6, 2025, 7, 1], [1447, 2, 17, 2025, 7, 12], + [1447, 2, 28, 2025, 7, 23], [1447, 3, 10, 2025, 8, 3], [1447, 3, 21, 2025, 8, 14], + [1447, 4, 2, 2025, 8, 25], [1447, 4, 13, 2025, 9, 6], [1447, 4, 24, 2025, 9, 17], + [1447, 5, 6, 2025, 9, 28], [1447, 5, 17, 2025, 10, 8], [1447, 5, 28, 2025, 10, 19], + [1447, 6, 9, 2025, 10, 30], [1447, 7, 13, 2026, 0, 2], [1447, 7, 24, 2026, 0, 13], + [1447, 8, 5, 2026, 0, 24], [1447, 8, 16, 2026, 1, 4], [1447, 8, 27, 2026, 1, 15], + [1447, 9, 9, 2026, 1, 26], [1447, 9, 20, 2026, 2, 9], [1447, 10, 1, 2026, 2, 20], + [1447, 10, 12, 2026, 2, 31], [1447, 10, 23, 2026, 3, 11], [1447, 11, 5, 2026, 3, 22], + [1447, 11, 16, 2026, 4, 3], [1447, 11, 27, 2026, 4, 14], [1447, 12, 8, 2026, 4, 25], + [1447, 12, 19, 2026, 5, 5], [1447, 12, 30, 2026, 5, 16], [1448, 1, 11, 2026, 5, 27], + [1448, 1, 22, 2026, 6, 8], [1448, 2, 3, 2026, 6, 19], [1448, 2, 14, 2026, 6, 30], + [1448, 2, 25, 2026, 7, 10], [1448, 3, 7, 2026, 7, 21], [1448, 3, 18, 2026, 8, 1], + [1420, 8, 22, 1999, 11, 1], [1424, 10, 6, 2003, 11, 1], [1428, 11, 21, 2007, 11, 1], + [1433, 1, 5, 2011, 11, 1] + ]; + + const calendar = new NgbCalendarIslamicCivil(); + describe('toGregorian', () => { + it('should convert correctly from Hijri to Gregorian', () => { + DATE_TABLE.forEach(element => { + let iDate = new NgbDate(element[0], element[1], element[2]); + let gDate = new Date(element[3], element[4], element[5]); + expect(calendar.toGregorian(iDate).getTime()) + .toEqual(gDate.getTime(), `Hijri ${iDate.year}-${iDate.month}-${iDate.day} should be Gregorian ${gDate}`); + }); + }); + }); + + describe('fromGregorian', () => { + it('should convert correctly from Gregorian to Hijri', () => { + DATE_TABLE.forEach(element => { + let iDate = new NgbDate(element[0], element[1], element[2]); + const gDate = new Date(element[3], element[4], element[5]); + let iDate2 = calendar.fromGregorian(gDate); + expect(iDate2.equals(iDate)) + .toBeTruthy(`Gregorian ${gDate} should be Hijri ${iDate.year}-${iDate.month}-${iDate.day}`); + }); + }); + }); + + it('should return number of days per week', () => { expect(calendar.getDaysPerWeek()).toBe(7); }); + + it('should return number of weeks per month', () => { expect(calendar.getWeeksPerMonth()).toBe(6); }); + + it('should return months of a year', () => { + expect(calendar.getMonths()).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + }); + + it('should return day of week', () => { + expect(calendar.getWeekday(new NgbDate(1437, 12, 15))).toEqual(7); + expect(calendar.getWeekday(new NgbDate(1437, 12, 16))).toEqual(1); + expect(calendar.getWeekday(new NgbDate(1437, 12, 17))).toEqual(2); + expect(calendar.getWeekday(new NgbDate(1437, 12, 18))).toEqual(3); + expect(calendar.getWeekday(new NgbDate(1437, 12, 19))).toEqual(4); + expect(calendar.getWeekday(new NgbDate(1437, 12, 20))).toEqual(5); + expect(calendar.getWeekday(new NgbDate(1437, 12, 21))).toEqual(6); + expect(calendar.getWeekday(new NgbDate(1431, 1, 11))).toEqual(1); + expect(calendar.getWeekday(new NgbDate(1431, 7, 22))).toEqual(7); + expect(calendar.getWeekday(new NgbDate(1431, 2, 3))).toEqual(2); + expect(calendar.getWeekday(new NgbDate(1431, 3, 10))).toEqual(3); + expect(calendar.getWeekday(new NgbDate(1431, 4, 23))).toEqual(4); + expect(calendar.getWeekday(new NgbDate(1202, 2, 19))).toEqual(5); + expect(calendar.getWeekday(new NgbDate(1431, 7, 21))).toEqual(6); + }); + it('should add days to date', () => { + expect(calendar.getNext(new NgbDate(1431, 1, 30))).toEqual(new NgbDate(1431, 2, 1)); + expect(calendar.getNext(new NgbDate(1437, 2, 28))).toEqual(new NgbDate(1437, 2, 29)); + expect(calendar.getNext(new NgbDate(1437, 2, 29))).toEqual(new NgbDate(1437, 3, 1)); + }); + + it('should subtract days from date', () => { + expect(calendar.getPrev(new NgbDate(1431, 2, 1))).toEqual(new NgbDate(1431, 1, 30)); + expect(calendar.getPrev(new NgbDate(1431, 3, 1))).toEqual(new NgbDate(1431, 2, 29)); + expect(calendar.getPrev(new NgbDate(1437, 3, 5))).toEqual(new NgbDate(1437, 3, 4)); + }); + + it('should add months to date', () => { + expect(calendar.getNext(new NgbDate(1437, 8, 22), 'm')).toEqual(new NgbDate(1437, 9, 1)); + expect(calendar.getNext(new NgbDate(1437, 8, 1), 'm')).toEqual(new NgbDate(1437, 9, 1)); + expect(calendar.getNext(new NgbDate(1437, 12, 22), 'm')).toEqual(new NgbDate(1438, 1, 1)); + }); + + it('should subtract months from date', () => { + expect(calendar.getPrev(new NgbDate(1437, 8, 22), 'm')).toEqual(new NgbDate(1437, 7, 1)); + expect(calendar.getPrev(new NgbDate(1437, 9, 1), 'm')).toEqual(new NgbDate(1437, 8, 1)); + expect(calendar.getPrev(new NgbDate(1437, 1, 22), 'm')).toEqual(new NgbDate(1436, 12, 1)); + }); + + it('should add years to date', () => { + expect(calendar.getNext(new NgbDate(1437, 2, 22), 'y')).toEqual(new NgbDate(1438, 1, 1)); + expect(calendar.getNext(new NgbDate(1438, 12, 22), 'y')).toEqual(new NgbDate(1439, 1, 1)); + }); + + it('should subtract years from date', () => { + expect(calendar.getPrev(new NgbDate(1437, 12, 22), 'y')).toEqual(new NgbDate(1436, 1, 1)); + expect(calendar.getPrev(new NgbDate(1438, 2, 22), 'y')).toEqual(new NgbDate(1437, 1, 1)); + }); + + it('should return week number', () => { + let week = [ + new NgbDate(1437, 1, 4), new NgbDate(1437, 1, 5), new NgbDate(1437, 1, 6), new NgbDate(1437, 1, 7), + new NgbDate(1437, 1, 8), new NgbDate(1437, 1, 9), new NgbDate(1437, 1, 10) + ]; + expect(calendar.getWeekNumber(week, 7)).toEqual(2); + week = [ + new NgbDate(1437, 12, 15), new NgbDate(1437, 12, 16), new NgbDate(1437, 12, 17), new NgbDate(1437, 12, 18), + new NgbDate(1437, 12, 19), new NgbDate(1437, 12, 20), new NgbDate(1437, 12, 21) + ]; + expect(calendar.getWeekNumber(week, 7)).toEqual(50); + week = [ + new NgbDate(1437, 12, 22), new NgbDate(1437, 12, 23), new NgbDate(1437, 12, 24), new NgbDate(1437, 12, 25), + new NgbDate(1437, 12, 26), new NgbDate(1437, 12, 27), new NgbDate(1437, 12, 28) + ]; + expect(calendar.getWeekNumber(week, 7)).toEqual(51); + }); + + describe('setDay', () => { + it('should return correct value of day', () => { + expect(calendar.getNext(new NgbDate(1202, 9, 1), 'd', 18).day).toEqual(19); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'd', 0).day).toEqual(1); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'd', 30).day).toEqual(1); + expect(calendar.getNext(new NgbDate(1437, 1, 1), 'd', 60).day).toEqual(2); + expect(calendar.getNext(new NgbDate(1431, 2, 1), 'd', -1).day).toEqual(30); + expect(calendar.getNext(new NgbDate(1431, 2, 1), 'd', -2).day).toEqual(29); + expect(calendar.getNext(new NgbDate(1431, 2, 1), 'd', -3).day).toEqual(28); + }); + }); + + describe('setMonth', () => { + it('should return correct value of month', () => { + expect(calendar.getNext(new NgbDate(1202, 9, 1), 'm', 0).month).toEqual(9); + expect(calendar.getNext(new NgbDate(1431, 1, 30), 'm', 0).month).toEqual(1); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'd', 30).month).toEqual(2); + expect(calendar.getNext(new NgbDate(1437, 1, 1), 'd', 60).month).toEqual(3); + expect(calendar.getNext(new NgbDate(1431, 2, 1), 'd', -2).month).toEqual(1); + expect(calendar.getNext(new NgbDate(1431, 2, 1), 'd', -31).month).toEqual(12); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'm', -1).month).toEqual(12); + }); + }); + + describe('setYear', () => { + it('should return correct value of yar', () => { + expect(calendar.getNext(new NgbDate(1200, 1, 1), 'y', 2).year).toEqual(1202); + expect(calendar.getNext(new NgbDate(1430, 11, 30), 'y', 1).year).toEqual(1431); + expect(calendar.getNext(new NgbDate(1431, 12, 1), 'd', 30).year).toEqual(1432); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'm', 12).year).toEqual(1432); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'm', 24).year).toEqual(1433); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'd', -2).year).toEqual(1430); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'm', -1).year).toEqual(1430); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'm', -14).year).toEqual(1429); + }); + }); +}); diff --git a/src/datepicker/hijri/ngb-calendar-islamic-civil.ts b/src/datepicker/hijri/ngb-calendar-islamic-civil.ts new file mode 100644 index 0000000..39e71a4 --- /dev/null +++ b/src/datepicker/hijri/ngb-calendar-islamic-civil.ts @@ -0,0 +1,132 @@ +import {NgbCalendarHijri} from './ngb-calendar-hijri'; +import {NgbDate} from '../ngb-date'; +import {Injectable} from '@angular/core'; + +/** + * Checks if islamic year is a leap year + */ +function isIslamicLeapYear(hYear: number): boolean { + return (14 + 11 * hYear) % 30 < 11; +} + +/** + * Checks if gregorian years is a leap year + */ +function isGregorianLeapYear(gDate: Date): boolean { + const year = gDate.getFullYear(); + return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0; +} + +/** + * Returns the start of Hijri Month. + * `hMonth` is 0 for Muharram, 1 for Safar, etc. + * `hYear` is any Hijri hYear. + */ +function getIslamicMonthStart(hYear: number, hMonth: number): number { + return Math.ceil(29.5 * hMonth) + (hYear - 1) * 354 + Math.floor((3 + 11 * hYear) / 30.0); +} + +/** + * Returns the start of Hijri year. + * `year` is any Hijri year. + */ +function getIslamicYearStart(year: number): number { + return (year - 1) * 354 + Math.floor((3 + 11 * year) / 30.0); +} + +function mod(a: number, b: number): number { + return a - b * Math.floor(a / b); +} + +/** + * The civil calendar is one type of Hijri calendars used in islamic countries. + * Uses a fixed cycle of alternating 29- and 30-day months, + * with a leap day added to the last month of 11 out of every 30 years. + * http://cldr.unicode.org/development/development-process/design-proposals/islamic-calendar-types + * All the calculations here are based on the equations from "Calendrical Calculations" By Edward M. Reingold, Nachum + * Dershowitz. + */ + +const GREGORIAN_EPOCH = 1721425.5; +const ISLAMIC_EPOCH = 1948439.5; + +@Injectable() +export class NgbCalendarIslamicCivil extends NgbCalendarHijri { + /** + * Returns the equivalent islamic(civil) date value for a give input Gregorian date. + * `gDate` is a JS Date to be converted to Hijri. + */ + fromGregorian(gDate: Date): NgbDate { + const gYear = gDate.getFullYear(), gMonth = gDate.getMonth(), gDay = gDate.getDate(); + + let julianDay = GREGORIAN_EPOCH - 1 + 365 * (gYear - 1) + Math.floor((gYear - 1) / 4) + + -Math.floor((gYear - 1) / 100) + Math.floor((gYear - 1) / 400) + + Math.floor( + (367 * (gMonth + 1) - 362) / 12 + (gMonth + 1 <= 2 ? 0 : isGregorianLeapYear(gDate) ? -1 : -2) + gDay); + julianDay = Math.floor(julianDay) + 0.5; + + const days = julianDay - ISLAMIC_EPOCH; + const hYear = Math.floor((30 * days + 10646) / 10631.0); + let hMonth = Math.ceil((days - 29 - getIslamicYearStart(hYear)) / 29.5); + hMonth = Math.min(hMonth, 11); + const hDay = Math.ceil(days - getIslamicMonthStart(hYear, hMonth)) + 1; + return new NgbDate(hYear, hMonth + 1, hDay); + } + + /** + * Returns the equivalent JS date value for a give input islamic(civil) date. + * `hDate` is an islamic(civil) date to be converted to Gregorian. + */ + toGregorian(hDate: NgbDate): Date { + const hYear = hDate.year; + const hMonth = hDate.month - 1; + const hDay = hDate.day; + const julianDay = + hDay + Math.ceil(29.5 * hMonth) + (hYear - 1) * 354 + Math.floor((3 + 11 * hYear) / 30) + ISLAMIC_EPOCH - 1; + + const wjd = Math.floor(julianDay - 0.5) + 0.5, depoch = wjd - GREGORIAN_EPOCH, + quadricent = Math.floor(depoch / 146097), dqc = mod(depoch, 146097), cent = Math.floor(dqc / 36524), + dcent = mod(dqc, 36524), quad = Math.floor(dcent / 1461), dquad = mod(dcent, 1461), + yindex = Math.floor(dquad / 365); + let year = quadricent * 400 + cent * 100 + quad * 4 + yindex; + if (!(cent === 4 || yindex === 4)) { + year++; + } + + const gYearStart = GREGORIAN_EPOCH + 365 * (year - 1) + Math.floor((year - 1) / 4) - Math.floor((year - 1) / 100) + + Math.floor((year - 1) / 400); + + const yearday = wjd - gYearStart; + + const tjd = GREGORIAN_EPOCH - 1 + 365 * (year - 1) + Math.floor((year - 1) / 4) - Math.floor((year - 1) / 100) + + Math.floor((year - 1) / 400) + Math.floor(739 / 12 + (isGregorianLeapYear(new Date(year, 3, 1)) ? -1 : -2) + 1); + + const leapadj = wjd < tjd ? 0 : isGregorianLeapYear(new Date(year, 3, 1)) ? 1 : 2; + + const month = Math.floor(((yearday + leapadj) * 12 + 373) / 367); + const tjd2 = GREGORIAN_EPOCH - 1 + 365 * (year - 1) + Math.floor((year - 1) / 4) - Math.floor((year - 1) / 100) + + Math.floor((year - 1) / 400) + + Math.floor( + (367 * month - 362) / 12 + (month <= 2 ? 0 : isGregorianLeapYear(new Date(year, month - 1, 1)) ? -1 : -2) + + 1); + + const day = wjd - tjd2 + 1; + + return new Date(year, month - 1, day); + } + + /** + * Returns the number of days in a specific Hijri month. + * `month` is 1 for Muharram, 2 for Safar, etc. + * `year` is any Hijri year. + */ + getDaysPerMonth(month: number, year: number): number { + year = year + Math.floor(month / 13); + month = ((month - 1) % 12) + 1; + let length = 29 + month % 2; + if (month === 12 && isIslamicLeapYear(year)) { + length++; + } + return length; + } +} diff --git a/src/datepicker/hijri/ngb-calendar-islamic-umalqura.spec.ts b/src/datepicker/hijri/ngb-calendar-islamic-umalqura.spec.ts new file mode 100644 index 0000000..c989832 --- /dev/null +++ b/src/datepicker/hijri/ngb-calendar-islamic-umalqura.spec.ts @@ -0,0 +1,1007 @@ +import {NgbCalendarIslamicUmalqura} from './ngb-calendar-islamic-umalqura'; +import {NgbDate} from '../ngb-date'; + +describe('ngb-calendar-islamic-umalqura', () => { + const DATE_TABLE = [ + [1882, 10, 12, 1300, 1, 1], [1882, 10, 14, 1300, 1, 3], [1883, 5, 20, 1300, 8, 14], + [1883, 5, 22, 1300, 8, 16], [1884, 0, 26, 1301, 3, 27], [1884, 0, 28, 1301, 3, 29], + [1884, 8, 2, 1301, 11, 11], [1884, 8, 4, 1301, 11, 13], [1885, 3, 10, 1302, 6, 24], + [1885, 3, 12, 1302, 6, 26], [1885, 10, 16, 1303, 2, 9], [1885, 10, 18, 1303, 2, 11], + [1886, 5, 24, 1303, 9, 21], [1886, 5, 26, 1303, 9, 23], [1887, 0, 30, 1304, 5, 6], + [1887, 1, 1, 1304, 5, 8], [1887, 8, 7, 1304, 12, 18], [1887, 8, 9, 1304, 12, 20], + [1888, 3, 14, 1305, 8, 2], [1888, 3, 16, 1305, 8, 4], [1888, 10, 20, 1306, 3, 16], + [1888, 10, 22, 1306, 3, 18], [1889, 5, 28, 1306, 10, 29], [1889, 5, 30, 1306, 11, 1], + [1890, 1, 3, 1307, 6, 13], [1890, 1, 5, 1307, 6, 15], [1890, 8, 11, 1308, 1, 26], + [1890, 8, 13, 1308, 1, 28], [1891, 3, 19, 1308, 9, 10], [1891, 3, 21, 1308, 9, 12], + [1891, 10, 25, 1309, 4, 23], [1891, 10, 27, 1309, 4, 25], [1892, 6, 2, 1309, 12, 7], + [1892, 6, 4, 1309, 12, 9], [1893, 1, 7, 1310, 7, 20], [1893, 1, 9, 1310, 7, 22], + [1893, 8, 15, 1311, 3, 4], [1893, 8, 17, 1311, 3, 6], [1894, 3, 23, 1311, 10, 17], + [1894, 3, 25, 1311, 10, 19], [1894, 10, 29, 1312, 6, 1], [1894, 11, 1, 1312, 6, 3], + [1895, 6, 7, 1313, 1, 14], [1895, 6, 9, 1313, 1, 16], [1896, 1, 12, 1313, 8, 28], + [1896, 1, 14, 1313, 8, 30], [1896, 8, 19, 1314, 4, 11], [1896, 8, 21, 1314, 4, 13], + [1897, 3, 27, 1314, 11, 24], [1897, 3, 29, 1314, 11, 26], [1897, 11, 3, 1315, 7, 8], + [1897, 11, 5, 1315, 7, 10], [1898, 6, 11, 1316, 2, 22], [1898, 6, 13, 1316, 2, 24], + [1899, 1, 16, 1316, 10, 5], [1899, 1, 18, 1316, 10, 7], [1899, 8, 24, 1317, 5, 18], + [1899, 8, 26, 1317, 5, 20], [1900, 4, 2, 1318, 1, 3], [1900, 4, 4, 1318, 1, 5], + [1900, 11, 8, 1318, 8, 15], [1900, 11, 10, 1318, 8, 17], [1901, 6, 16, 1319, 3, 29], + [1901, 6, 18, 1319, 4, 2], [1902, 1, 21, 1319, 11, 12], [1902, 1, 23, 1319, 11, 14], + [1902, 8, 29, 1320, 6, 26], [1902, 9, 1, 1320, 6, 28], [1903, 4, 7, 1321, 2, 9], + [1903, 4, 9, 1321, 2, 11], [1903, 11, 13, 1321, 9, 24], [1903, 11, 15, 1321, 9, 26], + [1904, 6, 20, 1322, 5, 6], [1904, 6, 22, 1322, 5, 8], [1905, 1, 25, 1322, 12, 20], + [1905, 1, 27, 1322, 12, 22], [1905, 9, 3, 1323, 8, 4], [1905, 9, 5, 1323, 8, 6], + [1906, 4, 11, 1324, 3, 17], [1906, 4, 13, 1324, 3, 19], [1906, 11, 17, 1324, 11, 1], + [1906, 11, 19, 1324, 11, 3], [1907, 6, 25, 1325, 6, 14], [1907, 6, 27, 1325, 6, 16], + [1908, 2, 1, 1326, 1, 27], [1908, 2, 3, 1326, 1, 29], [1908, 9, 7, 1326, 9, 11], + [1908, 9, 9, 1326, 9, 13], [1909, 4, 15, 1327, 4, 25], [1909, 4, 17, 1327, 4, 27], + [1909, 11, 21, 1327, 12, 8], [1909, 11, 23, 1327, 12, 10], [1910, 6, 29, 1328, 7, 22], + [1910, 6, 31, 1328, 7, 24], [1911, 2, 6, 1329, 3, 5], [1911, 2, 8, 1329, 3, 7], + [1911, 9, 12, 1329, 10, 19], [1911, 9, 14, 1329, 10, 21], [1912, 4, 19, 1330, 6, 2], + [1912, 4, 21, 1330, 6, 4], [1912, 11, 25, 1331, 1, 16], [1912, 11, 27, 1331, 1, 18], + [1913, 7, 2, 1331, 8, 29], [1913, 7, 4, 1331, 9, 1], [1914, 2, 10, 1332, 4, 13], + [1914, 2, 12, 1332, 4, 15], [1914, 9, 16, 1332, 11, 25], [1914, 9, 18, 1332, 11, 27], + [1915, 4, 24, 1333, 7, 10], [1915, 4, 26, 1333, 7, 12], [1915, 11, 30, 1334, 2, 23], + [1916, 0, 1, 1334, 2, 25], [1916, 7, 6, 1334, 10, 6], [1916, 7, 8, 1334, 10, 8], + [1917, 2, 14, 1335, 5, 20], [1917, 2, 16, 1335, 5, 22], [1917, 9, 20, 1336, 1, 3], + [1917, 9, 22, 1336, 1, 5], [1918, 4, 28, 1336, 8, 17], [1918, 4, 30, 1336, 8, 19], + [1919, 0, 3, 1337, 3, 30], [1919, 0, 5, 1337, 4, 2], [1919, 7, 11, 1337, 11, 14], + [1919, 7, 13, 1337, 11, 16], [1920, 2, 18, 1338, 6, 27], [1920, 2, 20, 1338, 6, 29], + [1920, 9, 24, 1339, 2, 11], [1920, 9, 26, 1339, 2, 13], [1921, 5, 1, 1339, 9, 24], + [1921, 5, 3, 1339, 9, 26], [1922, 0, 7, 1340, 5, 9], [1922, 0, 9, 1340, 5, 11], + [1922, 7, 15, 1340, 12, 21], [1922, 7, 17, 1340, 12, 23], [1923, 2, 23, 1341, 8, 5], + [1923, 2, 25, 1341, 8, 7], [1923, 9, 29, 1342, 3, 19], [1923, 9, 31, 1342, 3, 21], + [1924, 5, 5, 1342, 11, 2], [1924, 5, 7, 1342, 11, 4], [1925, 0, 11, 1343, 6, 16], + [1925, 0, 13, 1343, 6, 18], [1925, 7, 19, 1344, 1, 29], [1925, 7, 21, 1344, 2, 1], + [1926, 2, 27, 1344, 9, 13], [1926, 2, 29, 1344, 9, 15], [1926, 10, 2, 1345, 4, 26], + [1926, 10, 4, 1345, 4, 28], [1927, 5, 10, 1345, 12, 10], [1927, 5, 12, 1345, 12, 12], + [1928, 0, 16, 1346, 7, 22], [1928, 0, 18, 1346, 7, 24], [1928, 7, 23, 1347, 3, 7], + [1928, 7, 25, 1347, 3, 9], [1929, 2, 31, 1347, 10, 19], [1929, 3, 2, 1347, 10, 21], + [1929, 10, 6, 1348, 6, 4], [1929, 10, 8, 1348, 6, 6], [1930, 5, 14, 1349, 1, 17], + [1930, 5, 16, 1349, 1, 19], [1931, 0, 20, 1349, 9, 1], [1931, 0, 22, 1349, 9, 3], + [1931, 7, 28, 1350, 4, 14], [1931, 7, 30, 1350, 4, 16], [1932, 3, 4, 1350, 11, 27], + [1932, 3, 6, 1350, 11, 29], [1932, 10, 10, 1351, 7, 11], [1932, 10, 12, 1351, 7, 13], + [1933, 5, 18, 1352, 2, 24], [1933, 5, 20, 1352, 2, 26], [1934, 0, 24, 1352, 10, 8], + [1934, 0, 26, 1352, 10, 10], [1934, 8, 1, 1353, 5, 21], [1934, 8, 3, 1353, 5, 23], + [1935, 3, 9, 1354, 1, 5], [1935, 3, 11, 1354, 1, 7], [1935, 10, 15, 1354, 8, 18], + [1935, 10, 17, 1354, 8, 20], [1936, 5, 22, 1355, 4, 3], [1936, 5, 24, 1355, 4, 5], + [1937, 0, 28, 1355, 11, 15], [1937, 0, 30, 1355, 11, 17], [1937, 8, 5, 1356, 6, 29], + [1937, 8, 7, 1356, 7, 1], [1938, 3, 13, 1357, 2, 12], [1938, 3, 15, 1357, 2, 14], + [1938, 10, 19, 1357, 9, 26], [1938, 10, 21, 1357, 9, 28], [1939, 5, 27, 1358, 5, 9], + [1939, 5, 29, 1358, 5, 11], [1940, 1, 2, 1358, 12, 23], [1940, 1, 4, 1358, 12, 25], + [1940, 8, 9, 1359, 8, 6], [1940, 8, 11, 1359, 8, 8], [1941, 3, 17, 1360, 3, 20], + [1941, 3, 19, 1360, 3, 22], [1941, 10, 23, 1360, 11, 4], [1941, 10, 25, 1360, 11, 6], + [1942, 6, 1, 1361, 6, 17], [1942, 6, 3, 1361, 6, 19], [1943, 1, 6, 1362, 2, 1], + [1943, 1, 8, 1362, 2, 3], [1943, 8, 14, 1362, 9, 14], [1943, 8, 16, 1362, 9, 16], + [1944, 3, 21, 1363, 4, 27], [1944, 3, 23, 1363, 4, 29], [1944, 10, 27, 1363, 12, 11], + [1944, 10, 29, 1363, 12, 13], [1945, 6, 5, 1364, 7, 25], [1945, 6, 7, 1364, 7, 27], + [1946, 1, 10, 1365, 3, 7], [1946, 1, 12, 1365, 3, 9], [1946, 8, 18, 1365, 10, 22], + [1946, 8, 20, 1365, 10, 24], [1947, 3, 26, 1366, 6, 5], [1947, 3, 28, 1366, 6, 7], + [1947, 11, 2, 1367, 1, 19], [1947, 11, 4, 1367, 1, 21], [1948, 6, 9, 1367, 9, 2], + [1948, 6, 11, 1367, 9, 4], [1949, 1, 14, 1368, 4, 16], [1949, 1, 16, 1368, 4, 18], + [1949, 8, 22, 1368, 11, 29], [1949, 8, 24, 1368, 12, 1], [1950, 3, 30, 1369, 7, 12], + [1950, 4, 2, 1369, 7, 14], [1950, 11, 6, 1370, 2, 25], [1950, 11, 8, 1370, 2, 27], + [1951, 6, 14, 1370, 10, 10], [1951, 6, 16, 1370, 10, 12], [1952, 1, 19, 1371, 5, 23], + [1952, 1, 21, 1371, 5, 25], [1952, 8, 26, 1372, 1, 6], [1952, 8, 28, 1372, 1, 8], + [1953, 4, 4, 1372, 8, 20], [1953, 4, 6, 1372, 8, 22], [1953, 11, 10, 1373, 4, 3], + [1953, 11, 12, 1373, 4, 5], [1954, 6, 18, 1373, 11, 17], [1954, 6, 20, 1373, 11, 19], + [1955, 1, 23, 1374, 6, 30], [1955, 1, 25, 1374, 7, 2], [1955, 9, 1, 1375, 2, 14], + [1955, 9, 3, 1375, 2, 16], [1956, 4, 8, 1375, 9, 27], [1956, 4, 10, 1375, 9, 29], + [1956, 11, 14, 1376, 5, 11], [1956, 11, 16, 1376, 5, 13], [1957, 6, 22, 1376, 12, 24], + [1957, 6, 24, 1376, 12, 26], [1958, 1, 27, 1377, 8, 8], [1958, 2, 1, 1377, 8, 10], + [1958, 9, 5, 1378, 3, 21], [1958, 9, 7, 1378, 3, 23], [1959, 4, 13, 1378, 11, 5], + [1959, 4, 15, 1378, 11, 7], [1959, 11, 19, 1379, 6, 19], [1959, 11, 21, 1379, 6, 21], + [1960, 6, 26, 1380, 2, 2], [1960, 6, 28, 1380, 2, 4], [1961, 2, 3, 1380, 9, 15], + [1961, 2, 5, 1380, 9, 17], [1961, 9, 9, 1381, 4, 29], [1961, 9, 11, 1381, 5, 1], + [1962, 4, 17, 1381, 12, 13], [1962, 4, 19, 1381, 12, 15], [1962, 11, 23, 1382, 7, 26], + [1962, 11, 25, 1382, 7, 28], [1963, 6, 31, 1383, 3, 10], [1963, 7, 2, 1383, 3, 12], + [1964, 2, 7, 1383, 10, 22], [1964, 2, 9, 1383, 10, 24], [1964, 9, 13, 1384, 6, 7], + [1964, 9, 15, 1384, 6, 9], [1965, 4, 21, 1385, 1, 19], [1965, 4, 23, 1385, 1, 21], + [1965, 11, 27, 1385, 9, 4], [1965, 11, 29, 1385, 9, 6], [1966, 7, 4, 1386, 4, 17], + [1966, 7, 6, 1386, 4, 19], [1967, 2, 12, 1386, 11, 30], [1967, 2, 14, 1386, 12, 2], + [1967, 9, 18, 1387, 7, 14], [1967, 9, 20, 1387, 7, 16], [1968, 4, 25, 1388, 2, 27], + [1968, 4, 27, 1388, 2, 29], [1968, 11, 31, 1388, 10, 11], [1969, 0, 2, 1388, 10, 13], + [1969, 7, 8, 1389, 5, 24], [1969, 7, 10, 1389, 5, 26], [1970, 2, 16, 1390, 1, 8], + [1970, 2, 18, 1390, 1, 10], [1970, 9, 22, 1390, 8, 21], [1970, 9, 24, 1390, 8, 23], + [1971, 4, 30, 1391, 4, 5], [1971, 5, 1, 1391, 4, 7], [1972, 0, 5, 1391, 11, 18], + [1972, 0, 7, 1391, 11, 20], [1972, 7, 12, 1392, 7, 2], [1972, 7, 14, 1392, 7, 4], + [1973, 2, 20, 1393, 2, 15], [1973, 2, 22, 1393, 2, 17], [1973, 9, 26, 1393, 9, 29], + [1973, 9, 28, 1393, 10, 1], [1974, 5, 3, 1394, 5, 12], [1974, 5, 5, 1394, 5, 14], + [1975, 0, 9, 1394, 12, 26], [1975, 0, 11, 1394, 12, 28], [1975, 7, 17, 1395, 8, 9], + [1975, 7, 19, 1395, 8, 11], [1976, 2, 24, 1396, 3, 23], [1976, 2, 26, 1396, 3, 25], + [1976, 9, 30, 1396, 11, 6], [1976, 10, 1, 1396, 11, 8], [1977, 5, 7, 1397, 6, 20], + [1977, 5, 9, 1397, 6, 22], [1978, 0, 13, 1398, 2, 4], [1978, 0, 15, 1398, 2, 6], + [1978, 7, 21, 1398, 9, 16], [1978, 7, 23, 1398, 9, 18], [1979, 2, 29, 1399, 5, 1], + [1979, 2, 31, 1399, 5, 3], [1979, 10, 4, 1399, 12, 14], [1979, 10, 6, 1399, 12, 16], + [1980, 5, 11, 1400, 7, 27], [1980, 5, 13, 1400, 7, 29], [1981, 0, 17, 1401, 3, 10], + [1981, 0, 19, 1401, 3, 12], [1981, 7, 25, 1401, 10, 25], [1981, 7, 27, 1401, 10, 27], + [1982, 3, 2, 1402, 6, 7], [1982, 3, 4, 1402, 6, 9], [1982, 10, 8, 1403, 1, 22], + [1982, 10, 10, 1403, 1, 24], [1983, 5, 16, 1403, 9, 5], [1983, 5, 18, 1403, 9, 7], + [1984, 0, 22, 1404, 4, 18], [1984, 0, 24, 1404, 4, 20], [1984, 7, 29, 1404, 12, 2], + [1984, 7, 31, 1404, 12, 4], [1985, 3, 6, 1405, 7, 15], [1985, 3, 8, 1405, 7, 17], + [1985, 10, 12, 1406, 2, 28], [1985, 10, 14, 1406, 3, 1], [1986, 5, 20, 1406, 10, 12], + [1986, 5, 22, 1406, 10, 14], [1987, 0, 26, 1407, 5, 26], [1987, 0, 28, 1407, 5, 28], + [1987, 8, 3, 1408, 1, 9], [1987, 8, 5, 1408, 1, 11], [1988, 3, 10, 1408, 8, 23], + [1988, 3, 12, 1408, 8, 25], [1988, 10, 16, 1409, 4, 6], [1988, 10, 18, 1409, 4, 8], + [1989, 5, 24, 1409, 11, 20], [1989, 5, 26, 1409, 11, 22], [1990, 0, 30, 1410, 7, 3], + [1990, 1, 1, 1410, 7, 5], [1990, 8, 7, 1411, 2, 17], [1990, 8, 9, 1411, 2, 19], + [1991, 3, 15, 1411, 9, 29], [1991, 3, 17, 1411, 10, 2], [1991, 10, 21, 1412, 5, 14], + [1991, 10, 23, 1412, 5, 16], [1992, 5, 28, 1412, 12, 27], [1992, 5, 30, 1412, 12, 29], + [1993, 1, 3, 1413, 8, 11], [1993, 1, 5, 1413, 8, 13], [1993, 8, 11, 1414, 3, 24], + [1993, 8, 13, 1414, 3, 26], [1994, 3, 19, 1414, 11, 8], [1994, 3, 21, 1414, 11, 10], + [1994, 10, 25, 1415, 6, 21], [1994, 10, 27, 1415, 6, 23], [1995, 6, 3, 1416, 2, 4], + [1995, 6, 5, 1416, 2, 6], [1996, 1, 8, 1416, 9, 18], [1996, 1, 10, 1416, 9, 20], + [1996, 8, 15, 1417, 5, 1], [1996, 8, 17, 1417, 5, 3], [1997, 3, 23, 1417, 12, 15], + [1997, 3, 25, 1417, 12, 17], [1997, 10, 29, 1418, 7, 28], [1997, 11, 1, 1418, 8, 1], + [1998, 6, 7, 1419, 3, 12], [1998, 6, 9, 1419, 3, 14], [1999, 1, 12, 1419, 10, 25], + [1999, 1, 14, 1419, 10, 27], [1999, 8, 20, 1420, 6, 10], [1999, 8, 22, 1420, 6, 12], + [2000, 3, 27, 1421, 1, 22], [2000, 3, 29, 1421, 1, 24], [2000, 11, 3, 1421, 9, 7], + [2000, 11, 5, 1421, 9, 9], [2001, 6, 11, 1422, 4, 20], [2001, 6, 13, 1422, 4, 22], + [2002, 1, 16, 1422, 12, 4], [2002, 1, 18, 1422, 12, 6], [2002, 8, 24, 1423, 7, 17], + [2002, 8, 26, 1423, 7, 19], [2003, 4, 2, 1424, 3, 1], [2003, 4, 4, 1424, 3, 3], + [2003, 11, 8, 1424, 10, 14], [2003, 11, 10, 1424, 10, 16], [2004, 6, 15, 1425, 5, 27], + [2004, 6, 17, 1425, 5, 29], [2005, 1, 20, 1426, 1, 11], [2005, 1, 22, 1426, 1, 13], + [2005, 8, 28, 1426, 8, 24], [2005, 8, 30, 1426, 8, 26], [2006, 4, 6, 1427, 4, 8], + [2006, 4, 8, 1427, 4, 10], [2006, 11, 12, 1427, 11, 21], [2006, 11, 14, 1427, 11, 23], + [2007, 6, 20, 1428, 7, 6], [2007, 6, 22, 1428, 7, 8], [2008, 1, 25, 1429, 2, 18], + [2008, 1, 27, 1429, 2, 20], [2008, 9, 2, 1429, 10, 2], [2008, 9, 4, 1429, 10, 4], + [2009, 4, 10, 1430, 5, 15], [2009, 4, 12, 1430, 5, 17], [2009, 11, 16, 1430, 12, 29], + [2009, 11, 18, 1431, 1, 1], [2010, 6, 24, 1431, 8, 12], [2010, 6, 26, 1431, 8, 14], + [2011, 2, 1, 1432, 3, 26], [2011, 2, 3, 1432, 3, 28], [2011, 9, 7, 1432, 11, 9], + [2011, 9, 9, 1432, 11, 11], [2012, 4, 14, 1433, 6, 23], [2012, 4, 16, 1433, 6, 25], + [2012, 11, 20, 1434, 2, 7], [2012, 11, 22, 1434, 2, 9], [2013, 6, 28, 1434, 9, 20], + [2013, 6, 30, 1434, 9, 22], [2014, 2, 5, 1435, 5, 4], [2014, 2, 7, 1435, 5, 6], + [2014, 9, 11, 1435, 12, 17], [2014, 9, 13, 1435, 12, 19], [2015, 4, 19, 1436, 8, 1], + [2015, 4, 21, 1436, 8, 3], [2015, 11, 25, 1437, 3, 14], [2015, 11, 27, 1437, 3, 16], + [2016, 7, 1, 1437, 10, 27], [2016, 7, 3, 1437, 10, 29], [2017, 2, 9, 1438, 6, 10], + [2017, 2, 11, 1438, 6, 12], [2017, 9, 15, 1439, 1, 25], [2017, 9, 17, 1439, 1, 27], + [2018, 4, 23, 1439, 9, 8], [2018, 4, 25, 1439, 9, 10], [2018, 11, 29, 1440, 4, 22], + [2018, 11, 31, 1440, 4, 24], [2019, 7, 6, 1440, 12, 5], [2019, 7, 8, 1440, 12, 7], + [2020, 2, 13, 1441, 7, 18], [2020, 2, 15, 1441, 7, 20], [2020, 9, 19, 1442, 3, 2], + [2020, 9, 21, 1442, 3, 4], [2021, 4, 27, 1442, 10, 15], [2021, 4, 29, 1442, 10, 17], + [2022, 0, 2, 1443, 5, 29], [2022, 0, 4, 1443, 6, 1], [2022, 7, 10, 1444, 1, 12], + [2022, 7, 12, 1444, 1, 14], [2023, 2, 18, 1444, 8, 26], [2023, 2, 20, 1444, 8, 28], + [2023, 9, 24, 1445, 4, 9], [2023, 9, 26, 1445, 4, 11], [2024, 4, 31, 1445, 11, 23], + [2024, 5, 2, 1445, 11, 25], [2025, 0, 6, 1446, 7, 6], [2025, 0, 8, 1446, 7, 8], + [2025, 7, 14, 1447, 2, 20], [2025, 7, 16, 1447, 2, 22], [2026, 2, 22, 1447, 10, 3], + [2026, 2, 24, 1447, 10, 5], [2026, 9, 28, 1448, 5, 17], [2026, 9, 30, 1448, 5, 19], + [2027, 5, 5, 1448, 12, 30], [2027, 5, 7, 1449, 1, 2], [2028, 0, 11, 1449, 8, 14], + [2028, 0, 13, 1449, 8, 16], [2028, 7, 18, 1450, 3, 27], [2028, 7, 20, 1450, 3, 29], + [2029, 2, 26, 1450, 11, 11], [2029, 2, 28, 1450, 11, 13], [2029, 10, 1, 1451, 6, 24], + [2029, 10, 3, 1451, 6, 26], [2030, 5, 9, 1452, 2, 7], [2030, 5, 11, 1452, 2, 9], + [2031, 0, 15, 1452, 9, 21], [2031, 0, 17, 1452, 9, 23], [2031, 7, 23, 1453, 5, 4], + [2031, 7, 25, 1453, 5, 6], [2032, 2, 30, 1453, 12, 18], [2032, 3, 1, 1453, 12, 20], + [2032, 10, 5, 1454, 8, 1], [2032, 10, 7, 1454, 8, 3], [2033, 5, 13, 1455, 3, 15], + [2033, 5, 15, 1455, 3, 17], [2034, 0, 19, 1455, 10, 28], [2034, 0, 21, 1455, 10, 30], + [2034, 7, 27, 1456, 6, 12], [2034, 7, 29, 1456, 6, 14], [2035, 3, 4, 1457, 1, 25], + [2035, 3, 6, 1457, 1, 27], [2035, 10, 10, 1457, 9, 10], [2035, 10, 12, 1457, 9, 12], + [2036, 5, 17, 1458, 4, 22], [2036, 5, 19, 1458, 4, 24], [2037, 0, 23, 1458, 12, 6], + [2037, 0, 25, 1458, 12, 8], [2037, 7, 31, 1459, 7, 19], [2037, 8, 2, 1459, 7, 21], + [2038, 3, 8, 1460, 3, 3], [2038, 3, 10, 1460, 3, 5], [2038, 10, 14, 1460, 10, 17], + [2038, 10, 16, 1460, 10, 19], [2039, 5, 22, 1461, 5, 30], [2039, 5, 24, 1461, 6, 2], + [2040, 0, 28, 1462, 1, 14], [2040, 0, 30, 1462, 1, 16], [2040, 8, 4, 1462, 8, 27], + [2040, 8, 6, 1462, 8, 29], [2041, 3, 12, 1463, 4, 11], [2041, 3, 14, 1463, 4, 13], + [2041, 10, 18, 1463, 11, 23], [2041, 10, 20, 1463, 11, 25], [2042, 5, 26, 1464, 7, 8], + [2042, 5, 28, 1464, 7, 10], [2043, 1, 1, 1465, 2, 20], [2043, 1, 3, 1465, 2, 22], + [2043, 8, 9, 1465, 10, 5], [2043, 8, 11, 1465, 10, 7], [2044, 3, 16, 1466, 5, 17], + [2044, 3, 18, 1466, 5, 19], [2044, 10, 22, 1467, 1, 2], [2044, 10, 24, 1467, 1, 4], + [2045, 5, 30, 1467, 8, 15], [2045, 6, 2, 1467, 8, 17], [2046, 1, 5, 1468, 3, 28], + [2046, 1, 7, 1468, 4, 1], [2046, 8, 13, 1468, 11, 12], [2046, 8, 15, 1468, 11, 14], + [2047, 3, 21, 1469, 6, 25], [2047, 3, 23, 1469, 6, 27], [2047, 10, 27, 1470, 2, 9], + [2047, 10, 29, 1470, 2, 11], [2048, 6, 4, 1470, 9, 22], [2048, 6, 6, 1470, 9, 24], + [2049, 1, 9, 1471, 5, 6], [2049, 1, 11, 1471, 5, 8], [2049, 8, 17, 1471, 12, 19], + [2049, 8, 19, 1471, 12, 21], [2050, 3, 25, 1472, 8, 3], [2050, 3, 27, 1472, 8, 5], + [2050, 11, 1, 1473, 3, 16], [2050, 11, 3, 1473, 3, 18], [2051, 6, 9, 1473, 10, 30], + [2051, 6, 11, 1473, 11, 2], [2052, 1, 14, 1474, 6, 13], [2052, 1, 16, 1474, 6, 15], + [2052, 8, 21, 1475, 1, 27], [2052, 8, 23, 1475, 1, 29], [2053, 3, 29, 1475, 9, 10], + [2053, 4, 1, 1475, 9, 12], [2053, 11, 5, 1476, 4, 24], [2053, 11, 7, 1476, 4, 26], + [2054, 6, 13, 1476, 12, 7], [2054, 6, 15, 1476, 12, 9], [2055, 1, 18, 1477, 7, 21], + [2055, 1, 20, 1477, 7, 23], [2055, 8, 26, 1478, 3, 5], [2055, 8, 28, 1478, 3, 7], + [2056, 4, 3, 1478, 10, 18], [2056, 4, 5, 1478, 10, 20], [2056, 11, 9, 1479, 6, 2], + [2056, 11, 11, 1479, 6, 4], [2057, 6, 17, 1480, 1, 15], [2057, 6, 19, 1480, 1, 17], + [2058, 1, 22, 1480, 8, 28], [2058, 1, 24, 1480, 9, 1], [2058, 8, 30, 1481, 4, 12], + [2058, 9, 2, 1481, 4, 14], [2059, 4, 8, 1481, 11, 25], [2059, 4, 10, 1481, 11, 27], + [2059, 11, 14, 1482, 7, 8], [2059, 11, 16, 1482, 7, 10], [2060, 6, 21, 1483, 2, 23], + [2060, 6, 23, 1483, 2, 25], [2061, 1, 26, 1483, 10, 5], [2061, 1, 28, 1483, 10, 7], + [2061, 9, 4, 1484, 5, 20], [2061, 9, 6, 1484, 5, 22], [2062, 4, 12, 1485, 1, 3], + [2062, 4, 14, 1485, 1, 5], [2062, 11, 18, 1485, 8, 16], [2062, 11, 20, 1485, 8, 18], + [2063, 6, 26, 1486, 3, 29], [2063, 6, 28, 1486, 4, 2], [2064, 2, 2, 1486, 11, 13], + [2064, 2, 4, 1486, 11, 15], [2064, 9, 8, 1487, 6, 26], [2064, 9, 10, 1487, 6, 28], + [2065, 4, 16, 1488, 2, 10], [2065, 4, 18, 1488, 2, 12], [2065, 11, 22, 1488, 9, 24], + [2065, 11, 24, 1488, 9, 26], [2066, 6, 30, 1489, 5, 7], [2066, 7, 1, 1489, 5, 9], + [2067, 2, 7, 1489, 12, 21], [2067, 2, 9, 1489, 12, 23], [2067, 9, 13, 1490, 8, 4], + [2067, 9, 15, 1490, 8, 6], [2068, 4, 20, 1491, 3, 18], [2068, 4, 22, 1491, 3, 20], + [2068, 11, 26, 1491, 11, 1], [2068, 11, 28, 1491, 11, 3], [2069, 7, 3, 1492, 6, 15], + [2069, 7, 5, 1492, 6, 17], [2070, 2, 11, 1493, 1, 28], [2070, 2, 13, 1493, 1, 30], + [2070, 9, 17, 1493, 9, 12], [2070, 9, 19, 1493, 9, 14], [2071, 4, 25, 1494, 4, 25], + [2071, 4, 27, 1494, 4, 27], [2071, 11, 31, 1494, 12, 9], [2072, 0, 2, 1494, 12, 11], + [2072, 7, 7, 1495, 7, 22], [2072, 7, 9, 1495, 7, 24], [2073, 2, 15, 1496, 3, 6], + [2073, 2, 17, 1496, 3, 8], [2073, 9, 21, 1496, 10, 19], [2073, 9, 23, 1496, 10, 21], + [2074, 4, 29, 1497, 6, 3], [2074, 4, 31, 1497, 6, 5], [2075, 0, 4, 1498, 1, 16], + [2075, 0, 6, 1498, 1, 18], [2075, 7, 12, 1498, 8, 29], [2075, 7, 14, 1498, 9, 2], + [2076, 2, 19, 1499, 4, 13], [2076, 2, 21, 1499, 4, 15], [2076, 9, 25, 1499, 11, 27], + [2076, 9, 27, 1499, 11, 29], [2077, 5, 2, 1500, 7, 11], [2077, 5, 4, 1500, 7, 13], + [2078, 0, 8, 1501, 2, 23], [2078, 0, 10, 1501, 2, 25], [2078, 7, 16, 1501, 10, 8], + [2078, 7, 18, 1501, 10, 10], [2079, 2, 24, 1502, 5, 20], [2079, 2, 26, 1502, 5, 22], + [2079, 9, 30, 1503, 1, 5], [2079, 10, 1, 1503, 1, 7], [2080, 5, 6, 1503, 8, 17], + [2080, 5, 8, 1503, 8, 19], [2081, 0, 12, 1504, 4, 2], [2081, 0, 14, 1504, 4, 4], + [2081, 7, 20, 1504, 11, 15], [2081, 7, 22, 1504, 11, 17], [2082, 2, 28, 1505, 6, 28], + [2082, 2, 30, 1505, 6, 30], [2082, 10, 3, 1506, 2, 12], [2082, 10, 5, 1506, 2, 14], + [2083, 5, 11, 1506, 9, 25], [2083, 5, 13, 1506, 9, 27], [2084, 0, 17, 1507, 5, 9], + [2084, 0, 19, 1507, 5, 11], [2084, 7, 24, 1507, 12, 22], [2084, 7, 26, 1507, 12, 24], + [2085, 3, 1, 1508, 8, 6], [2085, 3, 3, 1508, 8, 8], [2085, 10, 7, 1509, 3, 19], + [2085, 10, 9, 1509, 3, 21], [2086, 5, 15, 1509, 11, 3], [2086, 5, 17, 1509, 11, 5], + [2087, 0, 21, 1510, 6, 16], [2087, 0, 23, 1510, 6, 18], [2087, 7, 29, 1511, 1, 30], + [2087, 7, 31, 1511, 2, 2], [2088, 3, 5, 1511, 9, 13], [2088, 3, 7, 1511, 9, 15], + [2088, 10, 11, 1512, 4, 27], [2088, 10, 13, 1512, 4, 29], [2089, 5, 19, 1512, 12, 10], + [2089, 5, 21, 1512, 12, 12], [2090, 0, 25, 1513, 7, 24], [2090, 0, 27, 1513, 7, 26], + [2090, 8, 2, 1514, 3, 7], [2090, 8, 4, 1514, 3, 9], [2091, 3, 10, 1514, 10, 21], + [2091, 3, 12, 1514, 10, 23], [2091, 10, 16, 1515, 6, 5], [2091, 10, 18, 1515, 6, 7], + [2092, 5, 23, 1516, 1, 17], [2092, 5, 25, 1516, 1, 19], [2093, 0, 29, 1516, 9, 2], + [2093, 0, 31, 1516, 9, 4], [2093, 8, 6, 1517, 4, 15], [2093, 8, 8, 1517, 4, 17], + [2094, 3, 14, 1517, 11, 28], [2094, 3, 16, 1517, 12, 1], [2094, 10, 20, 1518, 7, 12], + [2094, 10, 22, 1518, 7, 14], [2095, 5, 28, 1519, 2, 25], [2095, 5, 30, 1519, 2, 27], + [2096, 1, 3, 1519, 10, 8], [2096, 1, 5, 1519, 10, 10], [2096, 8, 10, 1520, 5, 23], + [2096, 8, 12, 1520, 5, 25], [2097, 3, 18, 1521, 1, 5], [2097, 3, 20, 1521, 1, 7], + [2097, 10, 24, 1521, 8, 19], [2097, 10, 26, 1521, 8, 21], [2098, 6, 2, 1522, 4, 3], + [2098, 6, 4, 1522, 4, 5], [2099, 1, 7, 1522, 11, 16], [2099, 1, 9, 1522, 11, 18], + [2099, 8, 15, 1523, 6, 29], [2099, 8, 17, 1523, 7, 2], [2100, 3, 23, 1524, 2, 13], + [2100, 3, 25, 1524, 2, 15], [2100, 10, 29, 1524, 9, 26], [2100, 11, 1, 1524, 9, 28], + [2101, 6, 7, 1525, 5, 10], [2101, 6, 9, 1525, 5, 12], [2102, 1, 12, 1525, 12, 24], + [2102, 1, 14, 1525, 12, 26], [2102, 8, 20, 1526, 8, 7], [2102, 8, 22, 1526, 8, 9], + [2103, 3, 28, 1527, 3, 21], [2103, 3, 30, 1527, 3, 23], [2103, 11, 4, 1527, 11, 4], + [2103, 11, 6, 1527, 11, 6], [2104, 6, 11, 1528, 6, 18], [2104, 6, 13, 1528, 6, 20], + [2105, 1, 16, 1529, 2, 1], [2105, 1, 18, 1529, 2, 3], [2105, 8, 24, 1529, 9, 15], + [2105, 8, 26, 1529, 9, 17], [2106, 4, 2, 1530, 4, 27], [2106, 4, 4, 1530, 4, 29], + [2106, 11, 8, 1530, 12, 12], [2106, 11, 10, 1530, 12, 14], [2107, 6, 16, 1531, 7, 25], + [2107, 6, 18, 1531, 7, 27], [2108, 1, 21, 1532, 3, 9], [2108, 1, 23, 1532, 3, 11], + [2108, 8, 28, 1532, 10, 22], [2108, 8, 30, 1532, 10, 24], [2109, 4, 6, 1533, 6, 5], + [2109, 4, 8, 1533, 6, 7], [2109, 11, 12, 1534, 1, 19], [2109, 11, 14, 1534, 1, 21], + [2110, 6, 20, 1534, 9, 2], [2110, 6, 22, 1534, 9, 4], [2111, 1, 25, 1535, 4, 16], + [2111, 1, 27, 1535, 4, 18], [2111, 9, 3, 1535, 11, 29], [2111, 9, 5, 1535, 12, 2], + [2112, 4, 10, 1536, 7, 13], [2112, 4, 12, 1536, 7, 15], [2112, 11, 16, 1537, 2, 26], + [2112, 11, 18, 1537, 2, 28], [2113, 6, 24, 1537, 10, 11], [2113, 6, 26, 1537, 10, 13], + [2114, 2, 1, 1538, 5, 23], [2114, 2, 3, 1538, 5, 25], [2114, 9, 7, 1539, 1, 8], + [2114, 9, 9, 1539, 1, 10], [2115, 4, 15, 1539, 8, 20], [2115, 4, 17, 1539, 8, 22], + [2115, 11, 21, 1540, 4, 4], [2115, 11, 23, 1540, 4, 6], [2116, 6, 28, 1540, 11, 18], + [2116, 6, 30, 1540, 11, 20], [2117, 2, 5, 1541, 7, 1], [2117, 2, 7, 1541, 7, 3], + [2117, 9, 11, 1542, 2, 15], [2117, 9, 13, 1542, 2, 17], [2118, 4, 19, 1542, 9, 28], + [2118, 4, 21, 1542, 9, 30], [2118, 11, 25, 1543, 5, 12], [2118, 11, 27, 1543, 5, 14], + [2119, 7, 2, 1543, 12, 25], [2119, 7, 4, 1543, 12, 27], [2120, 2, 9, 1544, 8, 9], + [2120, 2, 11, 1544, 8, 11], [2120, 9, 15, 1545, 3, 21], [2120, 9, 17, 1545, 3, 23], + [2121, 4, 23, 1545, 11, 6], [2121, 4, 25, 1545, 11, 8], [2121, 11, 29, 1546, 6, 19], + [2121, 11, 31, 1546, 6, 21], [2122, 7, 6, 1547, 2, 3], [2122, 7, 8, 1547, 2, 5], + [2123, 2, 14, 1547, 9, 16], [2123, 2, 16, 1547, 9, 18], [2123, 9, 20, 1548, 4, 30], + [2123, 9, 22, 1548, 5, 2], [2124, 4, 27, 1548, 12, 13], [2124, 4, 29, 1548, 12, 15], + [2125, 0, 2, 1549, 7, 27], [2125, 0, 4, 1549, 7, 29], [2125, 7, 10, 1550, 3, 10], + [2125, 7, 12, 1550, 3, 12], [2126, 2, 18, 1550, 10, 23], [2126, 2, 20, 1550, 10, 25], + [2126, 9, 24, 1551, 6, 7], [2126, 9, 26, 1551, 6, 9], [2127, 5, 1, 1552, 1, 20], + [2127, 5, 3, 1552, 1, 22], [2128, 0, 7, 1552, 9, 5], [2128, 0, 9, 1552, 9, 7], + [2128, 7, 14, 1553, 4, 17], [2128, 7, 16, 1553, 4, 19], [2129, 2, 22, 1553, 12, 1], + [2129, 2, 24, 1553, 12, 3], [2129, 9, 28, 1554, 7, 14], [2129, 9, 30, 1554, 7, 16], + [2130, 5, 5, 1555, 2, 28], [2130, 5, 7, 1555, 3, 1], [2131, 0, 11, 1555, 10, 11], + [2131, 0, 13, 1555, 10, 13], [2131, 7, 19, 1556, 5, 25], [2131, 7, 21, 1556, 5, 27], + [2132, 2, 26, 1557, 1, 8], [2132, 2, 28, 1557, 1, 10], [2132, 10, 1, 1557, 8, 23], + [2132, 10, 3, 1557, 8, 25], [2133, 5, 9, 1558, 4, 5], [2133, 5, 11, 1558, 4, 7], + [2134, 0, 15, 1558, 11, 19], [2134, 0, 17, 1558, 11, 21], [2134, 7, 23, 1559, 7, 3], + [2134, 7, 25, 1559, 7, 5], [2135, 2, 31, 1560, 2, 16], [2135, 3, 2, 1560, 2, 18], + [2135, 10, 6, 1560, 9, 29], [2135, 10, 8, 1560, 10, 2], [2136, 5, 13, 1561, 5, 13], + [2136, 5, 15, 1561, 5, 15], [2137, 0, 19, 1561, 12, 26], [2137, 0, 21, 1561, 12, 28], + [2137, 7, 27, 1562, 8, 10], [2137, 7, 29, 1562, 8, 12], [2138, 3, 4, 1563, 3, 23], + [2138, 3, 6, 1563, 3, 25], [2138, 10, 10, 1563, 11, 7], [2138, 10, 12, 1563, 11, 9], + [2139, 5, 18, 1564, 6, 21], [2139, 5, 20, 1564, 6, 23], [2140, 0, 24, 1565, 2, 4], + [2140, 0, 26, 1565, 2, 6], [2140, 7, 31, 1565, 9, 18], [2140, 8, 2, 1565, 9, 20], + [2141, 3, 8, 1566, 5, 1], [2141, 3, 10, 1566, 5, 3], [2141, 10, 14, 1566, 12, 15], + [2141, 10, 16, 1566, 12, 17], [2142, 5, 22, 1567, 7, 27], [2142, 5, 24, 1567, 7, 29], + [2143, 0, 28, 1568, 3, 12], [2143, 0, 30, 1568, 3, 14], [2143, 8, 5, 1568, 10, 24], + [2143, 8, 7, 1568, 10, 26], [2144, 3, 12, 1569, 6, 8], [2144, 3, 14, 1569, 6, 10], + [2144, 10, 18, 1570, 1, 22], [2144, 10, 20, 1570, 1, 24], [2145, 5, 26, 1570, 9, 5], + [2145, 5, 28, 1570, 9, 7], [2146, 1, 1, 1571, 4, 19], [2146, 1, 3, 1571, 4, 21], + [2146, 8, 9, 1571, 12, 2], [2146, 8, 11, 1571, 12, 4], [2147, 3, 17, 1572, 7, 16], + [2147, 3, 19, 1572, 7, 18], [2147, 10, 23, 1573, 2, 29], [2147, 10, 25, 1573, 3, 2], + [2148, 5, 30, 1573, 10, 13], [2148, 6, 2, 1573, 10, 15], [2149, 1, 5, 1574, 5, 26], + [2149, 1, 7, 1574, 5, 28], [2149, 8, 13, 1575, 1, 11], [2149, 8, 15, 1575, 1, 13], + [2150, 3, 21, 1575, 8, 23], [2150, 3, 23, 1575, 8, 25], [2150, 10, 27, 1576, 4, 7], + [2150, 10, 29, 1576, 4, 9], [2151, 6, 5, 1576, 11, 20], [2151, 6, 7, 1576, 11, 22], + [2152, 1, 10, 1577, 7, 4], [2152, 1, 12, 1577, 7, 6], [2152, 8, 17, 1578, 2, 18], + [2152, 8, 19, 1578, 2, 20], [2153, 3, 25, 1578, 10, 1], [2153, 3, 27, 1578, 10, 3], + [2153, 11, 1, 1579, 5, 14], [2153, 11, 3, 1579, 5, 16], [2154, 6, 9, 1579, 12, 28], + [2154, 6, 11, 1579, 12, 30], [2155, 1, 14, 1580, 8, 12], [2155, 1, 16, 1580, 8, 14], + [2155, 8, 22, 1581, 3, 24], [2155, 8, 24, 1581, 3, 26], [2156, 3, 29, 1581, 11, 9], + [2156, 4, 1, 1581, 11, 11], [2156, 11, 5, 1582, 6, 21], [2156, 11, 7, 1582, 6, 23], + [2157, 6, 13, 1583, 2, 6], [2157, 6, 15, 1583, 2, 8], [2158, 1, 18, 1583, 9, 18], + [2158, 1, 20, 1583, 9, 20], [2158, 8, 26, 1584, 5, 3], [2158, 8, 28, 1584, 5, 5], + [2159, 4, 4, 1584, 12, 15], [2159, 4, 6, 1584, 12, 17], [2159, 11, 10, 1585, 7, 29], + [2159, 11, 12, 1585, 8, 2], [2160, 6, 17, 1586, 3, 13], [2160, 6, 19, 1586, 3, 15], + [2161, 1, 22, 1586, 10, 26], [2161, 1, 24, 1586, 10, 28], [2161, 8, 30, 1587, 6, 10], + [2161, 9, 2, 1587, 6, 12], [2162, 4, 8, 1588, 1, 23], [2162, 4, 10, 1588, 1, 25], + [2162, 11, 14, 1588, 9, 7], [2162, 11, 16, 1588, 9, 9], [2163, 6, 22, 1589, 4, 20], + [2163, 6, 24, 1589, 4, 22], [2164, 1, 27, 1589, 12, 4], [2164, 1, 29, 1589, 12, 6], + [2164, 9, 4, 1590, 7, 17], [2164, 9, 6, 1590, 7, 19], [2165, 4, 12, 1591, 3, 1], + [2165, 4, 14, 1591, 3, 3], [2165, 11, 18, 1591, 10, 14], [2165, 11, 20, 1591, 10, 16], + [2166, 6, 26, 1592, 5, 28], [2166, 6, 28, 1592, 5, 30], [2167, 2, 3, 1593, 1, 11], + [2167, 2, 5, 1593, 1, 13], [2167, 9, 9, 1593, 8, 25], [2167, 9, 11, 1593, 8, 27], + [2168, 4, 16, 1594, 4, 8], [2168, 4, 18, 1594, 4, 10], [2168, 11, 22, 1594, 11, 22], + [2168, 11, 24, 1594, 11, 24], [2169, 6, 30, 1595, 7, 6], [2169, 7, 1, 1595, 7, 8], + [2170, 2, 7, 1596, 2, 19], [2170, 2, 9, 1596, 2, 21], [2170, 9, 13, 1596, 10, 3], + [2170, 9, 15, 1596, 10, 5], [2171, 4, 21, 1597, 5, 16], [2171, 4, 23, 1597, 5, 18], + [2171, 11, 27, 1597, 12, 29], [2171, 11, 29, 1598, 1, 2], [2172, 7, 3, 1598, 8, 13], + [2172, 7, 5, 1598, 8, 15], [2173, 2, 11, 1599, 3, 26], [2173, 2, 13, 1599, 3, 28], + [2173, 9, 17, 1599, 11, 9], [2173, 9, 19, 1599, 11, 11], [2174, 4, 25, 1600, 6, 23], + [2174, 4, 27, 1600, 6, 25], [2175, 10, 25, 1602, 1, 11], [1881, 10, 12, 1298, 12, 19] + ]; + const MONTH_LENGTH = [ + [1300, 1, 30], [1300, 2, 29], [1300, 3, 30], [1300, 4, 29], [1300, 5, 30], [1300, 6, 29], [1300, 7, 30], + [1300, 8, 29], [1300, 9, 30], [1300, 10, 29], [1300, 11, 30], [1300, 12, 29], [1301, 1, 30], [1301, 2, 30], + [1301, 3, 29], [1301, 4, 30], [1301, 5, 29], [1301, 6, 30], [1301, 7, 29], [1301, 8, 30], [1301, 9, 29], + [1301, 10, 30], [1301, 11, 29], [1301, 12, 29], [1302, 1, 30], [1302, 2, 30], [1302, 3, 30], [1302, 4, 29], + [1302, 5, 30], [1302, 6, 30], [1302, 7, 29], [1302, 8, 29], [1302, 9, 30], [1302, 10, 29], [1302, 11, 29], + [1302, 12, 30], [1303, 1, 29], [1303, 2, 30], [1303, 3, 30], [1303, 4, 29], [1303, 5, 30], [1303, 6, 30], + [1303, 7, 29], [1303, 8, 30], [1303, 9, 29], [1303, 10, 30], [1303, 11, 29], [1303, 12, 29], [1304, 1, 29], + [1304, 2, 30], [1304, 3, 30], [1304, 4, 29], [1304, 5, 30], [1304, 6, 30], [1304, 7, 30], [1304, 8, 29], + [1304, 9, 30], [1304, 10, 29], [1304, 11, 30], [1304, 12, 29], [1305, 1, 29], [1305, 2, 29], [1305, 3, 30], + [1305, 4, 30], [1305, 5, 29], [1305, 6, 30], [1305, 7, 30], [1305, 8, 29], [1305, 9, 30], [1305, 10, 30], + [1305, 11, 29], [1305, 12, 29], [1306, 1, 30], [1306, 2, 29], [1306, 3, 30], [1306, 4, 29], [1306, 5, 30], + [1306, 6, 29], [1306, 7, 30], [1306, 8, 29], [1306, 9, 30], [1306, 10, 30], [1306, 11, 29], [1306, 12, 30], + [1307, 1, 29], [1307, 2, 30], [1307, 3, 29], [1307, 4, 30], [1307, 5, 29], [1307, 6, 30], [1307, 7, 29], + [1307, 8, 30], [1307, 9, 29], [1307, 10, 30], [1307, 11, 29], [1307, 12, 30], [1308, 1, 29], [1308, 2, 30], + [1308, 3, 30], [1308, 4, 29], [1308, 5, 30], [1308, 6, 29], [1308, 7, 30], [1308, 8, 29], [1308, 9, 30], + [1308, 10, 29], [1308, 11, 29], [1308, 12, 30], [1309, 1, 29], [1309, 2, 30], [1309, 3, 30], [1309, 4, 30], + [1309, 5, 30], [1309, 6, 29], [1309, 7, 29], [1309, 8, 30], [1309, 9, 29], [1309, 10, 29], [1309, 11, 30], + [1309, 12, 29], [1310, 1, 30], [1310, 2, 29], [1310, 3, 30], [1310, 4, 30], [1310, 5, 30], [1310, 6, 29], + [1310, 7, 30], [1310, 8, 29], [1310, 9, 30], [1310, 10, 29], [1310, 11, 29], [1310, 12, 30], [1311, 1, 29], + [1311, 2, 30], [1311, 3, 29], [1311, 4, 30], [1311, 5, 30], [1311, 6, 30], [1311, 7, 29], [1311, 8, 30], + [1311, 9, 29], [1311, 10, 30], [1311, 11, 29], [1311, 12, 29], [1312, 1, 30], [1312, 2, 29], [1312, 3, 30], + [1312, 4, 29], [1312, 5, 30], [1312, 6, 30], [1312, 7, 29], [1312, 8, 30], [1312, 9, 30], [1312, 10, 29], + [1312, 11, 30], [1312, 12, 29], [1313, 1, 29], [1313, 2, 30], [1313, 3, 29], [1313, 4, 30], [1313, 5, 29], + [1313, 6, 30], [1313, 7, 29], [1313, 8, 30], [1313, 9, 30], [1313, 10, 30], [1313, 11, 29], [1313, 12, 29], + [1314, 1, 30], [1314, 2, 30], [1314, 3, 29], [1314, 4, 30], [1314, 5, 29], [1314, 6, 29], [1314, 7, 30], + [1314, 8, 29], [1314, 9, 30], [1314, 10, 30], [1314, 11, 29], [1314, 12, 30], [1315, 1, 29], [1315, 2, 30], + [1315, 3, 30], [1315, 4, 29], [1315, 5, 30], [1315, 6, 29], [1315, 7, 29], [1315, 8, 30], [1315, 9, 29], + [1315, 10, 30], [1315, 11, 29], [1315, 12, 30], [1316, 1, 29], [1316, 2, 30], [1316, 3, 30], [1316, 4, 30], + [1316, 5, 29], [1316, 6, 30], [1316, 7, 29], [1316, 8, 29], [1316, 9, 30], [1316, 10, 29], [1316, 11, 30], + [1316, 12, 29], [1317, 1, 30], [1317, 2, 29], [1317, 3, 30], [1317, 4, 30], [1317, 5, 29], [1317, 6, 30], + [1317, 7, 29], [1317, 8, 30], [1317, 9, 29], [1317, 10, 30], [1317, 11, 29], [1317, 12, 29], [1318, 1, 30], + [1318, 2, 29], [1318, 3, 30], [1318, 4, 30], [1318, 5, 29], [1318, 6, 30], [1318, 7, 30], [1318, 8, 29], + [1318, 9, 30], [1318, 10, 29], [1318, 11, 30], [1318, 12, 29], [1319, 1, 29], [1319, 2, 30], [1319, 3, 29], + [1319, 4, 30], [1319, 5, 30], [1319, 6, 29], [1319, 7, 30], [1319, 8, 29], [1319, 9, 30], [1319, 10, 30], + [1319, 11, 29], [1319, 12, 30], [1320, 1, 29], [1320, 2, 30], [1320, 3, 29], [1320, 4, 29], [1320, 5, 30], + [1320, 6, 29], [1320, 7, 30], [1320, 8, 29], [1320, 9, 30], [1320, 10, 30], [1320, 11, 30], [1320, 12, 29], + [1321, 1, 30], [1321, 2, 29], [1321, 3, 30], [1321, 4, 29], [1321, 5, 29], [1321, 6, 30], [1321, 7, 29], + [1321, 8, 29], [1321, 9, 30], [1321, 10, 30], [1321, 11, 30], [1321, 12, 30], [1322, 1, 29], [1322, 2, 30], + [1322, 3, 29], [1322, 4, 30], [1322, 5, 29], [1322, 6, 29], [1322, 7, 29], [1322, 8, 30], [1322, 9, 29], + [1322, 10, 30], [1322, 11, 30], [1322, 12, 30], [1323, 1, 29], [1323, 2, 30], [1323, 3, 30], [1323, 4, 29], + [1323, 5, 30], [1323, 6, 29], [1323, 7, 29], [1323, 8, 29], [1323, 9, 30], [1323, 10, 29], [1323, 11, 30], + [1323, 12, 30], [1324, 1, 29], [1324, 2, 30], [1324, 3, 30], [1324, 4, 29], [1324, 5, 30], [1324, 6, 29], + [1324, 7, 30], [1324, 8, 29], [1324, 9, 29], [1324, 10, 30], [1324, 11, 29], [1324, 12, 30], [1325, 1, 30], + [1325, 2, 29], [1325, 3, 30], [1325, 4, 29], [1325, 5, 30], [1325, 6, 30], [1325, 7, 29], [1325, 8, 30], + [1325, 9, 29], [1325, 10, 30], [1325, 11, 29], [1325, 12, 30], [1326, 1, 29], [1326, 2, 29], [1326, 3, 30], + [1326, 4, 29], [1326, 5, 30], [1326, 6, 30], [1326, 7, 29], [1326, 8, 30], [1326, 9, 29], [1326, 10, 30], + [1326, 11, 30], [1326, 12, 29], [1327, 1, 30], [1327, 2, 29], [1327, 3, 29], [1327, 4, 30], [1327, 5, 29], + [1327, 6, 30], [1327, 7, 29], [1327, 8, 30], [1327, 9, 30], [1327, 10, 29], [1327, 11, 30], [1327, 12, 30], + [1328, 1, 29], [1328, 2, 30], [1328, 3, 29], [1328, 4, 29], [1328, 5, 30], [1328, 6, 29], [1328, 7, 29], + [1328, 8, 30], [1328, 9, 30], [1328, 10, 30], [1328, 11, 29], [1328, 12, 30], [1329, 1, 30], [1329, 2, 29], + [1329, 3, 30], [1329, 4, 29], [1329, 5, 29], [1329, 6, 30], [1329, 7, 29], [1329, 8, 29], [1329, 9, 30], + [1329, 10, 30], [1329, 11, 29], [1329, 12, 30], [1330, 1, 30], [1330, 2, 30], [1330, 3, 29], [1330, 4, 30], + [1330, 5, 29], [1330, 6, 29], [1330, 7, 30], [1330, 8, 29], [1330, 9, 29], [1330, 10, 30], [1330, 11, 30], + [1330, 12, 29], [1331, 1, 30], [1331, 2, 30], [1331, 3, 29], [1331, 4, 30], [1331, 5, 30], [1331, 6, 29], + [1331, 7, 29], [1331, 8, 30], [1331, 9, 29], [1331, 10, 30], [1331, 11, 29], [1331, 12, 30], [1332, 1, 29], + [1332, 2, 30], [1332, 3, 29], [1332, 4, 30], [1332, 5, 30], [1332, 6, 29], [1332, 7, 30], [1332, 8, 29], + [1332, 9, 30], [1332, 10, 30], [1332, 11, 29], [1332, 12, 29], [1333, 1, 30], [1333, 2, 29], [1333, 3, 29], + [1333, 4, 30], [1333, 5, 30], [1333, 6, 29], [1333, 7, 30], [1333, 8, 30], [1333, 9, 29], [1333, 10, 30], + [1333, 11, 30], [1333, 12, 29], [1334, 1, 29], [1334, 2, 29], [1334, 3, 30], [1334, 4, 29], [1334, 5, 30], + [1334, 6, 29], [1334, 7, 30], [1334, 8, 30], [1334, 9, 30], [1334, 10, 29], [1334, 11, 30], [1334, 12, 29], + [1335, 1, 30], [1335, 2, 29], [1335, 3, 30], [1335, 4, 29], [1335, 5, 29], [1335, 6, 30], [1335, 7, 29], + [1335, 8, 30], [1335, 9, 30], [1335, 10, 29], [1335, 11, 30], [1335, 12, 30], [1336, 1, 29], [1336, 2, 30], + [1336, 3, 29], [1336, 4, 30], [1336, 5, 29], [1336, 6, 29], [1336, 7, 30], [1336, 8, 29], [1336, 9, 30], + [1336, 10, 29], [1336, 11, 30], [1336, 12, 30], [1337, 1, 30], [1337, 2, 29], [1337, 3, 30], [1337, 4, 29], + [1337, 5, 30], [1337, 6, 29], [1337, 7, 29], [1337, 8, 30], [1337, 9, 29], [1337, 10, 30], [1337, 11, 29], + [1337, 12, 30], [1338, 1, 29], [1338, 2, 30], [1338, 3, 30], [1338, 4, 29], [1338, 5, 30], [1338, 6, 30], + [1338, 7, 29], [1338, 8, 29], [1338, 9, 30], [1338, 10, 29], [1338, 11, 30], [1338, 12, 29], [1339, 1, 30], + [1339, 2, 29], [1339, 3, 30], [1339, 4, 29], [1339, 5, 30], [1339, 6, 30], [1339, 7, 30], [1339, 8, 29], + [1339, 9, 30], [1339, 10, 29], [1339, 11, 29], [1339, 12, 30], [1340, 1, 29], [1340, 2, 29], [1340, 3, 30], + [1340, 4, 29], [1340, 5, 30], [1340, 6, 30], [1340, 7, 30], [1340, 8, 30], [1340, 9, 29], [1340, 10, 30], + [1340, 11, 29], [1340, 12, 29], [1341, 1, 30], [1341, 2, 29], [1341, 3, 29], [1341, 4, 30], [1341, 5, 29], + [1341, 6, 30], [1341, 7, 30], [1341, 8, 30], [1341, 9, 29], [1341, 10, 30], [1341, 11, 30], [1341, 12, 29], + [1342, 1, 29], [1342, 2, 29], [1342, 3, 30], [1342, 4, 29], [1342, 5, 30], [1342, 6, 29], [1342, 7, 30], + [1342, 8, 30], [1342, 9, 29], [1342, 10, 30], [1342, 11, 30], [1342, 12, 29], [1343, 1, 30], [1343, 2, 29], + [1343, 3, 29], [1343, 4, 30], [1343, 5, 29], [1343, 6, 30], [1343, 7, 29], [1343, 8, 30], [1343, 9, 29], + [1343, 10, 30], [1343, 11, 30], [1343, 12, 29], [1344, 1, 30], [1344, 2, 29], [1344, 3, 30], [1344, 4, 29], + [1344, 5, 30], [1344, 6, 30], [1344, 7, 29], [1344, 8, 29], [1344, 9, 30], [1344, 10, 29], [1344, 11, 30], + [1344, 12, 29], [1345, 1, 30], [1345, 2, 29], [1345, 3, 30], [1345, 4, 30], [1345, 5, 30], [1345, 6, 29], + [1345, 7, 30], [1345, 8, 29], [1345, 9, 29], [1345, 10, 30], [1345, 11, 29], [1345, 12, 29], [1346, 1, 30], + [1346, 2, 29], [1346, 3, 30], [1346, 4, 30], [1346, 5, 30], [1346, 6, 30], [1346, 7, 29], [1346, 8, 30], + [1346, 9, 29], [1346, 10, 29], [1346, 11, 30], [1346, 12, 29], [1347, 1, 29], [1347, 2, 30], [1347, 3, 29], + [1347, 4, 30], [1347, 5, 30], [1347, 6, 30], [1347, 7, 29], [1347, 8, 30], [1347, 9, 30], [1347, 10, 29], + [1347, 11, 29], [1347, 12, 30], [1348, 1, 29], [1348, 2, 29], [1348, 3, 30], [1348, 4, 29], [1348, 5, 30], + [1348, 6, 30], [1348, 7, 29], [1348, 8, 30], [1348, 9, 30], [1348, 10, 30], [1348, 11, 29], [1348, 12, 29], + [1349, 1, 30], [1349, 2, 29], [1349, 3, 29], [1349, 4, 30], [1349, 5, 29], [1349, 6, 30], [1349, 7, 30], + [1349, 8, 29], [1349, 9, 30], [1349, 10, 30], [1349, 11, 29], [1349, 12, 30], [1350, 1, 29], [1350, 2, 30], + [1350, 3, 29], [1350, 4, 30], [1350, 5, 29], [1350, 6, 30], [1350, 7, 29], [1350, 8, 29], [1350, 9, 30], + [1350, 10, 30], [1350, 11, 29], [1350, 12, 30], [1351, 1, 30], [1351, 2, 29], [1351, 3, 30], [1351, 4, 29], + [1351, 5, 30], [1351, 6, 29], [1351, 7, 30], [1351, 8, 29], [1351, 9, 29], [1351, 10, 30], [1351, 11, 29], + [1351, 12, 30], [1352, 1, 30], [1352, 2, 29], [1352, 3, 30], [1352, 4, 30], [1352, 5, 29], [1352, 6, 30], + [1352, 7, 29], [1352, 8, 30], [1352, 9, 29], [1352, 10, 29], [1352, 11, 30], [1352, 12, 29], [1353, 1, 30], + [1353, 2, 29], [1353, 3, 30], [1353, 4, 30], [1353, 5, 30], [1353, 6, 29], [1353, 7, 30], [1353, 8, 29], + [1353, 9, 29], [1353, 10, 30], [1353, 11, 29], [1353, 12, 30], [1354, 1, 29], [1354, 2, 30], [1354, 3, 29], + [1354, 4, 30], [1354, 5, 30], [1354, 6, 29], [1354, 7, 30], [1354, 8, 30], [1354, 9, 29], [1354, 10, 30], + [1354, 11, 29], [1354, 12, 29], [1355, 1, 30], [1355, 2, 29], [1355, 3, 29], [1355, 4, 30], [1355, 5, 30], + [1355, 6, 29], [1355, 7, 30], [1355, 8, 30], [1355, 9, 29], [1355, 10, 30], [1355, 11, 30], [1355, 12, 29], + [1356, 1, 29], [1356, 2, 30], [1356, 3, 29], [1356, 4, 30], [1356, 5, 29], [1356, 6, 30], [1356, 7, 29], + [1356, 8, 30], [1356, 9, 29], [1356, 10, 30], [1356, 11, 30], [1356, 12, 30], [1357, 1, 29], [1357, 2, 29], + [1357, 3, 30], [1357, 4, 29], [1357, 5, 30], [1357, 6, 29], [1357, 7, 29], [1357, 8, 30], [1357, 9, 29], + [1357, 10, 30], [1357, 11, 30], [1357, 12, 30], [1358, 1, 29], [1358, 2, 30], [1358, 3, 29], [1358, 4, 30], + [1358, 5, 29], [1358, 6, 30], [1358, 7, 29], [1358, 8, 29], [1358, 9, 30], [1358, 10, 29], [1358, 11, 30], + [1358, 12, 30], [1359, 1, 29], [1359, 2, 30], [1359, 3, 30], [1359, 4, 29], [1359, 5, 30], [1359, 6, 29], + [1359, 7, 30], [1359, 8, 29], [1359, 9, 29], [1359, 10, 29], [1359, 11, 30], [1359, 12, 30], [1360, 1, 29], + [1360, 2, 30], [1360, 3, 30], [1360, 4, 30], [1360, 5, 29], [1360, 6, 30], [1360, 7, 29], [1360, 8, 30], + [1360, 9, 29], [1360, 10, 29], [1360, 11, 30], [1360, 12, 29], [1361, 1, 30], [1361, 2, 29], [1361, 3, 30], + [1361, 4, 30], [1361, 5, 29], [1361, 6, 30], [1361, 7, 30], [1361, 8, 29], [1361, 9, 29], [1361, 10, 30], + [1361, 11, 29], [1361, 12, 30], [1362, 1, 29], [1362, 2, 30], [1362, 3, 29], [1362, 4, 30], [1362, 5, 29], + [1362, 6, 30], [1362, 7, 30], [1362, 8, 29], [1362, 9, 30], [1362, 10, 29], [1362, 11, 30], [1362, 12, 29], + [1363, 1, 30], [1363, 2, 29], [1363, 3, 30], [1363, 4, 29], [1363, 5, 30], [1363, 6, 29], [1363, 7, 30], + [1363, 8, 29], [1363, 9, 30], [1363, 10, 29], [1363, 11, 30], [1363, 12, 30], [1364, 1, 29], [1364, 2, 30], + [1364, 3, 29], [1364, 4, 30], [1364, 5, 29], [1364, 6, 29], [1364, 7, 30], [1364, 8, 29], [1364, 9, 30], + [1364, 10, 29], [1364, 11, 30], [1364, 12, 30], [1365, 1, 30], [1365, 2, 30], [1365, 3, 29], [1365, 4, 29], + [1365, 5, 30], [1365, 6, 29], [1365, 7, 29], [1365, 8, 30], [1365, 9, 29], [1365, 10, 30], [1365, 11, 29], + [1365, 12, 30], [1366, 1, 30], [1366, 2, 30], [1366, 3, 29], [1366, 4, 30], [1366, 5, 29], [1366, 6, 30], + [1366, 7, 29], [1366, 8, 29], [1366, 9, 30], [1366, 10, 29], [1366, 11, 30], [1366, 12, 29], [1367, 1, 30], + [1367, 2, 30], [1367, 3, 29], [1367, 4, 30], [1367, 5, 30], [1367, 6, 29], [1367, 7, 30], [1367, 8, 29], + [1367, 9, 29], [1367, 10, 30], [1367, 11, 29], [1367, 12, 30], [1368, 1, 29], [1368, 2, 30], [1368, 3, 29], + [1368, 4, 30], [1368, 5, 30], [1368, 6, 30], [1368, 7, 29], [1368, 8, 29], [1368, 9, 30], [1368, 10, 29], + [1368, 11, 30], [1368, 12, 29], [1369, 1, 30], [1369, 2, 29], [1369, 3, 30], [1369, 4, 29], [1369, 5, 30], + [1369, 6, 30], [1369, 7, 29], [1369, 8, 30], [1369, 9, 29], [1369, 10, 30], [1369, 11, 30], [1369, 12, 29], + [1370, 1, 30], [1370, 2, 29], [1370, 3, 29], [1370, 4, 30], [1370, 5, 29], [1370, 6, 30], [1370, 7, 29], + [1370, 8, 30], [1370, 9, 29], [1370, 10, 30], [1370, 11, 30], [1370, 12, 30], [1371, 1, 29], [1371, 2, 30], + [1371, 3, 29], [1371, 4, 29], [1371, 5, 30], [1371, 6, 29], [1371, 7, 30], [1371, 8, 29], [1371, 9, 30], + [1371, 10, 29], [1371, 11, 30], [1371, 12, 30], [1372, 1, 30], [1372, 2, 29], [1372, 3, 29], [1372, 4, 30], + [1372, 5, 29], [1372, 6, 30], [1372, 7, 29], [1372, 8, 29], [1372, 9, 30], [1372, 10, 29], [1372, 11, 30], + [1372, 12, 30], [1373, 1, 30], [1373, 2, 29], [1373, 3, 30], [1373, 4, 29], [1373, 5, 30], [1373, 6, 29], + [1373, 7, 30], [1373, 8, 29], [1373, 9, 29], [1373, 10, 30], [1373, 11, 29], [1373, 12, 30], [1374, 1, 30], + [1374, 2, 29], [1374, 3, 30], [1374, 4, 30], [1374, 5, 29], [1374, 6, 30], [1374, 7, 29], [1374, 8, 30], + [1374, 9, 29], [1374, 10, 29], [1374, 11, 30], [1374, 12, 29], [1375, 1, 30], [1375, 2, 29], [1375, 3, 30], + [1375, 4, 30], [1375, 5, 29], [1375, 6, 30], [1375, 7, 30], [1375, 8, 29], [1375, 9, 30], [1375, 10, 29], + [1375, 11, 30], [1375, 12, 29], [1376, 1, 29], [1376, 2, 30], [1376, 3, 29], [1376, 4, 30], [1376, 5, 29], + [1376, 6, 30], [1376, 7, 30], [1376, 8, 30], [1376, 9, 29], [1376, 10, 30], [1376, 11, 29], [1376, 12, 30], + [1377, 1, 29], [1377, 2, 29], [1377, 3, 30], [1377, 4, 29], [1377, 5, 29], [1377, 6, 30], [1377, 7, 30], + [1377, 8, 30], [1377, 9, 29], [1377, 10, 30], [1377, 11, 30], [1377, 12, 29], [1378, 1, 30], [1378, 2, 29], + [1378, 3, 29], [1378, 4, 29], [1378, 5, 30], [1378, 6, 29], [1378, 7, 30], [1378, 8, 30], [1378, 9, 29], + [1378, 10, 30], [1378, 11, 30], [1378, 12, 30], [1379, 1, 29], [1379, 2, 30], [1379, 3, 29], [1379, 4, 29], + [1379, 5, 29], [1379, 6, 30], [1379, 7, 29], [1379, 8, 30], [1379, 9, 30], [1379, 10, 29], [1379, 11, 30], + [1379, 12, 30], [1380, 1, 29], [1380, 2, 30], [1380, 3, 29], [1380, 4, 30], [1380, 5, 29], [1380, 6, 30], + [1380, 7, 29], [1380, 8, 30], [1380, 9, 29], [1380, 10, 30], [1380, 11, 29], [1380, 12, 30], [1381, 1, 29], + [1381, 2, 30], [1381, 3, 29], [1381, 4, 30], [1381, 5, 30], [1381, 6, 29], [1381, 7, 30], [1381, 8, 29], + [1381, 9, 30], [1381, 10, 29], [1381, 11, 29], [1381, 12, 30], [1382, 1, 29], [1382, 2, 30], [1382, 3, 29], + [1382, 4, 30], [1382, 5, 30], [1382, 6, 29], [1382, 7, 30], [1382, 8, 30], [1382, 9, 29], [1382, 10, 30], + [1382, 11, 29], [1382, 12, 29], [1383, 1, 30], [1383, 2, 29], [1383, 3, 29], [1383, 4, 30], [1383, 5, 30], + [1383, 6, 30], [1383, 7, 29], [1383, 8, 30], [1383, 9, 30], [1383, 10, 29], [1383, 11, 30], [1383, 12, 29], + [1384, 1, 29], [1384, 2, 30], [1384, 3, 29], [1384, 4, 29], [1384, 5, 30], [1384, 6, 30], [1384, 7, 29], + [1384, 8, 30], [1384, 9, 30], [1384, 10, 30], [1384, 11, 29], [1384, 12, 30], [1385, 1, 29], [1385, 2, 29], + [1385, 3, 30], [1385, 4, 29], [1385, 5, 29], [1385, 6, 30], [1385, 7, 30], [1385, 8, 29], [1385, 9, 30], + [1385, 10, 30], [1385, 11, 30], [1385, 12, 29], [1386, 1, 30], [1386, 2, 29], [1386, 3, 29], [1386, 4, 30], + [1386, 5, 29], [1386, 6, 29], [1386, 7, 30], [1386, 8, 30], [1386, 9, 29], [1386, 10, 30], [1386, 11, 30], + [1386, 12, 29], [1387, 1, 30], [1387, 2, 29], [1387, 3, 30], [1387, 4, 29], [1387, 5, 30], [1387, 6, 29], + [1387, 7, 30], [1387, 8, 29], [1387, 9, 30], [1387, 10, 29], [1387, 11, 30], [1387, 12, 29], [1388, 1, 30], + [1388, 2, 30], [1388, 3, 29], [1388, 4, 30], [1388, 5, 29], [1388, 6, 30], [1388, 7, 29], [1388, 8, 30], + [1388, 9, 29], [1388, 10, 30], [1388, 11, 29], [1388, 12, 29], [1389, 1, 30], [1389, 2, 30], [1389, 3, 29], + [1389, 4, 30], [1389, 5, 30], [1389, 6, 29], [1389, 7, 30], [1389, 8, 30], [1389, 9, 29], [1389, 10, 29], + [1389, 11, 30], [1389, 12, 29], [1390, 1, 29], [1390, 2, 30], [1390, 3, 29], [1390, 4, 30], [1390, 5, 30], + [1390, 6, 30], [1390, 7, 29], [1390, 8, 30], [1390, 9, 29], [1390, 10, 30], [1390, 11, 29], [1390, 12, 30], + [1391, 1, 29], [1391, 2, 29], [1391, 3, 30], [1391, 4, 29], [1391, 5, 30], [1391, 6, 30], [1391, 7, 29], + [1391, 8, 30], [1391, 9, 30], [1391, 10, 29], [1391, 11, 30], [1391, 12, 29], [1392, 1, 30], [1392, 2, 29], + [1392, 3, 29], [1392, 4, 30], [1392, 5, 29], [1392, 6, 30], [1392, 7, 29], [1392, 8, 30], [1392, 9, 30], + [1392, 10, 29], [1392, 11, 30], [1392, 12, 30], [1393, 1, 29], [1393, 2, 30], [1393, 3, 29], [1393, 4, 29], + [1393, 5, 30], [1393, 6, 29], [1393, 7, 30], [1393, 8, 29], [1393, 9, 30], [1393, 10, 29], [1393, 11, 30], + [1393, 12, 30], [1394, 1, 30], [1394, 2, 29], [1394, 3, 30], [1394, 4, 29], [1394, 5, 29], [1394, 6, 30], + [1394, 7, 29], [1394, 8, 30], [1394, 9, 29], [1394, 10, 30], [1394, 11, 29], [1394, 12, 30], [1395, 1, 30], + [1395, 2, 29], [1395, 3, 30], [1395, 4, 30], [1395, 5, 29], [1395, 6, 30], [1395, 7, 29], [1395, 8, 29], + [1395, 9, 30], [1395, 10, 29], [1395, 11, 29], [1395, 12, 30], [1396, 1, 30], [1396, 2, 29], [1396, 3, 30], + [1396, 4, 30], [1396, 5, 29], [1396, 6, 30], [1396, 7, 30], [1396, 8, 29], [1396, 9, 29], [1396, 10, 30], + [1396, 11, 29], [1396, 12, 29], [1397, 1, 30], [1397, 2, 29], [1397, 3, 30], [1397, 4, 30], [1397, 5, 29], + [1397, 6, 30], [1397, 7, 30], [1397, 8, 30], [1397, 9, 29], [1397, 10, 29], [1397, 11, 29], [1397, 12, 30], + [1398, 1, 29], [1398, 2, 30], [1398, 3, 29], [1398, 4, 30], [1398, 5, 30], [1398, 6, 29], [1398, 7, 30], + [1398, 8, 30], [1398, 9, 29], [1398, 10, 30], [1398, 11, 29], [1398, 12, 29], [1399, 1, 30], [1399, 2, 29], + [1399, 3, 30], [1399, 4, 29], [1399, 5, 30], [1399, 6, 29], [1399, 7, 30], [1399, 8, 30], [1399, 9, 29], + [1399, 10, 30], [1399, 11, 29], [1399, 12, 30], [1400, 1, 30], [1400, 2, 29], [1400, 3, 30], [1400, 4, 29], + [1400, 5, 29], [1400, 6, 30], [1400, 7, 29], [1400, 8, 30], [1400, 9, 29], [1400, 10, 30], [1400, 11, 29], + [1400, 12, 30], [1401, 1, 30], [1401, 2, 30], [1401, 3, 29], [1401, 4, 30], [1401, 5, 29], [1401, 6, 29], + [1401, 7, 30], [1401, 8, 29], [1401, 9, 29], [1401, 10, 30], [1401, 11, 29], [1401, 12, 30], [1402, 1, 30], + [1402, 2, 30], [1402, 3, 30], [1402, 4, 29], [1402, 5, 30], [1402, 6, 29], [1402, 7, 29], [1402, 8, 30], + [1402, 9, 29], [1402, 10, 29], [1402, 11, 30], [1402, 12, 29], [1403, 1, 30], [1403, 2, 30], [1403, 3, 30], + [1403, 4, 29], [1403, 5, 30], [1403, 6, 30], [1403, 7, 29], [1403, 8, 29], [1403, 9, 30], [1403, 10, 29], + [1403, 11, 29], [1403, 12, 30], [1404, 1, 29], [1404, 2, 30], [1404, 3, 30], [1404, 4, 29], [1404, 5, 30], + [1404, 6, 30], [1404, 7, 29], [1404, 8, 30], [1404, 9, 29], [1404, 10, 30], [1404, 11, 29], [1404, 12, 29], + [1405, 1, 30], [1405, 2, 29], [1405, 3, 30], [1405, 4, 29], [1405, 5, 30], [1405, 6, 30], [1405, 7, 30], + [1405, 8, 29], [1405, 9, 30], [1405, 10, 29], [1405, 11, 29], [1405, 12, 30], [1406, 1, 30], [1406, 2, 29], + [1406, 3, 29], [1406, 4, 30], [1406, 5, 29], [1406, 6, 30], [1406, 7, 30], [1406, 8, 29], [1406, 9, 30], + [1406, 10, 29], [1406, 11, 30], [1406, 12, 30], [1407, 1, 29], [1407, 2, 30], [1407, 3, 29], [1407, 4, 29], + [1407, 5, 30], [1407, 6, 29], [1407, 7, 30], [1407, 8, 29], [1407, 9, 30], [1407, 10, 29], [1407, 11, 30], + [1407, 12, 30], [1408, 1, 30], [1408, 2, 29], [1408, 3, 30], [1408, 4, 29], [1408, 5, 30], [1408, 6, 29], + [1408, 7, 29], [1408, 8, 30], [1408, 9, 29], [1408, 10, 29], [1408, 11, 30], [1408, 12, 30], [1409, 1, 30], + [1409, 2, 30], [1409, 3, 29], [1409, 4, 30], [1409, 5, 29], [1409, 6, 30], [1409, 7, 29], [1409, 8, 29], + [1409, 9, 30], [1409, 10, 29], [1409, 11, 29], [1409, 12, 30], [1410, 1, 30], [1410, 2, 30], [1410, 3, 29], + [1410, 4, 30], [1410, 5, 30], [1410, 6, 29], [1410, 7, 30], [1410, 8, 29], [1410, 9, 29], [1410, 10, 30], + [1410, 11, 29], [1410, 12, 29], [1411, 1, 30], [1411, 2, 30], [1411, 3, 29], [1411, 4, 30], [1411, 5, 30], + [1411, 6, 29], [1411, 7, 30], [1411, 8, 30], [1411, 9, 29], [1411, 10, 29], [1411, 11, 30], [1411, 12, 29], + [1412, 1, 30], [1412, 2, 29], [1412, 3, 30], [1412, 4, 29], [1412, 5, 30], [1412, 6, 29], [1412, 7, 30], + [1412, 8, 30], [1412, 9, 30], [1412, 10, 29], [1412, 11, 29], [1412, 12, 30], [1413, 1, 29], [1413, 2, 30], + [1413, 3, 29], [1413, 4, 29], [1413, 5, 30], [1413, 6, 29], [1413, 7, 30], [1413, 8, 30], [1413, 9, 30], + [1413, 10, 29], [1413, 11, 30], [1413, 12, 29], [1414, 1, 30], [1414, 2, 29], [1414, 3, 30], [1414, 4, 29], + [1414, 5, 29], [1414, 6, 30], [1414, 7, 29], [1414, 8, 30], [1414, 9, 30], [1414, 10, 29], [1414, 11, 30], + [1414, 12, 30], [1415, 1, 29], [1415, 2, 30], [1415, 3, 29], [1415, 4, 30], [1415, 5, 29], [1415, 6, 29], + [1415, 7, 30], [1415, 8, 29], [1415, 9, 30], [1415, 10, 29], [1415, 11, 30], [1415, 12, 30], [1416, 1, 30], + [1416, 2, 29], [1416, 3, 30], [1416, 4, 29], [1416, 5, 30], [1416, 6, 29], [1416, 7, 29], [1416, 8, 30], + [1416, 9, 29], [1416, 10, 30], [1416, 11, 29], [1416, 12, 30], [1417, 1, 30], [1417, 2, 29], [1417, 3, 30], + [1417, 4, 30], [1417, 5, 29], [1417, 6, 29], [1417, 7, 30], [1417, 8, 29], [1417, 9, 30], [1417, 10, 29], + [1417, 11, 30], [1417, 12, 29], [1418, 1, 30], [1418, 2, 29], [1418, 3, 30], [1418, 4, 30], [1418, 5, 29], + [1418, 6, 30], [1418, 7, 29], [1418, 8, 30], [1418, 9, 29], [1418, 10, 30], [1418, 11, 29], [1418, 12, 30], + [1419, 1, 29], [1419, 2, 30], [1419, 3, 29], [1419, 4, 30], [1419, 5, 29], [1419, 6, 30], [1419, 7, 29], + [1419, 8, 30], [1419, 9, 30], [1419, 10, 30], [1419, 11, 29], [1419, 12, 29], [1420, 1, 29], [1420, 2, 30], + [1420, 3, 29], [1420, 4, 29], [1420, 5, 30], [1420, 6, 29], [1420, 7, 30], [1420, 8, 30], [1420, 9, 30], + [1420, 10, 30], [1420, 11, 29], [1420, 12, 30], [1421, 1, 29], [1421, 2, 29], [1421, 3, 30], [1421, 4, 29], + [1421, 5, 29], [1421, 6, 29], [1421, 7, 30], [1421, 8, 30], [1421, 9, 30], [1421, 10, 30], [1421, 11, 29], + [1421, 12, 30], [1422, 1, 30], [1422, 2, 29], [1422, 3, 29], [1422, 4, 30], [1422, 5, 29], [1422, 6, 29], + [1422, 7, 29], [1422, 8, 30], [1422, 9, 30], [1422, 10, 30], [1422, 11, 29], [1422, 12, 30], [1423, 1, 30], + [1423, 2, 29], [1423, 3, 30], [1423, 4, 29], [1423, 5, 30], [1423, 6, 29], [1423, 7, 29], [1423, 8, 30], + [1423, 9, 29], [1423, 10, 30], [1423, 11, 29], [1423, 12, 30], [1424, 1, 30], [1424, 2, 29], [1424, 3, 30], + [1424, 4, 30], [1424, 5, 29], [1424, 6, 30], [1424, 7, 29], [1424, 8, 29], [1424, 9, 30], [1424, 10, 29], + [1424, 11, 30], [1424, 12, 29], [1425, 1, 30], [1425, 2, 29], [1425, 3, 30], [1425, 4, 30], [1425, 5, 29], + [1425, 6, 30], [1425, 7, 29], [1425, 8, 30], [1425, 9, 30], [1425, 10, 29], [1425, 11, 30], [1425, 12, 29], + [1426, 1, 29], [1426, 2, 30], [1426, 3, 29], [1426, 4, 30], [1426, 5, 29], [1426, 6, 30], [1426, 7, 30], + [1426, 8, 29], [1426, 9, 30], [1426, 10, 30], [1426, 11, 29], [1426, 12, 30], [1427, 1, 29], [1427, 2, 29], + [1427, 3, 30], [1427, 4, 29], [1427, 5, 30], [1427, 6, 29], [1427, 7, 30], [1427, 8, 30], [1427, 9, 29], + [1427, 10, 30], [1427, 11, 30], [1427, 12, 29], [1428, 1, 30], [1428, 2, 29], [1428, 3, 29], [1428, 4, 30], + [1428, 5, 29], [1428, 6, 29], [1428, 7, 30], [1428, 8, 30], [1428, 9, 30], [1428, 10, 29], [1428, 11, 30], + [1428, 12, 30], [1429, 1, 29], [1429, 2, 30], [1429, 3, 29], [1429, 4, 29], [1429, 5, 30], [1429, 6, 29], + [1429, 7, 29], [1429, 8, 30], [1429, 9, 30], [1429, 10, 29], [1429, 11, 30], [1429, 12, 30], [1430, 1, 29], + [1430, 2, 30], [1430, 3, 30], [1430, 4, 29], [1430, 5, 29], [1430, 6, 30], [1430, 7, 29], [1430, 8, 30], + [1430, 9, 29], [1430, 10, 30], [1430, 11, 29], [1430, 12, 30], [1431, 1, 29], [1431, 2, 30], [1431, 3, 30], + [1431, 4, 29], [1431, 5, 30], [1431, 6, 29], [1431, 7, 30], [1431, 8, 29], [1431, 9, 30], [1431, 10, 29], + [1431, 11, 29], [1431, 12, 30], [1432, 1, 29], [1432, 2, 30], [1432, 3, 30], [1432, 4, 30], [1432, 5, 29], + [1432, 6, 30], [1432, 7, 29], [1432, 8, 30], [1432, 9, 29], [1432, 10, 30], [1432, 11, 29], [1432, 12, 29], + [1433, 1, 30], [1433, 2, 29], [1433, 3, 30], [1433, 4, 30], [1433, 5, 29], [1433, 6, 30], [1433, 7, 30], + [1433, 8, 29], [1433, 9, 30], [1433, 10, 29], [1433, 11, 30], [1433, 12, 29], [1434, 1, 29], [1434, 2, 30], + [1434, 3, 29], [1434, 4, 30], [1434, 5, 29], [1434, 6, 30], [1434, 7, 30], [1434, 8, 29], [1434, 9, 30], + [1434, 10, 30], [1434, 11, 29], [1434, 12, 29], [1435, 1, 30], [1435, 2, 29], [1435, 3, 30], [1435, 4, 29], + [1435, 5, 30], [1435, 6, 29], [1435, 7, 30], [1435, 8, 29], [1435, 9, 30], [1435, 10, 30], [1435, 11, 29], + [1435, 12, 30], [1436, 1, 29], [1436, 2, 30], [1436, 3, 29], [1436, 4, 30], [1436, 5, 29], [1436, 6, 30], + [1436, 7, 29], [1436, 8, 30], [1436, 9, 29], [1436, 10, 30], [1436, 11, 29], [1436, 12, 30], [1437, 1, 30], + [1437, 2, 29], [1437, 3, 30], [1437, 4, 30], [1437, 5, 29], [1437, 6, 29], [1437, 7, 30], [1437, 8, 29], + [1437, 9, 30], [1437, 10, 29], [1437, 11, 29], [1437, 12, 30], [1438, 1, 30], [1438, 2, 29], [1438, 3, 30], + [1438, 4, 30], [1438, 5, 30], [1438, 6, 29], [1438, 7, 29], [1438, 8, 30], [1438, 9, 29], [1438, 10, 29], + [1438, 11, 30], [1438, 12, 29], [1439, 1, 30], [1439, 2, 29], [1439, 3, 30], [1439, 4, 30], [1439, 5, 30], + [1439, 6, 29], [1439, 7, 30], [1439, 8, 29], [1439, 9, 30], [1439, 10, 29], [1439, 11, 29], [1439, 12, 30], + [1440, 1, 29], [1440, 2, 30], [1440, 3, 29], [1440, 4, 30], [1440, 5, 30], [1440, 6, 30], [1440, 7, 29], + [1440, 8, 30], [1440, 9, 29], [1440, 10, 30], [1440, 11, 29], [1440, 12, 29], [1441, 1, 30], [1441, 2, 29], + [1441, 3, 30], [1441, 4, 29], [1441, 5, 30], [1441, 6, 30], [1441, 7, 29], [1441, 8, 30], [1441, 9, 30], + [1441, 10, 29], [1441, 11, 30], [1441, 12, 29], [1442, 1, 29], [1442, 2, 30], [1442, 3, 29], [1442, 4, 30], + [1442, 5, 29], [1442, 6, 30], [1442, 7, 29], [1442, 8, 30], [1442, 9, 30], [1442, 10, 29], [1442, 11, 30], + [1442, 12, 29], [1443, 1, 30], [1443, 2, 29], [1443, 3, 30], [1443, 4, 29], [1443, 5, 30], [1443, 6, 29], + [1443, 7, 30], [1443, 8, 29], [1443, 9, 30], [1443, 10, 29], [1443, 11, 30], [1443, 12, 30], [1444, 1, 29], + [1444, 2, 30], [1444, 3, 29], [1444, 4, 30], [1444, 5, 30], [1444, 6, 29], [1444, 7, 29], [1444, 8, 30], + [1444, 9, 29], [1444, 10, 30], [1444, 11, 29], [1444, 12, 30], [1445, 1, 29], [1445, 2, 30], [1445, 3, 30], + [1445, 4, 30], [1445, 5, 29], [1445, 6, 30], [1445, 7, 29], [1445, 8, 29], [1445, 9, 30], [1445, 10, 29], + [1445, 11, 29], [1445, 12, 30], [1446, 1, 29], [1446, 2, 30], [1446, 3, 30], [1446, 4, 30], [1446, 5, 29], + [1446, 6, 30], [1446, 7, 30], [1446, 8, 29], [1446, 9, 29], [1446, 10, 30], [1446, 11, 29], [1446, 12, 29], + [1447, 1, 30], [1447, 2, 29], [1447, 3, 30], [1447, 4, 30], [1447, 5, 30], [1447, 6, 29], [1447, 7, 30], + [1447, 8, 29], [1447, 9, 30], [1447, 10, 29], [1447, 11, 30], [1447, 12, 29], [1448, 1, 29], [1448, 2, 30], + [1448, 3, 29], [1448, 4, 30], [1448, 5, 30], [1448, 6, 29], [1448, 7, 30], [1448, 8, 30], [1448, 9, 29], + [1448, 10, 30], [1448, 11, 29], [1448, 12, 30], [1449, 1, 29], [1449, 2, 29], [1449, 3, 30], [1449, 4, 29], + [1449, 5, 30], [1449, 6, 29], [1449, 7, 30], [1449, 8, 30], [1449, 9, 29], [1449, 10, 30], [1449, 11, 30], + [1449, 12, 29], [1450, 1, 30], [1450, 2, 29], [1450, 3, 30], [1450, 4, 29], [1450, 5, 29], [1450, 6, 30], + [1450, 7, 29], [1450, 8, 30], [1450, 9, 29], [1450, 10, 30], [1450, 11, 30], [1450, 12, 29], [1451, 1, 30], + [1451, 2, 30], [1451, 3, 30], [1451, 4, 29], [1451, 5, 29], [1451, 6, 30], [1451, 7, 29], [1451, 8, 29], + [1451, 9, 30], [1451, 10, 30], [1451, 11, 29], [1451, 12, 30], [1452, 1, 30], [1452, 2, 29], [1452, 3, 30], + [1452, 4, 30], [1452, 5, 29], [1452, 6, 29], [1452, 7, 30], [1452, 8, 29], [1452, 9, 29], [1452, 10, 30], + [1452, 11, 29], [1452, 12, 30], [1453, 1, 30], [1453, 2, 29], [1453, 3, 30], [1453, 4, 30], [1453, 5, 29], + [1453, 6, 30], [1453, 7, 29], [1453, 8, 30], [1453, 9, 29], [1453, 10, 29], [1453, 11, 30], [1453, 12, 29], + [1454, 1, 30], [1454, 2, 29], [1454, 3, 30], [1454, 4, 30], [1454, 5, 29], [1454, 6, 30], [1454, 7, 30], + [1454, 8, 29], [1454, 9, 30], [1454, 10, 29], [1454, 11, 30], [1454, 12, 29], [1455, 1, 29], [1455, 2, 30], + [1455, 3, 29], [1455, 4, 30], [1455, 5, 30], [1455, 6, 29], [1455, 7, 30], [1455, 8, 29], [1455, 9, 30], + [1455, 10, 30], [1455, 11, 29], [1455, 12, 30], [1456, 1, 29], [1456, 2, 29], [1456, 3, 30], [1456, 4, 29], + [1456, 5, 30], [1456, 6, 29], [1456, 7, 30], [1456, 8, 29], [1456, 9, 30], [1456, 10, 30], [1456, 11, 30], + [1456, 12, 29], [1457, 1, 30], [1457, 2, 29], [1457, 3, 29], [1457, 4, 30], [1457, 5, 29], [1457, 6, 29], + [1457, 7, 30], [1457, 8, 29], [1457, 9, 30], [1457, 10, 30], [1457, 11, 30], [1457, 12, 30], [1458, 1, 29], + [1458, 2, 30], [1458, 3, 29], [1458, 4, 29], [1458, 5, 30], [1458, 6, 29], [1458, 7, 29], [1458, 8, 30], + [1458, 9, 29], [1458, 10, 30], [1458, 11, 30], [1458, 12, 30], [1459, 1, 29], [1459, 2, 30], [1459, 3, 30], + [1459, 4, 29], [1459, 5, 29], [1459, 6, 30], [1459, 7, 29], [1459, 8, 29], [1459, 9, 30], [1459, 10, 29], + [1459, 11, 30], [1459, 12, 30], [1460, 1, 29], [1460, 2, 30], [1460, 3, 30], [1460, 4, 29], [1460, 5, 30], + [1460, 6, 29], [1460, 7, 30], [1460, 8, 29], [1460, 9, 29], [1460, 10, 30], [1460, 11, 29], [1460, 12, 30], + [1461, 1, 29], [1461, 2, 30], [1461, 3, 30], [1461, 4, 29], [1461, 5, 30], [1461, 6, 29], [1461, 7, 30], + [1461, 8, 29], [1461, 9, 30], [1461, 10, 30], [1461, 11, 29], [1461, 12, 29], [1462, 1, 30], [1462, 2, 29], + [1462, 3, 30], [1462, 4, 29], [1462, 5, 30], [1462, 6, 30], [1462, 7, 29], [1462, 8, 30], [1462, 9, 29], + [1462, 10, 30], [1462, 11, 30], [1462, 12, 29], [1463, 1, 29], [1463, 2, 30], [1463, 3, 29], [1463, 4, 30], + [1463, 5, 29], [1463, 6, 30], [1463, 7, 29], [1463, 8, 30], [1463, 9, 30], [1463, 10, 30], [1463, 11, 29], + [1463, 12, 30], [1464, 1, 29], [1464, 2, 30], [1464, 3, 29], [1464, 4, 29], [1464, 5, 30], [1464, 6, 29], + [1464, 7, 29], [1464, 8, 30], [1464, 9, 30], [1464, 10, 30], [1464, 11, 29], [1464, 12, 30], [1465, 1, 30], + [1465, 2, 29], [1465, 3, 30], [1465, 4, 29], [1465, 5, 29], [1465, 6, 30], [1465, 7, 29], [1465, 8, 29], + [1465, 9, 30], [1465, 10, 30], [1465, 11, 29], [1465, 12, 30], [1466, 1, 30], [1466, 2, 30], [1466, 3, 29], + [1466, 4, 30], [1466, 5, 29], [1466, 6, 29], [1466, 7, 29], [1466, 8, 30], [1466, 9, 29], [1466, 10, 30], + [1466, 11, 30], [1466, 12, 29], [1467, 1, 30], [1467, 2, 30], [1467, 3, 29], [1467, 4, 30], [1467, 5, 30], + [1467, 6, 29], [1467, 7, 29], [1467, 8, 30], [1467, 9, 29], [1467, 10, 30], [1467, 11, 29], [1467, 12, 30], + [1468, 1, 29], [1468, 2, 30], [1468, 3, 29], [1468, 4, 30], [1468, 5, 30], [1468, 6, 29], [1468, 7, 30], + [1468, 8, 29], [1468, 9, 30], [1468, 10, 29], [1468, 11, 30], [1468, 12, 29], [1469, 1, 29], [1469, 2, 30], + [1469, 3, 29], [1469, 4, 30], [1469, 5, 30], [1469, 6, 29], [1469, 7, 30], [1469, 8, 30], [1469, 9, 29], + [1469, 10, 30], [1469, 11, 29], [1469, 12, 30], [1470, 1, 29], [1470, 2, 29], [1470, 3, 30], [1470, 4, 29], + [1470, 5, 30], [1470, 6, 30], [1470, 7, 29], [1470, 8, 30], [1470, 9, 30], [1470, 10, 29], [1470, 11, 30], + [1470, 12, 29], [1471, 1, 30], [1471, 2, 29], [1471, 3, 29], [1471, 4, 30], [1471, 5, 29], [1471, 6, 30], + [1471, 7, 29], [1471, 8, 30], [1471, 9, 30], [1471, 10, 29], [1471, 11, 30], [1471, 12, 30], [1472, 1, 29], + [1472, 2, 30], [1472, 3, 29], [1472, 4, 29], [1472, 5, 30], [1472, 6, 29], [1472, 7, 30], [1472, 8, 29], + [1472, 9, 30], [1472, 10, 30], [1472, 11, 29], [1472, 12, 30], [1473, 1, 29], [1473, 2, 30], [1473, 3, 29], + [1473, 4, 30], [1473, 5, 30], [1473, 6, 29], [1473, 7, 29], [1473, 8, 30], [1473, 9, 29], [1473, 10, 30], + [1473, 11, 29], [1473, 12, 30], [1474, 1, 29], [1474, 2, 30], [1474, 3, 30], [1474, 4, 29], [1474, 5, 30], + [1474, 6, 30], [1474, 7, 29], [1474, 8, 29], [1474, 9, 30], [1474, 10, 29], [1474, 11, 30], [1474, 12, 29], + [1475, 1, 29], [1475, 2, 30], [1475, 3, 30], [1475, 4, 29], [1475, 5, 30], [1475, 6, 30], [1475, 7, 30], + [1475, 8, 29], [1475, 9, 29], [1475, 10, 30], [1475, 11, 29], [1475, 12, 29], [1476, 1, 30], [1476, 2, 29], + [1476, 3, 30], [1476, 4, 29], [1476, 5, 30], [1476, 6, 30], [1476, 7, 30], [1476, 8, 29], [1476, 9, 30], + [1476, 10, 29], [1476, 11, 30], [1476, 12, 29], [1477, 1, 29], [1477, 2, 30], [1477, 3, 29], [1477, 4, 29], + [1477, 5, 30], [1477, 6, 30], [1477, 7, 30], [1477, 8, 30], [1477, 9, 29], [1477, 10, 30], [1477, 11, 29], + [1477, 12, 30], [1478, 1, 29], [1478, 2, 29], [1478, 3, 30], [1478, 4, 29], [1478, 5, 30], [1478, 6, 29], + [1478, 7, 30], [1478, 8, 30], [1478, 9, 29], [1478, 10, 30], [1478, 11, 30], [1478, 12, 29], [1479, 1, 30], + [1479, 2, 29], [1479, 3, 29], [1479, 4, 30], [1479, 5, 29], [1479, 6, 30], [1479, 7, 29], [1479, 8, 30], + [1479, 9, 29], [1479, 10, 30], [1479, 11, 30], [1479, 12, 29], [1480, 1, 30], [1480, 2, 29], [1480, 3, 30], + [1480, 4, 29], [1480, 5, 30], [1480, 6, 29], [1480, 7, 30], [1480, 8, 29], [1480, 9, 30], [1480, 10, 29], + [1480, 11, 30], [1480, 12, 29], [1481, 1, 30], [1481, 2, 29], [1481, 3, 30], [1481, 4, 30], [1481, 5, 29], + [1481, 6, 30], [1481, 7, 29], [1481, 8, 30], [1481, 9, 29], [1481, 10, 30], [1481, 11, 29], [1481, 12, 29], + [1482, 1, 30], [1482, 2, 29], [1482, 3, 30], [1482, 4, 30], [1482, 5, 30], [1482, 6, 30], [1482, 7, 29], + [1482, 8, 30], [1482, 9, 29], [1482, 10, 29], [1482, 11, 30], [1482, 12, 29], [1483, 1, 29], [1483, 2, 30], + [1483, 3, 29], [1483, 4, 30], [1483, 5, 30], [1483, 6, 30], [1483, 7, 29], [1483, 8, 30], [1483, 9, 30], + [1483, 10, 29], [1483, 11, 29], [1483, 12, 30], [1484, 1, 29], [1484, 2, 29], [1484, 3, 30], [1484, 4, 29], + [1484, 5, 30], [1484, 6, 30], [1484, 7, 30], [1484, 8, 29], [1484, 9, 30], [1484, 10, 29], [1484, 11, 30], + [1484, 12, 29], [1485, 1, 30], [1485, 2, 29], [1485, 3, 29], [1485, 4, 30], [1485, 5, 29], [1485, 6, 30], + [1485, 7, 30], [1485, 8, 29], [1485, 9, 30], [1485, 10, 30], [1485, 11, 29], [1485, 12, 30], [1486, 1, 29], + [1486, 2, 30], [1486, 3, 29], [1486, 4, 29], [1486, 5, 30], [1486, 6, 29], [1486, 7, 30], [1486, 8, 29], + [1486, 9, 30], [1486, 10, 30], [1486, 11, 29], [1486, 12, 30], [1487, 1, 30], [1487, 2, 29], [1487, 3, 30], + [1487, 4, 29], [1487, 5, 30], [1487, 6, 29], [1487, 7, 29], [1487, 8, 30], [1487, 9, 29], [1487, 10, 30], + [1487, 11, 29], [1487, 12, 30], [1488, 1, 30], [1488, 2, 29], [1488, 3, 30], [1488, 4, 30], [1488, 5, 29], + [1488, 6, 30], [1488, 7, 29], [1488, 8, 29], [1488, 9, 30], [1488, 10, 29], [1488, 11, 30], [1488, 12, 29], + [1489, 1, 30], [1489, 2, 29], [1489, 3, 30], [1489, 4, 30], [1489, 5, 30], [1489, 6, 29], [1489, 7, 30], + [1489, 8, 29], [1489, 9, 29], [1489, 10, 30], [1489, 11, 29], [1489, 12, 30], [1490, 1, 29], [1490, 2, 30], + [1490, 3, 29], [1490, 4, 30], [1490, 5, 30], [1490, 6, 29], [1490, 7, 30], [1490, 8, 30], [1490, 9, 29], + [1490, 10, 29], [1490, 11, 30], [1490, 12, 29], [1491, 1, 30], [1491, 2, 29], [1491, 3, 29], [1491, 4, 30], + [1491, 5, 30], [1491, 6, 29], [1491, 7, 30], [1491, 8, 30], [1491, 9, 29], [1491, 10, 30], [1491, 11, 29], + [1491, 12, 30], [1492, 1, 29], [1492, 2, 30], [1492, 3, 29], [1492, 4, 29], [1492, 5, 30], [1492, 6, 30], + [1492, 7, 29], [1492, 8, 30], [1492, 9, 29], [1492, 10, 30], [1492, 11, 30], [1492, 12, 29], [1493, 1, 30], + [1493, 2, 29], [1493, 3, 30], [1493, 4, 29], [1493, 5, 30], [1493, 6, 29], [1493, 7, 29], [1493, 8, 30], + [1493, 9, 29], [1493, 10, 30], [1493, 11, 30], [1493, 12, 30], [1494, 1, 29], [1494, 2, 30], [1494, 3, 29], + [1494, 4, 30], [1494, 5, 29], [1494, 6, 30], [1494, 7, 29], [1494, 8, 29], [1494, 9, 29], [1494, 10, 30], + [1494, 11, 30], [1494, 12, 30], [1495, 1, 29], [1495, 2, 30], [1495, 3, 30], [1495, 4, 29], [1495, 5, 30], + [1495, 6, 29], [1495, 7, 29], [1495, 8, 30], [1495, 9, 29], [1495, 10, 29], [1495, 11, 30], [1495, 12, 30], + [1496, 1, 29], [1496, 2, 30], [1496, 3, 30], [1496, 4, 30], [1496, 5, 29], [1496, 6, 30], [1496, 7, 29], + [1496, 8, 29], [1496, 9, 30], [1496, 10, 29], [1496, 11, 29], [1496, 12, 30], [1497, 1, 30], [1497, 2, 29], + [1497, 3, 30], [1497, 4, 30], [1497, 5, 29], [1497, 6, 30], [1497, 7, 29], [1497, 8, 30], [1497, 9, 29], + [1497, 10, 30], [1497, 11, 29], [1497, 12, 30], [1498, 1, 29], [1498, 2, 30], [1498, 3, 29], [1498, 4, 30], + [1498, 5, 29], [1498, 6, 30], [1498, 7, 30], [1498, 8, 29], [1498, 9, 30], [1498, 10, 29], [1498, 11, 30], + [1498, 12, 29], [1499, 1, 30], [1499, 2, 29], [1499, 3, 30], [1499, 4, 29], [1499, 5, 29], [1499, 6, 30], + [1499, 7, 30], [1499, 8, 29], [1499, 9, 30], [1499, 10, 29], [1499, 11, 30], [1499, 12, 30], [1500, 1, 29], + [1500, 2, 30], [1500, 3, 29], [1500, 4, 30], [1500, 5, 29], [1500, 6, 29], [1500, 7, 30], [1500, 8, 29], + [1500, 9, 30], [1500, 10, 29], [1500, 11, 30], [1500, 12, 30], [1501, 1, 30], [1501, 2, 29], [1501, 3, 30], + [1501, 4, 29], [1501, 5, 30], [1501, 6, 29], [1501, 7, 29], [1501, 8, 29], [1501, 9, 30], [1501, 10, 29], + [1501, 11, 30], [1501, 12, 30], [1502, 1, 30], [1502, 2, 30], [1502, 3, 29], [1502, 4, 30], [1502, 5, 29], + [1502, 6, 30], [1502, 7, 29], [1502, 8, 29], [1502, 9, 29], [1502, 10, 30], [1502, 11, 30], [1502, 12, 29], + [1503, 1, 30], [1503, 2, 30], [1503, 3, 29], [1503, 4, 30], [1503, 5, 30], [1503, 6, 29], [1503, 7, 30], + [1503, 8, 29], [1503, 9, 29], [1503, 10, 29], [1503, 11, 30], [1503, 12, 30], [1504, 1, 29], [1504, 2, 30], + [1504, 3, 29], [1504, 4, 30], [1504, 5, 30], [1504, 6, 30], [1504, 7, 29], [1504, 8, 29], [1504, 9, 30], + [1504, 10, 29], [1504, 11, 30], [1504, 12, 29], [1505, 1, 30], [1505, 2, 29], [1505, 3, 30], [1505, 4, 29], + [1505, 5, 30], [1505, 6, 30], [1505, 7, 29], [1505, 8, 30], [1505, 9, 29], [1505, 10, 30], [1505, 11, 30], + [1505, 12, 29], [1506, 1, 29], [1506, 2, 30], [1506, 3, 29], [1506, 4, 29], [1506, 5, 30], [1506, 6, 30], + [1506, 7, 29], [1506, 8, 30], [1506, 9, 30], [1506, 10, 29], [1506, 11, 30], [1506, 12, 30], [1507, 1, 29], + [1507, 2, 29], [1507, 3, 30], [1507, 4, 29], [1507, 5, 29], [1507, 6, 30], [1507, 7, 30], [1507, 8, 29], + [1507, 9, 30], [1507, 10, 29], [1507, 11, 30], [1507, 12, 30], [1508, 1, 30], [1508, 2, 29], [1508, 3, 29], + [1508, 4, 30], [1508, 5, 29], [1508, 6, 30], [1508, 7, 29], [1508, 8, 29], [1508, 9, 30], [1508, 10, 29], + [1508, 11, 30], [1508, 12, 30], [1509, 1, 30], [1509, 2, 29], [1509, 3, 30], [1509, 4, 29], [1509, 5, 30], + [1509, 6, 29], [1509, 7, 30], [1509, 8, 29], [1509, 9, 29], [1509, 10, 30], [1509, 11, 29], [1509, 12, 30], + [1510, 1, 30], [1510, 2, 29], [1510, 3, 30], [1510, 4, 30], [1510, 5, 29], [1510, 6, 30], [1510, 7, 29], + [1510, 8, 30], [1510, 9, 29], [1510, 10, 29], [1510, 11, 30], [1510, 12, 29], [1511, 1, 30], [1511, 2, 29], + [1511, 3, 30], [1511, 4, 30], [1511, 5, 29], [1511, 6, 30], [1511, 7, 30], [1511, 8, 29], [1511, 9, 30], + [1511, 10, 29], [1511, 11, 29], [1511, 12, 30], [1512, 1, 29], [1512, 2, 30], [1512, 3, 29], [1512, 4, 30], + [1512, 5, 29], [1512, 6, 30], [1512, 7, 30], [1512, 8, 30], [1512, 9, 29], [1512, 10, 30], [1512, 11, 29], + [1512, 12, 30], [1513, 1, 29], [1513, 2, 29], [1513, 3, 29], [1513, 4, 30], [1513, 5, 29], [1513, 6, 30], + [1513, 7, 30], [1513, 8, 30], [1513, 9, 29], [1513, 10, 30], [1513, 11, 30], [1513, 12, 29], [1514, 1, 30], + [1514, 2, 29], [1514, 3, 29], [1514, 4, 29], [1514, 5, 30], [1514, 6, 29], [1514, 7, 30], [1514, 8, 30], + [1514, 9, 29], [1514, 10, 30], [1514, 11, 30], [1514, 12, 30], [1515, 1, 29], [1515, 2, 29], [1515, 3, 30], + [1515, 4, 29], [1515, 5, 29], [1515, 6, 30], [1515, 7, 29], [1515, 8, 30], [1515, 9, 30], [1515, 10, 29], + [1515, 11, 30], [1515, 12, 30], [1516, 1, 29], [1516, 2, 30], [1516, 3, 29], [1516, 4, 30], [1516, 5, 29], + [1516, 6, 29], [1516, 7, 30], [1516, 8, 29], [1516, 9, 30], [1516, 10, 29], [1516, 11, 30], [1516, 12, 30], + [1517, 1, 29], [1517, 2, 30], [1517, 3, 29], [1517, 4, 30], [1517, 5, 29], [1517, 6, 30], [1517, 7, 30], + [1517, 8, 29], [1517, 9, 29], [1517, 10, 30], [1517, 11, 29], [1517, 12, 30], [1518, 1, 29], [1518, 2, 30], + [1518, 3, 29], [1518, 4, 30], [1518, 5, 30], [1518, 6, 29], [1518, 7, 30], [1518, 8, 30], [1518, 9, 29], + [1518, 10, 30], [1518, 11, 29], [1518, 12, 29], [1519, 1, 30], [1519, 2, 29], [1519, 3, 29], [1519, 4, 30], + [1519, 5, 30], [1519, 6, 30], [1519, 7, 29], [1519, 8, 30], [1519, 9, 30], [1519, 10, 29], [1519, 11, 30], + [1519, 12, 29], [1520, 1, 29], [1520, 2, 30], [1520, 3, 29], [1520, 4, 29], [1520, 5, 30], [1520, 6, 30], + [1520, 7, 30], [1520, 8, 29], [1520, 9, 30], [1520, 10, 30], [1520, 11, 29], [1520, 12, 30], [1521, 1, 29], + [1521, 2, 29], [1521, 3, 29], [1521, 4, 30], [1521, 5, 29], [1521, 6, 30], [1521, 7, 30], [1521, 8, 29], + [1521, 9, 30], [1521, 10, 30], [1521, 11, 29], [1521, 12, 30], [1522, 1, 30], [1522, 2, 29], [1522, 3, 29], + [1522, 4, 29], [1522, 5, 30], [1522, 6, 29], [1522, 7, 30], [1522, 8, 30], [1522, 9, 29], [1522, 10, 30], + [1522, 11, 30], [1522, 12, 29], [1523, 1, 30], [1523, 2, 29], [1523, 3, 30], [1523, 4, 29], [1523, 5, 30], + [1523, 6, 29], [1523, 7, 30], [1523, 8, 29], [1523, 9, 29], [1523, 10, 30], [1523, 11, 30], [1523, 12, 29], + [1524, 1, 30], [1524, 2, 30], [1524, 3, 29], [1524, 4, 30], [1524, 5, 29], [1524, 6, 30], [1524, 7, 29], + [1524, 8, 30], [1524, 9, 29], [1524, 10, 29], [1524, 11, 30], [1524, 12, 29], [1525, 1, 30], [1525, 2, 30], + [1525, 3, 29], [1525, 4, 30], [1525, 5, 30], [1525, 6, 29], [1525, 7, 30], [1525, 8, 29], [1525, 9, 30], + [1525, 10, 29], [1525, 11, 29], [1525, 12, 30], [1526, 1, 29], [1526, 2, 30], [1526, 3, 29], [1526, 4, 30], + [1526, 5, 30], [1526, 6, 30], [1526, 7, 29], [1526, 8, 30], [1526, 9, 29], [1526, 10, 30], [1526, 11, 29], + [1526, 12, 29], [1527, 1, 30], [1527, 2, 29], [1527, 3, 30], [1527, 4, 29], [1527, 5, 30], [1527, 6, 30], + [1527, 7, 29], [1527, 8, 30], [1527, 9, 30], [1527, 10, 29], [1527, 11, 30], [1527, 12, 29], [1528, 1, 30], + [1528, 2, 29], [1528, 3, 29], [1528, 4, 30], [1528, 5, 29], [1528, 6, 30], [1528, 7, 29], [1528, 8, 30], + [1528, 9, 30], [1528, 10, 29], [1528, 11, 30], [1528, 12, 30], [1529, 1, 29], [1529, 2, 30], [1529, 3, 29], + [1529, 4, 29], [1529, 5, 30], [1529, 6, 29], [1529, 7, 30], [1529, 8, 29], [1529, 9, 30], [1529, 10, 29], + [1529, 11, 30], [1529, 12, 30], [1530, 1, 29], [1530, 2, 30], [1530, 3, 30], [1530, 4, 29], [1530, 5, 29], + [1530, 6, 30], [1530, 7, 29], [1530, 8, 30], [1530, 9, 29], [1530, 10, 29], [1530, 11, 30], [1530, 12, 30], + [1531, 1, 29], [1531, 2, 30], [1531, 3, 30], [1531, 4, 30], [1531, 5, 29], [1531, 6, 29], [1531, 7, 30], + [1531, 8, 29], [1531, 9, 30], [1531, 10, 29], [1531, 11, 29], [1531, 12, 30], [1532, 1, 29], [1532, 2, 30], + [1532, 3, 30], [1532, 4, 30], [1532, 5, 29], [1532, 6, 30], [1532, 7, 30], [1532, 8, 29], [1532, 9, 29], + [1532, 10, 29], [1532, 11, 30], [1532, 12, 29], [1533, 1, 30], [1533, 2, 29], [1533, 3, 30], [1533, 4, 30], + [1533, 5, 30], [1533, 6, 29], [1533, 7, 30], [1533, 8, 29], [1533, 9, 30], [1533, 10, 29], [1533, 11, 29], + [1533, 12, 30], [1534, 1, 29], [1534, 2, 30], [1534, 3, 29], [1534, 4, 30], [1534, 5, 30], [1534, 6, 29], + [1534, 7, 30], [1534, 8, 30], [1534, 9, 29], [1534, 10, 29], [1534, 11, 30], [1534, 12, 29], [1535, 1, 30], + [1535, 2, 29], [1535, 3, 30], [1535, 4, 29], [1535, 5, 30], [1535, 6, 29], [1535, 7, 30], [1535, 8, 30], + [1535, 9, 29], [1535, 10, 30], [1535, 11, 29], [1535, 12, 30], [1536, 1, 29], [1536, 2, 30], [1536, 3, 29], + [1536, 4, 30], [1536, 5, 29], [1536, 6, 30], [1536, 7, 29], [1536, 8, 30], [1536, 9, 29], [1536, 10, 30], + [1536, 11, 29], [1536, 12, 30], [1537, 1, 30], [1537, 2, 29], [1537, 3, 30], [1537, 4, 30], [1537, 5, 29], + [1537, 6, 29], [1537, 7, 30], [1537, 8, 29], [1537, 9, 29], [1537, 10, 30], [1537, 11, 29], [1537, 12, 30], + [1538, 1, 30], [1538, 2, 30], [1538, 3, 29], [1538, 4, 30], [1538, 5, 30], [1538, 6, 29], [1538, 7, 29], + [1538, 8, 30], [1538, 9, 29], [1538, 10, 29], [1538, 11, 30], [1538, 12, 29], [1539, 1, 30], [1539, 2, 30], + [1539, 3, 30], [1539, 4, 29], [1539, 5, 30], [1539, 6, 30], [1539, 7, 29], [1539, 8, 29], [1539, 9, 30], + [1539, 10, 29], [1539, 11, 29], [1539, 12, 30], [1540, 1, 29], [1540, 2, 30], [1540, 3, 30], [1540, 4, 29], + [1540, 5, 30], [1540, 6, 30], [1540, 7, 29], [1540, 8, 30], [1540, 9, 29], [1540, 10, 29], [1540, 11, 30], + [1540, 12, 29], [1541, 1, 30], [1541, 2, 29], [1541, 3, 30], [1541, 4, 29], [1541, 5, 30], [1541, 6, 30], + [1541, 7, 30], [1541, 8, 29], [1541, 9, 30], [1541, 10, 29], [1541, 11, 29], [1541, 12, 30], [1542, 1, 29], + [1542, 2, 30], [1542, 3, 29], [1542, 4, 30], [1542, 5, 29], [1542, 6, 30], [1542, 7, 30], [1542, 8, 29], + [1542, 9, 30], [1542, 10, 29], [1542, 11, 30], [1542, 12, 30], [1543, 1, 29], [1543, 2, 30], [1543, 3, 29], + [1543, 4, 29], [1543, 5, 30], [1543, 6, 29], [1543, 7, 30], [1543, 8, 29], [1543, 9, 30], [1543, 10, 29], + [1543, 11, 30], [1543, 12, 30], [1544, 1, 30], [1544, 2, 29], [1544, 3, 30], [1544, 4, 29], [1544, 5, 29], + [1544, 6, 30], [1544, 7, 29], [1544, 8, 30], [1544, 9, 29], [1544, 10, 30], [1544, 11, 29], [1544, 12, 30], + [1545, 1, 30], [1545, 2, 30], [1545, 3, 29], [1545, 4, 30], [1545, 5, 29], [1545, 6, 29], [1545, 7, 30], + [1545, 8, 29], [1545, 9, 30], [1545, 10, 29], [1545, 11, 29], [1545, 12, 30], [1546, 1, 30], [1546, 2, 30], + [1546, 3, 29], [1546, 4, 30], [1546, 5, 29], [1546, 6, 30], [1546, 7, 29], [1546, 8, 30], [1546, 9, 29], + [1546, 10, 30], [1546, 11, 29], [1546, 12, 29], [1547, 1, 30], [1547, 2, 30], [1547, 3, 29], [1547, 4, 30], + [1547, 5, 30], [1547, 6, 29], [1547, 7, 30], [1547, 8, 29], [1547, 9, 30], [1547, 10, 29], [1547, 11, 30], + [1547, 12, 29], [1548, 1, 30], [1548, 2, 29], [1548, 3, 29], [1548, 4, 30], [1548, 5, 30], [1548, 6, 29], + [1548, 7, 30], [1548, 8, 30], [1548, 9, 29], [1548, 10, 30], [1548, 11, 29], [1548, 12, 30], [1549, 1, 29], + [1549, 2, 30], [1549, 3, 29], [1549, 4, 29], [1549, 5, 30], [1549, 6, 29], [1549, 7, 30], [1549, 8, 30], + [1549, 9, 30], [1549, 10, 29], [1549, 11, 30], [1549, 12, 29], [1550, 1, 30], [1550, 2, 29], [1550, 3, 30], + [1550, 4, 29], [1550, 5, 29], [1550, 6, 29], [1550, 7, 30], [1550, 8, 30], [1550, 9, 30], [1550, 10, 29], + [1550, 11, 30], [1550, 12, 30], [1551, 1, 29], [1551, 2, 30], [1551, 3, 29], [1551, 4, 29], [1551, 5, 30], + [1551, 6, 29], [1551, 7, 29], [1551, 8, 30], [1551, 9, 30], [1551, 10, 29], [1551, 11, 30], [1551, 12, 30], + [1552, 1, 30], [1552, 2, 29], [1552, 3, 30], [1552, 4, 29], [1552, 5, 29], [1552, 6, 30], [1552, 7, 29], + [1552, 8, 29], [1552, 9, 30], [1552, 10, 30], [1552, 11, 29], [1552, 12, 30], [1553, 1, 30], [1553, 2, 29], + [1553, 3, 30], [1553, 4, 29], [1553, 5, 30], [1553, 6, 29], [1553, 7, 30], [1553, 8, 29], [1553, 9, 30], + [1553, 10, 29], [1553, 11, 30], [1553, 12, 29], [1554, 1, 30], [1554, 2, 29], [1554, 3, 30], [1554, 4, 29], + [1554, 5, 30], [1554, 6, 30], [1554, 7, 29], [1554, 8, 30], [1554, 9, 29], [1554, 10, 30], [1554, 11, 29], + [1554, 12, 30], [1555, 1, 29], [1555, 2, 29], [1555, 3, 30], [1555, 4, 29], [1555, 5, 30], [1555, 6, 30], + [1555, 7, 29], [1555, 8, 30], [1555, 9, 30], [1555, 10, 29], [1555, 11, 30], [1555, 12, 29], [1556, 1, 30], + [1556, 2, 29], [1556, 3, 29], [1556, 4, 30], [1556, 5, 29], [1556, 6, 30], [1556, 7, 29], [1556, 8, 30], + [1556, 9, 30], [1556, 10, 30], [1556, 11, 29], [1556, 12, 30], [1557, 1, 29], [1557, 2, 30], [1557, 3, 29], + [1557, 4, 29], [1557, 5, 29], [1557, 6, 30], [1557, 7, 29], [1557, 8, 30], [1557, 9, 30], [1557, 10, 30], + [1557, 11, 30], [1557, 12, 29], [1558, 1, 30], [1558, 2, 29], [1558, 3, 30], [1558, 4, 29], [1558, 5, 29], + [1558, 6, 29], [1558, 7, 30], [1558, 8, 29], [1558, 9, 30], [1558, 10, 30], [1558, 11, 30], [1558, 12, 29], + [1559, 1, 30], [1559, 2, 30], [1559, 3, 29], [1559, 4, 29], [1559, 5, 30], [1559, 6, 29], [1559, 7, 29], + [1559, 8, 30], [1559, 9, 30], [1559, 10, 29], [1559, 11, 30], [1559, 12, 29], [1560, 1, 30], [1560, 2, 30], + [1560, 3, 29], [1560, 4, 30], [1560, 5, 29], [1560, 6, 30], [1560, 7, 29], [1560, 8, 30], [1560, 9, 29], + [1560, 10, 30], [1560, 11, 29], [1560, 12, 30], [1561, 1, 29], [1561, 2, 30], [1561, 3, 30], [1561, 4, 29], + [1561, 5, 30], [1561, 6, 29], [1561, 7, 30], [1561, 8, 30], [1561, 9, 29], [1561, 10, 29], [1561, 11, 30], + [1561, 12, 29], [1562, 1, 29], [1562, 2, 30], [1562, 3, 30], [1562, 4, 29], [1562, 5, 30], [1562, 6, 29], + [1562, 7, 30], [1562, 8, 30], [1562, 9, 30], [1562, 10, 29], [1562, 11, 29], [1562, 12, 30], [1563, 1, 29], + [1563, 2, 30], [1563, 3, 29], [1563, 4, 29], [1563, 5, 30], [1563, 6, 29], [1563, 7, 30], [1563, 8, 30], + [1563, 9, 30], [1563, 10, 29], [1563, 11, 30], [1563, 12, 29], [1564, 1, 30], [1564, 2, 29], [1564, 3, 30], + [1564, 4, 29], [1564, 5, 29], [1564, 6, 30], [1564, 7, 29], [1564, 8, 30], [1564, 9, 30], [1564, 10, 30], + [1564, 11, 29], [1564, 12, 30], [1565, 1, 29], [1565, 2, 30], [1565, 3, 29], [1565, 4, 30], [1565, 5, 29], + [1565, 6, 29], [1565, 7, 30], [1565, 8, 29], [1565, 9, 30], [1565, 10, 30], [1565, 11, 29], [1565, 12, 30], + [1566, 1, 30], [1566, 2, 29], [1566, 3, 30], [1566, 4, 29], [1566, 5, 30], [1566, 6, 29], [1566, 7, 29], + [1566, 8, 30], [1566, 9, 29], [1566, 10, 30], [1566, 11, 29], [1566, 12, 30], [1567, 1, 30], [1567, 2, 29], + [1567, 3, 30], [1567, 4, 30], [1567, 5, 29], [1567, 6, 30], [1567, 7, 29], [1567, 8, 30], [1567, 9, 29], + [1567, 10, 29], [1567, 11, 30], [1567, 12, 29], [1568, 1, 30], [1568, 2, 29], [1568, 3, 30], [1568, 4, 30], + [1568, 5, 30], [1568, 6, 29], [1568, 7, 30], [1568, 8, 29], [1568, 9, 30], [1568, 10, 29], [1568, 11, 29], + [1568, 12, 29], [1569, 1, 30], [1569, 2, 29], [1569, 3, 30], [1569, 4, 30], [1569, 5, 30], [1569, 6, 29], + [1569, 7, 30], [1569, 8, 30], [1569, 9, 29], [1569, 10, 30], [1569, 11, 29], [1569, 12, 29], [1570, 1, 29], + [1570, 2, 30], [1570, 3, 29], [1570, 4, 30], [1570, 5, 30], [1570, 6, 29], [1570, 7, 30], [1570, 8, 30], + [1570, 9, 30], [1570, 10, 29], [1570, 11, 29], [1570, 12, 30], [1571, 1, 29], [1571, 2, 29], [1571, 3, 30], + [1571, 4, 29], [1571, 5, 30], [1571, 6, 30], [1571, 7, 29], [1571, 8, 30], [1571, 9, 30], [1571, 10, 29], + [1571, 11, 30], [1571, 12, 29], [1572, 1, 30], [1572, 2, 29], [1572, 3, 29], [1572, 4, 30], [1572, 5, 29], + [1572, 6, 30], [1572, 7, 29], [1572, 8, 30], [1572, 9, 30], [1572, 10, 29], [1572, 11, 30], [1572, 12, 29], + [1573, 1, 30], [1573, 2, 29], [1573, 3, 30], [1573, 4, 30], [1573, 5, 29], [1573, 6, 30], [1573, 7, 29], + [1573, 8, 29], [1573, 9, 30], [1573, 10, 29], [1573, 11, 30], [1573, 12, 29], [1574, 1, 30], [1574, 2, 30], + [1574, 3, 29], [1574, 4, 30], [1574, 5, 30], [1574, 6, 29], [1574, 7, 30], [1574, 8, 29], [1574, 9, 29], + [1574, 10, 30], [1574, 11, 29], [1574, 12, 29], [1575, 1, 30], [1575, 2, 30], [1575, 3, 30], [1575, 4, 29], + [1575, 5, 30], [1575, 6, 30], [1575, 7, 29], [1575, 8, 30], [1575, 9, 29], [1575, 10, 29], [1575, 11, 29], + [1575, 12, 30], [1576, 1, 29], [1576, 2, 30], [1576, 3, 30], [1576, 4, 29], [1576, 5, 30], [1576, 6, 30], + [1576, 7, 30], [1576, 8, 29], [1576, 9, 30], [1576, 10, 29], [1576, 11, 29], [1576, 12, 29], [1577, 1, 30], + [1577, 2, 29], [1577, 3, 30], [1577, 4, 30], [1577, 5, 29], [1577, 6, 30], [1577, 7, 30], [1577, 8, 29], + [1577, 9, 30], [1577, 10, 29], [1577, 11, 30], [1577, 12, 29], [1578, 1, 29], [1578, 2, 30], [1578, 3, 29], + [1578, 4, 30], [1578, 5, 29], [1578, 6, 30], [1578, 7, 30], [1578, 8, 29], [1578, 9, 30], [1578, 10, 30], + [1578, 11, 29], [1578, 12, 30], [1579, 1, 29], [1579, 2, 30], [1579, 3, 29], [1579, 4, 30], [1579, 5, 29], + [1579, 6, 29], [1579, 7, 30], [1579, 8, 30], [1579, 9, 29], [1579, 10, 30], [1579, 11, 29], [1579, 12, 30], + [1580, 1, 29], [1580, 2, 30], [1580, 3, 30], [1580, 4, 29], [1580, 5, 30], [1580, 6, 29], [1580, 7, 29], + [1580, 8, 30], [1580, 9, 29], [1580, 10, 30], [1580, 11, 29], [1580, 12, 30], [1581, 1, 30], [1581, 2, 30], + [1581, 3, 29], [1581, 4, 30], [1581, 5, 29], [1581, 6, 30], [1581, 7, 29], [1581, 8, 29], [1581, 9, 30], + [1581, 10, 29], [1581, 11, 30], [1581, 12, 29], [1582, 1, 30], [1582, 2, 30], [1582, 3, 29], [1582, 4, 30], + [1582, 5, 30], [1582, 6, 29], [1582, 7, 30], [1582, 8, 29], [1582, 9, 30], [1582, 10, 29], [1582, 11, 29], + [1582, 12, 29], [1583, 1, 30], [1583, 2, 30], [1583, 3, 29], [1583, 4, 30], [1583, 5, 30], [1583, 6, 30], + [1583, 7, 29], [1583, 8, 30], [1583, 9, 29], [1583, 10, 30], [1583, 11, 29], [1583, 12, 29], [1584, 1, 29], + [1584, 2, 30], [1584, 3, 30], [1584, 4, 29], [1584, 5, 30], [1584, 6, 30], [1584, 7, 29], [1584, 8, 30], + [1584, 9, 30], [1584, 10, 29], [1584, 11, 30], [1584, 12, 29], [1585, 1, 29], [1585, 2, 30], [1585, 3, 29], + [1585, 4, 30], [1585, 5, 29], [1585, 6, 30], [1585, 7, 29], [1585, 8, 30], [1585, 9, 30], [1585, 10, 29], + [1585, 11, 30], [1585, 12, 30], [1586, 1, 29], [1586, 2, 29], [1586, 3, 30], [1586, 4, 29], [1586, 5, 30], + [1586, 6, 29], [1586, 7, 29], [1586, 8, 30], [1586, 9, 30], [1586, 10, 30], [1586, 11, 29], [1586, 12, 30], + [1587, 1, 29], [1587, 2, 30], [1587, 3, 30], [1587, 4, 29], [1587, 5, 29], [1587, 6, 29], [1587, 7, 30], + [1587, 8, 29], [1587, 9, 30], [1587, 10, 29], [1587, 11, 30], [1587, 12, 30], [1588, 1, 30], [1588, 2, 29], + [1588, 3, 30], [1588, 4, 30], [1588, 5, 29], [1588, 6, 29], [1588, 7, 29], [1588, 8, 30], [1588, 9, 29], + [1588, 10, 30], [1588, 11, 29], [1588, 12, 30], [1589, 1, 30], [1589, 2, 29], [1589, 3, 30], [1589, 4, 30], + [1589, 5, 29], [1589, 6, 30], [1589, 7, 29], [1589, 8, 29], [1589, 9, 30], [1589, 10, 29], [1589, 11, 30], + [1589, 12, 29], [1590, 1, 30], [1590, 2, 29], [1590, 3, 30], [1590, 4, 30], [1590, 5, 30], [1590, 6, 29], + [1590, 7, 29], [1590, 8, 30], [1590, 9, 29], [1590, 10, 30], [1590, 11, 29], [1590, 12, 30], [1591, 1, 29], + [1591, 2, 30], [1591, 3, 29], [1591, 4, 30], [1591, 5, 30], [1591, 6, 29], [1591, 7, 30], [1591, 8, 29], + [1591, 9, 30], [1591, 10, 29], [1591, 11, 30], [1591, 12, 29], [1592, 1, 30], [1592, 2, 29], [1592, 3, 30], + [1592, 4, 29], [1592, 5, 30], [1592, 6, 29], [1592, 7, 30], [1592, 8, 29], [1592, 9, 30], [1592, 10, 30], + [1592, 11, 30], [1592, 12, 29], [1593, 1, 30], [1593, 2, 29], [1593, 3, 29], [1593, 4, 30], [1593, 5, 29], + [1593, 6, 29], [1593, 7, 30], [1593, 8, 29], [1593, 9, 30], [1593, 10, 30], [1593, 11, 30], [1593, 12, 29], + [1594, 1, 30], [1594, 2, 30], [1594, 3, 29], [1594, 4, 29], [1594, 5, 30], [1594, 6, 29], [1594, 7, 29], + [1594, 8, 29], [1594, 9, 30], [1594, 10, 30], [1594, 11, 30], [1594, 12, 30], [1595, 1, 29], [1595, 2, 30], + [1595, 3, 29], [1595, 4, 30], [1595, 5, 29], [1595, 6, 29], [1595, 7, 30], [1595, 8, 29], [1595, 9, 29], + [1595, 10, 30], [1595, 11, 30], [1595, 12, 30], [1596, 1, 29], [1596, 2, 30], [1596, 3, 30], [1596, 4, 29], + [1596, 5, 30], [1596, 6, 29], [1596, 7, 29], [1596, 8, 30], [1596, 9, 29], [1596, 10, 30], [1596, 11, 29], + [1596, 12, 30], [1597, 1, 29], [1597, 2, 30], [1597, 3, 30], [1597, 4, 29], [1597, 5, 30], [1597, 6, 29], + [1597, 7, 30], [1597, 8, 29], [1597, 9, 30], [1597, 10, 29], [1597, 11, 30], [1597, 12, 29], [1598, 1, 30], + [1598, 2, 29], [1598, 3, 30], [1598, 4, 29], [1598, 5, 30], [1598, 6, 30], [1598, 7, 29], [1598, 8, 30], + [1598, 9, 29], [1598, 10, 30], [1598, 11, 30], [1598, 12, 29], [1599, 1, 29], [1599, 2, 30], [1599, 3, 29], + [1599, 4, 30], [1599, 5, 29], [1599, 6, 30], [1599, 7, 29], [1599, 8, 30], [1599, 9, 30], [1599, 10, 30], + [1599, 11, 29], [1599, 12, 30], [1600, 1, 29], [1600, 2, 29], [1600, 3, 30], [1600, 4, 29], [1600, 5, 30], + [1600, 6, 29], [1600, 7, 29], [1600, 8, 30], [1600, 9, 30], [1600, 10, 30], [1600, 11, 29], [1600, 12, 30] + ]; + const calendar = new NgbCalendarIslamicUmalqura(); + describe('toGregorian', () => { + it('should convert correctly from Hijri to Gregorian', () => { + DATE_TABLE.forEach(element => { + const iDate = new NgbDate(element[3], element[4], element[5]); + const gDate = new Date(element[0], element[1], element[2]); + expect(calendar.toGregorian(iDate).getTime()) + .toEqual(gDate.getTime(), `Hijri ${iDate.year}-${iDate.month}-${iDate.day} should be Gregorian ${gDate}`); + }); + }); + }); + + describe('fromGregorian', () => { + it('should convert correctly from Gregorian to Hijri', () => { + DATE_TABLE.forEach(element => { + const iDate = new NgbDate(element[3], element[4], element[5]); + const gDate = new Date(element[0], element[1], element[2]); + const iDate2 = calendar.fromGregorian(gDate); + expect(iDate2.equals(iDate)) + .toBeTruthy(`Gregorian ${gDate} should be Hijri ${iDate.year}-${iDate.month}-${iDate.day}`); + }); + }); + + it('should convert correctly from Gregorian to Hijri with time 23:59:59.999', () => { + DATE_TABLE.forEach(element => { + const iDate = new NgbDate(element[3], element[4], element[5]); + const gDate = new Date(element[0], element[1], element[2], 23, 59, 59, 999); + const iDate2 = calendar.fromGregorian(gDate); + expect(iDate2.equals(iDate)) + .toBeTruthy(`Gregorian ${gDate} should be Hijri ${iDate.year}-${iDate.month}-${iDate.day}`); + }); + }); + }); + + it('should return number of days per week', () => { expect(calendar.getDaysPerWeek()).toBe(7); }); + + it('should return number of weeks per month', () => { expect(calendar.getWeeksPerMonth()).toBe(6); }); + + it('should return months of a year', () => { + expect(calendar.getMonths()).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + }); + + describe('getDaysInIslamicMonth', () => { + it('should return the correct number of days in islamic month', () => { + MONTH_LENGTH.forEach(element => { + expect(calendar.getDaysPerMonth(element[1], element[0])) + .toEqual(element[2], `Hijri month ${element[1]}-${element[0]} should contain ${element[2]} days`); + }); + }); + }); + + it('should return day of week', () => { + expect(calendar.getWeekday(new NgbDate(1438, 7, 6))).toEqual(1); + expect(calendar.getWeekday(new NgbDate(1438, 7, 7))).toEqual(2); + expect(calendar.getWeekday(new NgbDate(1438, 7, 8))).toEqual(3); + expect(calendar.getWeekday(new NgbDate(1438, 7, 9))).toEqual(4); + expect(calendar.getWeekday(new NgbDate(1438, 7, 10))).toEqual(5); + expect(calendar.getWeekday(new NgbDate(1438, 7, 11))).toEqual(6); + expect(calendar.getWeekday(new NgbDate(1438, 7, 12))).toEqual(7); + expect(calendar.getWeekday(new NgbDate(1420, 1, 12))).toEqual(3); + expect(calendar.getWeekday(new NgbDate(1420, 2, 9))).toEqual(1); + }); + + it('should add days to date', () => { + expect(calendar.getNext(new NgbDate(1431, 1, 29))).toEqual(new NgbDate(1431, 2, 1)); + expect(calendar.getNext(new NgbDate(1437, 2, 28))).toEqual(new NgbDate(1437, 2, 29)); + expect(calendar.getNext(new NgbDate(1437, 2, 29))).toEqual(new NgbDate(1437, 3, 1)); + }); + + it('should subtract days from date', () => { + expect(calendar.getPrev(new NgbDate(1431, 2, 1))).toEqual(new NgbDate(1431, 1, 29)); + expect(calendar.getPrev(new NgbDate(1431, 3, 1))).toEqual(new NgbDate(1431, 2, 30)); + expect(calendar.getPrev(new NgbDate(1437, 3, 5))).toEqual(new NgbDate(1437, 3, 4)); + }); + + it('should add months to date', () => { + expect(calendar.getNext(new NgbDate(1437, 8, 22), 'm')).toEqual(new NgbDate(1437, 9, 1)); + expect(calendar.getNext(new NgbDate(1437, 8, 1), 'm')).toEqual(new NgbDate(1437, 9, 1)); + expect(calendar.getNext(new NgbDate(1437, 12, 22), 'm')).toEqual(new NgbDate(1438, 1, 1)); + }); + + it('should subtract months from date', () => { + expect(calendar.getPrev(new NgbDate(1437, 8, 22), 'm')).toEqual(new NgbDate(1437, 7, 1)); + expect(calendar.getPrev(new NgbDate(1437, 9, 1), 'm')).toEqual(new NgbDate(1437, 8, 1)); + expect(calendar.getPrev(new NgbDate(1437, 1, 22), 'm')).toEqual(new NgbDate(1436, 12, 1)); + }); + + it('should add years to date', () => { + expect(calendar.getNext(new NgbDate(1437, 2, 22), 'y')).toEqual(new NgbDate(1438, 1, 1)); + expect(calendar.getNext(new NgbDate(1438, 12, 22), 'y')).toEqual(new NgbDate(1439, 1, 1)); + }); + + it('should subtract years from date', () => { + expect(calendar.getPrev(new NgbDate(1437, 12, 22), 'y')).toEqual(new NgbDate(1436, 1, 1)); + expect(calendar.getPrev(new NgbDate(1438, 2, 22), 'y')).toEqual(new NgbDate(1437, 1, 1)); + }); + + it('should return week number', () => { + let week = [ + new NgbDate(1437, 1, 4), new NgbDate(1437, 1, 5), new NgbDate(1437, 1, 6), new NgbDate(1437, 1, 7), + new NgbDate(1437, 1, 8), new NgbDate(1437, 1, 9), new NgbDate(1437, 1, 10) + ]; + expect(calendar.getWeekNumber(week, 7)).toEqual(2); + week = [ + new NgbDate(1437, 12, 15), new NgbDate(1437, 12, 16), new NgbDate(1437, 12, 17), new NgbDate(1437, 12, 18), + new NgbDate(1437, 12, 19), new NgbDate(1437, 12, 20), new NgbDate(1437, 12, 21) + ]; + expect(calendar.getWeekNumber(week, 7)).toEqual(50); + week = [ + new NgbDate(1437, 12, 22), new NgbDate(1437, 12, 23), new NgbDate(1437, 12, 24), new NgbDate(1437, 12, 25), + new NgbDate(1437, 12, 26), new NgbDate(1437, 12, 27), new NgbDate(1437, 12, 28) + ]; + expect(calendar.getWeekNumber(week, 7)).toEqual(51); + }); + + describe('setDay', () => { + it('should return correct value of day', () => { + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'd', 10).day).toEqual(11); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'd', 0).day).toEqual(1); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'd', 30).day).toEqual(2); + expect(calendar.getNext(new NgbDate(1437, 1, 1), 'd', 60).day).toEqual(2); + expect(calendar.getNext(new NgbDate(1431, 2, 1), 'd', -1).day).toEqual(29); + expect(calendar.getNext(new NgbDate(1431, 2, 1), 'd', -2).day).toEqual(28); + expect(calendar.getNext(new NgbDate(1431, 2, 1), 'd', -3).day).toEqual(27); + }); + }); + + describe('setMonth', () => { + it('should return correct value of month', () => { + expect(calendar.getNext(new NgbDate(1202, 1, 1), 'm', 8).month).toEqual(9); + expect(calendar.getNext(new NgbDate(1202, 1, 19), 'm', 7).month).toEqual(8); + expect(calendar.getNext(new NgbDate(1431, 2, 30), 'm', -1).month).toEqual(1); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'm', -1).month).toEqual(12); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'm', -2).month).toEqual(11); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'm', 11).month).toEqual(12); + expect(calendar.getNext(new NgbDate(1420, 1, 1), 'm', 23).month).toEqual(12); + expect(calendar.getNext(new NgbDate(1431, 1, 2), 'm', -25).month).toEqual(12); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'm', 12).month).toEqual(1); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'd', 29).month).toEqual(2); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'd', 30).month).toEqual(2); + expect(calendar.getNext(new NgbDate(1437, 1, 1), 'd', 60).month).toEqual(3); + expect(calendar.getNext(new NgbDate(1431, 2, 1), 'd', -2).month).toEqual(1); + expect(calendar.getNext(new NgbDate(1431, 2, 1), 'd', -31).month).toEqual(12); + }); + }); + + describe('setYear', () => { + it('should return correct value of yar', () => { + expect(calendar.getNext(new NgbDate(1200, 8, 19), 'y', 2).year).toEqual(1202); + expect(calendar.getNext(new NgbDate(1400, 11, 30), 'y', 31).year).toEqual(1431); + expect(calendar.getNext(new NgbDate(1431, 12, 1), 'd', 32).year).toEqual(1432); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'd', -2).year).toEqual(1430); + expect(calendar.getNext(new NgbDate(1431, 12, 1), 'm', 12).year).toEqual(1432); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'm', 24).year).toEqual(1433); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'm', -2).year).toEqual(1430); + expect(calendar.getNext(new NgbDate(1431, 1, 1), 'm', -14).year).toEqual(1429); + }); + }); +}); diff --git a/src/datepicker/hijri/ngb-calendar-islamic-umalqura.ts b/src/datepicker/hijri/ngb-calendar-islamic-umalqura.ts new file mode 100644 index 0000000..2eb7071 --- /dev/null +++ b/src/datepicker/hijri/ngb-calendar-islamic-umalqura.ts @@ -0,0 +1,222 @@ +import {NgbCalendarIslamicCivil} from './ngb-calendar-islamic-civil'; +import {NgbDate} from '../ngb-date'; +import {Injectable} from '@angular/core'; + +/** + * Umalqura calendar is one type of Hijri calendars used in islamic countries. + * This Calendar is used by Saudi Arabia for administrative purpose. + * Unlike tabular calendars, the algorithm involves astronomical calculation, but it's still deterministic. + * http://cldr.unicode.org/development/development-process/design-proposals/islamic-calendar-types + */ + +const GREGORIAN_FIRST_DATE = new Date(1882, 10, 12); +const GREGORIAN_LAST_DATE = new Date(2174, 10, 25); +const HIJRI_BEGIN = 1300; +const HIJRI_END = 1600; +const ONE_DAY = 1000 * 60 * 60 * 24; + +const MONTH_LENGTH = [ + // 1300-1304 + '101010101010', '110101010100', '111011001001', '011011010100', '011011101010', + // 1305-1309 + '001101101100', '101010101101', '010101010101', '011010101001', '011110010010', + // 1310-1314 + '101110101001', '010111010100', '101011011010', '010101011100', '110100101101', + // 1315-1319 + '011010010101', '011101001010', '101101010100', '101101101010', '010110101101', + // 1320-1324 + '010010101110', '101001001111', '010100010111', '011010001011', '011010100101', + // 1325-1329 + '101011010101', '001011010110', '100101011011', '010010011101', '101001001101', + // 1330-1334 + '110100100110', '110110010101', '010110101100', '100110110110', '001010111010', + // 1335-1339 + '101001011011', '010100101011', '101010010101', '011011001010', '101011101001', + // 1340-1344 + '001011110100', '100101110110', '001010110110', '100101010110', '101011001010', + // 1345-1349 + '101110100100', '101111010010', '010111011001', '001011011100', '100101101101', + // 1350-1354 + '010101001101', '101010100101', '101101010010', '101110100101', '010110110100', + // 1355-1359 + '100110110110', '010101010111', '001010010111', '010101001011', '011010100011', + // 1360-1364 + '011101010010', '101101100101', '010101101010', '101010101011', '010100101011', + // 1365-1369 + '110010010101', '110101001010', '110110100101', '010111001010', '101011010110', + // 1370-1374 + '100101010111', '010010101011', '100101001011', '101010100101', '101101010010', + // 1375-1379 + '101101101010', '010101110101', '001001110110', '100010110111', '010001011011', + // 1380-1384 + '010101010101', '010110101001', '010110110100', '100111011010', '010011011101', + // 1385-1389 + '001001101110', '100100110110', '101010101010', '110101010100', '110110110010', + // 1390-1394 + '010111010101', '001011011010', '100101011011', '010010101011', '101001010101', + // 1395-1399 + '101101001001', '101101100100', '101101110001', '010110110100', '101010110101', + // 1400-1404 + '101001010101', '110100100101', '111010010010', '111011001001', '011011010100', + // 1405-1409 + '101011101001', '100101101011', '010010101011', '101010010011', '110101001001', + // 1410-1414 + '110110100100', '110110110010', '101010111001', '010010111010', '101001011011', + // 1415-1419 + '010100101011', '101010010101', '101100101010', '101101010101', '010101011100', + // 1420-1424 + '010010111101', '001000111101', '100100011101', '101010010101', '101101001010', + // 1425-1429 + '101101011010', '010101101101', '001010110110', '100100111011', '010010011011', + // 1430-1434 + '011001010101', '011010101001', '011101010100', '101101101010', '010101101100', + // 1435-1439 + '101010101101', '010101010101', '101100101001', '101110010010', '101110101001', + // 1440-1444 + '010111010100', '101011011010', '010101011010', '101010101011', '010110010101', + // 1445-1449 + '011101001001', '011101100100', '101110101010', '010110110101', '001010110110', + // 1450-1454 + '101001010110', '111001001101', '101100100101', '101101010010', '101101101010', + // 1455-1459 + '010110101101', '001010101110', '100100101111', '010010010111', '011001001011', + // 1460-1464 + '011010100101', '011010101100', '101011010110', '010101011101', '010010011101', + // 1465-1469 + '101001001101', '110100010110', '110110010101', '010110101010', '010110110101', + // 1470-1474 + '001011011010', '100101011011', '010010101101', '010110010101', '011011001010', + // 1475-1479 + '011011100100', '101011101010', '010011110101', '001010110110', '100101010110', + // 1480-1484 + '101010101010', '101101010100', '101111010010', '010111011001', '001011101010', + // 1485-1489 + '100101101101', '010010101101', '101010010101', '101101001010', '101110100101', + // 1490-1494 + '010110110010', '100110110101', '010011010110', '101010010111', '010101000111', + // 1495-1499 + '011010010011', '011101001001', '101101010101', '010101101010', '101001101011', + // 1500-1504 + '010100101011', '101010001011', '110101000110', '110110100011', '010111001010', + // 1505-1509 + '101011010110', '010011011011', '001001101011', '100101001011', '101010100101', + // 1510-1514 + '101101010010', '101101101001', '010101110101', '000101110110', '100010110111', + // 1515-1519 + '001001011011', '010100101011', '010101100101', '010110110100', '100111011010', + // 1520-1524 + '010011101101', '000101101101', '100010110110', '101010100110', '110101010010', + // 1525-1529 + '110110101001', '010111010100', '101011011010', '100101011011', '010010101011', + // 1530-1534 + '011001010011', '011100101001', '011101100010', '101110101001', '010110110010', + // 1535-1539 + '101010110101', '010101010101', '101100100101', '110110010010', '111011001001', + // 1540-1544 + '011011010010', '101011101001', '010101101011', '010010101011', '101001010101', + // 1545-1549 + '110100101001', '110101010100', '110110101010', '100110110101', '010010111010', + // 1550-1554 + '101000111011', '010010011011', '101001001101', '101010101010', '101011010101', + // 1555-1559 + '001011011010', '100101011101', '010001011110', '101000101110', '110010011010', + // 1560-1564 + '110101010101', '011010110010', '011010111001', '010010111010', '101001011101', + // 1565-1569 + '010100101101', '101010010101', '101101010010', '101110101000', '101110110100', + // 1570-1574 + '010110111001', '001011011010', '100101011010', '101101001010', '110110100100', + // 1575-1579 + '111011010001', '011011101000', '101101101010', '010101101101', '010100110101', + // 1580-1584 + '011010010101', '110101001010', '110110101000', '110111010100', '011011011010', + // 1585-1589 + '010101011011', '001010011101', '011000101011', '101100010101', '101101001010', + // 1590-1594 + '101110010101', '010110101010', '101010101110', '100100101110', '110010001111', + // 1595-1599 + '010100100111', '011010010101', '011010101010', '101011010110', '010101011101', + // 1600 + '001010011101' +]; + +function getDaysDiff(date1: Date, date2: Date): number { + // Ignores the time part in date1 and date2: + const time1 = Date.UTC(date1.getFullYear(), date1.getMonth(), date1.getDate()); + const time2 = Date.UTC(date2.getFullYear(), date2.getMonth(), date2.getDate()); + const diff = Math.abs(time1 - time2); + return Math.round(diff / ONE_DAY); +} + +@Injectable() +export class NgbCalendarIslamicUmalqura extends NgbCalendarIslamicCivil { + /** + * Returns the equivalent islamic(Umalqura) date value for a give input Gregorian date. + * `gdate` is s JS Date to be converted to Hijri. + */ + fromGregorian(gDate: Date): NgbDate { + let hDay = 1, hMonth = 0, hYear = 1300; + let daysDiff = getDaysDiff(gDate, GREGORIAN_FIRST_DATE); + if (gDate.getTime() - GREGORIAN_FIRST_DATE.getTime() >= 0 && gDate.getTime() - GREGORIAN_LAST_DATE.getTime() <= 0) { + let year = 1300; + for (let i = 0; i < MONTH_LENGTH.length; i++, year++) { + for (let j = 0; j < 12; j++) { + let numOfDays = +MONTH_LENGTH[i][j] + 29; + if (daysDiff <= numOfDays) { + hDay = daysDiff + 1; + if (hDay > numOfDays) { + hDay = 1; + j++; + } + if (j > 11) { + j = 0; + year++; + } + hMonth = j; + hYear = year; + return new NgbDate(hYear, hMonth + 1, hDay); + } + daysDiff = daysDiff - numOfDays; + } + } + } else { + return super.fromGregorian(gDate); + } + } + /** + * Converts the current Hijri date to Gregorian. + */ + toGregorian(hDate: NgbDate): Date { + const hYear = hDate.year; + const hMonth = hDate.month - 1; + const hDay = hDate.day; + let gDate = new Date(GREGORIAN_FIRST_DATE); + let dayDiff = hDay - 1; + if (hYear >= HIJRI_BEGIN && hYear <= HIJRI_END) { + for (let y = 0; y < hYear - HIJRI_BEGIN; y++) { + for (let m = 0; m < 12; m++) { + dayDiff += +MONTH_LENGTH[y][m] + 29; + } + } + for (let m = 0; m < hMonth; m++) { + dayDiff += +MONTH_LENGTH[hYear - HIJRI_BEGIN][m] + 29; + } + gDate.setDate(GREGORIAN_FIRST_DATE.getDate() + dayDiff); + } else { + gDate = super.toGregorian(hDate); + } + return gDate; + } + /** + * Returns the number of days in a specific Hijri hMonth. + * `hMonth` is 1 for Muharram, 2 for Safar, etc. + * `hYear` is any Hijri hYear. + */ + getDaysPerMonth(hMonth: number, hYear: number): number { + if (hYear >= HIJRI_BEGIN && hYear <= HIJRI_END) { + const pos = hYear - HIJRI_BEGIN; + return +MONTH_LENGTH[pos][hMonth - 1] + 29; + } + return super.getDaysPerMonth(hMonth, hYear); + } +} diff --git a/src/datepicker/jalali/jalali.ts b/src/datepicker/jalali/jalali.ts new file mode 100644 index 0000000..d4d5315 --- /dev/null +++ b/src/datepicker/jalali/jalali.ts @@ -0,0 +1,227 @@ +import {NgbDate} from '../ngb-date'; + +/** + * Returns the equivalent JS date value for a give input Jalali date. + * `jalaliDate` is an Jalali date to be converted to Gregorian. + */ +export function toGregorian(jalaliDate: NgbDate): Date { + let jdn = jalaliToJulian(jalaliDate.year, jalaliDate.month, jalaliDate.day); + let date = julianToGregorian(jdn); + date.setHours(6, 30, 3, 200); + return date; +} + +/** + * Returns the equivalent jalali date value for a give input Gregorian date. + * `gdate` is a JS Date to be converted to jalali. + * utc to local + */ +export function fromGregorian(gdate: Date): NgbDate { + let g2d = gregorianToJulian(gdate.getFullYear(), gdate.getMonth() + 1, gdate.getDate()); + return julianToJalali(g2d); +} + +export function setJalaliYear(date: NgbDate, yearValue: number): NgbDate { + date.year = +yearValue; + return date; +} + +export function setJalaliMonth(date: NgbDate, month: number): NgbDate { + month = +month; + date.year = date.year + Math.floor((month - 1) / 12); + date.month = Math.floor(((month - 1) % 12 + 12) % 12) + 1; + return date; +} + +export function setJalaliDay(date: NgbDate, day: number): NgbDate { + let mDays = getDaysPerMonth(date.month, date.year); + if (day <= 0) { + while (day <= 0) { + date = setJalaliMonth(date, date.month - 1); + mDays = getDaysPerMonth(date.month, date.year); + day += mDays; + } + } else if (day > mDays) { + while (day > mDays) { + day -= mDays; + date = setJalaliMonth(date, date.month + 1); + mDays = getDaysPerMonth(date.month, date.year); + } + } + date.day = day; + return date; +} + +function mod(a: number, b: number): number { + return a - b * Math.floor(a / b); +} + +function div(a: number, b: number) { + return Math.trunc(a / b); +} + +/* + This function determines if the Jalali (Persian) year is + leap (366-day long) or is the common year (365 days), and + finds the day in March (Gregorian calendar) of the first + day of the Jalali year (jalaliYear). + @param jalaliYear Jalali calendar year (-61 to 3177) + @return + leap: number of years since the last leap year (0 to 4) + gYear: Gregorian year of the beginning of Jalali year + march: the March day of Farvardin the 1st (1st day of jalaliYear) + @see: http://www.astro.uni.torun.pl/~kb/Papers/EMP/PersianC-EMP.htm + @see: http://www.fourmilab.ch/documents/calendar/ + */ +function jalCal(jalaliYear: number) { + // Jalali years starting the 33-year rule. + let breaks = + [-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, 1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178]; + const breaksLength = breaks.length; + const gYear = jalaliYear + 621; + let leapJ = -14; + let jp = breaks[0]; + + if (jalaliYear < jp || jalaliYear >= breaks[breaksLength - 1]) { + throw new Error('Invalid Jalali year ' + jalaliYear); + } + + // Find the limiting years for the Jalali year jalaliYear. + let jump; + for (let i = 1; i < breaksLength; i += 1) { + const jm = breaks[i]; + jump = jm - jp; + if (jalaliYear < jm) { + break; + } + leapJ = leapJ + div(jump, 33) * 8 + div(mod(jump, 33), 4); + jp = jm; + } + let n = jalaliYear - jp; + + // Find the number of leap years from AD 621 to the beginning + // of the current Jalali year in the Persian calendar. + leapJ = leapJ + div(n, 33) * 8 + div(mod(n, 33) + 3, 4); + if (mod(jump, 33) === 4 && jump - n === 4) { + leapJ += 1; + } + + // And the same in the Gregorian calendar (until the year gYear). + const leapG = div(gYear, 4) - div((div(gYear, 100) + 1) * 3, 4) - 150; + + // Determine the Gregorian date of Farvardin the 1st. + const march = 20 + leapJ - leapG; + + // Find how many years have passed since the last leap year. + if (jump - n < 6) { + n = n - jump + div(jump + 4, 33) * 33; + } + let leap = mod(mod(n + 1, 33) - 1, 4); + if (leap === -1) { + leap = 4; + } + + return {leap: leap, gy: gYear, march: march}; +} + +/* + Calculates Gregorian and Julian calendar dates from the Julian Day number + (jdn) for the period since jdn=-34839655 (i.e. the year -100100 of both + calendars) to some millions years ahead of the present. + @param jdn Julian Day number + @return + gYear: Calendar year (years BC numbered 0, -1, -2, ...) + gMonth: Calendar month (1 to 12) + gDay: Calendar day of the month M (1 to 28/29/30/31) + */ +function julianToGregorian(julianDayNumber: number) { + let j = 4 * julianDayNumber + 139361631; + j = j + div(div(4 * julianDayNumber + 183187720, 146097) * 3, 4) * 4 - 3908; + const i = div(mod(j, 1461), 4) * 5 + 308; + const gDay = div(mod(i, 153), 5) + 1; + const gMonth = mod(div(i, 153), 12) + 1; + const gYear = div(j, 1461) - 100100 + div(8 - gMonth, 6); + + return new Date(gYear, gMonth - 1, gDay); +} + +/* + Converts a date of the Jalali calendar to the Julian Day number. + @param jy Jalali year (1 to 3100) + @param jm Jalali month (1 to 12) + @param jd Jalali day (1 to 29/31) + @return Julian Day number + */ +function gregorianToJulian(gy: number, gm: number, gd: number) { + let d = div((gy + div(gm - 8, 6) + 100100) * 1461, 4) + div(153 * mod(gm + 9, 12) + 2, 5) + gd - 34840408; + d = d - div(div(gy + 100100 + div(gm - 8, 6), 100) * 3, 4) + 752; + return d; +} + +/* + Converts the Julian Day number to a date in the Jalali calendar. + @param julianDayNumber Julian Day number + @return + jalaliYear: Jalali year (1 to 3100) + jalaliMonth: Jalali month (1 to 12) + jalaliDay: Jalali day (1 to 29/31) + */ +function julianToJalali(julianDayNumber: number) { + let gy = julianToGregorian(julianDayNumber).getFullYear() // Calculate Gregorian year (gy). + , + jalaliYear = gy - 621, r = jalCal(jalaliYear), gregorianDay = gregorianToJulian(gy, 3, r.march), jalaliDay, + jalaliMonth, numberOfDays; + + // Find number of days that passed since 1 Farvardin. + numberOfDays = julianDayNumber - gregorianDay; + if (numberOfDays >= 0) { + if (numberOfDays <= 185) { + // The first 6 months. + jalaliMonth = 1 + div(numberOfDays, 31); + jalaliDay = mod(numberOfDays, 31) + 1; + return new NgbDate(jalaliYear, jalaliMonth, jalaliDay); + } else { + // The remaining months. + numberOfDays -= 186; + } + } else { + // Previous Jalali year. + jalaliYear -= 1; + numberOfDays += 179; + if (r.leap === 1) { + numberOfDays += 1; + } + } + jalaliMonth = 7 + div(numberOfDays, 30); + jalaliDay = mod(numberOfDays, 30) + 1; + + return new NgbDate(jalaliYear, jalaliMonth, jalaliDay); +} + +/* + Converts a date of the Jalali calendar to the Julian Day number. + @param jYear Jalali year (1 to 3100) + @param jMonth Jalali month (1 to 12) + @param jDay Jalali day (1 to 29/31) + @return Julian Day number + */ +function jalaliToJulian(jYear: number, jMonth: number, jDay: number) { + let r = jalCal(jYear); + return gregorianToJulian(r.gy, 3, r.march) + (jMonth - 1) * 31 - div(jMonth, 7) * (jMonth - 7) + jDay - 1; +} + +/** + * Returns the number of days in a specific jalali month. + */ +function getDaysPerMonth(month: number, year: number): number { + if (month <= 6) { + return 31; + } + if (month <= 11) { + return 30; + } + if (jalCal(year).leap === 0) { + return 30; + } + return 29; +} diff --git a/src/datepicker/jalali/ngb-calendar-persian.ts b/src/datepicker/jalali/ngb-calendar-persian.ts new file mode 100644 index 0000000..939a2b4 --- /dev/null +++ b/src/datepicker/jalali/ngb-calendar-persian.ts @@ -0,0 +1,66 @@ +import {Injectable} from '@angular/core'; +import {NgbDate} from '../ngb-date'; +import {NgbCalendar, NgbPeriod} from '../ngb-calendar'; +import {isInteger} from '../../util/util'; + +import {fromGregorian, setJalaliDay, setJalaliMonth, setJalaliYear, toGregorian} from './jalali'; + +@Injectable() +export class NgbCalendarPersian extends NgbCalendar { + getDaysPerWeek() { return 7; } + + getMonths() { return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; } + + getWeeksPerMonth() { return 6; } + + getNext(date: NgbDate, period: NgbPeriod = 'd', number = 1) { + date = new NgbDate(date.year, date.month, date.day); + + switch (period) { + case 'y': + date = setJalaliYear(date, date.year + number); + date.month = 1; + date.day = 1; + return date; + case 'm': + date = setJalaliMonth(date, date.month + number); + date.day = 1; + return date; + case 'd': + return setJalaliDay(date, date.day + number); + default: + return date; + } + } + + getPrev(date: NgbDate, period: NgbPeriod = 'd', number = 1) { return this.getNext(date, period, -number); } + + getWeekday(date: NgbDate) { + const day = toGregorian(date).getDay(); + // in JS Date Sun=0, in ISO 8601 Sun=7 + return day === 0 ? 7 : day; + } + + getWeekNumber(week: NgbDate[], firstDayOfWeek: number) { + // in JS Date Sun=0, in ISO 8601 Sun=7 + if (firstDayOfWeek === 7) { + firstDayOfWeek = 0; + } + + const thursdayIndex = (4 + 7 - firstDayOfWeek) % 7; + const date = week[thursdayIndex]; + + const jsDate = toGregorian(date); + jsDate.setDate(jsDate.getDate() + 4 - (jsDate.getDay() || 7)); // Thursday + const time = jsDate.getTime(); + const startDate = toGregorian(new NgbDate(date.year, 1, 1)); + return Math.floor(Math.round((time - startDate.getTime()) / 86400000) / 7) + 1; + } + + getToday(): NgbDate { return fromGregorian(new Date()); } + + isValid(date: NgbDate): boolean { + return date && isInteger(date.year) && isInteger(date.month) && isInteger(date.day) && + !isNaN(toGregorian(date).getTime()); + } +} diff --git a/src/datepicker/ngb-calendar.spec.ts b/src/datepicker/ngb-calendar.spec.ts new file mode 100644 index 0000000..781decf --- /dev/null +++ b/src/datepicker/ngb-calendar.spec.ts @@ -0,0 +1,97 @@ +import {NgbCalendarGregorian} from './ngb-calendar'; +import {NgbDate} from './ngb-date'; + +describe('ngb-calendar-gregorian', () => { + + const calendar = new NgbCalendarGregorian(); + + it('should return today\'s date', () => { + const jsToday = new Date(); + const today = new NgbDate(jsToday.getFullYear(), jsToday.getMonth() + 1, jsToday.getDate()); + + expect(calendar.getToday()).toEqual(today); + }); + + it('should return number of days per week', () => { expect(calendar.getDaysPerWeek()).toBe(7); }); + + it('should return number of weeks per month', () => { expect(calendar.getWeeksPerMonth()).toBe(6); }); + + it('should return months of a year', () => { + expect(calendar.getMonths()).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]); + }); + + it('should return day of week', () => { + expect(calendar.getWeekday(new NgbDate(2017, 1, 2))).toBe(1); // Mon, 2 Jan 2017 + expect(calendar.getWeekday(new NgbDate(2017, 1, 3))).toBe(2); + expect(calendar.getWeekday(new NgbDate(2017, 1, 4))).toBe(3); + expect(calendar.getWeekday(new NgbDate(2017, 1, 5))).toBe(4); + expect(calendar.getWeekday(new NgbDate(2017, 1, 6))).toBe(5); + expect(calendar.getWeekday(new NgbDate(2017, 1, 7))).toBe(6); + expect(calendar.getWeekday(new NgbDate(2017, 1, 8))).toBe(7); // Sun, 8 Jan 2017 + }); + + it('should add days to date', () => { + expect(calendar.getNext(new NgbDate(2016, 12, 31))).toEqual(new NgbDate(2017, 1, 1)); + expect(calendar.getNext(new NgbDate(2016, 2, 28))).toEqual(new NgbDate(2016, 2, 29)); + expect(calendar.getNext(new NgbDate(2017, 2, 28))).toEqual(new NgbDate(2017, 3, 1)); + }); + + it('should subtract days from date', () => { + expect(calendar.getPrev(new NgbDate(2017, 1, 1))).toEqual(new NgbDate(2016, 12, 31)); + expect(calendar.getPrev(new NgbDate(2016, 2, 29))).toEqual(new NgbDate(2016, 2, 28)); + expect(calendar.getPrev(new NgbDate(2017, 3, 1))).toEqual(new NgbDate(2017, 2, 28)); + }); + + it('should add months to date', () => { + expect(calendar.getNext(new NgbDate(2016, 7, 22), 'm')).toEqual(new NgbDate(2016, 8, 1)); + expect(calendar.getNext(new NgbDate(2016, 7, 1), 'm')).toEqual(new NgbDate(2016, 8, 1)); + expect(calendar.getNext(new NgbDate(2016, 12, 22), 'm')).toEqual(new NgbDate(2017, 1, 1)); + }); + + it('should subtract months from date', () => { + expect(calendar.getPrev(new NgbDate(2016, 7, 22), 'm')).toEqual(new NgbDate(2016, 6, 1)); + expect(calendar.getPrev(new NgbDate(2016, 8, 1), 'm')).toEqual(new NgbDate(2016, 7, 1)); + expect(calendar.getPrev(new NgbDate(2017, 1, 22), 'm')).toEqual(new NgbDate(2016, 12, 1)); + }); + + it('should add years to date', () => { + expect(calendar.getNext(new NgbDate(2016, 1, 22), 'y')).toEqual(new NgbDate(2017, 1, 1)); + expect(calendar.getNext(new NgbDate(2017, 12, 22), 'y')).toEqual(new NgbDate(2018, 1, 1)); + }); + + it('should subtract years from date', () => { + expect(calendar.getPrev(new NgbDate(2016, 12, 22), 'y')).toEqual(new NgbDate(2015, 1, 1)); + expect(calendar.getPrev(new NgbDate(2017, 1, 22), 'y')).toEqual(new NgbDate(2016, 1, 1)); + }); + + it('should properly recognize invalid javascript date', () => { + expect(calendar.isValid(null)).toBeFalsy(); + expect(calendar.isValid(undefined)).toBeFalsy(); + expect(calendar.isValid(NaN)).toBeFalsy(); + expect(calendar.isValid(new Date())).toBeFalsy(); + expect(calendar.isValid(new NgbDate(null, null, null))).toBeFalsy(); + expect(calendar.isValid(new NgbDate(undefined, undefined, undefined))).toBeFalsy(); + expect(calendar.isValid(new NgbDate(NaN, NaN, NaN))).toBeFalsy(); + expect(calendar.isValid(new NgbDate('2017', '03', '10'))).toBeFalsy(); + }); + + it('should recognize dates outside of JS range as invalid', () => { + expect(calendar.isValid(new NgbDate(275760, 9, 14))).toBeFalsy(); + expect(calendar.isValid(new NgbDate(-271821, 4, 19))).toBeFalsy(); + }); + + it('should recognize dates outside of calendar range as invalid', () => { + expect(calendar.isValid(new NgbDate(0, 0, 0))).toBeFalsy(); + expect(calendar.isValid(new NgbDate(-1, -1, -1))).toBeFalsy(); + expect(calendar.isValid(new NgbDate(2016, 17, 1))).toBeFalsy(); + expect(calendar.isValid(new NgbDate(2017, 5, 35))).toBeFalsy(); + }); + + it('should mark valid JS dates as valid', () => { + expect(calendar.isValid(new NgbDate(275760, 9, 12))).toBeTruthy(); + expect(calendar.isValid(new NgbDate(2016, 8, 8))).toBeTruthy(); + }); + + it('should dates with year 0 as invalid', () => { expect(calendar.isValid(new NgbDate(0, 1, 1))).toBeFalsy(); }); + +}); diff --git a/src/datepicker/ngb-calendar.ts b/src/datepicker/ngb-calendar.ts new file mode 100644 index 0000000..f42a1de --- /dev/null +++ b/src/datepicker/ngb-calendar.ts @@ -0,0 +1,161 @@ +import {NgbDate} from './ngb-date'; +import {Injectable} from '@angular/core'; +import {isInteger} from '../util/util'; + +export function fromJSDate(jsDate: Date) { + return new NgbDate(jsDate.getFullYear(), jsDate.getMonth() + 1, jsDate.getDate()); +} +export function toJSDate(date: NgbDate) { + const jsDate = new Date(date.year, date.month - 1, date.day, 12); + // this is done avoid 30 -> 1930 conversion + if (!isNaN(jsDate.getTime())) { + jsDate.setFullYear(date.year); + } + return jsDate; +} + +export type NgbPeriod = 'y' | 'm' | 'd'; + +export function NGB_DATEPICKER_CALENDAR_FACTORY() { + return new NgbCalendarGregorian(); +} + +/** + * A service that represents the calendar used by the datepicker. + * + * The default implementation uses the Gregorian calendar. You can inject it in your own + * implementations if necessary to simplify `NgbDate` calculations. + */ +@Injectable({providedIn: 'root', useFactory: NGB_DATEPICKER_CALENDAR_FACTORY}) +export abstract class NgbCalendar { + /** + * Returns the number of days per week. + */ + abstract getDaysPerWeek(): number; + + /** + * Returns an array of months per year. + * + * With default calendar we use ISO 8601 and return [1, 2, ..., 12]; + */ + abstract getMonths(year?: number): number[]; + + /** + * Returns the number of weeks per month. + */ + abstract getWeeksPerMonth(): number; + + /** + * Returns the weekday number for a given day. + * + * With the default calendar we use ISO 8601: 'weekday' is 1=Mon ... 7=Sun + */ + abstract getWeekday(date: NgbDate): number; + + /** + * Adds a number of years, months or days to a given date. + * + * * `period` can be `y`, `m` or `d` and defaults to day. + * * `number` defaults to 1. + * + * Always returns a new date. + */ + abstract getNext(date: NgbDate, period?: NgbPeriod, number?: number): NgbDate; + + /** + * Subtracts a number of years, months or days from a given date. + * + * * `period` can be `y`, `m` or `d` and defaults to day. + * * `number` defaults to 1. + * + * Always returns a new date. + */ + abstract getPrev(date: NgbDate, period?: NgbPeriod, number?: number): NgbDate; + + /** + * Returns the week number for a given week. + */ + abstract getWeekNumber(week: NgbDate[], firstDayOfWeek: number): number; + + /** + * Returns the today's date. + */ + abstract getToday(): NgbDate; + + /** + * Checks if a date is valid in the current calendar. + */ + abstract isValid(date: NgbDate): boolean; +} + +@Injectable() +export class NgbCalendarGregorian extends NgbCalendar { + getDaysPerWeek() { return 7; } + + getMonths() { return [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; } + + getWeeksPerMonth() { return 6; } + + getNext(date: NgbDate, period: NgbPeriod = 'd', number = 1) { + let jsDate = toJSDate(date); + + switch (period) { + case 'y': + return new NgbDate(date.year + number, 1, 1); + case 'm': + jsDate = new Date(date.year, date.month + number - 1, 1, 12); + break; + case 'd': + jsDate.setDate(jsDate.getDate() + number); + break; + default: + return date; + } + + return fromJSDate(jsDate); + } + + getPrev(date: NgbDate, period: NgbPeriod = 'd', number = 1) { return this.getNext(date, period, -number); } + + getWeekday(date: NgbDate) { + let jsDate = toJSDate(date); + let day = jsDate.getDay(); + // in JS Date Sun=0, in ISO 8601 Sun=7 + return day === 0 ? 7 : day; + } + + getWeekNumber(week: NgbDate[], firstDayOfWeek: number) { + // in JS Date Sun=0, in ISO 8601 Sun=7 + if (firstDayOfWeek === 7) { + firstDayOfWeek = 0; + } + + const thursdayIndex = (4 + 7 - firstDayOfWeek) % 7; + let date = week[thursdayIndex]; + + const jsDate = toJSDate(date); + jsDate.setDate(jsDate.getDate() + 4 - (jsDate.getDay() || 7)); // Thursday + const time = jsDate.getTime(); + jsDate.setMonth(0); // Compare with Jan 1 + jsDate.setDate(1); + return Math.floor(Math.round((time - jsDate.getTime()) / 86400000) / 7) + 1; + } + + getToday(): NgbDate { return fromJSDate(new Date()); } + + isValid(date: NgbDate): boolean { + if (!date || !isInteger(date.year) || !isInteger(date.month) || !isInteger(date.day)) { + return false; + } + + // year 0 doesn't exist in Gregorian calendar + if (date.year === 0) { + return false; + } + + const jsDate = toJSDate(date); + + return !isNaN(jsDate.getTime()) && jsDate.getFullYear() === date.year && jsDate.getMonth() + 1 === date.month && + jsDate.getDate() === date.day; + } +} diff --git a/src/datepicker/ngb-date-parser-formatter.spec.ts b/src/datepicker/ngb-date-parser-formatter.spec.ts new file mode 100644 index 0000000..c4f1114 --- /dev/null +++ b/src/datepicker/ngb-date-parser-formatter.spec.ts @@ -0,0 +1,47 @@ +import {NgbDateISOParserFormatter} from './ngb-date-parser-formatter'; + +describe('ngb-date parsing and formatting', () => { + let pf: NgbDateISOParserFormatter; + + beforeEach(() => { pf = new NgbDateISOParserFormatter(); }); + + describe('parsing', () => { + + it('should parse null undefined and empty string as null', () => { + expect(pf.parse(null)).toBeNull(); + expect(pf.parse(undefined)).toBeNull(); + expect(pf.parse('')).toBeNull(); + expect(pf.parse(' ')).toBeNull(); + }); + + it('should parse valid date', () => { expect(pf.parse('2016-05-12')).toEqual({year: 2016, month: 5, day: 12}); }); + + it('should parse non-date as null', () => { + expect(pf.parse('foo-bar-baz')).toBeNull(); + expect(pf.parse('2014-bar')).toBeNull(); + expect(pf.parse('2014-11-12-15')).toBeNull(); + }); + + it('should do its best parsing incomplete dates', + () => { expect(pf.parse('2011-5')).toEqual({year: 2011, month: 5, day: null}); }); + }); + + describe('formatting', () => { + + it('should format null and undefined as an empty string', () => { + expect(pf.format(null)).toBe(''); + expect(pf.format(undefined)).toBe(''); + }); + + it('should format a valid date', () => { expect(pf.format({year: 2016, month: 10, day: 15})).toBe('2016-10-15'); }); + + it('should format a valid date with padding', + () => { expect(pf.format({year: 2016, month: 10, day: 5})).toBe('2016-10-05'); }); + + it('should try its best with invalid dates', () => { + expect(pf.format({year: 2016, month: NaN, day: undefined})).toBe('2016--'); + expect(pf.format({year: 2016, month: null, day: 0})).toBe('2016--00'); + }); + }); + +}); diff --git a/src/datepicker/ngb-date-parser-formatter.ts b/src/datepicker/ngb-date-parser-formatter.ts new file mode 100644 index 0000000..93522a8 --- /dev/null +++ b/src/datepicker/ngb-date-parser-formatter.ts @@ -0,0 +1,64 @@ +import {padNumber, toInteger, isNumber} from '../util/util'; +import {NgbDateStruct} from './ngb-date-struct'; +import {Injectable} from '@angular/core'; + +export function NGB_DATEPICKER_PARSER_FORMATTER_FACTORY() { + return new NgbDateISOParserFormatter(); +} + +/** + * An abstract service for parsing and formatting dates for the + * [`NgbInputDatepicker`](#/components/datepicker/api#NgbInputDatepicker) directive. + * Converts between the internal `NgbDateStruct` model presentation and a `string` that is displayed in the + * input element. + * + * When user types something in the input this service attempts to parse it into a `NgbDateStruct` object. + * And vice versa, when users selects a date in the calendar with the mouse, it must be displayed as a `string` + * in the input. + * + * Default implementation uses the ISO 8601 format, but you can provide another implementation via DI + * to use an alternative string format or a custom parsing logic. + * + * See the [date format overview](#/components/datepicker/overview#date-model) for more details. + */ +@Injectable({providedIn: 'root', useFactory: NGB_DATEPICKER_PARSER_FORMATTER_FACTORY}) +export abstract class NgbDateParserFormatter { + /** + * Parses the given `string` to an `NgbDateStruct`. + * + * Implementations should try their best to provide a result, even + * partial. They must return `null` if the value can't be parsed. + */ + abstract parse(value: string): NgbDateStruct; + + /** + * Formats the given `NgbDateStruct` to a `string`. + * + * Implementations should return an empty string if the given date is `null`, + * and try their best to provide a partial result if the given date is incomplete or invalid. + */ + abstract format(date: NgbDateStruct): string; +} + +@Injectable() +export class NgbDateISOParserFormatter extends NgbDateParserFormatter { + parse(value: string): NgbDateStruct { + if (value) { + const dateParts = value.trim().split('-'); + if (dateParts.length === 1 && isNumber(dateParts[0])) { + return {year: toInteger(dateParts[0]), month: null, day: null}; + } else if (dateParts.length === 2 && isNumber(dateParts[0]) && isNumber(dateParts[1])) { + return {year: toInteger(dateParts[0]), month: toInteger(dateParts[1]), day: null}; + } else if (dateParts.length === 3 && isNumber(dateParts[0]) && isNumber(dateParts[1]) && isNumber(dateParts[2])) { + return {year: toInteger(dateParts[0]), month: toInteger(dateParts[1]), day: toInteger(dateParts[2])}; + } + } + return null; + } + + format(date: NgbDateStruct): string { + return date ? + `${date.year}-${isNumber(date.month) ? padNumber(date.month) : ''}-${isNumber(date.day) ? padNumber(date.day) : ''}` : + ''; + } +} diff --git a/src/datepicker/ngb-date-struct.ts b/src/datepicker/ngb-date-struct.ts new file mode 100644 index 0000000..24fa47c --- /dev/null +++ b/src/datepicker/ngb-date-struct.ts @@ -0,0 +1,23 @@ +/** + * An interface of the date model used by the datepicker. + * + * All datepicker APIs consume `NgbDateStruct`, but return `NgbDate`. + * + * See the [date format overview](#/components/datepicker/overview#date-model) for more details. + */ +export interface NgbDateStruct { + /** + * The year, for example 2016 + */ + year: number; + + /** + * The month, for example 1=Jan ... 12=Dec + */ + month: number; + + /** + * The day of month, starting at 1 + */ + day: number; +} diff --git a/src/datepicker/ngb-date.spec.ts b/src/datepicker/ngb-date.spec.ts new file mode 100644 index 0000000..c2ba06a --- /dev/null +++ b/src/datepicker/ngb-date.spec.ts @@ -0,0 +1,86 @@ +import {NgbDate} from './ngb-date'; + +describe('ngb-date', () => { + + describe('from', () => { + + it('should create a date from a structure', + () => { expect(NgbDate.from({year: 2010, month: 10, day: 2})).toEqual(new NgbDate(2010, 10, 2)); }); + + it('should work with non-numeric values', () => { + expect(NgbDate.from({year: null, month: null, day: null})).toEqual(new NgbDate(null, null, null)); + expect(NgbDate.from({year: undefined, month: undefined, day: undefined})).toEqual(new NgbDate(null, null, null)); + expect(NgbDate.from({year: '2010', month: '10', day: '2'})).toEqual(new NgbDate(null, null, null)); + }); + + it('should return the same NgbDate object', () => { + const date = new NgbDate(2010, 10, 10); + expect(NgbDate.from(date)).toBe(date); + }); + }); + + describe('equals', () => { + const date = new NgbDate(2016, 8, 18); + + it('should return true for the same dates', () => { expect(date.equals(new NgbDate(2016, 8, 18))).toBeTruthy(); }); + + it('should work with structures', () => { expect(date.equals({day: 18, month: 8, year: 2016})).toBeTruthy(); }); + + it('should return false different dates', () => { + expect(date.equals(new NgbDate(0, 8, 18))).toBeFalsy(); + expect(date.equals(new NgbDate(2016, 0, 18))).toBeFalsy(); + expect(date.equals(new NgbDate(2016, 8, 0))).toBeFalsy(); + }); + + it('should return false undefined and null values', () => { + expect(date.equals(null)).toBeFalsy(); + expect(date.equals(undefined)).toBeFalsy(); + }); + }); + + describe('before', () => { + const date = new NgbDate(2016, 8, 18); + + it('should return false undefined and null values', () => { + expect(date.before(null)).toBeFalsy(); + expect(date.before(undefined)).toBeFalsy(); + }); + + it('should work with structures', () => { expect(date.before({day: 18, month: 9, year: 2016})).toBeTruthy(); }); + + it('should return true if current date is before the other one', () => { + expect(date.before(new NgbDate(2016, 8, 19))).toBeTruthy(); + expect(date.before(new NgbDate(2016, 9, 18))).toBeTruthy(); + expect(date.before(new NgbDate(2017, 8, 18))).toBeTruthy(); + }); + + it('should return false if current date is after the other one', () => { + expect(date.before(new NgbDate(2016, 8, 17))).toBeFalsy(); + expect(date.before(new NgbDate(2016, 7, 18))).toBeFalsy(); + expect(date.before(new NgbDate(2015, 8, 18))).toBeFalsy(); + }); + }); + + describe('after', () => { + const date = new NgbDate(2016, 8, 18); + + it('should return false undefined and null values', () => { + expect(date.after(null)).toBeFalsy(); + expect(date.after(undefined)).toBeFalsy(); + }); + + it('should work with structures', () => { expect(date.after({day: 17, month: 8, year: 2016})).toBeTruthy(); }); + + it('should return true if current date is after the other one', () => { + expect(date.after(new NgbDate(2016, 8, 17))).toBeTruthy(); + expect(date.after(new NgbDate(2016, 7, 18))).toBeTruthy(); + expect(date.after(new NgbDate(2015, 8, 18))).toBeTruthy(); + }); + + it('should return false if current date is before the other one', () => { + expect(date.after(new NgbDate(2016, 8, 19))).toBeFalsy(); + expect(date.after(new NgbDate(2016, 9, 18))).toBeFalsy(); + expect(date.after(new NgbDate(2017, 8, 18))).toBeFalsy(); + }); + }); +}); diff --git a/src/datepicker/ngb-date.ts b/src/datepicker/ngb-date.ts new file mode 100644 index 0000000..4f65790 --- /dev/null +++ b/src/datepicker/ngb-date.ts @@ -0,0 +1,98 @@ +import {NgbDateStruct} from './ngb-date-struct'; +import {isInteger} from '../util/util'; + +/** + * A simple class that represents a date that datepicker also uses internally. + * + * It is the implementation of the `NgbDateStruct` interface that adds some convenience methods, + * like `.equals()`, `.before()`, etc. + * + * All datepicker APIs consume `NgbDateStruct`, but return `NgbDate`. + * + * In many cases it is simpler to manipulate these objects together with + * [`NgbCalendar`](#/components/datepicker/api#NgbCalendar) than native JS Dates. + * + * See the [date format overview](#/components/datepicker/overview#date-model) for more details. + * + * @since 3.0.0 + */ +export class NgbDate implements NgbDateStruct { + /** + * The year, for example 2016 + */ + year: number; + + /** + * The month, for example 1=Jan ... 12=Dec as in ISO 8601 + */ + month: number; + + /** + * The day of month, starting with 1 + */ + day: number; + + /** + * A **static method** that creates a new date object from the `NgbDateStruct`, + * + * ex. `NgbDate.from({year: 2000, month: 5, day: 1})`. + * + * If the `date` is already of `NgbDate` type, the method will return the same object. + */ + static from(date: NgbDateStruct): NgbDate { + if (date instanceof NgbDate) { + return date; + } + return date ? new NgbDate(date.year, date.month, date.day) : null; + } + + constructor(year: number, month: number, day: number) { + this.year = isInteger(year) ? year : null; + this.month = isInteger(month) ? month : null; + this.day = isInteger(day) ? day : null; + } + + /** + * Checks if the current date is equal to another date. + */ + equals(other: NgbDateStruct): boolean { + return other && this.year === other.year && this.month === other.month && this.day === other.day; + } + + /** + * Checks if the current date is before another date. + */ + before(other: NgbDateStruct): boolean { + if (!other) { + return false; + } + + if (this.year === other.year) { + if (this.month === other.month) { + return this.day === other.day ? false : this.day < other.day; + } else { + return this.month < other.month; + } + } else { + return this.year < other.year; + } + } + + /** + * Checks if the current date is after another date. + */ + after(other: NgbDateStruct): boolean { + if (!other) { + return false; + } + if (this.year === other.year) { + if (this.month === other.month) { + return this.day === other.day ? false : this.day > other.day; + } else { + return this.month > other.month; + } + } else { + return this.year > other.year; + } + } +} diff --git a/src/dropdown/dropdown-config.spec.ts b/src/dropdown/dropdown-config.spec.ts new file mode 100644 index 0000000..8bd5917 --- /dev/null +++ b/src/dropdown/dropdown-config.spec.ts @@ -0,0 +1,21 @@ +import {NgbDropdownConfig} from './dropdown-config'; + +describe('ngb-dropdown-config', () => { + it('should have sensible default values', () => { + const config = new NgbDropdownConfig(); + + expect(config.placement).toEqual(['bottom-left', 'bottom-right', 'top-left', 'top-right']); + expect(config.autoClose).toBe(true); + }); + + it('should allow setting "inside" and "outside" value for autoClose', () => { + const config = new NgbDropdownConfig(); + + // This test looks like having trivial assertions but its goal + // is to prove that we've got TS typings right. + config.autoClose = 'outside'; + expect(config.autoClose).toBe('outside'); + config.autoClose = 'inside'; + expect(config.autoClose).toBe('inside'); + }); +}); diff --git a/src/dropdown/dropdown-config.ts b/src/dropdown/dropdown-config.ts new file mode 100644 index 0000000..a4766bf --- /dev/null +++ b/src/dropdown/dropdown-config.ts @@ -0,0 +1,15 @@ +import {Injectable} from '@angular/core'; +import {PlacementArray} from '../util/positioning'; + +/** + * A configuration service for the [`NgbDropdown`](#/components/dropdown/api#NgbDropdown) component. + * + * You can inject this service, typically in your root component, and customize the values of its properties in + * order to provide default values for all the dropdowns used in the application. + */ +@Injectable({providedIn: 'root'}) +export class NgbDropdownConfig { + autoClose: boolean | 'outside' | 'inside' = true; + placement: PlacementArray = ['bottom-left', 'bottom-right', 'top-left', 'top-right']; + container: null | 'body'; +} diff --git a/src/dropdown/dropdown.module.ts b/src/dropdown/dropdown.module.ts new file mode 100644 index 0000000..32cccfd --- /dev/null +++ b/src/dropdown/dropdown.module.ts @@ -0,0 +1,19 @@ +import {NgModule} from '@angular/core'; +import { + NgbDropdown, + NgbDropdownAnchor, + NgbDropdownToggle, + NgbDropdownMenu, + NgbDropdownItem, + NgbNavbar +} from './dropdown'; + +export {NgbDropdown, NgbDropdownAnchor, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem} from './dropdown'; +export {NgbDropdownConfig} from './dropdown-config'; + +const NGB_DROPDOWN_DIRECTIVES = + [NgbDropdown, NgbDropdownAnchor, NgbDropdownToggle, NgbDropdownMenu, NgbDropdownItem, NgbNavbar]; + +@NgModule({declarations: NGB_DROPDOWN_DIRECTIVES, exports: NGB_DROPDOWN_DIRECTIVES}) +export class NgbDropdownModule { +} diff --git a/src/dropdown/dropdown.spec.ts b/src/dropdown/dropdown.spec.ts new file mode 100644 index 0000000..456252d --- /dev/null +++ b/src/dropdown/dropdown.spec.ts @@ -0,0 +1,492 @@ +import {ComponentFixture, inject, TestBed} from '@angular/core/testing'; +import {createGenericTestComponent} from '../test/common'; + +import {Component} from '@angular/core'; + +import {NgbDropdown, NgbDropdownModule} from './dropdown.module'; +import {NgbDropdownConfig} from './dropdown-config'; +import {By} from '@angular/platform-browser'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +function getDropdownEl(tc) { + return tc.querySelector(`[ngbDropdown]`); +} + +function getMenuEl(tc) { + return tc.querySelector(`[ngbDropdownMenu]`); +} + +const jasmineMatchers = { + toBeShown: function(util, customEqualityTests) { + return { + compare: function(actual, content?, selector?) { + const dropdownEl = getDropdownEl(actual); + const menuEl = getMenuEl(actual); + const isOpen = dropdownEl.classList.contains('show') && menuEl.classList.contains('show'); + + return { + pass: isOpen, + message: `Expected ${actual.outerHTML} to have the "show class on both container and menu"` + }; + }, + negativeCompare: function(actual) { + const dropdownEl = getDropdownEl(actual); + const menuEl = getMenuEl(actual); + const isClosed = !dropdownEl.classList.contains('show') && !menuEl.classList.contains('show'); + + return { + pass: isClosed, + message: `Expected ${actual.outerHTML} not to have the "show class both container and menu"` + }; + } + }; + } +}; + +describe('ngb-dropdown', () => { + + beforeEach(() => { + jasmine.addMatchers(jasmineMatchers); + TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbDropdownModule]}); + }); + + it('should be closed and down by default', () => { + const html = ` + `; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + + expect(compiled).not.toBeShown(); + }); + + it('should have dropup CSS class if placed on top', () => { + const html = ` + `; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + + expect(getDropdownEl(compiled)).toHaveCssClass('dropup'); + }); + + it('should have dropdown CSS class if placement is other than top', () => { + const html = ` + `; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + + expect(getDropdownEl(compiled)).toHaveCssClass('dropdown'); + }); + + it('should have x-placement attribute reflecting placement', () => { + const html = ` + `; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + + fixture.detectChanges(); + expect(getMenuEl(compiled).getAttribute('x-placement')).toBe('bottom-right'); + }); + + it('should have x-placement attribute reflecting placement with a template', () => { + + const html = ` + `; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + + fixture.detectChanges(); + expect(getMenuEl(compiled).getAttribute('x-placement')).toBe('bottom-right'); + }); + + it('should be open initially if open expression is true', () => { + const html = ` + `; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + + expect(compiled).toBeShown(); + }); + + it('should toggle open on "open" binding change', () => { + const html = ` +
+ +
+
`; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + + expect(compiled).not.toBeShown(); + + fixture.componentInstance.isOpen = true; + fixture.detectChanges(); + expect(compiled).toBeShown(); + + fixture.componentInstance.isOpen = false; + fixture.detectChanges(); + expect(compiled).not.toBeShown(); + }); + + it('should allow toggling dropdown from outside', () => { + const html = ` + + + +
+ +
+
`; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + let buttonEls = compiled.querySelectorAll('button'); + + buttonEls[0].click(); + fixture.detectChanges(); + expect(compiled).toBeShown(); + + buttonEls[1].click(); + fixture.detectChanges(); + expect(compiled).not.toBeShown(); + + buttonEls[2].click(); + fixture.detectChanges(); + expect(compiled).toBeShown(); + + buttonEls[2].click(); + fixture.detectChanges(); + expect(compiled).not.toBeShown(); + }); + + it('should allow binding to open output', () => { + const html = ` + +
`; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + let buttonEl = compiled.querySelector('button'); + + expect(fixture.componentInstance.isOpen).toBe(false); + + buttonEl.click(); + fixture.detectChanges(); + + expect(fixture.componentInstance.isOpen).toBe(true); + + buttonEl.click(); + fixture.detectChanges(); + + expect(fixture.componentInstance.isOpen).toBe(false); + }); + + it('should not raise open events if open state does not change', () => { + const html = ` + + +
`; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + let buttonEls = compiled.querySelectorAll('button'); + + expect(fixture.componentInstance.isOpen).toBe(false); + expect(fixture.componentInstance.stateChanges).toEqual([]); + + buttonEls[1].click(); // close a closed one + fixture.detectChanges(); + expect(fixture.componentInstance.isOpen).toBe(false); + expect(fixture.componentInstance.stateChanges).toEqual([]); + + buttonEls[0].click(); // open a closed one + fixture.detectChanges(); + expect(fixture.componentInstance.isOpen).toBe(true); + expect(fixture.componentInstance.stateChanges).toEqual([true]); + + buttonEls[0].click(); // open an opened one + fixture.detectChanges(); + expect(fixture.componentInstance.isOpen).toBe(true); + expect(fixture.componentInstance.stateChanges).toEqual([true]); + + buttonEls[1].click(); // close an opened one + fixture.detectChanges(); + expect(fixture.componentInstance.isOpen).toBe(false); + expect(fixture.componentInstance.stateChanges).toEqual([true, false]); + }); +}); + +describe('ngb-dropdown-toggle', () => { + beforeEach(() => { + jasmine.addMatchers(jasmineMatchers); + TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbDropdownModule]}); + }); + + it('should toggle dropdown on click', () => { + const html = ` +
+ +
+
`; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + let dropdownEl = getDropdownEl(compiled); + let buttonEl = compiled.querySelector('button'); + + expect(dropdownEl).not.toHaveCssClass('show'); + expect(buttonEl.getAttribute('aria-haspopup')).toBe('true'); + expect(buttonEl.getAttribute('aria-expanded')).toBe('false'); + + buttonEl.click(); + fixture.detectChanges(); + expect(compiled).toBeShown(); + expect(buttonEl.getAttribute('aria-expanded')).toBe('true'); + + buttonEl.click(); + fixture.detectChanges(); + expect(compiled).not.toBeShown(); + expect(buttonEl.getAttribute('aria-expanded')).toBe('false'); + }); + + it('should toggle dropdown on click of child of toggle', () => { + const html = ` +
+ +
+
`; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + const toggleEl = compiled.querySelector('.toggle'); + + expect(compiled).not.toBeShown(); + + toggleEl.click(); + fixture.detectChanges(); + expect(compiled).toBeShown(); + + toggleEl.click(); + fixture.detectChanges(); + expect(compiled).not.toBeShown(); + }); + + it('should be appended to body', () => { + const html = ` +
+ +
+
`; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + const dropdown = fixture.debugElement.query(By.directive(NgbDropdown)).injector.get(NgbDropdown); + dropdown.open(); + fixture.detectChanges(); + const dropdownElement = document.querySelector('div[ngbDropdownMenu]'); + const parentContainer = dropdownElement.parentNode; + expect(parentContainer).toHaveCssClass('dropdown'); + expect(parentContainer.parentNode).toBe(document.body, 'The dropdown should be attached to the body'); + + }); + + it(`should second placement if the first one doesn't fit`, () => { + const html = ` +
+ + +
`; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + const dropdown = fixture.debugElement.query(By.directive(NgbDropdown)).injector.get(NgbDropdown); + dropdown.open(); + fixture.detectChanges(); + const dropdownEl = compiled.querySelector('[ngbdropdownmenu]'); + const targetElement = compiled.querySelector('button'); + expect(Math.round(dropdownEl.getBoundingClientRect().left)) + .toBe(Math.round(targetElement.getBoundingClientRect().right), 'Wrong dropdown placement'); + + }); + + describe('ngb-dropdown-navbar', () => { + it(`shouldn't position the menu`, () => { + const html = ` + + `; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + const dropdown = fixture.debugElement.query(By.directive(NgbDropdown)).injector.get(NgbDropdown); + dropdown.open(); + fixture.detectChanges(); + const dropdownEl: HTMLElement = compiled.querySelector('[ngbdropdownmenu]'); + + expect(dropdownEl.getAttribute('style')).toBeNull(`The dropdown element shouldn't have calculated styles`); + expect(dropdownEl.getAttribute('x-placement')).toBeNull(`The dropdown element shouldn't have x-placement set`); + + }); + + it(`can override the defaut display value`, () => { + const html = ` + + `; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + const dropdown = fixture.debugElement.query(By.directive(NgbDropdown)).injector.get(NgbDropdown); + dropdown.open(); + fixture.detectChanges(); + const dropdownEl: HTMLElement = compiled.querySelector('[ngbdropdownmenu]'); + + expect(dropdownEl.getAttribute('style')).not.toBeNull(`The dropdown element should have calculated styles`); + + }); + + }); + + describe('Custom config', () => { + let config: NgbDropdownConfig; + + beforeEach(() => { + TestBed.configureTestingModule({imports: [NgbDropdownModule]}); + TestBed.overrideComponent(TestComponent, { + set: { + template: ` + ` + } + }); + }); + + beforeEach(inject([NgbDropdownConfig], (c: NgbDropdownConfig) => { + config = c; + config.placement = 'top-right'; + })); + + it('should initialize inputs with provided config', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + + expect(getDropdownEl(compiled)).toHaveCssClass('dropup'); + }); + }); + + describe('Custom config as provider', () => { + let config = new NgbDropdownConfig(); + config.placement = 'top-right'; + + beforeEach(() => { + TestBed.configureTestingModule( + {imports: [NgbDropdownModule], providers: [{provide: NgbDropdownConfig, useValue: config}]}); + }); + + it('should initialize inputs with provided config as provider', () => { + const fixture = createTestComponent(` + `); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + + expect(getDropdownEl(compiled)).toHaveCssClass('dropup'); + }); + }); +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + isOpen = false; + stateChanges = []; + items = []; + + recordStateChange($event) { + this.stateChanges.push($event); + this.isOpen = $event; + } +} diff --git a/src/dropdown/dropdown.ts b/src/dropdown/dropdown.ts new file mode 100644 index 0000000..937f569 --- /dev/null +++ b/src/dropdown/dropdown.ts @@ -0,0 +1,437 @@ +import { + ChangeDetectorRef, + ContentChild, + ContentChildren, + Directive, + ElementRef, + EventEmitter, + forwardRef, + Inject, + Input, + NgZone, + AfterContentInit, + OnDestroy, + Output, + QueryList, + Renderer2, + SimpleChanges, + Optional +} from '@angular/core'; +import {DOCUMENT} from '@angular/common'; +import {Subject, Subscription} from 'rxjs'; +import {take} from 'rxjs/operators'; + +import {Placement, PlacementArray, positionElements} from '../util/positioning'; +import {ngbAutoClose} from '../util/autoclose'; +import {Key} from '../util/key'; + +import {NgbDropdownConfig} from './dropdown-config'; + +@Directive({selector: '.navbar'}) +export class NgbNavbar { +} + +/** + * A directive you should put put on a dropdown item to enable keyboard navigation. + * Arrow keys will move focus between items marked with this directive. + * + * @since 4.1.0 + */ +@Directive({selector: '[ngbDropdownItem]', host: {'class': 'dropdown-item', '[class.disabled]': 'disabled'}}) +export class NgbDropdownItem { + private _disabled = false; + + @Input() + set disabled(value: boolean) { + this._disabled = value === '' || value === true; // accept an empty attribute as true + } + + get disabled(): boolean { return this._disabled; } + + constructor(public elementRef: ElementRef) {} +} + +/** + * A directive that wraps dropdown menu content and dropdown items. + */ +@Directive({ + selector: '[ngbDropdownMenu]', + host: { + '[class.dropdown-menu]': 'true', + '[class.show]': 'dropdown.isOpen()', + '[attr.x-placement]': 'placement', + '(keydown.ArrowUp)': 'dropdown.onKeyDown($event)', + '(keydown.ArrowDown)': 'dropdown.onKeyDown($event)', + '(keydown.Home)': 'dropdown.onKeyDown($event)', + '(keydown.End)': 'dropdown.onKeyDown($event)', + '(keydown.Enter)': 'dropdown.onKeyDown($event)', + '(keydown.Space)': 'dropdown.onKeyDown($event)' + } +}) +export class NgbDropdownMenu { + placement: Placement = 'bottom'; + isOpen = false; + + @ContentChildren(NgbDropdownItem) menuItems: QueryList; + + constructor(@Inject(forwardRef(() => NgbDropdown)) public dropdown) {} +} + +/** + * A directive to mark an element to which dropdown menu will be anchored. + * + * This is a simple version of the `NgbDropdownToggle` directive. + * It plays the same role, but doesn't listen to click events to toggle dropdown menu thus enabling support + * for events other than click. + * + * @since 1.1.0 + */ +@Directive({ + selector: '[ngbDropdownAnchor]', + host: {'class': 'dropdown-toggle', 'aria-haspopup': 'true', '[attr.aria-expanded]': 'dropdown.isOpen()'} +}) +export class NgbDropdownAnchor { + anchorEl; + + constructor(@Inject(forwardRef(() => NgbDropdown)) public dropdown, private _elementRef: ElementRef) { + this.anchorEl = _elementRef.nativeElement; + } + + getNativeElement() { return this._elementRef.nativeElement; } +} + +/** + * A directive to mark an element that will toggle dropdown via the `click` event. + * + * You can also use `NgbDropdownAnchor` as an alternative. + */ +@Directive({ + selector: '[ngbDropdownToggle]', + host: { + 'class': 'dropdown-toggle', + 'aria-haspopup': 'true', + '[attr.aria-expanded]': 'dropdown.isOpen()', + '(click)': 'dropdown.toggle()', + '(keydown.ArrowUp)': 'dropdown.onKeyDown($event)', + '(keydown.ArrowDown)': 'dropdown.onKeyDown($event)', + '(keydown.Home)': 'dropdown.onKeyDown($event)', + '(keydown.End)': 'dropdown.onKeyDown($event)' + }, + providers: [{provide: NgbDropdownAnchor, useExisting: forwardRef(() => NgbDropdownToggle)}] +}) +export class NgbDropdownToggle extends NgbDropdownAnchor { + constructor(@Inject(forwardRef(() => NgbDropdown)) dropdown, elementRef: ElementRef) { + super(dropdown, elementRef); + } +} + +/** + * A directive that provides contextual overlays for displaying lists of links and more. + */ +@Directive({selector: '[ngbDropdown]', exportAs: 'ngbDropdown', host: {'[class.show]': 'isOpen()'}}) +export class NgbDropdown implements AfterContentInit, OnDestroy { + private _closed$ = new Subject(); + private _zoneSubscription: Subscription; + private _bodyContainer: HTMLElement; + + @ContentChild(NgbDropdownMenu, {static: false}) private _menu: NgbDropdownMenu; + @ContentChild(NgbDropdownMenu, {read: ElementRef, static: false}) private _menuElement: ElementRef; + @ContentChild(NgbDropdownAnchor, {static: false}) private _anchor: NgbDropdownAnchor; + + /** + * Indicates whether the dropdown should be closed when clicking one of dropdown items or pressing ESC. + * + * * `true` - the dropdown will close on both outside and inside (menu) clicks. + * * `false` - the dropdown can only be closed manually via `close()` or `toggle()` methods. + * * `"inside"` - the dropdown will close on inside menu clicks, but not outside clicks. + * * `"outside"` - the dropdown will close only on the outside clicks and not on menu clicks. + */ + @Input() autoClose: boolean | 'outside' | 'inside'; + + /** + * Defines whether or not the dropdown menu is opened initially. + */ + @Input('open') _open = false; + + /** + * The preferred placement of the dropdown. + * + * Possible values are `"top"`, `"top-left"`, `"top-right"`, `"bottom"`, `"bottom-left"`, + * `"bottom-right"`, `"left"`, `"left-top"`, `"left-bottom"`, `"right"`, `"right-top"`, + * `"right-bottom"` + * + * Accepts an array of strings or a string with space separated possible values. + * + * The default order of preference is `"bottom-left bottom-right top-left top-right"` + * + * Please see the [positioning overview](#/positioning) for more details. + */ + @Input() placement: PlacementArray; + + /** + * A selector specifying the element the dropdown should be appended to. + * Currently only supports "body". + * + * @since 4.1.0 + */ + @Input() container: null | 'body'; + + /** + * Enable or disable the dynamic positioning. The default value is dynamic unless the dropdown is used + * inside a Bootstrap navbar. If you need custom placement for a dropdown in a navbar, set it to + * dynamic explicitly. See the [positioning of dropdown](#/positioning#dropdown) + * and the [navbar demo](/#/components/dropdown/examples#navbar) for more details. + * + * @since 4.2.0 + */ + @Input() display: 'dynamic' | 'static'; + + /** + * An event fired when the dropdown is opened or closed. + * + * The event payload is a `boolean`: + * * `true` - the dropdown was opened + * * `false` - the dropdown was closed + */ + @Output() openChange = new EventEmitter(); + + constructor( + private _changeDetector: ChangeDetectorRef, config: NgbDropdownConfig, @Inject(DOCUMENT) private _document: any, + private _ngZone: NgZone, private _elementRef: ElementRef, private _renderer: Renderer2, + @Optional() ngbNavbar: NgbNavbar) { + this.placement = config.placement; + this.container = config.container; + this.autoClose = config.autoClose; + + this.display = ngbNavbar ? 'static' : 'dynamic'; + + this._zoneSubscription = _ngZone.onStable.subscribe(() => { this._positionMenu(); }); + } + + ngAfterContentInit() { + this._ngZone.onStable.pipe(take(1)).subscribe(() => { + this._applyPlacementClasses(); + if (this._open) { + this._setCloseHandlers(); + } + }); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.container && this._open) { + this._applyContainer(this.container); + } + + if (changes.placement && !changes.placement.isFirstChange) { + this._applyPlacementClasses(); + } + } + + /** + * Checks if the dropdown menu is open. + */ + isOpen(): boolean { return this._open; } + + /** + * Opens the dropdown menu. + */ + open(): void { + if (!this._open) { + this._open = true; + this._applyContainer(this.container); + this.openChange.emit(true); + this._setCloseHandlers(); + } + } + + private _setCloseHandlers() { + const anchor = this._anchor; + ngbAutoClose( + this._ngZone, this._document, this.autoClose, () => this.close(), this._closed$, + this._menu ? [this._menuElement.nativeElement] : [], anchor ? [anchor.getNativeElement()] : [], + '.dropdown-item,.dropdown-divider'); + } + + /** + * Closes the dropdown menu. + */ + close(): void { + if (this._open) { + this._open = false; + this._resetContainer(); + this._closed$.next(); + this.openChange.emit(false); + this._changeDetector.markForCheck(); + } + } + + /** + * Toggles the dropdown menu. + */ + toggle(): void { + if (this.isOpen()) { + this.close(); + } else { + this.open(); + } + } + + ngOnDestroy() { + this._resetContainer(); + + this._closed$.next(); + this._zoneSubscription.unsubscribe(); + } + + onKeyDown(event: KeyboardEvent) { + // tslint:disable-next-line:deprecation + const key = event.which; + const itemElements = this._getMenuElements(); + + let position = -1; + let isEventFromItems = false; + const isEventFromToggle = this._isEventFromToggle(event); + + if (!isEventFromToggle && itemElements.length) { + itemElements.forEach((itemElement, index) => { + if (itemElement.contains(event.target as HTMLElement)) { + isEventFromItems = true; + } + if (itemElement === this._document.activeElement) { + position = index; + } + }); + } + + // closing on Enter / Space + if (key === Key.Space || key === Key.Enter) { + if (isEventFromItems && (this.autoClose === true || this.autoClose === 'inside')) { + this.close(); + } + return; + } + + // opening / navigating + if (isEventFromToggle || isEventFromItems) { + this.open(); + + if (itemElements.length) { + switch (key) { + case Key.ArrowDown: + position = Math.min(position + 1, itemElements.length - 1); + break; + case Key.ArrowUp: + if (this._isDropup() && position === -1) { + position = itemElements.length - 1; + break; + } + position = Math.max(position - 1, 0); + break; + case Key.Home: + position = 0; + break; + case Key.End: + position = itemElements.length - 1; + break; + } + itemElements[position].focus(); + } + event.preventDefault(); + } + } + + private _isDropup(): boolean { return this._elementRef.nativeElement.classList.contains('dropup'); } + + private _isEventFromToggle(event: KeyboardEvent) { + return this._anchor.getNativeElement().contains(event.target as HTMLElement); + } + + private _getMenuElements(): HTMLElement[] { + const menu = this._menu; + if (menu == null) { + return []; + } + return menu.menuItems.filter(item => !item.disabled).map(item => item.elementRef.nativeElement); + } + + private _positionMenu() { + const menu = this._menu; + if (this.isOpen() && menu) { + this._applyPlacementClasses( + this.display === 'dynamic' ? + positionElements( + this._anchor.anchorEl, this._bodyContainer || this._menuElement.nativeElement, this.placement, + this.container === 'body') : + this._getFirstPlacement(this.placement)); + } + } + + private _getFirstPlacement(placement: PlacementArray): Placement { + return Array.isArray(placement) ? placement[0] : placement.split(' ')[0] as Placement; + } + + private _resetContainer() { + const renderer = this._renderer; + const menuElement = this._menuElement; + if (menuElement) { + const dropdownElement = this._elementRef.nativeElement; + const dropdownMenuElement = menuElement.nativeElement; + + renderer.appendChild(dropdownElement, dropdownMenuElement); + renderer.removeStyle(dropdownMenuElement, 'position'); + renderer.removeStyle(dropdownMenuElement, 'transform'); + } + if (this._bodyContainer) { + renderer.removeChild(this._document.body, this._bodyContainer); + this._bodyContainer = null; + } + } + + private _applyContainer(container: null | 'body' = null) { + this._resetContainer(); + if (container === 'body') { + const renderer = this._renderer; + const dropdownMenuElement = this._menuElement.nativeElement; + const bodyContainer = this._bodyContainer = this._bodyContainer || renderer.createElement('div'); + + // Override some styles to have the positionning working + renderer.setStyle(bodyContainer, 'position', 'absolute'); + renderer.setStyle(dropdownMenuElement, 'position', 'static'); + renderer.setStyle(bodyContainer, 'z-index', '1050'); + + renderer.appendChild(bodyContainer, dropdownMenuElement); + renderer.appendChild(this._document.body, bodyContainer); + } + } + + private _applyPlacementClasses(placement?: Placement) { + const menu = this._menu; + if (menu) { + if (!placement) { + placement = this._getFirstPlacement(this.placement); + } + + const renderer = this._renderer; + const dropdownElement = this._elementRef.nativeElement; + + // remove the current placement classes + renderer.removeClass(dropdownElement, 'dropup'); + renderer.removeClass(dropdownElement, 'dropdown'); + menu.placement = this.display === 'static' ? null : placement; + + /* + * apply the new placement + * in case of top use up-arrow or down-arrow otherwise + */ + const dropdownClass = placement.search('^top') !== -1 ? 'dropup' : 'dropdown'; + renderer.addClass(dropdownElement, dropdownClass); + + const bodyContainer = this._bodyContainer; + if (bodyContainer) { + renderer.removeClass(bodyContainer, 'dropup'); + renderer.removeClass(bodyContainer, 'dropdown'); + renderer.addClass(bodyContainer, dropdownClass); + } + } + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..87cf090 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,138 @@ +import {NgModule} from '@angular/core'; + +import {NgbAccordionModule} from './accordion/accordion.module'; +import {NgbAlertModule} from './alert/alert.module'; +import {NgbButtonsModule} from './buttons/buttons.module'; +import {NgbCarouselModule} from './carousel/carousel.module'; +import {NgbCollapseModule} from './collapse/collapse.module'; +import {NgbDatepickerModule} from './datepicker/datepicker.module'; +import {NgbDropdownModule} from './dropdown/dropdown.module'; +import {NgbModalModule} from './modal/modal.module'; +import {NgbPaginationModule} from './pagination/pagination.module'; +import {NgbPopoverModule} from './popover/popover.module'; +import {NgbProgressbarModule} from './progressbar/progressbar.module'; +import {NgbRatingModule} from './rating/rating.module'; +import {NgbTabsetModule} from './tabset/tabset.module'; +import {NgbTimepickerModule} from './timepicker/timepicker.module'; +import {NgbToastModule} from './toast/toast.module'; +import {NgbTooltipModule} from './tooltip/tooltip.module'; +import {NgbTypeaheadModule} from './typeahead/typeahead.module'; + + + +export { + NgbAccordion, + NgbAccordionConfig, + NgbAccordionModule, + NgbPanel, + NgbPanelChangeEvent, + NgbPanelContent, + NgbPanelHeader, + NgbPanelHeaderContext, + NgbPanelTitle, + NgbPanelToggle +} from './accordion/accordion.module'; +export {NgbAlert, NgbAlertConfig, NgbAlertModule} from './alert/alert.module'; +export {NgbButtonLabel, NgbButtonsModule, NgbCheckBox, NgbRadio, NgbRadioGroup} from './buttons/buttons.module'; +export { + NgbCarousel, + NgbCarouselConfig, + NgbCarouselModule, + NgbSlide, + NgbSlideEvent, + NgbSlideEventDirection, + NgbSlideEventSource +} from './carousel/carousel.module'; +export {NgbCollapse, NgbCollapseModule} from './collapse/collapse.module'; +export { + NgbCalendar, + NgbCalendarGregorian, + NgbCalendarHebrew, + NgbCalendarIslamicCivil, + NgbCalendarIslamicUmalqura, + NgbCalendarPersian, + NgbDate, + NgbDateAdapter, + NgbDateNativeAdapter, + NgbDateNativeUTCAdapter, + NgbDateParserFormatter, + NgbDatepicker, + NgbDatepickerConfig, + NgbDatepickerI18n, + NgbDatepickerI18nHebrew, + NgbDatepickerModule, + NgbDatepickerNavigateEvent, + NgbDateStruct, + NgbInputDatepicker, + NgbPeriod +} from './datepicker/datepicker.module'; +export { + NgbDropdown, + NgbDropdownAnchor, + NgbDropdownConfig, + NgbDropdownItem, + NgbDropdownMenu, + NgbDropdownModule, + NgbDropdownToggle +} from './dropdown/dropdown.module'; +export { + ModalDismissReasons, + NgbActiveModal, + NgbModal, + NgbModalConfig, + NgbModalModule, + NgbModalOptions, + NgbModalRef +} from './modal/modal.module'; +export { + NgbPagination, + NgbPaginationConfig, + NgbPaginationEllipsis, + NgbPaginationFirst, + NgbPaginationLast, + NgbPaginationModule, + NgbPaginationNext, + NgbPaginationNumber, + NgbPaginationPrevious +} from './pagination/pagination.module'; +export {NgbPopover, NgbPopoverConfig, NgbPopoverModule} from './popover/popover.module'; +export {NgbProgressbar, NgbProgressbarConfig, NgbProgressbarModule} from './progressbar/progressbar.module'; +export {NgbRating, NgbRatingConfig, NgbRatingModule} from './rating/rating.module'; +export { + NgbTab, + NgbTabChangeEvent, + NgbTabContent, + NgbTabset, + NgbTabsetConfig, + NgbTabsetModule, + NgbTabTitle +} from './tabset/tabset.module'; +export { + NgbTimeAdapter, + NgbTimepickerI18n, + NgbTimepicker, + NgbTimepickerConfig, + NgbTimepickerModule, + NgbTimeStruct +} from './timepicker/timepicker.module'; +export {NgbToast, NgbToastConfig, NgbToastHeader, NgbToastModule} from './toast/toast.module'; +export {NgbTooltip, NgbTooltipConfig, NgbTooltipModule} from './tooltip/tooltip.module'; +export { + NgbHighlight, + NgbTypeahead, + NgbTypeaheadConfig, + NgbTypeaheadModule, + NgbTypeaheadSelectItemEvent +} from './typeahead/typeahead.module'; +export {Placement} from './util/positioning'; + + +const NGB_MODULES = [ + NgbAccordionModule, NgbAlertModule, NgbButtonsModule, NgbCarouselModule, NgbCollapseModule, NgbDatepickerModule, + NgbDropdownModule, NgbModalModule, NgbPaginationModule, NgbPopoverModule, NgbProgressbarModule, NgbRatingModule, + NgbTabsetModule, NgbTimepickerModule, NgbToastModule, NgbTooltipModule, NgbTypeaheadModule +]; + +@NgModule({imports: NGB_MODULES, exports: NGB_MODULES}) +export class NgbModule { +} diff --git a/src/karma-ie.sauce.conf.js b/src/karma-ie.sauce.conf.js new file mode 100644 index 0000000..9eb9591 --- /dev/null +++ b/src/karma-ie.sauce.conf.js @@ -0,0 +1,62 @@ +// Configuration used testing via Sauce Labs on Travis CI + +process.env.SAUCE_ACCESS_KEY = process.env.SAUCE_ACCESS_KEY.split('').reverse().join(''); + +const BROWSERS = { + 'SL_IE10': { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 8', + version: '10' + }, + 'SL_IE11': { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 10', + version: '11' + }, +}; + +module.exports = function (config) { + config.set({ + basePath: '', + files: ['../node_modules/bootstrap/dist/css/bootstrap.min.css'], + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-sauce-launcher'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + sauceLabs: { + build: `TRAVIS #${process.env.TRAVIS_BUILD_NUMBER} (${process.env.TRAVIS_BUILD_ID})`, + tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER, + testName: 'ng-bootstrap/ie', + retryLimit: 3, + startConnect: false, + recordVideo: false, + recordScreenshots: false, + options: { + commandTimeout: 600, + idleTimeout: 600, + maxDuration: 5400 + } + }, + + customLaunchers: BROWSERS, + + reporters: ['dots', 'saucelabs'], + + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + browsers: Object.keys(BROWSERS), + singleRun: true, + captureTimeout: 180000, + browserDisconnectTimeout: 180000, + browserDisconnectTolerance: 3, + browserNoActivityTimeout: 300000 + }); +}; diff --git a/src/karma.conf.js b/src/karma.conf.js new file mode 100644 index 0000000..c30cd35 --- /dev/null +++ b/src/karma.conf.js @@ -0,0 +1,46 @@ +// Configuration used for local testing and Travis CI + +const reporters = process.env.TRAVIS ? ['dots'] : ['progress']; +const browsers = process.env.TRAVIS ? ['ChromeHeadlessNoSandbox'] : ['ChromeNoExtensions']; + +module.exports = function (config) { + config.set({ + basePath: '', + files: ['../node_modules/bootstrap/dist/css/bootstrap.min.css'], + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-firefox-launcher'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '..', 'coverage'), + reports: ['html', 'json', 'lcovonly'], + fixWebpackSourcePaths: true + }, + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: 'ChromeHeadless', + flags: ['--no-sandbox'] + }, + ChromeNoExtensions: { + base: 'Chrome', + flags: ['--disable-extensions'] + } + }, + reporters, + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers, + singleRun: false, + restartOnFileChange: true, + browserNoActivityTimeout: 20000 + }); +}; diff --git a/src/karma.sauce.conf.js b/src/karma.sauce.conf.js new file mode 100644 index 0000000..5c9189f --- /dev/null +++ b/src/karma.sauce.conf.js @@ -0,0 +1,84 @@ +// Configuration used testing via Sauce Labs on Travis CI + +process.env.SAUCE_ACCESS_KEY = process.env.SAUCE_ACCESS_KEY.split('').reverse().join(''); + +const BROWSERS = { + 'SL_CHROME': { + base: 'SauceLabs', + browserName: 'chrome', + version: 'latest' + }, + 'SL_FIREFOX': { + base: 'SauceLabs', + browserName: 'firefox', + version: 'latest' + }, + 'SL_EDGE16': { + base: 'SauceLabs', + browserName: 'MicrosoftEdge', + platform: 'Windows 10', + version: '16.16299' + }, + 'SL_EDGE17': { + base: 'SauceLabs', + browserName: 'MicrosoftEdge', + platform: 'Windows 10', + version: '17.17134' + }, + 'SL_SAFARI11': { + base: 'SauceLabs', + browserName: 'safari', + platform: 'macOS 10.13', + version: '11' + }, + 'SL_SAFARI12': { + base: 'SauceLabs', + browserName: 'safari', + platform: 'macOS 10.13', + version: '12' + }, +}; + +module.exports = function (config) { + config.set({ + basePath: '', + files: ['../node_modules/bootstrap/dist/css/bootstrap.min.css'], + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-sauce-launcher'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + sauceLabs: { + build: `TRAVIS #${process.env.TRAVIS_BUILD_NUMBER} (${process.env.TRAVIS_BUILD_ID})`, + tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER, + testName: 'ng-bootstrap', + retryLimit: 3, + startConnect: false, + recordVideo: false, + recordScreenshots: false, + options: { + commandTimeout: 600, + idleTimeout: 600, + maxDuration: 5400 + } + }, + + customLaunchers: BROWSERS, + + reporters: ['dots', 'saucelabs'], + + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + browsers: Object.keys(BROWSERS), + singleRun: true, + captureTimeout: 180000, + browserDisconnectTimeout: 180000, + browserDisconnectTolerance: 3, + browserNoActivityTimeout: 300000 + }); +}; diff --git a/src/modal/modal-backdrop.spec.ts b/src/modal/modal-backdrop.spec.ts new file mode 100644 index 0000000..4237e8e --- /dev/null +++ b/src/modal/modal-backdrop.spec.ts @@ -0,0 +1,15 @@ +import {TestBed} from '@angular/core/testing'; + +import {NgbModalBackdrop} from './modal-backdrop'; + +describe('ngb-modal-backdrop', () => { + + beforeEach(() => { TestBed.configureTestingModule({declarations: [NgbModalBackdrop]}); }); + + it('should render backdrop with required CSS classes', () => { + const fixture = TestBed.createComponent(NgbModalBackdrop); + + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveCssClass('modal-backdrop'); + }); +}); diff --git a/src/modal/modal-backdrop.ts b/src/modal/modal-backdrop.ts new file mode 100644 index 0000000..cffcbef --- /dev/null +++ b/src/modal/modal-backdrop.ts @@ -0,0 +1,11 @@ +import {Component, Input} from '@angular/core'; + +@Component({ + selector: 'ngb-modal-backdrop', + template: '', + host: + {'[class]': '"modal-backdrop fade show" + (backdropClass ? " " + backdropClass : "")', 'style': 'z-index: 1050'} +}) +export class NgbModalBackdrop { + @Input() backdropClass: string; +} diff --git a/src/modal/modal-config.ts b/src/modal/modal-config.ts new file mode 100644 index 0000000..f0339a6 --- /dev/null +++ b/src/modal/modal-config.ts @@ -0,0 +1,100 @@ +import {Injectable, Injector} from '@angular/core'; + +/** + * Options available when opening new modal windows with `NgbModal.open()` method. + */ +export interface NgbModalOptions { + /** + * `aria-labelledby` attribute value to set on the modal window. + * + * @since 2.2.0 + */ + ariaLabelledBy?: string; + + /** + * If `true`, the backdrop element will be created for a given modal. + * + * Alternatively, specify `'static'` for a backdrop which doesn't close the modal on click. + * + * Default value is `true`. + */ + backdrop?: boolean | 'static'; + + /** + * Callback right before the modal will be dismissed. + * + * If this function returns: + * * `false` + * * a promise resolved with `false` + * * a promise that is rejected + * + * then the modal won't be dismissed. + */ + beforeDismiss?: () => boolean | Promise; + + /** + * If `true`, the modal will be centered vertically. + * + * Default value is `false`. + * + * @since 1.1.0 + */ + centered?: boolean; + + /** + * A selector specifying the element all new modal windows should be appended to. + * + * If not specified, will be `body`. + */ + container?: string; + + /** + * The `Injector` to use for modal content. + */ + injector?: Injector; + + /** + * If `true`, the modal will be closed when `Escape` key is pressed + * + * Default value is `true`. + */ + keyboard?: boolean; + + /** + * Scrollable modal content (false by default). + * + * @since 5.0.0 + */ + scrollable?: boolean; + + /** + * Size of a new modal window. + */ + size?: 'sm' | 'lg' | 'xl'; + + /** + * A custom class to append to the modal window. + */ + windowClass?: string; + + /** + * A custom class to append to the modal backdrop. + * + * @since 1.1.0 + */ + backdropClass?: string; +} + +/** + * A configuration service for the [`NgbModal`](#/components/modal/api#NgbModal) service. + * + * You can inject this service, typically in your root component, and customize the values of its properties in + * order to provide default values for all modals used in the application. +* +* @since 3.1.0 +*/ +@Injectable({providedIn: 'root'}) +export class NgbModalConfig implements NgbModalOptions { + backdrop: boolean | 'static' = true; + keyboard = true; +} diff --git a/src/modal/modal-dismiss-reasons.ts b/src/modal/modal-dismiss-reasons.ts new file mode 100644 index 0000000..e494fc3 --- /dev/null +++ b/src/modal/modal-dismiss-reasons.ts @@ -0,0 +1,4 @@ +export enum ModalDismissReasons { + BACKDROP_CLICK, + ESC +} diff --git a/src/modal/modal-ref.ts b/src/modal/modal-ref.ts new file mode 100644 index 0000000..ced43a0 --- /dev/null +++ b/src/modal/modal-ref.ts @@ -0,0 +1,127 @@ +import {ComponentRef} from '@angular/core'; + +import {NgbModalBackdrop} from './modal-backdrop'; +import {NgbModalWindow} from './modal-window'; + +import {ContentRef} from '../util/popup'; + +/** + * A reference to the currently opened (active) modal. + * + * Instances of this class can be injected into your component passed as modal content. + * So you can `.close()` or `.dismiss()` the modal window from your component. + */ +export class NgbActiveModal { + /** + * Closes the modal with an optional `result` value. + * + * The `NgbMobalRef.result` promise will be resolved with the provided value. + */ + close(result?: any): void {} + + /** + * Dismisses the modal with an optional `reason` value. + * + * The `NgbModalRef.result` promise will be rejected with the provided value. + */ + dismiss(reason?: any): void {} +} + +/** + * A reference to the newly opened modal returned by the `NgbModal.open()` method. + */ +export class NgbModalRef { + private _resolve: (result?: any) => void; + private _reject: (reason?: any) => void; + + /** + * The instance of a component used for the modal content. + * + * When a `TemplateRef` is used as the content, will return `undefined`. + */ + get componentInstance(): any { + if (this._contentRef.componentRef) { + return this._contentRef.componentRef.instance; + } + } + + /** + * The promise that is resolved when the modal is closed and rejected when the modal is dismissed. + */ + result: Promise; + + constructor( + private _windowCmptRef: ComponentRef, private _contentRef: ContentRef, + private _backdropCmptRef?: ComponentRef, private _beforeDismiss?: Function) { + _windowCmptRef.instance.dismissEvent.subscribe((reason: any) => { this.dismiss(reason); }); + + this.result = new Promise((resolve, reject) => { + this._resolve = resolve; + this._reject = reject; + }); + this.result.then(null, () => {}); + } + + /** + * Closes the modal with an optional `result` value. + * + * The `NgbMobalRef.result` promise will be resolved with the provided value. + */ + close(result?: any): void { + if (this._windowCmptRef) { + this._resolve(result); + this._removeModalElements(); + } + } + + private _dismiss(reason?: any) { + this._reject(reason); + this._removeModalElements(); + } + + /** + * Dismisses the modal with an optional `reason` value. + * + * The `NgbModalRef.result` promise will be rejected with the provided value. + */ + dismiss(reason?: any): void { + if (this._windowCmptRef) { + if (!this._beforeDismiss) { + this._dismiss(reason); + } else { + const dismiss = this._beforeDismiss(); + if (dismiss && dismiss.then) { + dismiss.then( + result => { + if (result !== false) { + this._dismiss(reason); + } + }, + () => {}); + } else if (dismiss !== false) { + this._dismiss(reason); + } + } + } + } + + private _removeModalElements() { + const windowNativeEl = this._windowCmptRef.location.nativeElement; + windowNativeEl.parentNode.removeChild(windowNativeEl); + this._windowCmptRef.destroy(); + + if (this._backdropCmptRef) { + const backdropNativeEl = this._backdropCmptRef.location.nativeElement; + backdropNativeEl.parentNode.removeChild(backdropNativeEl); + this._backdropCmptRef.destroy(); + } + + if (this._contentRef && this._contentRef.viewRef) { + this._contentRef.viewRef.destroy(); + } + + this._windowCmptRef = null; + this._backdropCmptRef = null; + this._contentRef = null; + } +} diff --git a/src/modal/modal-stack.ts b/src/modal/modal-stack.ts new file mode 100644 index 0000000..5cda3d2 --- /dev/null +++ b/src/modal/modal-stack.ts @@ -0,0 +1,223 @@ +import {DOCUMENT} from '@angular/common'; +import { + ApplicationRef, + ComponentFactoryResolver, + ComponentRef, + Inject, + Injectable, + Injector, + RendererFactory2, + TemplateRef, +} from '@angular/core'; +import {Subject} from 'rxjs'; + +import {ngbFocusTrap} from '../util/focus-trap'; +import {ContentRef} from '../util/popup'; +import {ScrollBar} from '../util/scrollbar'; +import {isDefined, isString} from '../util/util'; +import {NgbModalBackdrop} from './modal-backdrop'; +import {NgbModalOptions} from './modal-config'; +import {NgbActiveModal, NgbModalRef} from './modal-ref'; +import {NgbModalWindow} from './modal-window'; + +@Injectable({providedIn: 'root'}) +export class NgbModalStack { + private _activeWindowCmptHasChanged = new Subject(); + private _ariaHiddenValues: Map = new Map(); + private _backdropAttributes = ['backdropClass']; + private _modalRefs: NgbModalRef[] = []; + private _windowAttributes = + ['ariaLabelledBy', 'backdrop', 'centered', 'keyboard', 'scrollable', 'size', 'windowClass']; + private _windowCmpts: ComponentRef[] = []; + + constructor( + private _applicationRef: ApplicationRef, private _injector: Injector, @Inject(DOCUMENT) private _document: any, + private _scrollBar: ScrollBar, private _rendererFactory: RendererFactory2) { + // Trap focus on active WindowCmpt + this._activeWindowCmptHasChanged.subscribe(() => { + if (this._windowCmpts.length) { + const activeWindowCmpt = this._windowCmpts[this._windowCmpts.length - 1]; + ngbFocusTrap(activeWindowCmpt.location.nativeElement, this._activeWindowCmptHasChanged); + this._revertAriaHidden(); + this._setAriaHidden(activeWindowCmpt.location.nativeElement); + } + }); + } + + open(moduleCFR: ComponentFactoryResolver, contentInjector: Injector, content: any, options): NgbModalRef { + const containerEl = + isDefined(options.container) ? this._document.querySelector(options.container) : this._document.body; + const renderer = this._rendererFactory.createRenderer(null, null); + + const revertPaddingForScrollBar = this._scrollBar.compensate(); + const removeBodyClass = () => { + if (!this._modalRefs.length) { + renderer.removeClass(this._document.body, 'modal-open'); + this._revertAriaHidden(); + } + }; + + if (!containerEl) { + throw new Error(`The specified modal container "${options.container || 'body'}" was not found in the DOM.`); + } + + const activeModal = new NgbActiveModal(); + const contentRef = + this._getContentRef(moduleCFR, options.injector || contentInjector, content, activeModal, options); + + let backdropCmptRef: ComponentRef = + options.backdrop !== false ? this._attachBackdrop(moduleCFR, containerEl) : null; + let windowCmptRef: ComponentRef = this._attachWindowComponent(moduleCFR, containerEl, contentRef); + let ngbModalRef: NgbModalRef = new NgbModalRef(windowCmptRef, contentRef, backdropCmptRef, options.beforeDismiss); + + this._registerModalRef(ngbModalRef); + this._registerWindowCmpt(windowCmptRef); + ngbModalRef.result.then(revertPaddingForScrollBar, revertPaddingForScrollBar); + ngbModalRef.result.then(removeBodyClass, removeBodyClass); + activeModal.close = (result: any) => { ngbModalRef.close(result); }; + activeModal.dismiss = (reason: any) => { ngbModalRef.dismiss(reason); }; + + this._applyWindowOptions(windowCmptRef.instance, options); + if (this._modalRefs.length === 1) { + renderer.addClass(this._document.body, 'modal-open'); + } + + if (backdropCmptRef && backdropCmptRef.instance) { + this._applyBackdropOptions(backdropCmptRef.instance, options); + } + return ngbModalRef; + } + + dismissAll(reason?: any) { this._modalRefs.forEach(ngbModalRef => ngbModalRef.dismiss(reason)); } + + hasOpenModals(): boolean { return this._modalRefs.length > 0; } + + private _attachBackdrop(moduleCFR: ComponentFactoryResolver, containerEl: any): ComponentRef { + let backdropFactory = moduleCFR.resolveComponentFactory(NgbModalBackdrop); + let backdropCmptRef = backdropFactory.create(this._injector); + this._applicationRef.attachView(backdropCmptRef.hostView); + containerEl.appendChild(backdropCmptRef.location.nativeElement); + return backdropCmptRef; + } + + private _attachWindowComponent(moduleCFR: ComponentFactoryResolver, containerEl: any, contentRef: any): + ComponentRef { + let windowFactory = moduleCFR.resolveComponentFactory(NgbModalWindow); + let windowCmptRef = windowFactory.create(this._injector, contentRef.nodes); + this._applicationRef.attachView(windowCmptRef.hostView); + containerEl.appendChild(windowCmptRef.location.nativeElement); + return windowCmptRef; + } + + private _applyWindowOptions(windowInstance: NgbModalWindow, options: Object): void { + this._windowAttributes.forEach((optionName: string) => { + if (isDefined(options[optionName])) { + windowInstance[optionName] = options[optionName]; + } + }); + } + + private _applyBackdropOptions(backdropInstance: NgbModalBackdrop, options: Object): void { + this._backdropAttributes.forEach((optionName: string) => { + if (isDefined(options[optionName])) { + backdropInstance[optionName] = options[optionName]; + } + }); + } + + private _getContentRef( + moduleCFR: ComponentFactoryResolver, contentInjector: Injector, content: any, activeModal: NgbActiveModal, + options: NgbModalOptions): ContentRef { + if (!content) { + return new ContentRef([]); + } else if (content instanceof TemplateRef) { + return this._createFromTemplateRef(content, activeModal); + } else if (isString(content)) { + return this._createFromString(content); + } else { + return this._createFromComponent(moduleCFR, contentInjector, content, activeModal, options); + } + } + + private _createFromTemplateRef(content: TemplateRef, activeModal: NgbActiveModal): ContentRef { + const context = { + $implicit: activeModal, + close(result) { activeModal.close(result); }, + dismiss(reason) { activeModal.dismiss(reason); } + }; + const viewRef = content.createEmbeddedView(context); + this._applicationRef.attachView(viewRef); + return new ContentRef([viewRef.rootNodes], viewRef); + } + + private _createFromString(content: string): ContentRef { + const component = this._document.createTextNode(`${content}`); + return new ContentRef([[component]]); + } + + private _createFromComponent( + moduleCFR: ComponentFactoryResolver, contentInjector: Injector, content: any, context: NgbActiveModal, + options: NgbModalOptions): ContentRef { + const contentCmptFactory = moduleCFR.resolveComponentFactory(content); + const modalContentInjector = + Injector.create({providers: [{provide: NgbActiveModal, useValue: context}], parent: contentInjector}); + const componentRef = contentCmptFactory.create(modalContentInjector); + const componentNativeEl = componentRef.location.nativeElement; + if (options.scrollable) { + (componentNativeEl as HTMLElement).classList.add('component-host-scrollable'); + } + this._applicationRef.attachView(componentRef.hostView); + // FIXME: we should here get rid of the component nativeElement + // and use `[Array.from(componentNativeEl.childNodes)]` instead and remove the above CSS class. + return new ContentRef([[componentNativeEl]], componentRef.hostView, componentRef); + } + + private _setAriaHidden(element: Element) { + const parent = element.parentElement; + if (parent && element !== this._document.body) { + Array.from(parent.children).forEach(sibling => { + if (sibling !== element && sibling.nodeName !== 'SCRIPT') { + this._ariaHiddenValues.set(sibling, sibling.getAttribute('aria-hidden')); + sibling.setAttribute('aria-hidden', 'true'); + } + }); + + this._setAriaHidden(parent); + } + } + + private _revertAriaHidden() { + this._ariaHiddenValues.forEach((value, element) => { + if (value) { + element.setAttribute('aria-hidden', value); + } else { + element.removeAttribute('aria-hidden'); + } + }); + this._ariaHiddenValues.clear(); + } + + private _registerModalRef(ngbModalRef: NgbModalRef) { + const unregisterModalRef = () => { + const index = this._modalRefs.indexOf(ngbModalRef); + if (index > -1) { + this._modalRefs.splice(index, 1); + } + }; + this._modalRefs.push(ngbModalRef); + ngbModalRef.result.then(unregisterModalRef, unregisterModalRef); + } + + private _registerWindowCmpt(ngbWindowCmpt: ComponentRef) { + this._windowCmpts.push(ngbWindowCmpt); + this._activeWindowCmptHasChanged.next(); + + ngbWindowCmpt.onDestroy(() => { + const index = this._windowCmpts.indexOf(ngbWindowCmpt); + if (index > -1) { + this._windowCmpts.splice(index, 1); + this._activeWindowCmptHasChanged.next(); + } + }); + } +} diff --git a/src/modal/modal-window.spec.ts b/src/modal/modal-window.spec.ts new file mode 100644 index 0000000..2f55d07 --- /dev/null +++ b/src/modal/modal-window.spec.ts @@ -0,0 +1,112 @@ +import {TestBed, ComponentFixture} from '@angular/core/testing'; + +import {NgbModalWindow} from './modal-window'; +import {ModalDismissReasons} from './modal-dismiss-reasons'; + +describe('ngb-modal-dialog', () => { + + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({declarations: [NgbModalWindow]}); + fixture = TestBed.createComponent(NgbModalWindow); + }); + + describe('basic rendering functionality', () => { + + it('should render default modal window', () => { + fixture.detectChanges(); + + const modalEl: Element = fixture.nativeElement; + const dialogEl: Element = fixture.nativeElement.querySelector('.modal-dialog'); + + expect(modalEl).toHaveCssClass('modal'); + expect(dialogEl).toHaveCssClass('modal-dialog'); + }); + + it('should render default modal window with a specified size', () => { + fixture.componentInstance.size = 'sm'; + fixture.detectChanges(); + + const dialogEl: Element = fixture.nativeElement.querySelector('.modal-dialog'); + expect(dialogEl).toHaveCssClass('modal-dialog'); + expect(dialogEl).toHaveCssClass('modal-sm'); + }); + + it('should render default modal window with a specified class', () => { + fixture.componentInstance.windowClass = 'custom-class'; + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveCssClass('custom-class'); + }); + + it('aria attributes', () => { + fixture.detectChanges(); + const dialogEl: Element = fixture.nativeElement.querySelector('.modal-dialog'); + + expect(fixture.nativeElement.getAttribute('role')).toBe('dialog'); + expect(dialogEl.getAttribute('role')).toBe('document'); + }); + }); + + describe('dismiss', () => { + + it('should dismiss on backdrop click by default', (done) => { + fixture.detectChanges(); + + fixture.componentInstance.dismissEvent.subscribe(($event) => { + expect($event).toBe(ModalDismissReasons.BACKDROP_CLICK); + done(); + }); + + fixture.nativeElement.click(); + }); + + it('should not dismiss on modal content click when there is active backdrop', (done) => { + fixture.detectChanges(); + fixture.componentInstance.dismissEvent.subscribe( + () => { done.fail(new Error('Should not trigger dismiss event')); }); + + fixture.nativeElement.querySelector('.modal-content').click(); + setTimeout(done, 200); + }); + + it('should ignore backdrop clicks when there is no backdrop', (done) => { + fixture.componentInstance.backdrop = false; + fixture.detectChanges(); + + fixture.componentInstance.dismissEvent.subscribe(($event) => { + expect($event).toBe(ModalDismissReasons.BACKDROP_CLICK); + done.fail(new Error('Should not trigger dismiss event')); + }); + + fixture.nativeElement.querySelector('.modal-dialog').click(); + setTimeout(done, 200); + }); + + it('should ignore backdrop clicks when backdrop is "static"', (done) => { + fixture.componentInstance.backdrop = 'static'; + fixture.detectChanges(); + + fixture.componentInstance.dismissEvent.subscribe(($event) => { + expect($event).toBe(ModalDismissReasons.BACKDROP_CLICK); + done.fail(new Error('Should not trigger dismiss event')); + }); + + fixture.nativeElement.querySelector('.modal-dialog').click(); + setTimeout(done, 200); + }); + + it('should dismiss on esc press by default', (done) => { + fixture.detectChanges(); + + fixture.componentInstance.dismissEvent.subscribe(($event) => { + expect($event).toBe(ModalDismissReasons.ESC); + done(); + }); + + fixture.debugElement.triggerEventHandler('keyup.esc', {}); + }); + }); + +}); diff --git a/src/modal/modal-window.ts b/src/modal/modal-window.ts new file mode 100644 index 0000000..9ea8a5f --- /dev/null +++ b/src/modal/modal-window.ts @@ -0,0 +1,93 @@ +import {DOCUMENT} from '@angular/common'; +import { + AfterViewInit, + Component, + ElementRef, + EventEmitter, + Inject, + Input, + OnDestroy, + OnInit, + Output, + ViewEncapsulation, +} from '@angular/core'; + +import {getFocusableBoundaryElements} from '../util/focus-trap'; +import {ModalDismissReasons} from './modal-dismiss-reasons'; + +@Component({ + selector: 'ngb-modal-window', + host: { + '[class]': '"modal fade show d-block" + (windowClass ? " " + windowClass : "")', + 'role': 'dialog', + 'tabindex': '-1', + '(keyup.esc)': 'escKey($event)', + '(click)': 'backdropClick($event)', + '[attr.aria-modal]': 'true', + '[attr.aria-labelledby]': 'ariaLabelledBy', + }, + template: ` +
+ +
+ `, + encapsulation: ViewEncapsulation.None, + styleUrls: ['./modal.scss'] +}) +export class NgbModalWindow implements OnInit, + AfterViewInit, OnDestroy { + private _elWithFocus: Element; // element that is focused prior to modal opening + + @Input() ariaLabelledBy: string; + @Input() backdrop: boolean | string = true; + @Input() centered: string; + @Input() keyboard = true; + @Input() scrollable: string; + @Input() size: string; + @Input() windowClass: string; + + @Output('dismiss') dismissEvent = new EventEmitter(); + + constructor(@Inject(DOCUMENT) private _document: any, private _elRef: ElementRef) {} + + backdropClick($event): void { + if (this.backdrop === true && this._elRef.nativeElement === $event.target) { + this.dismiss(ModalDismissReasons.BACKDROP_CLICK); + } + } + + escKey($event): void { + if (this.keyboard && !$event.defaultPrevented) { + this.dismiss(ModalDismissReasons.ESC); + } + } + + dismiss(reason): void { this.dismissEvent.emit(reason); } + + ngOnInit() { this._elWithFocus = this._document.activeElement; } + + ngAfterViewInit() { + if (!this._elRef.nativeElement.contains(document.activeElement)) { + const autoFocusable = this._elRef.nativeElement.querySelector(`[ngbAutofocus]`) as HTMLElement; + const firstFocusable = getFocusableBoundaryElements(this._elRef.nativeElement)[0]; + + const elementToFocus = autoFocusable || firstFocusable || this._elRef.nativeElement; + elementToFocus.focus(); + } + } + + ngOnDestroy() { + const body = this._document.body; + const elWithFocus = this._elWithFocus; + + let elementToFocus; + if (elWithFocus && elWithFocus['focus'] && body.contains(elWithFocus)) { + elementToFocus = elWithFocus; + } else { + elementToFocus = body; + } + elementToFocus.focus(); + this._elWithFocus = null; + } +} diff --git a/src/modal/modal.module.ts b/src/modal/modal.module.ts new file mode 100644 index 0000000..424edca --- /dev/null +++ b/src/modal/modal.module.ts @@ -0,0 +1,18 @@ +import {NgModule} from '@angular/core'; + +import {NgbModal} from './modal'; +import {NgbModalBackdrop} from './modal-backdrop'; +import {NgbModalWindow} from './modal-window'; + +export {NgbModal} from './modal'; +export {NgbModalConfig, NgbModalOptions} from './modal-config'; +export {NgbModalRef, NgbActiveModal} from './modal-ref'; +export {ModalDismissReasons} from './modal-dismiss-reasons'; + +@NgModule({ + declarations: [NgbModalBackdrop, NgbModalWindow], + entryComponents: [NgbModalBackdrop, NgbModalWindow], + providers: [NgbModal] +}) +export class NgbModalModule { +} diff --git a/src/modal/modal.scss b/src/modal/modal.scss new file mode 100644 index 0000000..eeb9feb --- /dev/null +++ b/src/modal/modal.scss @@ -0,0 +1,7 @@ +ngb-modal-window { + .component-host-scrollable { + display: flex; + flex-direction: column; + overflow: hidden; + } +} diff --git a/src/modal/modal.spec.ts b/src/modal/modal.spec.ts new file mode 100644 index 0000000..80af80c --- /dev/null +++ b/src/modal/modal.spec.ts @@ -0,0 +1,1157 @@ +import {CommonModule} from '@angular/common'; +import { + Component, + DebugElement, + getDebugNode, + Injectable, + Injector, + NgModule, + OnDestroy, + ViewChild +} from '@angular/core'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {NgbModalConfig} from './modal-config'; +import {NgbActiveModal, NgbModal, NgbModalModule, NgbModalRef} from './modal.module'; + +const NOOP = () => {}; + +@Injectable() +class SpyService { + called = false; +} + +@Injectable() +class CustomSpyService { + called = false; +} + +describe('ngb-modal', () => { + + let fixture: ComponentFixture; + + beforeEach(() => { + jasmine.addMatchers({ + toHaveModal: function(util, customEqualityTests) { + return { + compare: function(actual, content?, selector?) { + const allModalsContent = document.querySelector(selector || 'body').querySelectorAll('.modal-content'); + let pass = true; + let errMsg; + + if (!content) { + pass = allModalsContent.length > 0; + errMsg = 'at least one modal open but found none'; + } else if (Array.isArray(content)) { + pass = allModalsContent.length === content.length; + errMsg = `${content.length} modals open but found ${allModalsContent.length}`; + } else { + pass = allModalsContent.length === 1 && allModalsContent[0].textContent.trim() === content; + errMsg = `exactly one modal open but found ${allModalsContent.length}`; + } + + return {pass: pass, message: `Expected ${actual.outerHTML} to have ${errMsg}`}; + }, + negativeCompare: function(actual) { + const allOpenModals = actual.querySelectorAll('ngb-modal-window'); + + return { + pass: allOpenModals.length === 0, + message: `Expected ${actual.outerHTML} not to have any modals open but found ${allOpenModals.length}` + }; + } + }; + } + }); + + jasmine.addMatchers({ + toHaveBackdrop: function(util, customEqualityTests) { + return { + compare: function(actual) { + return { + pass: document.querySelectorAll('ngb-modal-backdrop').length === 1, + message: `Expected ${actual.outerHTML} to have exactly one backdrop element` + }; + }, + negativeCompare: function(actual) { + const allOpenModals = document.querySelectorAll('ngb-modal-backdrop'); + + return { + pass: allOpenModals.length === 0, + message: `Expected ${actual.outerHTML} not to have any backdrop elements` + }; + } + }; + } + }); + }); + + afterEach(() => { + // detect left-over modals and report errors when found + + const remainingModalWindows = document.querySelectorAll('ngb-modal-window'); + if (remainingModalWindows.length) { + fail(`${remainingModalWindows.length} modal windows were left in the DOM.`); + } + + const remainingModalBackdrops = document.querySelectorAll('ngb-modal-backdrop'); + if (remainingModalBackdrops.length) { + fail(`${remainingModalBackdrops.length} modal backdrops were left in the DOM.`); + } + }); + + describe('default configuration', () => { + + beforeEach(() => { + TestBed.configureTestingModule({imports: [NgbModalTestModule]}); + fixture = TestBed.createComponent(TestComponent); + }); + + describe('basic functionality', () => { + + it('should open and close modal with default options', () => { + const modalInstance = fixture.componentInstance.open('foo'); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('foo'); + + modalInstance.close('some result'); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should open and close modal from a TemplateRef content', () => { + const modalInstance = fixture.componentInstance.openTpl(); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('Hello, World!'); + + modalInstance.close('some result'); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should properly destroy TemplateRef content', () => { + const spyService = fixture.debugElement.injector.get(SpyService); + const modalInstance = fixture.componentInstance.openDestroyableTpl(); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('Some content'); + expect(spyService.called).toBeFalsy(); + + modalInstance.close('some result'); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + expect(spyService.called).toBeTruthy(); + }); + + it('should open and close modal from a component type', () => { + const spyService = fixture.debugElement.injector.get(SpyService); + const modalInstance = fixture.componentInstance.openCmpt(DestroyableCmpt); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('Some content'); + expect(spyService.called).toBeFalsy(); + + modalInstance.close('some result'); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + expect(spyService.called).toBeTruthy(); + }); + + it('should inject active modal ref when component is used as content', () => { + fixture.componentInstance.openCmpt(WithActiveModalCmpt); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('Close'); + + (document.querySelector('button.closeFromInside')).click(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should expose component used as modal content', () => { + const modalInstance = fixture.componentInstance.openCmpt(WithActiveModalCmpt); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('Close'); + expect(modalInstance.componentInstance instanceof WithActiveModalCmpt).toBeTruthy(); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should open and close modal from inside', () => { + fixture.componentInstance.openTplClose(); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + (document.querySelector('button#close')).click(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should open and dismiss modal from inside', () => { + fixture.componentInstance.openTplDismiss().result.catch(NOOP); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + (document.querySelector('button#dismiss')).click(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should open and close modal from template implicit context', () => { + fixture.componentInstance.openTplImplicitContext(); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + (document.querySelector('button#close')).click(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should open and dismiss modal from template implicit context', () => { + fixture.componentInstance.openTplImplicitContext().result.catch(NOOP); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + (document.querySelector('button#dismiss')).click(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should resolve result promise on close', () => { + let resolvedResult; + fixture.componentInstance.openTplClose().result.then((result) => resolvedResult = result); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + (document.querySelector('button#close')).click(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + + fixture.whenStable().then(() => { expect(resolvedResult).toBe('myResult'); }); + }); + + it('should reject result promise on dismiss', () => { + let rejectReason; + fixture.componentInstance.openTplDismiss().result.catch((reason) => rejectReason = reason); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + (document.querySelector('button#dismiss')).click(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + + fixture.whenStable().then(() => { expect(rejectReason).toBe('myReason'); }); + }); + + it('should add / remove "modal-open" class to body when modal is open', async(() => { + const modalRef = fixture.componentInstance.open('bar'); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + expect(document.body).toHaveCssClass('modal-open'); + + modalRef.close('bar result'); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(fixture.nativeElement).not.toHaveModal(); + expect(document.body).not.toHaveCssClass('modal-open'); + }); + })); + + it('should not throw when close called multiple times', () => { + const modalInstance = fixture.componentInstance.open('foo'); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('foo'); + + modalInstance.close('some result'); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + + modalInstance.close('some result'); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should dismiss with dismissAll', () => { + fixture.componentInstance.open('foo'); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('foo'); + + fixture.componentInstance.dismissAll('dismissAllArg'); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should not throw when dismissAll called with no active modal', () => { + expect(fixture.nativeElement).not.toHaveModal(); + + fixture.componentInstance.dismissAll(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should not throw when dismiss called multiple times', () => { + const modalRef = fixture.componentInstance.open('foo'); + modalRef.result.catch(NOOP); + + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('foo'); + + modalRef.dismiss('some reason'); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + + modalRef.dismiss('some reason'); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should indicate if there are open modal windows', async(() => { + fixture.componentInstance.open('foo'); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('foo'); + expect(fixture.componentInstance.modalService.hasOpenModals()).toBeTruthy(); + + fixture.componentInstance.dismissAll(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + fixture.whenStable().then( + () => { expect(fixture.componentInstance.modalService.hasOpenModals()).toBeFalsy(); }); + })); + }); + + describe('stacked modals', () => { + + it('should not remove "modal-open" class on body when closed modal is not last', async(() => { + const modalRef1 = fixture.componentInstance.open('foo'); + const modalRef2 = fixture.componentInstance.open('bar'); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + expect(document.body).toHaveCssClass('modal-open'); + + modalRef1.close('foo result'); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(fixture.nativeElement).toHaveModal(); + expect(document.body).toHaveCssClass('modal-open'); + + modalRef2.close('bar result'); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(fixture.nativeElement).not.toHaveModal(); + expect(document.body).not.toHaveCssClass('modal-open'); + }); + }); + })); + + it('should dismiss modals on ESC in correct order', () => { + fixture.componentInstance.open('foo').result.catch(NOOP); + fixture.componentInstance.open('bar').result.catch(NOOP); + const ngbModalWindow1 = document.querySelectorAll('ngb-modal-window')[0]; + const ngbModalWindow2 = document.querySelectorAll('ngb-modal-window')[1]; + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(['foo', 'bar']); + expect(document.activeElement).toBe(ngbModalWindow2); + + (getDebugNode(document.activeElement)).triggerEventHandler('keyup.esc', {}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(['foo']); + expect(document.activeElement).toBe(ngbModalWindow1); + + (getDebugNode(document.activeElement)).triggerEventHandler('keyup.esc', {}); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + expect(document.activeElement).toBe(document.body); + }); + }); + + describe('backdrop options', () => { + + it('should have backdrop by default', () => { + const modalInstance = fixture.componentInstance.open('foo'); + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveModal('foo'); + expect(fixture.nativeElement).toHaveBackdrop(); + + modalInstance.close('some reason'); + fixture.detectChanges(); + + expect(fixture.nativeElement).not.toHaveModal(); + expect(fixture.nativeElement).not.toHaveBackdrop(); + }); + + it('should open and close modal without backdrop', () => { + const modalInstance = fixture.componentInstance.open('foo', {backdrop: false}); + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveModal('foo'); + expect(fixture.nativeElement).not.toHaveBackdrop(); + + modalInstance.close('some reason'); + fixture.detectChanges(); + + expect(fixture.nativeElement).not.toHaveModal(); + expect(fixture.nativeElement).not.toHaveBackdrop(); + }); + + it('should open and close modal without backdrop from template content', () => { + const modalInstance = fixture.componentInstance.openTpl({backdrop: false}); + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveModal('Hello, World!'); + expect(fixture.nativeElement).not.toHaveBackdrop(); + + modalInstance.close('some reason'); + fixture.detectChanges(); + + expect(fixture.nativeElement).not.toHaveModal(); + expect(fixture.nativeElement).not.toHaveBackdrop(); + }); + + it('should dismiss on backdrop click', () => { + fixture.componentInstance.open('foo').result.catch(NOOP); + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveModal('foo'); + expect(fixture.nativeElement).toHaveBackdrop(); + + (document.querySelector('ngb-modal-window')).click(); + fixture.detectChanges(); + + expect(fixture.nativeElement).not.toHaveModal(); + expect(fixture.nativeElement).not.toHaveBackdrop(); + }); + + it('should not dismiss on "static" backdrop click', () => { + const modalInstance = fixture.componentInstance.open('foo', {backdrop: 'static'}); + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveModal('foo'); + expect(fixture.nativeElement).toHaveBackdrop(); + + (document.querySelector('ngb-modal-window')).click(); + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveModal(); + expect(fixture.nativeElement).toHaveBackdrop(); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should not dismiss on clicks outside content where there is no backdrop', () => { + const modalInstance = fixture.componentInstance.open('foo', {backdrop: false}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('foo'); + + (document.querySelector('ngb-modal-window')).click(); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should not dismiss on clicks that result in detached elements', () => { + const modalInstance = fixture.componentInstance.openTplIf({}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + (document.querySelector('button#if')).click(); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + }); + + describe('beforeDismiss options', () => { + + it('should not dismiss when the callback returns false', () => { + const modalInstance = fixture.componentInstance.openTplDismiss({beforeDismiss: () => { return false; }}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + (document.querySelector('button#dismiss')).click(); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should dismiss when the callback does not return false', () => { + fixture.componentInstance.openTplDismiss({beforeDismiss: () => {}}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + (document.querySelector('button#dismiss')).click(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should not dismiss when the returned promise is resolved with false', async(() => { + const modalInstance = + fixture.componentInstance.openTplDismiss({beforeDismiss: () => Promise.resolve(false)}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + (document.querySelector('button#dismiss')).click(); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(fixture.nativeElement).toHaveModal(); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + })); + + it('should not dismiss when the returned promise is rejected', async(() => { + const modalInstance = + fixture.componentInstance.openTplDismiss({beforeDismiss: () => Promise.reject('error')}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + (document.querySelector('button#dismiss')).click(); + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(fixture.nativeElement).toHaveModal(); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + })); + + it('should dismiss when the returned promise is not resolved with false', async(() => { + fixture.componentInstance.openTplDismiss({beforeDismiss: () => Promise.resolve()}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + (document.querySelector('button#dismiss')).click(); + fixture.detectChanges(); + fixture.whenStable().then(() => { expect(fixture.nativeElement).not.toHaveModal(); }); + })); + + it('should dismiss when the callback is not defined', () => { + fixture.componentInstance.openTplDismiss({}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + (document.querySelector('button#dismiss')).click(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + }); + + describe('container options', () => { + + it('should attach window and backdrop elements to the specified container', () => { + const modalInstance = fixture.componentInstance.open('foo', {container: '#testContainer'}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('foo', '#testContainer'); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should throw when the specified container element doesn\'t exist', () => { + const brokenSelector = '#notInTheDOM'; + expect(() => { + fixture.componentInstance.open('foo', {container: brokenSelector}); + }).toThrowError(`The specified modal container "${brokenSelector}" was not found in the DOM.`); + }); + }); + + describe('keyboard options', () => { + + it('should dismiss modals on ESC by default', () => { + fixture.componentInstance.open('foo').result.catch(NOOP); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('foo'); + + (getDebugNode(document.querySelector('ngb-modal-window'))).triggerEventHandler('keyup.esc', {}); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should not dismiss modals on ESC when keyboard option is false', () => { + const modalInstance = fixture.componentInstance.open('foo', {keyboard: false}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('foo'); + + (getDebugNode(document.querySelector('ngb-modal-window'))).triggerEventHandler('keyup.esc', {}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should not dismiss modals on ESC when default is prevented', () => { + const modalInstance = fixture.componentInstance.open('foo', {keyboard: true}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('foo'); + + (getDebugNode(document.querySelector('ngb-modal-window'))).triggerEventHandler('keyup.esc', { + defaultPrevented: true + }); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal(); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + }); + + describe('size options', () => { + + it('should render modals with specified size', () => { + const modalInstance = fixture.componentInstance.open('foo', {size: 'sm'}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('foo'); + expect(document.querySelector('.modal-dialog')).toHaveCssClass('modal-sm'); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + }); + + describe('window custom class options', () => { + + it('should render modals with the correct window custom classes', () => { + const modalInstance = fixture.componentInstance.open('foo', {windowClass: 'bar'}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('foo'); + expect(document.querySelector('ngb-modal-window')).toHaveCssClass('bar'); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + }); + + describe('backdrop custom class options', () => { + + it('should render modals with the correct backdrop custom classes', () => { + const modalInstance = fixture.componentInstance.open('foo', {backdropClass: 'my-fancy-backdrop'}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('foo'); + expect(document.querySelector('ngb-modal-backdrop')).toHaveCssClass('my-fancy-backdrop'); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + }); + + describe('custom injector option', () => { + + it('should render modal with a custom injector', () => { + const customInjector = + Injector.create({providers: [{provide: CustomSpyService, useClass: CustomSpyService, deps: []}]}); + const modalInstance = fixture.componentInstance.openCmpt(CustomInjectorCmpt, {injector: customInjector}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('Some content'); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + }); + + describe('focus management', () => { + + it('should return focus to previously focused element', () => { + fixture.detectChanges(); + const openButtonEl = fixture.nativeElement.querySelector('button#open'); + openButtonEl.focus(); + openButtonEl.click(); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('from button'); + + fixture.componentInstance.close(); + expect(fixture.nativeElement).not.toHaveModal(); + expect(document.activeElement).toBe(openButtonEl); + }); + + + it('should return focus to body if no element focused prior to modal opening', () => { + const modalInstance = fixture.componentInstance.open('foo'); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('foo'); + expect(document.activeElement).toBe(document.querySelector('ngb-modal-window')); + + modalInstance.close('ok!'); + expect(document.activeElement).toBe(document.body); + }); + + it('should return focus to body if the opening element is not stored as previously focused element', () => { + fixture.detectChanges(); + const openElement = fixture.nativeElement.querySelector('#open-no-focus'); + + openElement.click(); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('from non focusable element'); + expect(document.activeElement).toBe(document.querySelector('ngb-modal-window')); + + fixture.componentInstance.close(); + expect(fixture.nativeElement).not.toHaveModal(); + expect(document.activeElement).toBe(document.body); + }); + + it('should return focus to body if the opening element is stored but cannot be focused', () => { + fixture.detectChanges(); + const openElement = fixture.nativeElement.querySelector('#open-no-focus-ie'); + + openElement.click(); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('from non focusable element but stored as activeElement on IE'); + expect(document.activeElement).toBe(document.querySelector('ngb-modal-window')); + + fixture.componentInstance.close(); + expect(fixture.nativeElement).not.toHaveModal(); + expect(document.activeElement).toBe(document.body); + }); + + describe('initial focus', () => { + it('should focus the proper specified element when [ngbAutofocus] is used', () => { + fixture.detectChanges(); + const modal = fixture.componentInstance.openCmpt(WithAutofocusModalCmpt); + fixture.detectChanges(); + + expect(document.activeElement).toBe(document.querySelector('button.withNgbAutofocus')); + modal.close(); + }); + + it('should focus the first focusable element when [ngbAutofocus] is not used', () => { + fixture.detectChanges(); + const modal = fixture.componentInstance.openCmpt(WithFirstFocusableModalCmpt); + fixture.detectChanges(); + + expect(document.activeElement).toBe(document.querySelector('button.firstFocusable')); + modal.close(); + fixture.detectChanges(); + }); + + it('should skip element with tabindex=-1 when finding the first focusable element', () => { + fixture.detectChanges(); + const modal = fixture.componentInstance.openCmpt(WithSkipTabindexFirstFocusableModalCmpt); + fixture.detectChanges(); + + expect(document.activeElement).toBe(document.querySelector('button.other')); + modal.close(); + fixture.detectChanges(); + }); + + it('should focus modal window as a default fallback option', () => { + fixture.detectChanges(); + const modal = fixture.componentInstance.open('content'); + fixture.detectChanges(); + + expect(document.activeElement).toBe(document.querySelector('ngb-modal-window')); + modal.close(); + fixture.detectChanges(); + }); + }); + }); + + describe('window element ordering', () => { + it('should place newer windows on top of older ones', () => { + const modalInstance1 = fixture.componentInstance.open('foo', {windowClass: 'window-1'}); + fixture.detectChanges(); + + const modalInstance2 = fixture.componentInstance.open('bar', {windowClass: 'window-2'}); + fixture.detectChanges(); + + let windows = document.querySelectorAll('ngb-modal-window'); + expect(windows.length).toBe(2); + expect(windows[0]).toHaveCssClass('window-1'); + expect(windows[1]).toHaveCssClass('window-2'); + + modalInstance2.close(); + modalInstance1.close(); + fixture.detectChanges(); + }); + }); + + describe('vertically centered', () => { + + it('should render modals vertically centered', () => { + const modalInstance = fixture.componentInstance.open('foo', {centered: true}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('foo'); + expect(document.querySelector('.modal-dialog')).toHaveCssClass('modal-dialog-centered'); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + }); + + describe('scrollable content', () => { + + it('should render scrollable content modals', () => { + const modalInstance = fixture.componentInstance.open('foo', {scrollable: true}); + fixture.detectChanges(); + expect(fixture.nativeElement).toHaveModal('foo'); + expect(document.querySelector('.modal-dialog')).toHaveCssClass('modal-dialog-scrollable'); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should add specific styling to content component host', () => { + const modalInstance = fixture.componentInstance.openCmpt(DestroyableCmpt, {scrollable: true}); + fixture.detectChanges(); + expect(document.querySelector('destroyable-cmpt')).toHaveCssClass('component-host-scrollable'); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + }); + + describe('accessibility', () => { + + it('should support aria-labelledby', () => { + const id = 'aria-labelledby-id'; + + const modalInstance = fixture.componentInstance.open('foo', {ariaLabelledBy: id}); + fixture.detectChanges(); + + const modalElement = document.querySelector('ngb-modal-window'); + expect(modalElement.getAttribute('aria-labelledby')).toBe(id); + + modalInstance.close('some result'); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should have aria-modal attribute', () => { + const a11yFixture = TestBed.createComponent(TestA11yComponent); + const modalInstance = a11yFixture.componentInstance.open(); + a11yFixture.detectChanges(); + + const modalElement = document.querySelector('ngb-modal-window'); + expect(modalElement.getAttribute('aria-modal')).toBe('true'); + + modalInstance.close(); + fixture.detectChanges(); + expect(fixture.nativeElement).not.toHaveModal(); + }); + + it('should add aria-hidden attributes to siblings when attached to body', async(async() => { + const a11yFixture = TestBed.createComponent(TestA11yComponent); + const modalInstance = a11yFixture.componentInstance.open(); + a11yFixture.detectChanges(); + + const modal = document.querySelector('ngb-modal-window'); + const backdrop = document.querySelector('ngb-modal-backdrop'); + const application = document.querySelector('div[ng-version]'); + let ariaHidden = document.querySelectorAll('[aria-hidden]'); + + expect(ariaHidden.length).toBeGreaterThan(2); // 2 exist in the DOM initially + expect(document.body.hasAttribute('aria-hidden')).toBe(false); + expect(application.getAttribute('aria-hidden')).toBe('true'); + expect(backdrop.getAttribute('aria-hidden')).toBe('true'); + expect(modal.hasAttribute('aria-hidden')).toBe(false); + + modalInstance.close(); + fixture.detectChanges(); + await a11yFixture.whenStable(); + + ariaHidden = document.querySelectorAll('[aria-hidden]'); + + expect(ariaHidden.length).toBe(2); // 2 exist in the DOM initially + expect(a11yFixture.nativeElement).not.toHaveModal(); + })); + + it('should add aria-hidden attributes to siblings when attached to a container', async(async() => { + const a11yFixture = TestBed.createComponent(TestA11yComponent); + const modalInstance = a11yFixture.componentInstance.open({container: '#container'}); + a11yFixture.detectChanges(); + + const modal = document.querySelector('ngb-modal-window'); + const backdrop = document.querySelector('ngb-modal-backdrop'); + const application = document.querySelector('div[ng-version]'); + const ariaRestoreTrue = document.querySelector('.to-restore-true'); + const ariaRestoreFalse = document.querySelector('.to-restore-false'); + + expect(document.body.hasAttribute('aria-hidden')).toBe(false); + expect(application.hasAttribute('aria-hidden')).toBe(false); + expect(modal.hasAttribute('aria-hidden')).toBe(false); + expect(backdrop.getAttribute('aria-hidden')).toBe('true'); + expect(ariaRestoreTrue.getAttribute('aria-hidden')).toBe('true'); + expect(ariaRestoreFalse.getAttribute('aria-hidden')).toBe('true'); + + Array.from(document.querySelectorAll('.to-hide')).forEach(element => { + expect(element.getAttribute('aria-hidden')).toBe('true'); + }); + + Array.from(document.querySelectorAll('.not-to-hide')).forEach(element => { + expect(element.hasAttribute('aria-hidden')).toBe(false); + }); + + modalInstance.close(); + fixture.detectChanges(); + await a11yFixture.whenStable(); + + const ariaHidden = document.querySelectorAll('[aria-hidden]'); + + expect(ariaHidden.length).toBe(2); // 2 exist in the DOM initially + expect(ariaRestoreTrue.getAttribute('aria-hidden')).toBe('true'); + expect(ariaRestoreFalse.getAttribute('aria-hidden')).toBe('false'); + expect(a11yFixture.nativeElement).not.toHaveModal(); + })); + + it('should add aria-hidden attributes with modal stacks', async(async() => { + const a11yFixture = TestBed.createComponent(TestA11yComponent); + const firstModalInstance = a11yFixture.componentInstance.open(); + const secondModalInstance = a11yFixture.componentInstance.open(); + a11yFixture.detectChanges(); + + let modals = document.querySelectorAll('ngb-modal-window'); + let backdrops = document.querySelectorAll('ngb-modal-backdrop'); + let ariaHidden = document.querySelectorAll('[aria-hidden]'); + + const hiddenElements = ariaHidden.length; + expect(hiddenElements).toBeGreaterThan(2); // 2 exist in the DOM initially + + expect(modals.length).toBe(2); + expect(backdrops.length).toBe(2); + + expect(modals[0].hasAttribute('aria-hidden')).toBe(true); + expect(backdrops[0].hasAttribute('aria-hidden')).toBe(true); + + expect(modals[1].hasAttribute('aria-hidden')).toBe(false); + expect(backdrops[1].hasAttribute('aria-hidden')).toBe(true); + + secondModalInstance.close(); + fixture.detectChanges(); + await a11yFixture.whenStable(); + + ariaHidden = document.querySelectorAll('[aria-hidden]'); + expect(document.querySelectorAll('ngb-modal-window').length).toBe(1); + expect(document.querySelectorAll('ngb-modal-backdrop').length).toBe(1); + + expect(ariaHidden.length).toBe(hiddenElements - 2); + + expect(modals[0].hasAttribute('aria-hidden')).toBe(false); + expect(backdrops[0].hasAttribute('aria-hidden')).toBe(true); + + firstModalInstance.close(); + fixture.detectChanges(); + await a11yFixture.whenStable(); + + ariaHidden = document.querySelectorAll('[aria-hidden]'); + + expect(ariaHidden.length).toBe(2); // 2 exist in the DOM initially + expect(a11yFixture.nativeElement).not.toHaveModal(); + })); + }); + + }); + + describe('custom global configuration', () => { + + beforeEach(() => { + TestBed.configureTestingModule( + {imports: [NgbModalTestModule], providers: [{provide: NgbModalConfig, useValue: {size: 'sm'}}]}); + fixture = TestBed.createComponent(TestComponent); + }); + + it('should accept global configuration under the NgbModalConfig token', () => { + const modalInstance = fixture.componentInstance.open('foo'); + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveModal('foo'); + expect(document.querySelector('.modal-dialog')).toHaveCssClass('modal-sm'); + + modalInstance.close('some reason'); + fixture.detectChanges(); + }); + + it('should override global configuration with local options', () => { + const modalInstance = fixture.componentInstance.open('foo', {size: 'lg'}); + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveModal('foo'); + expect(document.querySelector('.modal-dialog')).toHaveCssClass('modal-lg'); + expect(document.querySelector('.modal-dialog')).not.toHaveCssClass('modal-sm'); + + modalInstance.close('some reason'); + fixture.detectChanges(); + }); + }); +}); + + + +@Component({selector: 'custom-injector-cmpt', template: 'Some content'}) +export class CustomInjectorCmpt implements OnDestroy { + constructor(private _spyService: CustomSpyService) {} + + ngOnDestroy(): void { this._spyService.called = true; } +} + +@Component({selector: 'destroyable-cmpt', template: 'Some content'}) +export class DestroyableCmpt implements OnDestroy { + constructor(private _spyService: SpyService) {} + + ngOnDestroy(): void { this._spyService.called = true; } +} + +@Component( + {selector: 'modal-content-cmpt', template: ''}) +export class WithActiveModalCmpt { + constructor(public activeModal: NgbActiveModal) {} + + close() { this.activeModal.close('from inside'); } +} + +@Component( + {selector: 'modal-autofocus-cmpt', template: ``}) +export class WithAutofocusModalCmpt { +} + +@Component({ + selector: 'modal-firstfocusable-cmpt', + template: ` + + +` +}) +export class WithFirstFocusableModalCmpt { +} + +@Component({ + selector: 'modal-skip-tabindex-firstfocusable-cmpt', + template: ` + + +` +}) +export class WithSkipTabindexFirstFocusableModalCmpt { +} + +@Component({ + selector: 'test-cmpt', + template: ` +
+ Hello, {{name}}! + + + + + + + + + + + + + + + + + +
Open
+
Open
+ ` +}) +class TestComponent { + name = 'World'; + openedModal: NgbModalRef; + show = true; + @ViewChild('content', {static: true}) tplContent; + @ViewChild('destroyableContent', {static: true}) tplDestroyableContent; + @ViewChild('contentWithClose', {static: true}) tplContentWithClose; + @ViewChild('contentWithDismiss', {static: true}) tplContentWithDismiss; + @ViewChild('contentWithImplicitContext', {static: true}) tplContentWithImplicitContext; + @ViewChild('contentWithIf', {static: true}) tplContentWithIf; + + constructor(public modalService: NgbModal) {} + + open(content: string, options?: Object) { + this.openedModal = this.modalService.open(content, options); + return this.openedModal; + } + close() { + if (this.openedModal) { + this.openedModal.close('ok'); + } + } + dismissAll(reason?: any) { this.modalService.dismissAll(reason); } + openTpl(options?: Object) { return this.modalService.open(this.tplContent, options); } + openCmpt(cmptType: any, options?: Object) { return this.modalService.open(cmptType, options); } + openDestroyableTpl(options?: Object) { return this.modalService.open(this.tplDestroyableContent, options); } + openTplClose(options?: Object) { return this.modalService.open(this.tplContentWithClose, options); } + openTplDismiss(options?: Object) { return this.modalService.open(this.tplContentWithDismiss, options); } + openTplImplicitContext(options?: Object) { + return this.modalService.open(this.tplContentWithImplicitContext, options); + } + openTplIf(options?: Object) { return this.modalService.open(this.tplContentWithIf, options); } +} + +@Component({ + selector: 'test-a11y-cmpt', + template: ` + +
+
+
+
+ +
+ +
+
+
+
+
+
+
+ ` +}) +class TestA11yComponent { + constructor(private modalService: NgbModal) {} + + open(options?: any) { return this.modalService.open('foo', options); } +} + +@NgModule({ + declarations: [ + TestComponent, CustomInjectorCmpt, DestroyableCmpt, WithActiveModalCmpt, WithAutofocusModalCmpt, + WithFirstFocusableModalCmpt, WithSkipTabindexFirstFocusableModalCmpt, TestA11yComponent + ], + exports: [TestComponent, DestroyableCmpt], + imports: [CommonModule, NgbModalModule], + entryComponents: [ + CustomInjectorCmpt, DestroyableCmpt, WithActiveModalCmpt, WithAutofocusModalCmpt, WithFirstFocusableModalCmpt, + WithSkipTabindexFirstFocusableModalCmpt + ], + providers: [SpyService] +}) +class NgbModalTestModule { +} diff --git a/src/modal/modal.ts b/src/modal/modal.ts new file mode 100644 index 0000000..38e832d --- /dev/null +++ b/src/modal/modal.ts @@ -0,0 +1,46 @@ +import {Injectable, Injector, ComponentFactoryResolver} from '@angular/core'; + +import {NgbModalOptions, NgbModalConfig} from './modal-config'; +import {NgbModalRef} from './modal-ref'; +import {NgbModalStack} from './modal-stack'; + +/** + * A service for opening modal windows. + * + * Creating a modal is straightforward: create a component or a template and pass it as an argument to + * the `.open()` method. + */ +@Injectable({providedIn: 'root'}) +export class NgbModal { + constructor( + private _moduleCFR: ComponentFactoryResolver, private _injector: Injector, private _modalStack: NgbModalStack, + private _config: NgbModalConfig) {} + + /** + * Opens a new modal window with the specified content and supplied options. + * + * Content can be provided as a `TemplateRef` or a component type. If you pass a component type as content, + * then instances of those components can be injected with an instance of the `NgbActiveModal` class. You can then + * use `NgbActiveModal` methods to close / dismiss modals from "inside" of your component. + * + * Also see the [`NgbModalOptions`](#/components/modal/api#NgbModalOptions) for the list of supported options. + */ + open(content: any, options: NgbModalOptions = {}): NgbModalRef { + const combinedOptions = Object.assign({}, this._config, options); + return this._modalStack.open(this._moduleCFR, this._injector, content, combinedOptions); + } + + /** + * Dismisses all currently displayed modal windows with the supplied reason. + * + * @since 3.1.0 + */ + dismissAll(reason?: any) { this._modalStack.dismissAll(reason); } + + /** + * Indicates if there are currently any open modal windows in the application. + * + * @since 3.3.0 + */ + hasOpenModals(): boolean { return this._modalStack.hasOpenModals(); } +} diff --git a/src/ng-package.json b/src/ng-package.json new file mode 100644 index 0000000..2e19181 --- /dev/null +++ b/src/ng-package.json @@ -0,0 +1,11 @@ +{ + "$schema": "../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../dist/ng-bootstrap", + "deleteDestPath": false, + "lib": { + "flatModuleFile": "ng-bootstrap", + "entryFile": "./index.ts", + "umdId": "ngb", + "amdId": "ngb" + } +} diff --git a/src/ng-package.prod.json b/src/ng-package.prod.json new file mode 100644 index 0000000..d56b502 --- /dev/null +++ b/src/ng-package.prod.json @@ -0,0 +1,10 @@ +{ + "$schema": "../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../dist/ng-bootstrap", + "lib": { + "flatModuleFile": "ng-bootstrap", + "entryFile": "./index.ts", + "umdId": "ngb", + "amdId": "ngb" + } +} diff --git a/src/package.json b/src/package.json new file mode 100644 index 0000000..44324e7 --- /dev/null +++ b/src/package.json @@ -0,0 +1,43 @@ +{ + "name": "@ng-bootstrap/ng-bootstrap", + "version": "5.1.0", + "description": "Angular powered Bootstrap", + "keywords": [ + "angular", + "bootstrap", + "components", + "accordion", + "alert", + "buttons", + "carousel", + "collapse", + "datepicker", + "dropdown", + "modal", + "pagination", + "popover", + "progressbar", + "rating", + "table", + "tabset", + "timepicker", + "tooltip", + "typeahead" + ], + "author": "https://github.com/ng-bootstrap/ng-bootstrap/graphs/contributors", + "repository": { + "type": "git", + "url": "git+https://github.com/ng-bootstrap/ng-bootstrap.git" + }, + "license": "MIT", + "bugs": { + "url": "https://github.com/ng-bootstrap/ng-bootstrap/issues" + }, + "homepage": "https://github.com/ng-bootstrap/ng-bootstrap#readme", + "peerDependencies": { + "@angular/common": "^8.0.0", + "@angular/core": "^8.0.0", + "@angular/forms": "^8.0.0", + "rxjs": "^6.4.0" + } +} diff --git a/src/pagination/pagination-config.spec.ts b/src/pagination/pagination-config.spec.ts new file mode 100644 index 0000000..1673e01 --- /dev/null +++ b/src/pagination/pagination-config.spec.ts @@ -0,0 +1,16 @@ +import {NgbPaginationConfig} from './pagination-config'; + +describe('ngb-pagination-config', () => { + it('should have sensible default values', () => { + const config = new NgbPaginationConfig(); + + expect(config.disabled).toBe(false); + expect(config.boundaryLinks).toBe(false); + expect(config.directionLinks).toBe(true); + expect(config.ellipses).toBe(true); + expect(config.maxSize).toBe(0); + expect(config.pageSize).toBe(10); + expect(config.rotate).toBe(false); + expect(config.size).toBeUndefined(); + }); +}); diff --git a/src/pagination/pagination-config.ts b/src/pagination/pagination-config.ts new file mode 100644 index 0000000..8ea3707 --- /dev/null +++ b/src/pagination/pagination-config.ts @@ -0,0 +1,19 @@ +import {Injectable} from '@angular/core'; + +/** + * A configuration service for the [`NgbPagination`](#/components/pagination/api#NgbPagination) component. + * + * You can inject this service, typically in your root component, and customize the values of its properties in + * order to provide default values for all the paginations used in the application. + */ +@Injectable({providedIn: 'root'}) +export class NgbPaginationConfig { + disabled = false; + boundaryLinks = false; + directionLinks = true; + ellipses = true; + maxSize = 0; + pageSize = 10; + rotate = false; + size: 'sm' | 'lg'; +} diff --git a/src/pagination/pagination.module.ts b/src/pagination/pagination.module.ts new file mode 100644 index 0000000..200db5c --- /dev/null +++ b/src/pagination/pagination.module.ts @@ -0,0 +1,32 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; + +import { + NgbPagination, + NgbPaginationEllipsis, + NgbPaginationFirst, + NgbPaginationLast, + NgbPaginationNext, + NgbPaginationNumber, + NgbPaginationPrevious +} from './pagination'; + +export { + NgbPagination, + NgbPaginationEllipsis, + NgbPaginationFirst, + NgbPaginationLast, + NgbPaginationNext, + NgbPaginationNumber, + NgbPaginationPrevious +} from './pagination'; +export {NgbPaginationConfig} from './pagination-config'; + +const DIRECTIVES = [ + NgbPagination, NgbPaginationEllipsis, NgbPaginationFirst, NgbPaginationLast, NgbPaginationNext, NgbPaginationNumber, + NgbPaginationPrevious +]; + +@NgModule({declarations: DIRECTIVES, exports: DIRECTIVES, imports: [CommonModule]}) +export class NgbPaginationModule { +} diff --git a/src/pagination/pagination.spec.ts b/src/pagination/pagination.spec.ts new file mode 100644 index 0000000..576262d --- /dev/null +++ b/src/pagination/pagination.spec.ts @@ -0,0 +1,708 @@ +import {TestBed, ComponentFixture, inject, fakeAsync, tick} from '@angular/core/testing'; +import {createGenericTestComponent} from '../test/common'; + +import {Component} from '@angular/core'; + +import {NgbPaginationModule} from './pagination.module'; +import {NgbPagination} from './pagination'; +import {NgbPaginationConfig} from './pagination-config'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +function expectPages(nativeEl: HTMLElement, pagesDef: string[], ellipsis = '...'): void { + const pages = nativeEl.querySelectorAll('li'); + + expect(pages.length).toEqual(pagesDef.length); + + for (let i = 0; i < pagesDef.length; i++) { + let pageDef = pagesDef[i]; + let classIndicator = pageDef.charAt(0); + + if (classIndicator === '+') { + expect(pages[i]).toHaveCssClass('active'); + expect(pages[i]).not.toHaveCssClass('disabled'); + expect(normalizeText(pages[i].textContent)).toEqual(pageDef.substr(1) + ' (current)'); + } else if (classIndicator === '-') { + expect(pages[i]).not.toHaveCssClass('active'); + expect(pages[i]).toHaveCssClass('disabled'); + expect(normalizeText(pages[i].textContent)).toEqual(pageDef.substr(1)); + if (normalizeText(pages[i].textContent) !== ellipsis) { + expect(pages[i].querySelector('a').getAttribute('tabindex')).toEqual('-1'); + } + } else { + expect(pages[i]).not.toHaveCssClass('active'); + expect(pages[i]).not.toHaveCssClass('disabled'); + expect(normalizeText(pages[i].textContent)).toEqual(pageDef); + if (normalizeText(pages[i].textContent) !== ellipsis) { + expect(pages[i].querySelector('a').hasAttribute('tabindex')).toBeFalsy(); + } + } + } +} + +function getLink(nativeEl: HTMLElement, idx: number): HTMLAnchorElement { + return nativeEl.querySelectorAll('li')[idx].querySelector('a'); +} + +function getList(nativeEl: HTMLElement) { + return nativeEl.querySelector('ul'); +} + +function getSpan(nativeEl: HTMLElement): HTMLSpanElement { + return nativeEl.querySelector('span'); +} + +function normalizeText(txt: string): string { + return txt.trim().replace(/\s+/g, ' '); +} + +function expectSameValues(pagination: NgbPagination, config: NgbPaginationConfig) { + expect(pagination.disabled).toBe(config.disabled); + expect(pagination.boundaryLinks).toBe(config.boundaryLinks); + expect(pagination.directionLinks).toBe(config.directionLinks); + expect(pagination.ellipses).toBe(config.ellipses); + expect(pagination.maxSize).toBe(config.maxSize); + expect(pagination.pageSize).toBe(config.pageSize); + expect(pagination.rotate).toBe(config.rotate); + expect(pagination.size).toBe(config.size); +} + +describe('ngb-pagination', () => { + describe('business logic', () => { + + let pagination: NgbPagination; + + beforeEach(() => { pagination = new NgbPagination(new NgbPaginationConfig()); }); + + it('should initialize inputs with default values', () => { + const defaultConfig = new NgbPaginationConfig(); + expectSameValues(pagination, defaultConfig); + }); + + it('should calculate and update no of pages (default page size)', () => { + pagination.collectionSize = 100; + pagination.ngOnChanges(null); + expect(pagination.pages.length).toEqual(10); + + pagination.collectionSize = 200; + pagination.ngOnChanges(null); + expect(pagination.pages.length).toEqual(20); + }); + + it('should calculate and update no of pages (custom page size)', () => { + pagination.collectionSize = 100; + pagination.pageSize = 20; + pagination.ngOnChanges(null); + expect(pagination.pages.length).toEqual(5); + + pagination.collectionSize = 200; + pagination.ngOnChanges(null); + expect(pagination.pages.length).toEqual(10); + + pagination.pageSize = 10; + pagination.ngOnChanges(null); + expect(pagination.pages.length).toEqual(20); + }); + + it('should allow setting a page within a valid range (default page size)', () => { + pagination.collectionSize = 100; + pagination.page = 2; + pagination.ngOnChanges(null); + expect(pagination.page).toEqual(2); + }); + + it('should auto-correct page no if outside of valid range (default page size)', () => { + pagination.collectionSize = 100; + pagination.page = 100; + pagination.ngOnChanges(null); + expect(pagination.page).toEqual(10); + + pagination.page = -100; + pagination.ngOnChanges(null); + expect(pagination.page).toEqual(1); + + pagination.page = 5; + pagination.collectionSize = 10; + pagination.ngOnChanges(null); + expect(pagination.page).toEqual(1); + }); + + it('should allow setting a page within a valid range (custom page size)', () => { + pagination.collectionSize = 100; + pagination.pageSize = 20; + pagination.page = 2; + pagination.ngOnChanges(null); + expect(pagination.page).toEqual(2); + }); + + }); + + describe('UI logic', () => { + + beforeEach( + () => { TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbPaginationModule]}); }); + + it('should render and respond to collectionSize change', () => { + const html = ''; + const fixture = createTestComponent(html); + + fixture.componentInstance.collectionSize = 30; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '»']); + + fixture.componentInstance.collectionSize = 40; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '4', '»']); + }); + + it('should render and respond to pageSize change', () => { + const html = + ''; + const fixture = createTestComponent(html); + + fixture.componentInstance.collectionSize = 30; + fixture.componentInstance.pageSize = 5; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '4', '5', '6', '»']); + + fixture.componentInstance.pageSize = 10; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '»']); + }); + + it('should render and respond to active page change', () => { + const html = ''; + const fixture = createTestComponent(html); + + fixture.componentInstance.collectionSize = 30; + fixture.componentInstance.page = 2; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '+2', '3', '»']); + + fixture.componentInstance.page = 3; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '2', '+3', '-»']); + }); + + it('should update selected page model on page no click', () => { + const html = ''; + const fixture = createTestComponent(html); + + fixture.componentInstance.collectionSize = 30; + fixture.componentInstance.page = 2; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '+2', '3', '»']); + + getLink(fixture.nativeElement, 0).click(); + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '»']); + + + getLink(fixture.nativeElement, 3).click(); + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '2', '+3', '-»']); + }); + + it('should update selected page model on prev / next click', () => { + const html = + ''; + const fixture = createTestComponent(html); + + fixture.componentInstance.collectionSize = 30; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['+1', '2', '3']); + + fixture.componentInstance.directionLinks = true; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '»']); + + getLink(fixture.nativeElement, 0).click(); + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '»']); + + getLink(fixture.nativeElement, 4).click(); + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '+2', '3', '»']); + + getLink(fixture.nativeElement, 4).click(); + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '2', '+3', '-»']); + + getLink(fixture.nativeElement, 4).click(); + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '2', '+3', '-»']); + }); + + it('should update selected page model on first / last click', () => { + const html = ``; + const fixture = createTestComponent(html); + + fixture.componentInstance.collectionSize = 30; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '»']); + + fixture.componentInstance.boundaryLinks = true; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-««', '-«', '+1', '2', '3', '»', '»»']); + + getLink(fixture.nativeElement, 0).click(); + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-««', '-«', '+1', '2', '3', '»', '»»']); + + getLink(fixture.nativeElement, 6).click(); + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['««', '«', '1', '2', '+3', '-»', '-»»']); + + getLink(fixture.nativeElement, 3).click(); + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['««', '«', '1', '+2', '3', '»', '»»']); + + getLink(fixture.nativeElement, 0).click(); + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-««', '-«', '+1', '2', '3', '»', '»»']); + + // maxSize < number of pages + fixture.componentInstance.collectionSize = 70; + fixture.componentInstance.maxSize = 3; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-««', '-«', '+1', '2', '3', '-...', '7', '»', '»»']); + + getLink(fixture.nativeElement, 8).click(); + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['««', '«', '1', '-...', '+7', '-»', '-»»']); + + getLink(fixture.nativeElement, 0).click(); + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-««', '-«', '+1', '2', '3', '-...', '7', '»', '»»']); + }); + + it('should update page when it becomes out of range', fakeAsync(() => { + const html = + ''; + const fixture = createTestComponent(html); + + fixture.componentInstance.collectionSize = 30; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '»']); + + getLink(fixture.nativeElement, 3).click(); + fixture.detectChanges(); + tick(); + expectPages(fixture.nativeElement, ['«', '1', '2', '+3', '-»']); + expect(fixture.componentInstance.page).toBe(3); + + fixture.componentInstance.collectionSize = 20; + fixture.detectChanges(); + tick(); + expectPages(fixture.nativeElement, ['«', '1', '+2', '-»']); + expect(fixture.componentInstance.page).toBe(2); + })); + + it('should render and respond to size change', () => { + const html = ''; + + const fixture = createTestComponent(html); + const listEl = getList(fixture.nativeElement); + + // default case + expectPages(fixture.nativeElement, ['-«', '+1', '2', '»']); + expect(listEl).toHaveCssClass('pagination'); + + // large size + fixture.componentInstance.size = 'lg'; + fixture.detectChanges(); + expect(listEl).toHaveCssClass('pagination'); + expect(listEl).toHaveCssClass('pagination-lg'); + + // removing large size + fixture.componentInstance.size = ''; + fixture.detectChanges(); + expect(listEl).toHaveCssClass('pagination'); + expect(listEl).not.toHaveCssClass('pagination-lg'); + + // arbitrary string + fixture.componentInstance.size = '123'; + fixture.detectChanges(); + expect(listEl).toHaveCssClass('pagination'); + expect(listEl).toHaveCssClass('pagination-123'); + }); + + it('should render and respond to maxSize change correctly', () => { + const html = + ''; + const fixture = createTestComponent(html); + + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '4', '5', '6', '7', '»']); + + // disabling ellipsis + fixture.componentInstance.ellipses = false; + + // limiting to 3 page numbers + fixture.componentInstance.maxSize = 3; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '»']); + + fixture.componentInstance.page = 3; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '2', '+3', '»']); + + fixture.componentInstance.page = 4; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '+4', '5', '6', '»']); + + fixture.componentInstance.page = 7; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '+7', '-»']); + + // checking that maxSize > total pages works + fixture.componentInstance.maxSize = 100; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '2', '3', '4', '5', '6', '+7', '-»']); + }); + + it('should render and rotate pages correctly', () => { + const html = ``; + const fixture = createTestComponent(html); + + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '4', '5', '6', '7', '»']); + + // disabling ellipsis + fixture.componentInstance.ellipses = false; + + // limiting to 3 (odd) page numbers + fixture.componentInstance.maxSize = 3; + fixture.componentInstance.rotate = true; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '»']); + + fixture.componentInstance.page = 2; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '+2', '3', '»']); + + fixture.componentInstance.page = 3; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '2', '+3', '4', '»']); + + fixture.componentInstance.page = 7; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '5', '6', '+7', '-»']); + + // limiting to 4 (even) page numbers + fixture.componentInstance.maxSize = 4; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '4', '5', '6', '+7', '-»']); + + fixture.componentInstance.page = 5; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '3', '4', '+5', '6', '»']); + + fixture.componentInstance.page = 3; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '2', '+3', '4', '»']); + }); + + it('should display ellipsis correctly', () => { + const html = ``; + const fixture = createTestComponent(html); + + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '4', '5', '6', '7', '»']); + + // limiting to 3 page numbers + fixture.componentInstance.maxSize = 3; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '-...', '7', '»']); + + fixture.componentInstance.page = 4; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '-...', '+4', '5', '6', '7', '»']); + + fixture.componentInstance.page = 7; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '-...', '+7', '-»']); + + // enabling rotation + fixture.componentInstance.rotate = true; + fixture.componentInstance.page = 1; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '-...', '7', '»']); + + fixture.componentInstance.page = 2; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '+2', '3', '-...', '7', '»']); + + fixture.componentInstance.page = 3; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '2', '+3', '4', '-...', '7', '»']); + + fixture.componentInstance.page = 4; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '-...', '3', '+4', '5', '-...', '7', '»']); + + fixture.componentInstance.page = 5; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '-...', '4', '+5', '6', '7', '»']); + + fixture.componentInstance.page = 6; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '-...', '5', '+6', '7', '»']); + + fixture.componentInstance.page = 7; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '-...', '5', '6', '+7', '-»']); + + // no ellipsis when maxPage > total pages + fixture.componentInstance.maxSize = 100; + fixture.componentInstance.page = 5; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '2', '3', '4', '+5', '6', '7', '»']); + }); + + it('should handle edge "maxSize" values', () => { + const html = ''; + const fixture = createTestComponent(html); + + fixture.componentInstance.maxSize = 2; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '-...', '5', '»']); + + fixture.componentInstance.maxSize = 0; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '4', '5', '»']); + + fixture.componentInstance.maxSize = 100; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '4', '5', '»']); + + fixture.componentInstance.maxSize = NaN; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '4', '5', '»']); + + fixture.componentInstance.maxSize = null; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '3', '4', '5', '»']); + }); + + it('should handle edge "collectionSize" values', () => { + const html = ''; + const fixture = createTestComponent(html); + + fixture.componentInstance.collectionSize = 0; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '-»']); + + fixture.componentInstance.collectionSize = NaN; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '-»']); + + fixture.componentInstance.collectionSize = null; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '-»']); + }); + + it('should handle edge "pageSize" values', () => { + const html = ''; + const fixture = createTestComponent(html); + + fixture.componentInstance.pageSize = 0; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '-»']); + + fixture.componentInstance.pageSize = NaN; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '-»']); + + fixture.componentInstance.pageSize = null; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '-»']); + }); + + it('should handle edge "page" values', () => { + const html = ''; + const fixture = createTestComponent(html); + + fixture.componentInstance.page = 0; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['-«', '+1', '2', '»']); + + fixture.componentInstance.page = 2016; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['«', '1', '+2', '-»']); + + fixture.componentInstance.page = NaN; + expectPages(fixture.nativeElement, ['«', '1', '+2', '-»']); + + fixture.componentInstance.page = null; + expectPages(fixture.nativeElement, ['«', '1', '+2', '-»']); + }); + + it('should not emit "pageChange" for incorrect input values', fakeAsync(() => { + const html = ``; + const fixture = createTestComponent(html); + tick(); + + spyOn(fixture.componentInstance, 'onPageChange'); + + fixture.componentInstance.collectionSize = NaN; + fixture.detectChanges(); + tick(); + + fixture.componentInstance.maxSize = NaN; + fixture.detectChanges(); + tick(); + + fixture.componentInstance.pageSize = NaN; + fixture.detectChanges(); + tick(); + + expect(fixture.componentInstance.onPageChange).not.toHaveBeenCalled(); + })); + + it('should not emit "pageChange" when collection size is not set', fakeAsync(() => { + const html = ``; + const fixture = createTestComponent(html); + tick(); + + spyOn(fixture.componentInstance, 'onPageChange'); + + fixture.componentInstance.page = 5; + fixture.detectChanges(); + tick(); + + expect(fixture.componentInstance.onPageChange).not.toHaveBeenCalled(); + })); + + it('should set classes correctly for disabled state', fakeAsync(() => { + const html = ``; + const fixture = createTestComponent(html); + tick(); + + const buttons = fixture.nativeElement.querySelectorAll('li'); + for (let i = 0; i < buttons.length; i++) { + expect(buttons[i]).toHaveCssClass('disabled'); + } + })); + }); + + describe('Customization', () => { + + beforeEach( + () => { TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbPaginationModule]}); }); + + it('should allow overriding link templates', () => { + const fixture = createTestComponent(` + + F + L + P + N + E + + {{ page }}! + (current) + + + `); + + expectPages(fixture.nativeElement, ['-F', '-P', '+1!', '2!', '-E', '5!', 'N', 'L'], 'E'); + }); + + it('should pass disabled value to custom link templates', () => { + const fixture = createTestComponent(` + + {{ disabled ? 'dF' : 'F' }} + {{ disabled ? 'dL' : 'L' }} + {{ disabled ? 'dP' : 'P' }} + {{ disabled ? 'dN' : 'N' }} + + {{ disabled ? 'd'+page : page }} + (current) + + + `); + + expectPages(fixture.nativeElement, ['-dF', '-dP', '+1', '2', '3', 'N', 'L']); + + fixture.componentInstance.page = 3; + fixture.detectChanges(); + expectPages(fixture.nativeElement, ['F', 'P', '1', '2', '+3', '-dN', '-dL']); + + fixture.componentInstance.disabled = true; + fixture.detectChanges(); + const firstPage = getLink(fixture.nativeElement, 2); + expect(firstPage.parentElement).toHaveCssClass('disabled'); + expect(firstPage.textContent.trim()).toBe('d1'); + }); + }); + + describe('Custom config', () => { + let config: NgbPaginationConfig; + + beforeEach(() => { TestBed.configureTestingModule({imports: [NgbPaginationModule]}); }); + + beforeEach(inject([NgbPaginationConfig], (c: NgbPaginationConfig) => { + config = c; + config.boundaryLinks = true; + config.directionLinks = false; + config.ellipses = false; + config.maxSize = 42; + config.pageSize = 7; + config.rotate = true; + config.size = 'sm'; + })); + + it('should initialize inputs with provided config', () => { + const fixture = TestBed.createComponent(NgbPagination); + fixture.detectChanges(); + + let pagination = fixture.componentInstance; + expectSameValues(pagination, config); + }); + }); + + describe('Custom config as provider', () => { + let config = new NgbPaginationConfig(); + config.disabled = true; + config.boundaryLinks = true; + config.directionLinks = false; + config.ellipses = false; + config.maxSize = 42; + config.pageSize = 7; + config.rotate = true; + config.size = 'sm'; + + beforeEach(() => { + TestBed.configureTestingModule( + {imports: [NgbPaginationModule], providers: [{provide: NgbPaginationConfig, useValue: config}]}); + }); + + it('should initialize inputs with provided config as provider', () => { + const fixture = TestBed.createComponent(NgbPagination); + fixture.detectChanges(); + + let pagination = fixture.componentInstance; + expectSameValues(pagination, config); + }); + }); +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + disabled = false; + pageSize = 10; + collectionSize = 100; + page = 1; + boundaryLinks = false; + directionLinks = false; + size = ''; + maxSize = 0; + ellipses = true; + rotate = false; + + onPageChange = () => {}; +} diff --git a/src/pagination/pagination.ts b/src/pagination/pagination.ts new file mode 100644 index 0000000..4a4851f --- /dev/null +++ b/src/pagination/pagination.ts @@ -0,0 +1,386 @@ +import { + Component, + ContentChild, + Directive, + EventEmitter, + Input, + Output, + OnChanges, + ChangeDetectionStrategy, + SimpleChanges, + TemplateRef +} from '@angular/core'; +import {getValueInRange, isNumber} from '../util/util'; +import {NgbPaginationConfig} from './pagination-config'; + +/** + * A context for the + * * `NgbPaginationFirst` + * * `NgbPaginationPrevious` + * * `NgbPaginationNext` + * * `NgbPaginationLast` + * * `NgbPaginationEllipsis` + * + * link templates in case you want to override one. + * + * @since 4.1.0 + */ +export interface NgbPaginationLinkContext { + /** + * The currently selected page number + */ + currentPage: number; + + /** + * If `true`, the current link is disabled + */ + disabled: boolean; +} + +/** + * A context for the `NgbPaginationNumber` link template in case you want to override one. + * + * Extends `NgbPaginationLinkContext`. + * + * @since 4.1.0 + */ +export interface NgbPaginationNumberContext extends NgbPaginationLinkContext { + /** + * The page number, displayed by the current page link. + */ + $implicit: number; +} + +/** + * A directive to match the 'ellipsis' link template + * + * @since 4.1.0 + */ +@Directive({selector: 'ng-template[ngbPaginationEllipsis]'}) +export class NgbPaginationEllipsis { + constructor(public templateRef: TemplateRef) {} +} + +/** + * A directive to match the 'first' link template + * + * @since 4.1.0 + */ +@Directive({selector: 'ng-template[ngbPaginationFirst]'}) +export class NgbPaginationFirst { + constructor(public templateRef: TemplateRef) {} +} + +/** + * A directive to match the 'last' link template + * + * @since 4.1.0 + */ +@Directive({selector: 'ng-template[ngbPaginationLast]'}) +export class NgbPaginationLast { + constructor(public templateRef: TemplateRef) {} +} + +/** + * A directive to match the 'next' link template + * + * @since 4.1.0 + */ +@Directive({selector: 'ng-template[ngbPaginationNext]'}) +export class NgbPaginationNext { + constructor(public templateRef: TemplateRef) {} +} + +/** + * A directive to match the page 'number' link template + * + * @since 4.1.0 + */ +@Directive({selector: 'ng-template[ngbPaginationNumber]'}) +export class NgbPaginationNumber { + constructor(public templateRef: TemplateRef) {} +} + +/** + * A directive to match the 'previous' link template + * + * @since 4.1.0 + */ +@Directive({selector: 'ng-template[ngbPaginationPrevious]'}) +export class NgbPaginationPrevious { + constructor(public templateRef: TemplateRef) {} +} + +/** + * A component that displays page numbers and allows to customize them in several ways. + */ +@Component({ + selector: 'ngb-pagination', + changeDetection: ChangeDetectionStrategy.OnPush, + host: {'role': 'navigation'}, + template: ` + + + + + ... + + {{ page }} + (current) + + + ` +}) +export class NgbPagination implements OnChanges { + pageCount = 0; + pages: number[] = []; + + @ContentChild(NgbPaginationEllipsis, {static: false}) tplEllipsis: NgbPaginationEllipsis; + @ContentChild(NgbPaginationFirst, {static: false}) tplFirst: NgbPaginationFirst; + @ContentChild(NgbPaginationLast, {static: false}) tplLast: NgbPaginationLast; + @ContentChild(NgbPaginationNext, {static: false}) tplNext: NgbPaginationNext; + @ContentChild(NgbPaginationNumber, {static: false}) tplNumber: NgbPaginationNumber; + @ContentChild(NgbPaginationPrevious, {static: false}) tplPrevious: NgbPaginationPrevious; + + /** + * If `true`, pagination links will be disabled. + */ + @Input() disabled: boolean; + + /** + * If `true`, the "First" and "Last" page links are shown. + */ + @Input() boundaryLinks: boolean; + + /** + * If `true`, the "Next" and "Previous" page links are shown. + */ + @Input() directionLinks: boolean; + + /** + * If `true`, the ellipsis symbols and first/last page numbers will be shown when `maxSize` > number of pages. + */ + @Input() ellipses: boolean; + + /** + * Whether to rotate pages when `maxSize` > number of pages. + * + * The current page always stays in the middle if `true`. + */ + @Input() rotate: boolean; + + /** + * The number of items in your paginated collection. + * + * Note, that this is not the number of pages. Page numbers are calculated dynamically based on + * `collectionSize` and `pageSize`. Ex. if you have 100 items in your collection and displaying 20 items per page, + * you'll end up with 5 pages. + */ + @Input() collectionSize: number; + + /** + * The maximum number of pages to display. + */ + @Input() maxSize: number; + + /** + * The current page. + * + * Page numbers start with `1`. + */ + @Input() page = 1; + + /** + * The number of items per page. + */ + @Input() pageSize: number; + + /** + * An event fired when the page is changed. Will fire only if collection size is set and all values are valid. + * + * Event payload is the number of the newly selected page. + * + * Page numbers start with `1`. + */ + @Output() pageChange = new EventEmitter(true); + + /** + * The pagination display size. + * + * Bootstrap currently supports small and large sizes. + */ + @Input() size: 'sm' | 'lg'; + + constructor(config: NgbPaginationConfig) { + this.disabled = config.disabled; + this.boundaryLinks = config.boundaryLinks; + this.directionLinks = config.directionLinks; + this.ellipses = config.ellipses; + this.maxSize = config.maxSize; + this.pageSize = config.pageSize; + this.rotate = config.rotate; + this.size = config.size; + } + + hasPrevious(): boolean { return this.page > 1; } + + hasNext(): boolean { return this.page < this.pageCount; } + + nextDisabled(): boolean { return !this.hasNext() || this.disabled; } + + previousDisabled(): boolean { return !this.hasPrevious() || this.disabled; } + + selectPage(pageNumber: number): void { this._updatePages(pageNumber); } + + ngOnChanges(changes: SimpleChanges): void { this._updatePages(this.page); } + + isEllipsis(pageNumber): boolean { return pageNumber === -1; } + + /** + * Appends ellipses and first/last page number to the displayed pages + */ + private _applyEllipses(start: number, end: number) { + if (this.ellipses) { + if (start > 0) { + if (start > 1) { + this.pages.unshift(-1); + } + this.pages.unshift(1); + } + if (end < this.pageCount) { + if (end < (this.pageCount - 1)) { + this.pages.push(-1); + } + this.pages.push(this.pageCount); + } + } + } + + /** + * Rotates page numbers based on maxSize items visible. + * Currently selected page stays in the middle: + * + * Ex. for selected page = 6: + * [5,*6*,7] for maxSize = 3 + * [4,5,*6*,7] for maxSize = 4 + */ + private _applyRotation(): [number, number] { + let start = 0; + let end = this.pageCount; + let leftOffset = Math.floor(this.maxSize / 2); + let rightOffset = this.maxSize % 2 === 0 ? leftOffset - 1 : leftOffset; + + if (this.page <= leftOffset) { + // very beginning, no rotation -> [0..maxSize] + end = this.maxSize; + } else if (this.pageCount - this.page < leftOffset) { + // very end, no rotation -> [len-maxSize..len] + start = this.pageCount - this.maxSize; + } else { + // rotate + start = this.page - leftOffset - 1; + end = this.page + rightOffset; + } + + return [start, end]; + } + + /** + * Paginates page numbers based on maxSize items per page. + */ + private _applyPagination(): [number, number] { + let page = Math.ceil(this.page / this.maxSize) - 1; + let start = page * this.maxSize; + let end = start + this.maxSize; + + return [start, end]; + } + + private _setPageInRange(newPageNo) { + const prevPageNo = this.page; + this.page = getValueInRange(newPageNo, this.pageCount, 1); + + if (this.page !== prevPageNo && isNumber(this.collectionSize)) { + this.pageChange.emit(this.page); + } + } + + private _updatePages(newPage: number) { + this.pageCount = Math.ceil(this.collectionSize / this.pageSize); + + if (!isNumber(this.pageCount)) { + this.pageCount = 0; + } + + // fill-in model needed to render pages + this.pages.length = 0; + for (let i = 1; i <= this.pageCount; i++) { + this.pages.push(i); + } + + // set page within 1..max range + this._setPageInRange(newPage); + + // apply maxSize if necessary + if (this.maxSize > 0 && this.pageCount > this.maxSize) { + let start = 0; + let end = this.pageCount; + + // either paginating or rotating page numbers + if (this.rotate) { + [start, end] = this._applyRotation(); + } else { + [start, end] = this._applyPagination(); + } + + this.pages = this.pages.slice(start, end); + + // adding ellipses + this._applyEllipses(start, end); + } + } +} diff --git a/src/popover/popover-config.spec.ts b/src/popover/popover-config.spec.ts new file mode 100644 index 0000000..b6ff3e5 --- /dev/null +++ b/src/popover/popover-config.spec.ts @@ -0,0 +1,16 @@ +import {NgbPopoverConfig} from './popover-config'; + +describe('ngb-popover-config', () => { + it('should have sensible default values', () => { + const config = new NgbPopoverConfig(); + + expect(config.autoClose).toBe(true); + expect(config.placement).toBe('auto'); + expect(config.triggers).toBe('click'); + expect(config.container).toBeUndefined(); + expect(config.disablePopover).toBe(false); + expect(config.popoverClass).toBeUndefined(); + expect(config.openDelay).toBe(0); + expect(config.closeDelay).toBe(0); + }); +}); diff --git a/src/popover/popover-config.ts b/src/popover/popover-config.ts new file mode 100644 index 0000000..d2a7ad9 --- /dev/null +++ b/src/popover/popover-config.ts @@ -0,0 +1,20 @@ +import {Injectable} from '@angular/core'; +import {PlacementArray} from '../util/positioning'; + +/** + * A configuration service for the [`NgbPopover`](#/components/popover/api#NgbPopover) component. + * + * You can inject this service, typically in your root component, and customize the values of its properties in + * order to provide default values for all the popovers used in the application. + */ +@Injectable({providedIn: 'root'}) +export class NgbPopoverConfig { + autoClose: boolean | 'inside' | 'outside' = true; + placement: PlacementArray = 'auto'; + triggers = 'click'; + container: string; + disablePopover = false; + popoverClass: string; + openDelay = 0; + closeDelay = 0; +} diff --git a/src/popover/popover.module.ts b/src/popover/popover.module.ts new file mode 100644 index 0000000..59bce97 --- /dev/null +++ b/src/popover/popover.module.ts @@ -0,0 +1,17 @@ +import {NgModule} from '@angular/core'; + +import {NgbPopover, NgbPopoverWindow} from './popover'; +import {CommonModule} from '@angular/common'; + +export {NgbPopover} from './popover'; +export {NgbPopoverConfig} from './popover-config'; +export {Placement} from '../util/positioning'; + +@NgModule({ + declarations: [NgbPopover, NgbPopoverWindow], + exports: [NgbPopover], + imports: [CommonModule], + entryComponents: [NgbPopoverWindow] +}) +export class NgbPopoverModule { +} diff --git a/src/popover/popover.scss b/src/popover/popover.scss new file mode 100644 index 0000000..b5cd242 --- /dev/null +++ b/src/popover/popover.scss @@ -0,0 +1,38 @@ + +$arrow-size: 1rem; + +ngb-popover-window { + &.bs-popover-top > .arrow, + &.bs-popover-bottom > .arrow { + left: 50%; + margin-left: -$arrow-size / 2; + } + + &.bs-popover-top-left > .arrow, + &.bs-popover-bottom-left > .arrow { + left: 2em; + } + + &.bs-popover-top-right > .arrow, + &.bs-popover-bottom-right > .arrow { + left: auto; + right: 2em; + } + + &.bs-popover-left > .arrow, + &.bs-popover-right > .arrow { + top: 50%; + margin-top: -$arrow-size / 2; + } + + &.bs-popover-left-top > .arrow, + &.bs-popover-right-top > .arrow { + top: 0.7em; + } + + &.bs-popover-left-bottom > .arrow, + &.bs-popover-right-bottom > .arrow { + top: auto; + bottom: 0.7em; + } +} diff --git a/src/popover/popover.spec.ts b/src/popover/popover.spec.ts new file mode 100644 index 0000000..3bde7a1 --- /dev/null +++ b/src/popover/popover.spec.ts @@ -0,0 +1,760 @@ +import {TestBed, ComponentFixture, inject, fakeAsync, tick} from '@angular/core/testing'; +import {createGenericTestComponent, createKeyEvent, triggerEvent} from '../test/common'; + +import {By} from '@angular/platform-browser'; +import { + Component, + ViewChild, + ChangeDetectionStrategy, + Injectable, + OnDestroy, + TemplateRef, + ViewContainerRef, + AfterViewInit +} from '@angular/core'; + +import {Key} from '../util/key'; + +import {NgbPopoverModule} from './popover.module'; +import {NgbPopoverWindow, NgbPopover} from './popover'; +import {NgbPopoverConfig} from './popover-config'; + +function dispatchEscapeKeyUpEvent() { + document.dispatchEvent(createKeyEvent(Key.Escape)); +} + +@Injectable() +class SpyService { + called = false; +} + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +const createOnPushTestComponent = + (html: string) => >createGenericTestComponent(html, TestOnPushComponent); + +describe('ngb-popover-window', () => { + beforeEach(() => { TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbPopoverModule]}); }); + + afterEach(() => { + // Cleaning elements, because of a TestBed issue with the id attribute + Array.from(document.body.children).map((element: HTMLElement) => { + if (element.tagName.toLocaleLowerCase() === 'div') { + element.parentNode.removeChild(element); + } + }); + }); + + it('should render popover on top by default', () => { + const fixture = TestBed.createComponent(NgbPopoverWindow); + fixture.componentInstance.title = 'Test title'; + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveCssClass('popover'); + expect(fixture.nativeElement).not.toHaveCssClass('bs-popover-top'); + expect(fixture.nativeElement.getAttribute('role')).toBe('tooltip'); + expect(fixture.nativeElement.querySelector('.popover-header').textContent).toBe('Test title'); + }); + + it('should optionally have a custom class', () => { + const fixture = TestBed.createComponent(NgbPopoverWindow); + fixture.detectChanges(); + + expect(fixture.nativeElement).not.toHaveCssClass('my-custom-class'); + + fixture.componentInstance.popoverClass = 'my-custom-class'; + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveCssClass('my-custom-class'); + }); +}); + +describe('ngb-popover', () => { + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestComponent, TestOnPushComponent, DestroyableCmpt, TestHooksComponent], + imports: [NgbPopoverModule], + providers: [SpyService] + }); + }); + + function getWindow(element) { return element.querySelector('ngb-popover-window'); } + + describe('basic functionality', () => { + + it('should open and close a popover - default settings and content as string', () => { + const fixture = + createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + const id = windowEl.getAttribute('id'); + + expect(windowEl).toHaveCssClass('popover'); + expect(windowEl).toHaveCssClass('bs-popover-top'); + expect(windowEl.textContent.trim()).toBe('TitleGreat tip!'); + expect(windowEl.getAttribute('role')).toBe('tooltip'); + expect(windowEl.parentNode).toBe(fixture.nativeElement); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBe(id); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBeNull(); + }); + + it('should open and close a popover - default settings and content from a template', () => { + const fixture = createTestComponent(` + Hello, {{name}}! +
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + const id = windowEl.getAttribute('id'); + + expect(windowEl).toHaveCssClass('popover'); + expect(windowEl).toHaveCssClass('bs-popover-top'); + expect(windowEl.textContent.trim()).toBe('TitleHello, World!'); + expect(windowEl.getAttribute('role')).toBe('tooltip'); + expect(windowEl.parentNode).toBe(fixture.nativeElement); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBe(id); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBeNull(); + }); + + it('should open and close a popover - default settings, content from a template and context supplied', () => { + const fixture = createTestComponent(` + Hello, {{name}}! +
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + directive.context.popover.open({name: 'John'}); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + const id = windowEl.getAttribute('id'); + + expect(windowEl).toHaveCssClass('popover'); + expect(windowEl).toHaveCssClass('bs-popover-top'); + expect(windowEl.textContent.trim()).toBe('TitleHello, John!'); + expect(windowEl.getAttribute('role')).toBe('tooltip'); + expect(windowEl.parentNode).toBe(fixture.nativeElement); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBe(id); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBeNull(); + }); + + it('should open and close a popover - default settings and custom class', () => { + const fixture = createTestComponent(` +
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + const id = windowEl.getAttribute('id'); + + expect(windowEl).toHaveCssClass('popover'); + expect(windowEl).toHaveCssClass('bs-popover-top'); + expect(windowEl).toHaveCssClass('my-custom-class'); + expect(windowEl.textContent.trim()).toBe('TitleGreat tip!'); + expect(windowEl.getAttribute('role')).toBe('tooltip'); + expect(windowEl.parentNode).toBe(fixture.nativeElement); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBe(id); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBeNull(); + }); + + it('should accept a template for the title and properly destroy it when closing', () => { + const fixture = createTestComponent(` + Hello, {{name}}! +
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + const spyService = fixture.debugElement.injector.get(SpyService); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + expect(windowEl.textContent.trim()).toBe('Hello, World! Some contentBody'); + expect(spyService.called).toBeFalsy(); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(spyService.called).toBeTruthy(); + }); + + it('should pass the context to the template for the title', () => { + const fixture = createTestComponent(` + {{greeting}}, {{name}}! +
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + fixture.componentInstance.name = 'tout le monde'; + fixture.componentInstance.popover.open({greeting: 'Bonjour'}); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + expect(windowEl.textContent.trim()).toBe('Bonjour, tout le monde!!!'); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should properly destroy TemplateRef content', () => { + const fixture = createTestComponent(` + +
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + const spyService = fixture.debugElement.injector.get(SpyService); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + expect(spyService.called).toBeFalsy(); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(spyService.called).toBeTruthy(); + }); + + it('should not show a header if title is empty', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + expect(windowEl.querySelector('.popover-header')).toBeNull(); + }); + + it('should not open a popover if content and title are empty', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toBeNull(); + }); + + it('should not open a popover if [disablePopover] flag', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toBeNull(); + }); + + it('should close the popover if content and title become empty', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + fixture.componentInstance.name = ''; + fixture.componentInstance.title = ''; + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should open the popover if content is empty but title has value', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).not.toBeNull(); + }); + + it('should not close the popover if content becomes empty but title has value', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + fixture.componentInstance.name = ''; + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + }); + + it('should allow re-opening previously closed popovers', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + }); + + it('should not leave dangling popovers in the DOM', () => { + const fixture = createTestComponent( + `
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should properly cleanup popovers with manual triggers', () => { + const fixture = createTestComponent(` +
+
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should open popover from hooks', () => { + const fixture = TestBed.createComponent(TestHooksComponent); + fixture.detectChanges(); + + const popoverWindow = fixture.debugElement.query(By.directive(NgbPopoverWindow)); + expect(popoverWindow.nativeElement).toHaveCssClass('popover'); + }); + }); + + + describe('positioning', () => { + + it('should use requested position', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('popover'); + expect(windowEl).toHaveCssClass('bs-popover-left'); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + }); + + it('should properly position popovers when a component is using the OnPush strategy', () => { + const fixture = createOnPushTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('popover'); + expect(windowEl).toHaveCssClass('bs-popover-left'); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + }); + + it('should have proper arrow placement', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('popover'); + expect(windowEl).toHaveCssClass('bs-popover-right'); + expect(windowEl).toHaveCssClass('bs-popover-right-top'); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + }); + + it('should accept placement in array (second value of the array should be applied)', () => { + const fixture = createTestComponent( + `
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('popover'); + expect(windowEl).toHaveCssClass('bs-popover-top'); + expect(windowEl).toHaveCssClass('bs-popover-top-left'); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + }); + + it('should accept placement with space separated values (second value should be applied)', () => { + const fixture = createTestComponent( + `
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('popover'); + expect(windowEl).toHaveCssClass('bs-popover-top'); + expect(windowEl).toHaveCssClass('bs-popover-top-left'); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + }); + + it('should apply auto placement', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('popover'); + // actual placement with auto is not known in advance, so use regex to check it + expect(windowEl.getAttribute('class')).toMatch('bs-popover-\.'); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + }); + + }); + + describe('container', () => { + + it('should be appended to the element matching the selector passed to "container"', () => { + const selector = 'body'; + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(getWindow(window.document.querySelector(selector))).not.toBeNull(); + }); + + it('should properly destroy popovers when the "container" option is used', () => { + const selector = 'body'; + const fixture = + createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + + expect(getWindow(document.querySelector(selector))).not.toBeNull(); + fixture.componentRef.instance.show = false; + fixture.detectChanges(); + expect(getWindow(document.querySelector(selector))).toBeNull(); + }); + + }); + + describe('visibility', () => { + it('should emit events when showing and hiding popover', () => { + const fixture = createTestComponent( + `
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + let shownSpy = spyOn(fixture.componentInstance, 'shown'); + let hiddenSpy = spyOn(fixture.componentInstance, 'hidden'); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + expect(shownSpy).toHaveBeenCalled(); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(hiddenSpy).toHaveBeenCalled(); + }); + + it('should not emit close event when already closed', () => { + const fixture = createTestComponent( + `
`); + + let shownSpy = spyOn(fixture.componentInstance, 'shown'); + let hiddenSpy = spyOn(fixture.componentInstance, 'hidden'); + + fixture.componentInstance.popover.open(); + fixture.detectChanges(); + + fixture.componentInstance.popover.open(); + fixture.detectChanges(); + + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + expect(shownSpy).toHaveBeenCalled(); + expect(shownSpy.calls.count()).toEqual(1); + expect(hiddenSpy).not.toHaveBeenCalled(); + }); + + it('should not emit open event when already opened', () => { + const fixture = createTestComponent( + `
`); + + let shownSpy = spyOn(fixture.componentInstance, 'shown'); + let hiddenSpy = spyOn(fixture.componentInstance, 'hidden'); + + fixture.componentInstance.popover.close(); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(shownSpy).not.toHaveBeenCalled(); + expect(hiddenSpy).not.toHaveBeenCalled(); + }); + + it('should report correct visibility', () => { + const fixture = createTestComponent(`
`); + fixture.detectChanges(); + + expect(fixture.componentInstance.popover.isOpen()).toBeFalsy(); + + fixture.componentInstance.popover.open(); + fixture.detectChanges(); + expect(fixture.componentInstance.popover.isOpen()).toBeTruthy(); + + fixture.componentInstance.popover.close(); + fixture.detectChanges(); + expect(fixture.componentInstance.popover.isOpen()).toBeFalsy(); + }); + }); + + describe('triggers', () => { + beforeEach(() => { TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbPopoverModule]}); }); + + it('should support toggle triggers', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should non-default toggle triggers', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should support multiple triggers', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should not use default for manual triggers', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbPopover)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should allow toggling for manual triggers', () => { + const fixture = createTestComponent(` +
+ `); + const button = fixture.nativeElement.querySelector('button'); + + triggerEvent(button, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + triggerEvent(button, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should allow open / close for manual triggers', () => { + const fixture = createTestComponent(`
+ + `); + const buttons = fixture.nativeElement.querySelectorAll('button'); + + triggerEvent(buttons[0], 'click'); // open + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + triggerEvent(buttons[1], 'click'); // close + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should not throw when open called for manual triggers and open popover', () => { + const fixture = createTestComponent(` +
+ `); + const button = fixture.nativeElement.querySelector('button'); + + triggerEvent(button, 'click'); // open + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + triggerEvent(button, 'click'); // open + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + }); + + it('should not throw when closed called for manual triggers and closed popover', () => { + const fixture = createTestComponent(` +
+ `); + const button = fixture.nativeElement.querySelector('button'); + + triggerEvent(button, 'click'); // close + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + }); + + describe('Custom config', () => { + let config: NgbPopoverConfig; + + beforeEach(() => { + TestBed.configureTestingModule({imports: [NgbPopoverModule]}); + TestBed.overrideComponent(TestComponent, {set: {template: `
`}}); + }); + + beforeEach(inject([NgbPopoverConfig], (c: NgbPopoverConfig) => { + config = c; + config.placement = 'bottom'; + config.triggers = 'hover'; + config.container = 'body'; + config.popoverClass = 'my-custom-class'; + })); + + it('should initialize inputs with provided config', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const popover = fixture.componentInstance.popover; + + expect(popover.placement).toBe(config.placement); + expect(popover.triggers).toBe(config.triggers); + expect(popover.container).toBe(config.container); + expect(popover.popoverClass).toBe(config.popoverClass); + }); + }); + + describe('Custom config as provider', () => { + let config = new NgbPopoverConfig(); + config.placement = 'bottom'; + config.triggers = 'hover'; + config.popoverClass = 'my-custom-class'; + + beforeEach(() => { + TestBed.configureTestingModule( + {imports: [NgbPopoverModule], providers: [{provide: NgbPopoverConfig, useValue: config}]}); + }); + + it('should initialize inputs with provided config as provider', () => { + const fixture = createTestComponent(`
`); + const popover = fixture.componentInstance.popover; + + expect(popover.placement).toBe(config.placement); + expect(popover.triggers).toBe(config.triggers); + expect(popover.popoverClass).toBe(config.popoverClass); + }); + }); + + describe('non-regression', () => { + + /** + * Under very specific conditions ngOnDestroy can be invoked without calling ngOnInit first. + * See discussion in https://github.com/ng-bootstrap/ng-bootstrap/issues/2199 for more details. + */ + it('should not try to call listener cleanup function when no listeners registered', () => { + const fixture = createTestComponent(` +
+ + `); + const buttonEl = fixture.debugElement.query(By.css('button')); + triggerEvent(buttonEl, 'click'); + }); + }); +}); + +@Component({selector: 'test-cmpt', template: ``}) +export class TestComponent { + name = 'World'; + show = true; + title: string; + placement: string; + + @ViewChild(NgbPopover, {static: true}) popover: NgbPopover; + + constructor(private _vcRef: ViewContainerRef) {} + + createAndDestroyTplWithAPopover(tpl: TemplateRef) { + this._vcRef.createEmbeddedView(tpl, {}, 0); + this._vcRef.remove(0); + } + + shown() {} + hidden() {} +} + +@Component({selector: 'test-onpush-cmpt', changeDetection: ChangeDetectionStrategy.OnPush, template: ``}) +export class TestOnPushComponent { +} + +@Component({selector: 'destroyable-cmpt', template: 'Some content'}) +export class DestroyableCmpt implements OnDestroy { + constructor(private _spyService: SpyService) {} + + ngOnDestroy(): void { this._spyService.called = true; } +} + +@Component({selector: 'test-hooks', template: `
`}) +export class TestHooksComponent implements AfterViewInit { + @ViewChild(NgbPopover, {static: true}) popover; + + ngAfterViewInit() { this.popover.open(); } +} diff --git a/src/popover/popover.ts b/src/popover/popover.ts new file mode 100644 index 0000000..1504d72 --- /dev/null +++ b/src/popover/popover.ts @@ -0,0 +1,293 @@ +import { + Component, + Directive, + Input, + Output, + EventEmitter, + ChangeDetectionStrategy, + OnInit, + OnDestroy, + OnChanges, + Inject, + Injector, + Renderer2, + ComponentRef, + ElementRef, + TemplateRef, + ViewContainerRef, + ComponentFactoryResolver, + NgZone, + SimpleChanges, + ViewEncapsulation, + ChangeDetectorRef, + ApplicationRef +} from '@angular/core'; +import {DOCUMENT} from '@angular/common'; + +import {listenToTriggers} from '../util/triggers'; +import {ngbAutoClose} from '../util/autoclose'; +import {positionElements, PlacementArray} from '../util/positioning'; +import {PopupService} from '../util/popup'; + +import {NgbPopoverConfig} from './popover-config'; + +let nextId = 0; + +@Component({ + selector: 'ngb-popover-window', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + host: {'[class]': '"popover" + (popoverClass ? " " + popoverClass : "")', 'role': 'tooltip', '[id]': 'id'}, + template: ` +
+

+ {{title}} + +

+
`, + styleUrls: ['./popover.scss'] +}) +export class NgbPopoverWindow { + @Input() title: undefined | string | TemplateRef; + @Input() id: string; + @Input() popoverClass: string; + @Input() context: any; + + isTitleTemplate() { return this.title instanceof TemplateRef; } +} + +/** + * A lightweight and extensible directive for fancy popover creation. + */ +@Directive({selector: '[ngbPopover]', exportAs: 'ngbPopover'}) +export class NgbPopover implements OnInit, OnDestroy, OnChanges { + /** + * Indicates whether the popover should be closed on `Escape` key and inside/outside clicks: + * + * * `true` - closes on both outside and inside clicks as well as `Escape` presses + * * `false` - disables the autoClose feature (NB: triggers still apply) + * * `"inside"` - closes on inside clicks as well as Escape presses + * * `"outside"` - closes on outside clicks (sometimes also achievable through triggers) + * as well as `Escape` presses + * + * @since 3.0.0 + */ + @Input() autoClose: boolean | 'inside' | 'outside'; + + /** + * The string content or a `TemplateRef` for the content to be displayed in the popover. + * + * If the title and the content are empty, the popover won't open. + */ + @Input() ngbPopover: string | TemplateRef; + + /** + * The title of the popover. + * + * If the title and the content are empty, the popover won't open. + */ + @Input() popoverTitle: string | TemplateRef; + + /** + * The preferred placement of the popover. + * + * Possible values are `"top"`, `"top-left"`, `"top-right"`, `"bottom"`, `"bottom-left"`, + * `"bottom-right"`, `"left"`, `"left-top"`, `"left-bottom"`, `"right"`, `"right-top"`, + * `"right-bottom"` + * + * Accepts an array of strings or a string with space separated possible values. + * + * The default order of preference is `"auto"` (same as the sequence above). + * + * Please see the [positioning overview](#/positioning) for more details. + */ + @Input() placement: PlacementArray; + + /** + * Specifies events that should trigger the tooltip. + * + * Supports a space separated list of event names. + * For more details see the [triggers demo](#/components/popover/examples#triggers). + */ + @Input() triggers: string; + + /** + * A selector specifying the element the popover should be appended to. + * + * Currently only supports `body`. + */ + @Input() container: string; + + /** + * If `true`, popover is disabled and won't be displayed. + * + * @since 1.1.0 + */ + @Input() disablePopover: boolean; + + /** + * An optional class applied to the popover window element. + * + * @since 2.2.0 + */ + @Input() popoverClass: string; + + /** + * The opening delay in ms. Works only for "non-manual" opening triggers defined by the `triggers` input. + * + * @since 4.1.0 + */ + @Input() openDelay: number; + + /** + * The closing delay in ms. Works only for "non-manual" opening triggers defined by the `triggers` input. + * + * @since 4.1.0 + */ + @Input() closeDelay: number; + + /** + * An event emitted when the popover is shown. Contains no payload. + */ + @Output() shown = new EventEmitter(); + + /** + * An event emitted when the popover is hidden. Contains no payload. + */ + @Output() hidden = new EventEmitter(); + + private _ngbPopoverWindowId = `ngb-popover-${nextId++}`; + private _popupService: PopupService; + private _windowRef: ComponentRef; + private _unregisterListenersFn; + private _zoneSubscription: any; + private _isDisabled(): boolean { + if (this.disablePopover) { + return true; + } + if (!this.ngbPopover && !this.popoverTitle) { + return true; + } + return false; + } + + constructor( + private _elementRef: ElementRef, private _renderer: Renderer2, injector: Injector, + componentFactoryResolver: ComponentFactoryResolver, viewContainerRef: ViewContainerRef, config: NgbPopoverConfig, + private _ngZone: NgZone, @Inject(DOCUMENT) private _document: any, private _changeDetector: ChangeDetectorRef, + private _applicationRef: ApplicationRef) { + this.autoClose = config.autoClose; + this.placement = config.placement; + this.triggers = config.triggers; + this.container = config.container; + this.disablePopover = config.disablePopover; + this.popoverClass = config.popoverClass; + this.openDelay = config.openDelay; + this.closeDelay = config.closeDelay; + this._popupService = new PopupService( + NgbPopoverWindow, injector, viewContainerRef, _renderer, componentFactoryResolver, _applicationRef); + + this._zoneSubscription = _ngZone.onStable.subscribe(() => { + if (this._windowRef) { + positionElements( + this._elementRef.nativeElement, this._windowRef.location.nativeElement, this.placement, + this.container === 'body', 'bs-popover'); + } + }); + } + + /** + * Opens the popover. + * + * This is considered to be a "manual" triggering. + * The `context` is an optional value to be injected into the popover template when it is created. + */ + open(context?: any) { + if (!this._windowRef && !this._isDisabled()) { + this._windowRef = this._popupService.open(this.ngbPopover, context); + this._windowRef.instance.title = this.popoverTitle; + this._windowRef.instance.context = context; + this._windowRef.instance.popoverClass = this.popoverClass; + this._windowRef.instance.id = this._ngbPopoverWindowId; + + this._renderer.setAttribute(this._elementRef.nativeElement, 'aria-describedby', this._ngbPopoverWindowId); + + if (this.container === 'body') { + this._document.querySelector(this.container).appendChild(this._windowRef.location.nativeElement); + } + + // We need to detect changes, because we don't know where .open() might be called from. + // Ex. opening popover from one of lifecycle hooks that run after the CD + // (say from ngAfterViewInit) will result in 'ExpressionHasChanged' exception + this._windowRef.changeDetectorRef.detectChanges(); + + // We need to mark for check, because popover won't work inside the OnPush component. + // Ex. when we use expression like `{{ popover.isOpen() : 'opened' : 'closed' }}` + // inside the template of an OnPush component and we change the popover from + // open -> closed, the expression in question won't be updated unless we explicitly + // mark the parent component to be checked. + this._windowRef.changeDetectorRef.markForCheck(); + + ngbAutoClose( + this._ngZone, this._document, this.autoClose, () => this.close(), this.hidden, + [this._windowRef.location.nativeElement]); + this.shown.emit(); + } + } + + /** + * Closes the popover. + * + * This is considered to be a "manual" triggering of the popover. + */ + close(): void { + if (this._windowRef) { + this._renderer.removeAttribute(this._elementRef.nativeElement, 'aria-describedby'); + this._popupService.close(); + this._windowRef = null; + this.hidden.emit(); + this._changeDetector.markForCheck(); + } + } + + /** + * Toggles the popover. + * + * This is considered to be a "manual" triggering of the popover. + */ + toggle(): void { + if (this._windowRef) { + this.close(); + } else { + this.open(); + } + } + + /** + * Returns `true`, if the popover is currently shown. + */ + isOpen(): boolean { return this._windowRef != null; } + + ngOnInit() { + this._unregisterListenersFn = listenToTriggers( + this._renderer, this._elementRef.nativeElement, this.triggers, this.isOpen.bind(this), this.open.bind(this), + this.close.bind(this), +this.openDelay, +this.closeDelay); + } + + ngOnChanges(changes: SimpleChanges) { + // close popover if title and content become empty, or disablePopover set to true + if ((changes['ngbPopover'] || changes['popoverTitle'] || changes['disablePopover']) && this._isDisabled()) { + this.close(); + } + } + + ngOnDestroy() { + this.close(); + // This check is needed as it might happen that ngOnDestroy is called before ngOnInit + // under certain conditions, see: https://github.com/ng-bootstrap/ng-bootstrap/issues/2199 + if (this._unregisterListenersFn) { + this._unregisterListenersFn(); + } + this._zoneSubscription.unsubscribe(); + } +} diff --git a/src/progressbar/progressbar-config.spec.ts b/src/progressbar/progressbar-config.spec.ts new file mode 100644 index 0000000..4d8da36 --- /dev/null +++ b/src/progressbar/progressbar-config.spec.ts @@ -0,0 +1,13 @@ +import {NgbProgressbarConfig} from './progressbar-config'; + +describe('ngb-progressbar-config', () => { + it('should have sensible default values', () => { + const config = new NgbProgressbarConfig(); + + expect(config.max).toBe(100); + expect(config.striped).toBe(false); + expect(config.animated).toBe(false); + expect(config.type).toBeUndefined(); + expect(config.showValue).toBe(false); + }); +}); diff --git a/src/progressbar/progressbar-config.ts b/src/progressbar/progressbar-config.ts new file mode 100644 index 0000000..cc0379c --- /dev/null +++ b/src/progressbar/progressbar-config.ts @@ -0,0 +1,17 @@ +import {Injectable} from '@angular/core'; + +/** + * A configuration service for the [`NgbProgressbar`](#/components/progressbar/api#NgbProgressbar) component. + * + * You can inject this service, typically in your root component, and customize the values of its properties in + * order to provide default values for all the progress bars used in the application. + */ +@Injectable({providedIn: 'root'}) +export class NgbProgressbarConfig { + max = 100; + animated = false; + striped = false; + type: string; + showValue = false; + height: string; +} diff --git a/src/progressbar/progressbar.module.ts b/src/progressbar/progressbar.module.ts new file mode 100644 index 0000000..da984d0 --- /dev/null +++ b/src/progressbar/progressbar.module.ts @@ -0,0 +1,11 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; + +import {NgbProgressbar} from './progressbar'; + +export {NgbProgressbar} from './progressbar'; +export {NgbProgressbarConfig} from './progressbar-config'; + +@NgModule({declarations: [NgbProgressbar], exports: [NgbProgressbar], imports: [CommonModule]}) +export class NgbProgressbarModule { +} diff --git a/src/progressbar/progressbar.spec.ts b/src/progressbar/progressbar.spec.ts new file mode 100644 index 0000000..105c67c --- /dev/null +++ b/src/progressbar/progressbar.spec.ts @@ -0,0 +1,275 @@ +import {TestBed, ComponentFixture, inject} from '@angular/core/testing'; +import {createGenericTestComponent} from '../test/common'; + +import {Component} from '@angular/core'; + +import {NgbProgressbarModule} from './progressbar.module'; +import {NgbProgressbar} from './progressbar'; +import {NgbProgressbarConfig} from './progressbar-config'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +function getBarWidth(nativeEl): string { + return getProgressbar(nativeEl).style.width; +} + +function getBarHeight(nativeEl): string { + return nativeEl.querySelector('.progress').style.height; +} + +function getBarValue(nativeEl): number { + return parseInt(getProgressbar(nativeEl).getAttribute('aria-valuenow'), 10); +} + +function getProgressbar(nativeEl: Element): HTMLElement { + return nativeEl.querySelector('.progress-bar') as HTMLElement; +} + +describe('ngb-progressbar', () => { + describe('business logic', () => { + let progressCmp: NgbProgressbar; + + beforeEach(() => { progressCmp = new NgbProgressbar(new NgbProgressbarConfig()); }); + + it('should initialize inputs with default values', () => { + const defaultConfig = new NgbProgressbarConfig(); + expect(progressCmp.max).toBe(defaultConfig.max); + expect(progressCmp.animated).toBe(defaultConfig.animated); + expect(progressCmp.striped).toBe(defaultConfig.striped); + expect(progressCmp.type).toBe(defaultConfig.type); + }); + + it('should calculate the percentage (default max size)', () => { + progressCmp.value = 50; + expect(progressCmp.getPercentValue()).toBe(50); + + progressCmp.value = 25; + expect(progressCmp.getPercentValue()).toBe(25); + }); + + it('should calculate the percentage (custom max size)', () => { + progressCmp.max = 150; + + progressCmp.value = 75; + expect(progressCmp.getPercentValue()).toBe(50); + + progressCmp.value = 30; + expect(progressCmp.getPercentValue()).toBe(20); + }); + + it('should set the value to 0 for negative numbers', () => { + progressCmp.value = -20; + expect(progressCmp.getValue()).toBe(0); + }); + + it('should set the value to max if it is higher than max (default max size)', () => { + progressCmp.value = 120; + expect(progressCmp.getValue()).toBe(100); + }); + + it('should set the value to max if it is higher than max (custom max size)', () => { + progressCmp.max = 150; + progressCmp.value = 170; + expect(progressCmp.getValue()).toBe(150); + }); + + it('should update the value if max updates to a smaller value', () => { + progressCmp.value = 80; + progressCmp.max = 70; + expect(progressCmp.getValue()).toBe(70); + }); + + it('should not update the value if max updates to a larger value', () => { + progressCmp.value = 120; + progressCmp.max = 150; + expect(progressCmp.getValue()).toBe(120); + }); + }); + + describe('UI logic', () => { + + beforeEach( + () => { TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbProgressbarModule]}); }); + + it('accepts a value and respond to value changes', () => { + const html = ''; + const fixture = createTestComponent(html); + + expect(getBarWidth(fixture.nativeElement)).toBe('10%'); + + // this might fail in IE11 if attribute binding order is not respected for the element: + // will fail with value = 1 + // will work with value = 10 + expect(getBarValue(fixture.nativeElement)).toBe(10); + + fixture.componentInstance.value = 30; + fixture.detectChanges(); + expect(getBarWidth(fixture.nativeElement)).toBe('30%'); + expect(getBarValue(fixture.nativeElement)).toBe(30); + }); + + it('accepts a max value and respond to max changes', () => { + const html = ''; + const fixture = createTestComponent(html); + + expect(getBarWidth(fixture.nativeElement)).toBe('20%'); + + fixture.componentInstance.max = 200; + fixture.detectChanges(); + expect(getBarWidth(fixture.nativeElement)).toBe('5%'); + }); + + + it('accepts a value and max value above default values', () => { + const html = ''; + const fixture = createTestComponent(html); + + expect(getBarWidth(fixture.nativeElement)).toBe('100%'); + }); + + + it('accepts a custom type', () => { + const html = ''; + const fixture = createTestComponent(html); + + expect(getProgressbar(fixture.nativeElement)).toHaveCssClass('bg-warning'); + + fixture.componentInstance.type = 'info'; + fixture.detectChanges(); + expect(getProgressbar(fixture.nativeElement)).toHaveCssClass('bg-info'); + }); + + it('accepts animated as normal attr', () => { + const html = ''; + const fixture = createTestComponent(html); + + expect(getProgressbar(fixture.nativeElement)).toHaveCssClass('progress-bar-animated'); + + fixture.componentInstance.animated = false; + fixture.detectChanges(); + expect(getProgressbar(fixture.nativeElement)).not.toHaveCssClass('progress-bar-animated'); + }); + + + it('accepts striped as normal attr', () => { + const html = ''; + const fixture = createTestComponent(html); + + expect(getProgressbar(fixture.nativeElement)).toHaveCssClass('progress-bar-striped'); + + fixture.componentInstance.striped = false; + fixture.detectChanges(); + expect(getProgressbar(fixture.nativeElement)).not.toHaveCssClass('progress-bar-striped'); + }); + + + it('should not add "false" CSS class', () => { + const html = ''; + const fixture = createTestComponent(html); + + expect(getProgressbar(fixture.nativeElement)).toHaveCssClass('progress-bar-striped'); + expect(getProgressbar(fixture.nativeElement)).not.toHaveCssClass('false'); + }); + + it('should stay striped when the type changes', () => { + const html = ''; + const fixture = createTestComponent(html); + + expect(getProgressbar(fixture.nativeElement)).toHaveCssClass('bg-warning'); + expect(getProgressbar(fixture.nativeElement)).toHaveCssClass('progress-bar-striped'); + + fixture.componentInstance.type = 'success'; + fixture.detectChanges(); + expect(getProgressbar(fixture.nativeElement)).toHaveCssClass('bg-success'); + expect(getProgressbar(fixture.nativeElement)).toHaveCssClass('progress-bar-striped'); + }); + + it('sets the min and max values as aria attributes', () => { + const html = ''; + const fixture = createTestComponent(html); + + expect(getProgressbar(fixture.nativeElement).getAttribute('aria-valuemin')).toBe('0'); + expect(getProgressbar(fixture.nativeElement).getAttribute('aria-valuemax')).toBe('150'); + }); + + it('should display the progress-bar label', () => { + const html = 'label goes here'; + const fixture = createTestComponent(html); + + expect(fixture.nativeElement.textContent).toContain('label goes here'); + }); + + it('should display the current percentage value', () => { + const html = ''; + const fixture = createTestComponent(html); + + expect(fixture.nativeElement.textContent).toContain('100%'); + }); + + it('should accepts height values', () => { + const html = ''; + const fixture = createTestComponent(html); + + expect(getBarHeight(fixture.nativeElement)).toBe('10px'); + }); + }); + + describe('Custom config', () => { + let config: NgbProgressbarConfig; + + beforeEach(() => { TestBed.configureTestingModule({imports: [NgbProgressbarModule]}); }); + + beforeEach(inject([NgbProgressbarConfig], (c: NgbProgressbarConfig) => { + config = c; + config.max = 1000; + config.striped = true; + config.animated = true; + config.type = 'success'; + })); + + it('should initialize inputs with provided config', () => { + const fixture = TestBed.createComponent(NgbProgressbar); + fixture.detectChanges(); + + let progressbar = fixture.componentInstance; + expect(progressbar.max).toBe(config.max); + expect(progressbar.striped).toBe(config.striped); + expect(progressbar.animated).toBe(config.animated); + expect(progressbar.type).toBe(config.type); + }); + }); + + describe('Custom config as provider', () => { + let config = new NgbProgressbarConfig(); + config.max = 1000; + config.striped = true; + config.animated = true; + config.type = 'success'; + + beforeEach(() => { + TestBed.configureTestingModule( + {imports: [NgbProgressbarModule], providers: [{provide: NgbProgressbarConfig, useValue: config}]}); + }); + + it('should initialize inputs with provided config as provider', () => { + const fixture = TestBed.createComponent(NgbProgressbar); + fixture.detectChanges(); + + let progressbar = fixture.componentInstance; + expect(progressbar.max).toBe(config.max); + expect(progressbar.striped).toBe(config.striped); + expect(progressbar.animated).toBe(config.animated); + expect(progressbar.type).toBe(config.type); + }); + }); +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + value = 10; + max = 50; + animated = true; + striped = true; + type = 'warning'; +} diff --git a/src/progressbar/progressbar.ts b/src/progressbar/progressbar.ts new file mode 100644 index 0000000..cbc39fa --- /dev/null +++ b/src/progressbar/progressbar.ts @@ -0,0 +1,77 @@ +import {Component, Input, ChangeDetectionStrategy} from '@angular/core'; +import {getValueInRange} from '../util/util'; +import {NgbProgressbarConfig} from './progressbar-config'; + +/** + * A directive that provides feedback on the progress of a workflow or an action. + */ +@Component({ + selector: 'ngb-progressbar', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+
+ {{getPercentValue()}}% +
+
+ ` +}) +export class NgbProgressbar { + /** + * The maximal value to be displayed in the progressbar. + */ + @Input() max: number; + + /** + * If `true`, the stripes on the progressbar are animated. + * + * Takes effect only for browsers supporting CSS3 animations, and if `striped` is `true`. + */ + @Input() animated: boolean; + + /** + * If `true`, the progress bars will be displayed as striped. + */ + @Input() striped: boolean; + + /** + * If `true`, the current percentage will be shown in the `xx%` format. + */ + @Input() showValue: boolean; + + /** + * The type of the progress bar. + * + * Currently Bootstrap supports `"success"`, `"info"`, `"warning"` or `"danger"`. + */ + @Input() type: string; + + /** + * The current value for the progress bar. + * + * Should be in the `[0, max]` range. + */ + @Input() value = 0; + + /** + * THe height of the progress bar. + * + * Accepts any valid CSS height values, ex. `"2rem"` + */ + @Input() height: string; + + constructor(config: NgbProgressbarConfig) { + this.max = config.max; + this.animated = config.animated; + this.striped = config.striped; + this.type = config.type; + this.showValue = config.showValue; + this.height = config.height; + } + + getValue() { return getValueInRange(this.value, this.max); } + + getPercentValue() { return 100 * this.getValue() / this.max; } +} diff --git a/src/rating/rating-config.spec.ts b/src/rating/rating-config.spec.ts new file mode 100644 index 0000000..8bcce3e --- /dev/null +++ b/src/rating/rating-config.spec.ts @@ -0,0 +1,11 @@ +import {NgbRatingConfig} from './rating-config'; + +describe('ngb-rating-config', () => { + it('should have sensible default values', () => { + const config = new NgbRatingConfig(); + + expect(config.max).toBe(10); + expect(config.readonly).toBe(false); + expect(config.resettable).toBe(false); + }); +}); diff --git a/src/rating/rating-config.ts b/src/rating/rating-config.ts new file mode 100644 index 0000000..e3e581a --- /dev/null +++ b/src/rating/rating-config.ts @@ -0,0 +1,14 @@ +import {Injectable} from '@angular/core'; + +/** + * A configuration service for the [`NgbRating`](#/components/rating/api#NgbRating) component. + * + * You can inject this service, typically in your root component, and customize the values of its properties in + * order to provide default values for all the ratings used in the application. + */ +@Injectable({providedIn: 'root'}) +export class NgbRatingConfig { + max = 10; + readonly = false; + resettable = false; +} diff --git a/src/rating/rating.module.ts b/src/rating/rating.module.ts new file mode 100644 index 0000000..28d631d --- /dev/null +++ b/src/rating/rating.module.ts @@ -0,0 +1,11 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; + +import {NgbRating} from './rating'; + +export {NgbRating} from './rating'; +export {NgbRatingConfig} from './rating-config'; + +@NgModule({declarations: [NgbRating], exports: [NgbRating], imports: [CommonModule]}) +export class NgbRatingModule { +} diff --git a/src/rating/rating.spec.ts b/src/rating/rating.spec.ts new file mode 100644 index 0000000..95aebcf --- /dev/null +++ b/src/rating/rating.spec.ts @@ -0,0 +1,713 @@ +import {TestBed, ComponentFixture, inject, async, fakeAsync, tick} from '@angular/core/testing'; +import {createGenericTestComponent} from '../test/common'; +import {Key} from '../util/key'; + +import {Component, DebugElement} from '@angular/core'; +import {FormsModule, ReactiveFormsModule, FormGroup, FormControl, Validators} from '@angular/forms'; + +import {NgbRatingModule} from './rating.module'; +import {NgbRating} from './rating'; +import {NgbRatingConfig} from './rating-config'; +import {By} from '@angular/platform-browser'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +function createKeyDownEvent(key: number) { + const event = {which: key, preventDefault: () => {}}; + spyOn(event, 'preventDefault'); + return event; +} + +function getAriaState(compiled) { + const stars = getStars(compiled, '.sr-only'); + return stars.map(star => star.textContent === '(*)'); +} + +function getStar(compiled, num: number) { + return getStars(compiled)[num - 1]; +} + +function getStars(element, selector = 'span:not(.sr-only)') { + return Array.from(element.querySelectorAll(selector)); +} + +function getDbgStar(element, num: number) { + return element.queryAll(By.css('span:not(.sr-only)'))[num - 1]; +} + +function getState(element: DebugElement | HTMLElement) { + const stars = getStars(element instanceof DebugElement ? element.nativeElement : element); + return stars.map(star => star.textContent.trim() === String.fromCharCode(9733)); +} + +function getStateText(compiled) { + const stars = getStars(compiled); + return stars.map(star => star.textContent.trim()); +} + +describe('ngb-rating', () => { + beforeEach(() => { + TestBed.configureTestingModule( + {declarations: [TestComponent], imports: [NgbRatingModule, FormsModule, ReactiveFormsModule]}); + }); + + it('should initialize inputs with default values', () => { + const defaultConfig = new NgbRatingConfig(); + const rating = new NgbRating(new NgbRatingConfig(), null); + expect(rating.max).toBe(defaultConfig.max); + expect(rating.readonly).toBe(defaultConfig.readonly); + }); + + it('should show as many stars as the configured max by default', () => { + const fixture = TestBed.createComponent(NgbRating); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + + const stars = getStars(compiled); + expect(stars.length).toBe(new NgbRatingConfig().max); + }); + + it('should change the num of stars with `max`', () => { + const fixture = createTestComponent(''); + + const compiled = fixture.nativeElement; + const stars = getStars(compiled); + expect(stars.length).toBe(3); + }); + + it('initializes the default star icons as selected', () => { + const fixture = createTestComponent(''); + + const compiled = fixture.nativeElement; + expect(getState(compiled)).toEqual([true, true, true, false, false]); + }); + + it('sets stars within 0..max limits', () => { + const fixture = createTestComponent(''); + + const compiled = fixture.nativeElement; + expect(getState(compiled)).toEqual([true, true, true, false, false]); + + fixture.componentInstance.rate = 0; + fixture.detectChanges(); + expect(getState(compiled)).toEqual([false, false, false, false, false]); + + fixture.componentInstance.rate = -5; + fixture.detectChanges(); + expect(getState(compiled)).toEqual([false, false, false, false, false]); + + fixture.componentInstance.rate = 20; + fixture.detectChanges(); + expect(getState(compiled)).toEqual([true, true, true, true, true]); + }); + + it('should now fire change event initially', fakeAsync(() => { + const fixture = createTestComponent(''); + tick(); + expect(fixture.componentInstance.changed).toBeFalsy(); + })); + + it('handles correctly the click event', fakeAsync(() => { + const fixture = createTestComponent(''); + const el = fixture.debugElement; + const rating = el.query(By.directive(NgbRating)).children[0]; + + // 3/5 + expect(getState(el)).toEqual([true, true, true, false, false]); + + // enter 2 -> 2/5, rate = 3 + getDbgStar(el, 2).triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expect(getState(el)).toEqual([true, true, false, false, false]); + expect(fixture.componentInstance.rate).toBe(3); + + // click 2 -> 2/5, rate = 2 + getStar(el.nativeElement, 2).click(); + fixture.detectChanges(); + tick(); + expect(getState(el)).toEqual([true, true, false, false, false]); + expect(fixture.componentInstance.rate).toBe(2); + + // leave 2 -> 2/5, rate = 2 + rating.triggerEventHandler('mouseleave', {}); + fixture.detectChanges(); + expect(getState(el)).toEqual([true, true, false, false, false]); + expect(fixture.componentInstance.rate).toBe(2); + })); + + it('ignores the click event on a readonly rating', () => { + const fixture = createTestComponent(''); + const el = fixture.debugElement; + const rating = el.query(By.directive(NgbRating)).children[0]; + + // 3/5 + expect(getState(el)).toEqual([true, true, true, false, false]); + + // enter 2 -> 3/5 + getDbgStar(el, 2).triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expect(getState(el)).toEqual([true, true, true, false, false]); + expect(fixture.componentInstance.rate).toBe(3); + + // click 2 -> 2/5 + getStar(el.nativeElement, 2).click(); + fixture.detectChanges(); + expect(getState(el)).toEqual([true, true, true, false, false]); + expect(fixture.componentInstance.rate).toBe(3); + + // leave 2 -> 3/5 + rating.triggerEventHandler('mouseleave', {}); + fixture.detectChanges(); + expect(getState(el)).toEqual([true, true, true, false, false]); + expect(fixture.componentInstance.rate).toBe(3); + }); + + it('should not reset rating to 0 by default', fakeAsync(() => { + const fixture = createTestComponent(''); + const el = fixture.debugElement; + + // 3/5 initially + expect(getState(el)).toEqual([true, true, true, false, false]); + expect(fixture.componentInstance.rate).toBe(3); + + // click 3 -> 3/5 + getStar(el.nativeElement, 3).click(); + fixture.detectChanges(); + expect(getState(el)).toEqual([true, true, true, false, false]); + expect(fixture.componentInstance.rate).toBe(3); + })); + + it('should set `resettable` rating to 0', fakeAsync(() => { + const fixture = createTestComponent(''); + const el = fixture.debugElement; + + // 3/5 initially + expect(getState(el)).toEqual([true, true, true, false, false]); + expect(fixture.componentInstance.rate).toBe(3); + + // click 3 -> 0/5 + getStar(el.nativeElement, 3).click(); + tick(); + fixture.detectChanges(); + expect(getState(el)).toEqual([false, false, false, false, false]); + expect(fixture.componentInstance.rate).toBe(0); + + // click 2 -> 2/5 + getStar(el.nativeElement, 2).click(); + tick(); + fixture.detectChanges(); + expect(getState(el)).toEqual([true, true, false, false, false]); + expect(fixture.componentInstance.rate).toBe(2); + })); + + it('handles correctly the mouse enter/leave', () => { + const fixture = createTestComponent(''); + const el = fixture.debugElement; + const rating = el.query(By.directive(NgbRating)); + + // 3/5 + expect(getState(el)).toEqual([true, true, true, false, false]); + + // enter 1 -> 1/5, rate = 3 + getDbgStar(el, 1).triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expect(getState(el)).toEqual([true, false, false, false, false]); + expect(fixture.componentInstance.rate).toBe(3); + + // leave -> 3/5, rate = 3 + rating.triggerEventHandler('mouseleave', {}); + fixture.detectChanges(); + expect(getState(el)).toEqual([true, true, true, false, false]); + expect(fixture.componentInstance.rate).toBe(3); + + // enter 5 -> 5/5, rate = 3 + getDbgStar(el, 5).triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expect(getState(el)).toEqual([true, true, true, true, true]); + expect(fixture.componentInstance.rate).toBe(3); + + // enter 4 -> 4/5, rate = 3 + getDbgStar(el, 4).triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expect(getState(el)).toEqual([true, true, true, true, false]); + expect(fixture.componentInstance.rate).toBe(3); + }); + + it('handles correctly the mouse enter/leave on readonly rating', () => { + const fixture = createTestComponent(''); + const el = fixture.debugElement; + const rating = el.query(By.directive(NgbRating)).children[0]; + + // 3/5 + expect(getState(el)).toEqual([true, true, true, false, false]); + + // enter 1 -> 3/5, rate = 3 + getDbgStar(el, 1).triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expect(getState(el)).toEqual([true, true, true, false, false]); + expect(fixture.componentInstance.rate).toBe(3); + + // leave -> 3/5, rate = 3 + rating.triggerEventHandler('mouseleave', {}); + fixture.detectChanges(); + expect(getState(el)).toEqual([true, true, true, false, false]); + expect(fixture.componentInstance.rate).toBe(3); + + // enter 5 -> 3/5, rate = 3 + getDbgStar(el, 5).triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expect(getState(el)).toEqual([true, true, true, false, false]); + expect(fixture.componentInstance.rate).toBe(3); + + // enter 4 -> 3/5, rate = 3 + getDbgStar(el, 4).triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expect(getState(el)).toEqual([true, true, true, false, false]); + expect(fixture.componentInstance.rate).toBe(3); + }); + + it('should set pointer cursor on stars when not readonly', () => { + const fixture = TestBed.createComponent(NgbRating); + fixture.detectChanges(); + + const compiled = fixture.nativeElement; + + expect(window.getComputedStyle(getStar(compiled, 1)).getPropertyValue('cursor')).toBe('pointer'); + }); + + it('should set default cursor on stars when readonly', () => { + const fixture = createTestComponent(''); + + const compiled = fixture.nativeElement; + + expect(window.getComputedStyle(getStar(compiled, 1)).getPropertyValue('cursor')).toBe('default'); + }); + + it('should allow custom star template', () => { + const fixture = createTestComponent(` + {{ fill === 100 ? 'x' : 'o' }} + `); + + const compiled = fixture.nativeElement; + expect(getStateText(compiled)).toEqual(['x', 'x', 'o', 'o']); + }); + + it('should allow custom template as a child element', () => { + const fixture = createTestComponent(` + + {{ fill === 100 ? 'x' : 'o' }} + `); + + const compiled = fixture.nativeElement; + expect(getStateText(compiled)).toEqual(['x', 'x', 'o', 'o']); + }); + + it('should prefer explicitly set custom template to a child one', () => { + const fixture = createTestComponent(` + {{ fill === 100 ? 'a' : 'b' }} + + {{ fill === 100 ? 'c' : 'd' }} + `); + + const compiled = fixture.nativeElement; + expect(getStateText(compiled)).toEqual(['a', 'a', 'b', 'b']); + }); + + it('should calculate fill percentage correctly', () => { + const fixture = createTestComponent(` + {{fill}} + `); + + const compiled = fixture.nativeElement; + expect(getStateText(compiled)).toEqual(['100', '100', '100', '0']); + + fixture.componentInstance.rate = 0; + fixture.detectChanges(); + expect(getStateText(compiled)).toEqual(['0', '0', '0', '0']); + + fixture.componentInstance.rate = 2.2; + fixture.detectChanges(); + expect(getStateText(compiled)).toEqual(['100', '100', '20', '0']); + + fixture.componentInstance.rate = 2.25; + fixture.detectChanges(); + expect(getStateText(compiled)).toEqual(['100', '100', '25', '0']); + + fixture.componentInstance.rate = 2.2548; + fixture.detectChanges(); + expect(getStateText(compiled)).toEqual(['100', '100', '25', '0']); + + fixture.componentInstance.rate = 7; + fixture.detectChanges(); + expect(getStateText(compiled)).toEqual(['100', '100', '100', '100']); + }); + + it('should allow custom star template based on index', () => { + const fixture = createTestComponent(` + {{ index === 1 ? 'x' : 'o' }} + `); + + const compiled = fixture.nativeElement; + expect(getStateText(compiled)).toEqual(['o', 'x', 'o', 'o']); + }); + + it('should allow custom template based on index as a child element', () => { + const fixture = createTestComponent(` + + {{ index === 1 ? 'x' : 'o' }} + `); + + const compiled = fixture.nativeElement; + expect(getStateText(compiled)).toEqual(['o', 'x', 'o', 'o']); + }); + + it('should prefer explicitly set custom template based on index to a child one', () => { + const fixture = createTestComponent(` + {{ index === 1 ? 'a' : 'b' }} + + {{ index === 1 ? 'c' : 'd' }} + `); + + const compiled = fixture.nativeElement; + expect(getStateText(compiled)).toEqual(['b', 'a', 'b', 'b']); + }); + + describe('aria support', () => { + it('contains aria-valuemax with the number of stars', () => { + const fixture = createTestComponent(''); + + const rating = fixture.debugElement.query(By.directive(NgbRating)); + + expect(rating.attributes['aria-valuemax']).toBe('10'); + }); + + it('contains aria-valuemin', () => { + const fixture = createTestComponent(''); + + const rating = fixture.debugElement.query(By.directive(NgbRating)); + + expect(rating.attributes['aria-valuemin']).toBe('0'); + }); + + it('contains a hidden span for each star for screenreaders', () => { + const fixture = createTestComponent(''); + + const compiled = fixture.nativeElement; + const hiddenStars = getStars(compiled, '.sr-only'); + + expect(hiddenStars.length).toBe(5); + }); + + it('initializes populates the current rate for screenreaders', () => { + const fixture = createTestComponent(''); + + const compiled = fixture.nativeElement; + expect(getAriaState(compiled)).toEqual([true, true, true, false, false]); + }); + + it('contains aria-valuenow with the current rate', () => { + const fixture = createTestComponent(''); + + const rating = fixture.debugElement.query(By.directive(NgbRating)); + + expect(rating.attributes['aria-valuenow']).toBe('3'); + }); + + it('updates aria-valuenow when the rate changes', () => { + const fixture = createTestComponent(''); + + const rating = fixture.debugElement.query(By.directive(NgbRating)); + + getStar(rating.nativeElement, 7).click(); + fixture.detectChanges(); + + expect(rating.attributes['aria-valuenow']).toBe('7'); + }); + + it('updates aria-valuetext when the rate changes', () => { + const fixture = createTestComponent(''); + + const rating = fixture.debugElement.query(By.directive(NgbRating)); + + getStar(rating.nativeElement, 7).click(); + fixture.detectChanges(); + + expect(rating.attributes['aria-valuetext']).toBe('7 out of 10'); + }); + + it('updates aria-disabled when readonly', () => { + const fixture = createTestComponent(''); + let ratingEl = fixture.debugElement.query(By.directive(NgbRating)); + fixture.detectChanges(); + expect(ratingEl.attributes['aria-disabled'] == null).toBeTruthy(); + + let ratingComp = ratingEl.componentInstance; + ratingComp.readonly = true; + fixture.detectChanges(); + expect(ratingEl.attributes['aria-disabled']).toBe('true'); + }); + }); + + describe('keyboard support', () => { + + it('should handle arrow keys', () => { + const fixture = createTestComponent(''); + + const element = fixture.debugElement.query(By.directive(NgbRating)); + + // right -> +1 + let event = createKeyDownEvent(Key.ArrowRight); + element.triggerEventHandler('keydown', event); + fixture.detectChanges(); + expect(getState(element.nativeElement)).toEqual([true, true, true, true, false]); + expect(event.preventDefault).toHaveBeenCalled(); + + // up -> +1 + event = createKeyDownEvent(Key.ArrowUp); + element.triggerEventHandler('keydown', event); + fixture.detectChanges(); + expect(getState(element.nativeElement)).toEqual([true, true, true, true, true]); + expect(event.preventDefault).toHaveBeenCalled(); + + // left -> -1 + event = createKeyDownEvent(Key.ArrowLeft); + element.triggerEventHandler('keydown', event); + fixture.detectChanges(); + expect(getState(element.nativeElement)).toEqual([true, true, true, true, false]); + expect(event.preventDefault).toHaveBeenCalled(); + + // down -> -1 + event = createKeyDownEvent(Key.ArrowDown); + element.triggerEventHandler('keydown', event); + fixture.detectChanges(); + expect(getState(element.nativeElement)).toEqual([true, true, true, false, false]); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should handle home/end keys', () => { + const fixture = createTestComponent(''); + + const element = fixture.debugElement.query(By.directive(NgbRating)); + + // home -> 0 + let event = createKeyDownEvent(Key.Home); + element.triggerEventHandler('keydown', event); + fixture.detectChanges(); + expect(getState(element.nativeElement)).toEqual([false, false, false, false, false]); + expect(event.preventDefault).toHaveBeenCalled(); + + // end -> max + event = createKeyDownEvent(Key.End); + element.triggerEventHandler('keydown', event); + fixture.detectChanges(); + expect(getState(element.nativeElement)).toEqual([true, true, true, true, true]); + expect(event.preventDefault).toHaveBeenCalled(); + }); + }); + + describe('forms', () => { + + it('should work with template-driven form validation', async(() => { + const html = ` +
+ +
`; + + const fixture = createTestComponent(html); + const element = fixture.debugElement.query(By.directive(NgbRating)); + + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + expect(getState(element.nativeElement)).toEqual([false, false, false, false, false]); + expect(element.nativeElement).toHaveCssClass('ng-invalid'); + expect(element.nativeElement).toHaveCssClass('ng-untouched'); + + fixture.componentInstance.model = 1; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + fixture.detectChanges(); + expect(getState(element.nativeElement)).toEqual([true, false, false, false, false]); + expect(element.nativeElement).toHaveCssClass('ng-valid'); + expect(element.nativeElement).toHaveCssClass('ng-untouched'); + + fixture.componentInstance.model = 0; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + fixture.detectChanges(); + expect(getState(element.nativeElement)).toEqual([false, false, false, false, false]); + expect(element.nativeElement).toHaveCssClass('ng-valid'); + expect(element.nativeElement).toHaveCssClass('ng-untouched'); + }); + })); + + it('should work with reactive form validation', () => { + const html = ` +
+ +
`; + + const fixture = createTestComponent(html); + const element = fixture.debugElement.query(By.directive(NgbRating)); + + expect(getState(element.nativeElement)).toEqual([false, false, false, false, false]); + expect(element.nativeElement).toHaveCssClass('ng-invalid'); + expect(element.nativeElement).toHaveCssClass('ng-untouched'); + + fixture.componentInstance.form.patchValue({rating: 3}); + fixture.detectChanges(); + expect(getState(element.nativeElement)).toEqual([true, true, true, false, false]); + expect(element.nativeElement).toHaveCssClass('ng-valid'); + expect(element.nativeElement).toHaveCssClass('ng-untouched'); + + fixture.componentInstance.form.patchValue({rating: 0}); + fixture.detectChanges(); + expect(getState(element.nativeElement)).toEqual([false, false, false, false, false]); + expect(element.nativeElement).toHaveCssClass('ng-valid'); + expect(element.nativeElement).toHaveCssClass('ng-untouched'); + }); + + it('should handle clicks and update form control', () => { + const html = ` +
+ +
`; + + const fixture = createTestComponent(html); + const element = fixture.debugElement.query(By.directive(NgbRating)); + + expect(getState(element.nativeElement)).toEqual([false, false, false, false, false]); + expect(element.nativeElement).toHaveCssClass('ng-invalid'); + expect(element.nativeElement).toHaveCssClass('ng-untouched'); + + getStar(element.nativeElement, 3).click(); + fixture.detectChanges(); + expect(getState(element.nativeElement)).toEqual([true, true, true, false, false]); + expect(element.nativeElement).toHaveCssClass('ng-valid'); + expect(element.nativeElement).toHaveCssClass('ng-touched'); + }); + + it('should work with both rate input and form control', fakeAsync(() => { + const html = ` +
+ +
`; + + const fixture = createTestComponent(html); + const element = fixture.debugElement.query(By.directive(NgbRating)); + + expect(getState(element.nativeElement)).toEqual([false, false, false, false, false]); + expect(element.nativeElement).toHaveCssClass('ng-invalid'); + + getStar(element.nativeElement, 2).click(); + fixture.detectChanges(); + tick(); + expect(getState(element.nativeElement)).toEqual([true, true, false, false, false]); + expect(fixture.componentInstance.rate).toBe(2); + expect(element.nativeElement).toHaveCssClass('ng-valid'); + + fixture.componentInstance.rate = 4; + fixture.detectChanges(); + tick(); + expect(getState(element.nativeElement)).toEqual([true, true, true, true, false]); + expect(fixture.componentInstance.form.get('rating').value).toBe(4); + expect(element.nativeElement).toHaveCssClass('ng-valid'); + })); + + it('should disable widget when a control is disabled', fakeAsync(() => { + const html = ` +
+ +
`; + + const fixture = createTestComponent(html); + const element = fixture.debugElement.query(By.directive(NgbRating)); + + expect(getState(element.nativeElement)).toEqual([false, false, false, false, false]); + expect(fixture.componentInstance.form.get('rating').disabled).toBeFalsy(); + + fixture.componentInstance.form.get('rating').disable(); + fixture.detectChanges(); + expect(fixture.componentInstance.form.get('rating').disabled).toBeTruthy(); + + getStar(element.nativeElement, 3).click(); + fixture.detectChanges(); + expect(getState(element.nativeElement)).toEqual([false, false, false, false, false]); + })); + + it('should mark control as touched on blur', fakeAsync(() => { + const html = ` +
+ +
`; + + const fixture = createTestComponent(html); + const element = fixture.debugElement.query(By.directive(NgbRating)); + + expect(getState(element.nativeElement)).toEqual([false, false, false, false, false]); + expect(element.nativeElement).toHaveCssClass('ng-untouched'); + + element.triggerEventHandler('blur', {}); + fixture.detectChanges(); + expect(getState(element.nativeElement)).toEqual([false, false, false, false, false]); + expect(element.nativeElement).toHaveCssClass('ng-touched'); + })); + }); + + describe('Custom config', () => { + let config: NgbRatingConfig; + + beforeEach(() => { TestBed.configureTestingModule({imports: [NgbRatingModule]}); }); + + beforeEach(inject([NgbRatingConfig], (c: NgbRatingConfig) => { + config = c; + config.max = 5; + config.readonly = true; + })); + + it('should initialize inputs with provided config', () => { + const fixture = TestBed.createComponent(NgbRating); + fixture.detectChanges(); + + let rating = fixture.componentInstance; + expect(rating.max).toBe(config.max); + expect(rating.readonly).toBe(config.readonly); + }); + }); + + describe('Custom config as provider', () => { + let config = new NgbRatingConfig(); + config.max = 5; + config.readonly = true; + + beforeEach(() => { + TestBed.configureTestingModule( + {imports: [NgbRatingModule], providers: [{provide: NgbRatingConfig, useValue: config}]}); + }); + + it('should initialize inputs with provided config as provider', () => { + const fixture = TestBed.createComponent(NgbRating); + fixture.detectChanges(); + + let rating = fixture.componentInstance; + expect(rating.max).toBe(config.max); + expect(rating.readonly).toBe(config.readonly); + }); + }); +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + changed = false; + form = new FormGroup({rating: new FormControl(null, Validators.required)}); + max = 10; + model; + rate = 3; +} diff --git a/src/rating/rating.ts b/src/rating/rating.ts new file mode 100644 index 0000000..b7299e3 --- /dev/null +++ b/src/rating/rating.ts @@ -0,0 +1,231 @@ +import { + Component, + ChangeDetectionStrategy, + Input, + Output, + EventEmitter, + OnInit, + TemplateRef, + OnChanges, + SimpleChanges, + ContentChild, + forwardRef, + ChangeDetectorRef +} from '@angular/core'; +import {NgbRatingConfig} from './rating-config'; +import {getValueInRange} from '../util/util'; +import {Key} from '../util/key'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; + +/** + * The context for the custom star display template defined in the `starTemplate`. + */ +export interface StarTemplateContext { + /** + * The star fill percentage, an integer in the `[0, 100]` range. + */ + fill: number; + + /** + * Index of the star, starts with `0`. + */ + index: number; +} + +const NGB_RATING_VALUE_ACCESSOR = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NgbRating), + multi: true +}; + +/** + * A directive that helps visualising and interacting with a star rating bar. + */ +@Component({ + selector: 'ngb-rating', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + 'class': 'd-inline-flex', + 'tabindex': '0', + 'role': 'slider', + 'aria-valuemin': '0', + '[attr.aria-valuemax]': 'max', + '[attr.aria-valuenow]': 'nextRate', + '[attr.aria-valuetext]': 'ariaValueText()', + '[attr.aria-disabled]': 'readonly ? true : null', + '(blur)': 'handleBlur()', + '(keydown)': 'handleKeyDown($event)', + '(mouseleave)': 'reset()' + }, + template: ` + {{ fill === 100 ? '★' : '☆' }} + + ({{ index < nextRate ? '*' : ' ' }}) + + + + + + `, + providers: [NGB_RATING_VALUE_ACCESSOR] +}) +export class NgbRating implements ControlValueAccessor, + OnInit, OnChanges { + contexts: StarTemplateContext[] = []; + disabled = false; + nextRate: number; + + + /** + * The maximal rating that can be given. + */ + @Input() max: number; + + /** + * The current rating. Could be a decimal value like `3.75`. + */ + @Input() rate: number; + + /** + * If `true`, the rating can't be changed. + */ + @Input() readonly: boolean; + + /** + * If `true`, the rating can be reset to `0` by mouse clicking currently set rating. + */ + @Input() resettable: boolean; + + /** + * The template to override the way each star is displayed. + * + * Alternatively put an `` as the only child of your `` element + */ + @Input() starTemplate: TemplateRef; + @ContentChild(TemplateRef, {static: false}) starTemplateFromContent: TemplateRef; + + /** + * An event emitted when the user is hovering over a given rating. + * + * Event payload equals to the rating being hovered over. + */ + @Output() hover = new EventEmitter(); + + /** + * An event emitted when the user stops hovering over a given rating. + * + * Event payload equals to the rating of the last item being hovered over. + */ + @Output() leave = new EventEmitter(); + + /** + * An event emitted when the user selects a new rating. + * + * Event payload equals to the newly selected rating. + */ + @Output() rateChange = new EventEmitter(true); + + onChange = (_: any) => {}; + onTouched = () => {}; + + constructor(config: NgbRatingConfig, private _changeDetectorRef: ChangeDetectorRef) { + this.max = config.max; + this.readonly = config.readonly; + } + + ariaValueText() { return `${this.nextRate} out of ${this.max}`; } + + enter(value: number): void { + if (!this.readonly && !this.disabled) { + this._updateState(value); + } + this.hover.emit(value); + } + + handleBlur() { this.onTouched(); } + + handleClick(value: number) { this.update(this.resettable && this.rate === value ? 0 : value); } + + handleKeyDown(event: KeyboardEvent) { + // tslint:disable-next-line:deprecation + switch (event.which) { + case Key.ArrowDown: + case Key.ArrowLeft: + this.update(this.rate - 1); + break; + case Key.ArrowUp: + case Key.ArrowRight: + this.update(this.rate + 1); + break; + case Key.Home: + this.update(0); + break; + case Key.End: + this.update(this.max); + break; + default: + return; + } + + // note 'return' in default case + event.preventDefault(); + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['rate']) { + this.update(this.rate); + } + } + + ngOnInit(): void { + this.contexts = Array.from({length: this.max}, (v, k) => ({fill: 0, index: k})); + this._updateState(this.rate); + } + + registerOnChange(fn: (value: any) => any): void { this.onChange = fn; } + + registerOnTouched(fn: () => any): void { this.onTouched = fn; } + + reset(): void { + this.leave.emit(this.nextRate); + this._updateState(this.rate); + } + + setDisabledState(isDisabled: boolean) { this.disabled = isDisabled; } + + update(value: number, internalChange = true): void { + const newRate = getValueInRange(value, this.max, 0); + if (!this.readonly && !this.disabled && this.rate !== newRate) { + this.rate = newRate; + this.rateChange.emit(this.rate); + } + if (internalChange) { + this.onChange(this.rate); + this.onTouched(); + } + this._updateState(this.rate); + } + + writeValue(value) { + this.update(value, false); + this._changeDetectorRef.markForCheck(); + } + + private _getFillValue(index: number): number { + const diff = this.nextRate - index; + + if (diff >= 1) { + return 100; + } + if (diff < 1 && diff > 0) { + return parseInt((diff * 100).toFixed(2), 10); + } + + return 0; + } + + private _updateState(nextValue: number) { + this.nextRate = nextValue; + this.contexts.forEach((context, index) => context.fill = this._getFillValue(index)); + } +} diff --git a/src/tabset/tabset-config.spec.ts b/src/tabset/tabset-config.spec.ts new file mode 100644 index 0000000..a6d395d --- /dev/null +++ b/src/tabset/tabset-config.spec.ts @@ -0,0 +1,10 @@ +import {NgbTabsetConfig} from './tabset-config'; + +describe('ngb-tabset-config', () => { + it('should have sensible default values', () => { + const config = new NgbTabsetConfig(); + + expect(config.type).toBe('tabs'); + expect(config.justify).toBe('start'); + }); +}); diff --git a/src/tabset/tabset-config.ts b/src/tabset/tabset-config.ts new file mode 100644 index 0000000..e751879 --- /dev/null +++ b/src/tabset/tabset-config.ts @@ -0,0 +1,14 @@ +import {Injectable} from '@angular/core'; + +/** + * A configuration service for the [`NgbTabset`](#/components/tabset/api#NgbTabset) component. + * + * You can inject this service, typically in your root component, and customize the values of its properties in + * order to provide default values for all the tabsets used in the application. + */ +@Injectable({providedIn: 'root'}) +export class NgbTabsetConfig { + justify: 'start' | 'center' | 'end' | 'fill' | 'justified' = 'start'; + orientation: 'horizontal' | 'vertical' = 'horizontal'; + type: 'tabs' | 'pills' = 'tabs'; +} diff --git a/src/tabset/tabset.module.ts b/src/tabset/tabset.module.ts new file mode 100644 index 0000000..fc89cda --- /dev/null +++ b/src/tabset/tabset.module.ts @@ -0,0 +1,13 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; + +import {NgbTabset, NgbTab, NgbTabContent, NgbTabTitle} from './tabset'; + +export {NgbTabset, NgbTab, NgbTabContent, NgbTabTitle, NgbTabChangeEvent} from './tabset'; +export {NgbTabsetConfig} from './tabset-config'; + +const NGB_TABSET_DIRECTIVES = [NgbTabset, NgbTab, NgbTabContent, NgbTabTitle]; + +@NgModule({declarations: NGB_TABSET_DIRECTIVES, exports: NGB_TABSET_DIRECTIVES, imports: [CommonModule]}) +export class NgbTabsetModule { +} diff --git a/src/tabset/tabset.spec.ts b/src/tabset/tabset.spec.ts new file mode 100644 index 0000000..e9dbccb --- /dev/null +++ b/src/tabset/tabset.spec.ts @@ -0,0 +1,593 @@ +import {TestBed, ComponentFixture, inject} from '@angular/core/testing'; +import {createGenericTestComponent} from '../test/common'; + +import {Component} from '@angular/core'; + +import {NgbTabsetModule} from './tabset.module'; +import {NgbTabsetConfig} from './tabset-config'; +import {NgbTabset} from './tabset'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +function getTabTitles(nativeEl: HTMLElement) { + return nativeEl.querySelectorAll('.nav-link'); +} + +function getTabContent(nativeEl: HTMLElement) { + return nativeEl.querySelectorAll('.tab-content .tab-pane'); +} + +function expectTabs(nativeEl: HTMLElement, active: boolean[], disabled?: boolean[]) { + const tabTitles = getTabTitles(nativeEl); + const tabContent = getTabContent(nativeEl); + const anyTabsActive = active.reduce((prev, curr) => prev || curr, false); + + expect(tabTitles.length).toBe(active.length); + expect(tabContent.length).toBe(anyTabsActive ? 1 : 0); // only 1 tab content in DOM at a time + + if (disabled) { + expect(disabled.length).toBe(active.length); + } else { + disabled = new Array(active.length); // tabs are not disabled by default + } + + for (let i = 0; i < active.length; i++) { + if (active[i]) { + expect(tabTitles[i]).toHaveCssClass('active'); + } else { + expect(tabTitles[i]).not.toHaveCssClass('active'); + } + + if (disabled[i]) { + expect(tabTitles[i]).toHaveCssClass('disabled'); + expect(tabTitles[i].getAttribute('aria-disabled')).toBe('true'); + expect(tabTitles[i].getAttribute('tabindex')).toBe('-1'); + } else { + expect(tabTitles[i]).not.toHaveCssClass('disabled'); + expect(tabTitles[i].getAttribute('aria-disabled')).toBe('false'); + expect(tabTitles[i].getAttribute('tabindex')).toBeNull(); + } + } +} + +function getButton(nativeEl: HTMLElement) { + return nativeEl.querySelectorAll('button'); +} + +describe('ngb-tabset', () => { + beforeEach(() => { TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbTabsetModule]}); }); + + it('should initialize inputs with default values', () => { + const defaultConfig = new NgbTabsetConfig(); + const tabset = new NgbTabset(new NgbTabsetConfig()); + expect(tabset.type).toBe(defaultConfig.type); + }); + + it('should render tabs and select first tab as active by default', () => { + const fixture = createTestComponent(` + + Foo + Bar + + `); + + const tabTitles = getTabTitles(fixture.nativeElement); + const tabContent = getTabContent(fixture.nativeElement); + + expect(tabTitles[0].textContent).toMatch(/foo/); + expect(tabTitles[1].textContent).toMatch(/bar/); + expect(tabContent.length).toBe(1); + expect(tabContent[0].textContent).toMatch(/Foo/); + + expectTabs(fixture.nativeElement, [true, false]); + }); + + it('should have aria attributes', () => { + const fixture = createTestComponent(` + + Foo + Bar + + `); + + const compiled: HTMLElement = fixture.nativeElement; + const tabTitles = getTabTitles(compiled); + const tabContent = getTabContent(compiled); + + expect(tabTitles[0].getAttribute('role')).toBe('tab'); + expect(tabTitles[0].getAttribute('aria-selected')).toBe('true'); + expect(tabTitles[0].getAttribute('aria-controls')).toBe(tabContent[0].getAttribute('id')); + + expect(tabContent[0].getAttribute('role')).toBe('tabpanel'); + expect(tabContent[0].getAttribute('aria-labelledby')).toBe(tabTitles[0].id); + + expect(tabTitles[1].getAttribute('role')).toBe('tab'); + expect(tabTitles[1].getAttribute('aria-selected')).toBe('false'); + expect(tabTitles[1].getAttribute('aria-controls')).toBeNull(); + }); + + it('should remove aria-controls when tab content is not in DOM', () => { + const fixture = createTestComponent(` + + Foo + Bar + + `); + + const compiled: HTMLElement = fixture.nativeElement; + const tabTitles = getTabTitles(compiled); + const tabContent = getTabContent(compiled); + + expect(tabTitles[0].getAttribute('aria-controls')).toBe(tabContent[0].getAttribute('id')); + expect(tabTitles[0].getAttribute('aria-selected')).toBe('true'); + + expect(tabTitles[1].getAttribute('aria-controls')).toBeNull(); + expect(tabContent[1]).toBeUndefined(); + }); + + it('should have aria-controls and aria-selected when tab content is hidden', () => { + const fixture = createTestComponent(` + + Foo + Bar + + `); + + const compiled: HTMLElement = fixture.nativeElement; + const tabTitles = getTabTitles(compiled); + const tabContent = getTabContent(compiled); + + expect(tabTitles[0].getAttribute('aria-controls')).toBe(tabContent[0].id); + expect(tabTitles[0].getAttribute('aria-selected')).toBe('true'); + + expect(tabTitles[1].getAttribute('aria-controls')).toBe(tabContent[1].id); + expect(tabTitles[1].getAttribute('aria-selected')).toBe('false'); + }); + + it('should allow mix of text and HTML in tab titles', () => { + const fixture = createTestComponent(` + + Foo + + bar + Bar + + + baz + Baz + + + `); + + const tabTitles = getTabTitles(fixture.nativeElement); + + expect(tabTitles[0].textContent).toMatch(/foo/); + expect(tabTitles[1].innerHTML).toMatch(/bar<\/b>/); + expect(tabTitles[2].textContent).toMatch(/bazbaz/); + }); + + it('should not pick up titles from nested tabsets', () => { + const testHtml = ` + + + + + + child + + + + + + + `; + const fixture = createTestComponent(testHtml); + // additional change detection is required to reproduce the problem in the test environment + fixture.detectChanges(); + + const titles = getTabTitles(fixture.nativeElement); + const parentTitle = titles[0].textContent.trim(); + const childTitle = titles[1].textContent.trim(); + + expect(parentTitle).toContain('parent'); + expect(parentTitle).not.toContain('child'); + expect(childTitle).toContain('child'); + expect(childTitle).not.toContain('parent'); + }); + + + it('should not crash for empty tabsets', () => { + const fixture = createTestComponent(``); + expectTabs(fixture.nativeElement, []); + }); + + it('should not crash for tabsets with empty tab content', () => { + const fixture = createTestComponent(``); + expectTabs(fixture.nativeElement, [true]); + }); + + + it('should mark the requested tab as active', () => { + const fixture = createTestComponent(` + + Foo + Bar + + `); + + expectTabs(fixture.nativeElement, [false, true]); + }); + + + it('should auto-correct requested active tab id', () => { + const fixture = createTestComponent(` + + Foo + Bar + + `); + + expectTabs(fixture.nativeElement, [true, false]); + }); + + + it('should auto-correct requested active tab id for undefined ids', () => { + const fixture = createTestComponent(` + + Foo + Bar + + `); + + expectTabs(fixture.nativeElement, [true, false]); + }); + + + it('should change active tab on tab title click', () => { + const fixture = createTestComponent(` + + Foo + Bar + + `); + + const tabTitles = getTabTitles(fixture.nativeElement); + + (tabTitles[1]).click(); + fixture.detectChanges(); + expectTabs(fixture.nativeElement, [false, true]); + + (tabTitles[0]).click(); + fixture.detectChanges(); + expectTabs(fixture.nativeElement, [true, false]); + }); + + + it('should support disabled tabs', () => { + const fixture = createTestComponent(` + + Foo + Bar + + `); + + expectTabs(fixture.nativeElement, [true, false], [false, true]); + }); + + + it('should not change active tab on disabled tab title click', () => { + const fixture = createTestComponent(` + + Foo + Bar + + `); + + expectTabs(fixture.nativeElement, [true, false], [false, true]); + + (getTabTitles(fixture.nativeElement)[1]).click(); + fixture.detectChanges(); + expectTabs(fixture.nativeElement, [true, false], [false, true]); + }); + + + it('should allow initially active and disabled tabs', () => { + const fixture = createTestComponent(` + + Bar + + `); + + expectTabs(fixture.nativeElement, [true], [true]); + }); + + + it('should have nav-tabs default', () => { + const fixture = createTestComponent(` + + Bar + + `); + + expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('nav-tabs'); + expect(fixture.nativeElement.querySelector('ul')).not.toHaveCssClass('nav-pills'); + }); + + + it('should have pills upon setting pills', () => { + const fixture = createTestComponent(` + + Bar + + `); + + expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('nav-pills'); + expect(fixture.nativeElement.querySelector('ul')).not.toHaveCssClass('nav-tabs'); + }); + + it('should allow arbitrary nav type', () => { + const fixture = createTestComponent(` + + Bar + + `); + + expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('nav-bordered'); + expect(fixture.nativeElement.querySelector('ul')).not.toHaveCssClass('nav-pills'); + expect(fixture.nativeElement.querySelector('ul')).not.toHaveCssClass('nav-tabs'); + }); + + it('should have the "justify-content-start" class by default', () => { + const fixture = createTestComponent(` + + Bar + + `); + + expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('justify-content-start'); + }); + + it('should have the "justify-content-center" class upon setting justify to center', () => { + const fixture = createTestComponent(` + + Bar + + `); + + expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('justify-content-center'); + }); + + it('should have the "justify-content-end" upon setting justify to end', () => { + const fixture = createTestComponent(` + + Bar + + `); + + expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('justify-content-end'); + }); + + it('should have the "nav-fill" class upon setting justify to fill', () => { + const fixture = createTestComponent(` + + Bar + + `); + + expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('nav-fill'); + }); + + it('should have the "nav-justified" class upon setting justify to justified', () => { + const fixture = createTestComponent(` + + Bar + + `); + + expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('nav-justified'); + }); + + it('should have the "justify-content-start" class upon setting orientation to horizontal', () => { + const fixture = createTestComponent(` + + Bar + + `); + + expect(fixture.nativeElement.querySelector('ul')).not.toHaveCssClass('flex-column'); + expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('justify-content-start'); + }); + + it('should have the "flex-column" class upon setting orientation to vertical', () => { + const fixture = createTestComponent(` + + Bar + + `); + + expect(fixture.nativeElement.querySelector('ul')).toHaveCssClass('flex-column'); + expect(fixture.nativeElement.querySelector('ul')).not.toHaveCssClass('justify-content-start'); + }); + + + it('should change active tab by calling select on an exported directive instance', () => { + const fixture = createTestComponent(` + + Foo + Bar + + + + `); + + const button = getButton(fixture.nativeElement); + + // Click on a button to select the second tab + (button[1]).click(); + fixture.detectChanges(); + expectTabs(fixture.nativeElement, [false, true]); + + // Click on a button to select the first tab + (button[0]).click(); + fixture.detectChanges(); + expectTabs(fixture.nativeElement, [true, false]); + }); + + + it('should not change active tab by calling select on an exported directive instance in case of disable tab', () => { + const fixture = createTestComponent(` + + Foo + Bar + + + `); + + const button = getButton(fixture.nativeElement); + + // Click on a button to select the second disabled tab (should not change active tab). + (button[0]).click(); + fixture.detectChanges(); + expectTabs(fixture.nativeElement, [true, false], [false, true]); + }); + + it('should not remove inactive tabs content from DOM with `destroyOnHide` flag', () => { + const fixture = createTestComponent(` + + Foo + Bar + + + `); + + const button = getButton(fixture.nativeElement); + + // Click on a button to select the second tab + (button[0]).click(); + fixture.detectChanges(); + let tabContents = getTabContent(fixture.nativeElement); + expect(tabContents.length).toBe(2); + expect(tabContents[1]).toHaveCssClass('active'); + }); + + it('should emit tab change event when switching tabs', () => { + const fixture = createTestComponent(` + + First + Second + + + + `); + + const button = getButton(fixture.nativeElement); + + spyOn(fixture.componentInstance, 'changeCallback'); + + // Select the second tab -> change event + (button[1]).click(); + fixture.detectChanges(); + expect(fixture.componentInstance.changeCallback) + .toHaveBeenCalledWith(jasmine.objectContaining({activeId: 'first', nextId: 'second'})); + + // Select the first tab again -> change event + (button[0]).click(); + fixture.detectChanges(); + expect(fixture.componentInstance.changeCallback) + .toHaveBeenCalledWith(jasmine.objectContaining({activeId: 'second', nextId: 'first'})); + }); + + it('should not emit tab change event when selecting currently active and disabled tabs', () => { + const fixture = createTestComponent(` + + First + Second + + + + `); + + const button = getButton(fixture.nativeElement); + + spyOn(fixture.componentInstance, 'changeCallback'); + + // Select the currently active tab -> no change event + (button[0]).click(); + fixture.detectChanges(); + expect(fixture.componentInstance.changeCallback).not.toHaveBeenCalled(); + + // Select the disabled tab -> no change event + (button[1]).click(); + fixture.detectChanges(); + expect(fixture.componentInstance.changeCallback).not.toHaveBeenCalled(); + }); + + it('should cancel tab change when preventDefault() is called', () => { + const fixture = createTestComponent(` + + First + Second + + + + `); + + const button = getButton(fixture.nativeElement); + + let changeEvent = null; + fixture.componentInstance.changeCallback = (event) => { + changeEvent = event; + event.preventDefault(); + }; + + // Select the second tab -> selection will be canceled + (button[1]).click(); + fixture.detectChanges(); + expect(changeEvent).toEqual(jasmine.objectContaining({activeId: 'first', nextId: 'second'})); + expectTabs(fixture.nativeElement, [true, false]); + }); + + describe('Custom config', () => { + let config: NgbTabsetConfig; + + beforeEach(() => { TestBed.configureTestingModule({imports: [NgbTabsetModule]}); }); + + beforeEach(inject([NgbTabsetConfig], (c: NgbTabsetConfig) => { + config = c; + config.type = 'pills'; + })); + + it('should initialize inputs with provided config', () => { + const fixture = TestBed.createComponent(NgbTabset); + fixture.detectChanges(); + + let tabset = fixture.componentInstance; + expect(tabset.type).toBe(config.type); + }); + }); + + describe('Custom config as provider', () => { + let config = new NgbTabsetConfig(); + config.type = 'pills'; + + beforeEach(() => { + TestBed.configureTestingModule( + {imports: [NgbTabsetModule], providers: [{provide: NgbTabsetConfig, useValue: config}]}); + }); + + it('should initialize inputs with provided config as provider', () => { + const fixture = TestBed.createComponent(NgbTabset); + fixture.detectChanges(); + + let tabset = fixture.componentInstance; + expect(tabset.type).toBe(config.type); + }); + }); +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + activeTabId: string; + changeCallback = (event: any) => {}; +} diff --git a/src/tabset/tabset.ts b/src/tabset/tabset.ts new file mode 100644 index 0000000..356cac9 --- /dev/null +++ b/src/tabset/tabset.ts @@ -0,0 +1,210 @@ +import { + Component, + Input, + ContentChildren, + QueryList, + Directive, + TemplateRef, + AfterContentChecked, + Output, + EventEmitter +} from '@angular/core'; +import {NgbTabsetConfig} from './tabset-config'; + +let nextId = 0; + +/** + * A directive to wrap tab titles that need to contain HTML markup or other directives. + * + * Alternatively you could use the `NgbTab.title` input for string titles. + */ +@Directive({selector: 'ng-template[ngbTabTitle]'}) +export class NgbTabTitle { + constructor(public templateRef: TemplateRef) {} +} + +/** + * A directive to wrap content to be displayed in a tab. + */ +@Directive({selector: 'ng-template[ngbTabContent]'}) +export class NgbTabContent { + constructor(public templateRef: TemplateRef) {} +} + +/** + * A directive representing an individual tab. + */ +@Directive({selector: 'ngb-tab'}) +export class NgbTab implements AfterContentChecked { + /** + * The tab identifier. + * + * Must be unique for the entire document for proper accessibility support. + */ + @Input() id = `ngb-tab-${nextId++}`; + + /** + * The tab title. + * + * Use the [`NgbTabTitle`](#/components/tabset/api#NgbTabTitle) directive for non-string titles. + */ + @Input() title: string; + + /** + * If `true`, the current tab is disabled and can't be toggled. + */ + @Input() disabled = false; + + titleTpl: NgbTabTitle | null; + contentTpl: NgbTabContent | null; + + @ContentChildren(NgbTabTitle, {descendants: false}) titleTpls: QueryList; + @ContentChildren(NgbTabContent, {descendants: false}) contentTpls: QueryList; + + ngAfterContentChecked() { + // We are using @ContentChildren instead of @ContentChild as in the Angular version being used + // only @ContentChildren allows us to specify the {descendants: false} option. + // Without {descendants: false} we are hitting bugs described in: + // https://github.com/ng-bootstrap/ng-bootstrap/issues/2240 + this.titleTpl = this.titleTpls.first; + this.contentTpl = this.contentTpls.first; + } +} + +/** + * The payload of the change event fired right before the tab change. + */ +export interface NgbTabChangeEvent { + /** + * The id of the currently active tab. + */ + activeId: string; + + /** + * The id of the newly selected tab. + */ + nextId: string; + + /** + * Calling this function will prevent tab switching. + */ + preventDefault: () => void; +} + +/** + * A component that makes it easy to create tabbed interface. + */ +@Component({ + selector: 'ngb-tabset', + exportAs: 'ngbTabset', + template: ` + +
+ +
+ +
+
+
+ ` +}) +export class NgbTabset implements AfterContentChecked { + justifyClass: string; + + @ContentChildren(NgbTab) tabs: QueryList; + + /** + * The identifier of the tab that should be opened **initially**. + * + * For subsequent tab switches use the `.select()` method and the `(tabChange)` event. + */ + @Input() activeId: string; + + /** + * If `true`, non-visible tabs content will be removed from DOM. Otherwise it will just be hidden. + */ + @Input() destroyOnHide = true; + + /** + * The horizontal alignment of the tabs with flexbox utilities. + */ + @Input() + set justify(className: 'start' | 'center' | 'end' | 'fill' | 'justified') { + if (className === 'fill' || className === 'justified') { + this.justifyClass = `nav-${className}`; + } else { + this.justifyClass = `justify-content-${className}`; + } + } + + /** + * The orientation of the tabset. + */ + @Input() orientation: 'horizontal' | 'vertical'; + + /** + * Type of navigation to be used for tabs. + * + * Currently Bootstrap supports only `"tabs"` and `"pills"`. + * + * Since `3.0.0` can also be an arbitrary string (ex. for custom themes). + */ + @Input() type: 'tabs' | 'pills' | string; + + /** + * A tab change event emitted right before the tab change happens. + * + * See [`NgbTabChangeEvent`](#/components/tabset/api#NgbTabChangeEvent) for payload details. + */ + @Output() tabChange = new EventEmitter(); + + constructor(config: NgbTabsetConfig) { + this.type = config.type; + this.justify = config.justify; + this.orientation = config.orientation; + } + + /** + * Selects the tab with the given id and shows its associated content panel. + * + * Any other tab that was previously selected becomes unselected and its associated pane is removed from DOM or + * hidden depending on the `destroyOnHide` value. + */ + select(tabId: string) { + let selectedTab = this._getTabById(tabId); + if (selectedTab && !selectedTab.disabled && this.activeId !== selectedTab.id) { + let defaultPrevented = false; + + this.tabChange.emit( + {activeId: this.activeId, nextId: selectedTab.id, preventDefault: () => { defaultPrevented = true; }}); + + if (!defaultPrevented) { + this.activeId = selectedTab.id; + } + } + } + + ngAfterContentChecked() { + // auto-correct activeId that might have been set incorrectly as input + let activeTab = this._getTabById(this.activeId); + this.activeId = activeTab ? activeTab.id : (this.tabs.length ? this.tabs.first.id : null); + } + + private _getTabById(id: string): NgbTab { + let tabsWithId: NgbTab[] = this.tabs.filter(tab => tab.id === id); + return tabsWithId.length ? tabsWithId[0] : null; + } +} diff --git a/src/test.ts b/src/test.ts new file mode 100644 index 0000000..15b1235 --- /dev/null +++ b/src/test.ts @@ -0,0 +1,18 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/zone'; +import 'zone.js/dist/zone-testing'; + +import {getTestBed} from '@angular/core/testing'; +import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing'; + +import './test/jasmine.config'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting()); +// Then we find all the tests. +const context = require.context('.', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/src/test/common.spec.ts b/src/test/common.spec.ts new file mode 100644 index 0000000..0e38273 --- /dev/null +++ b/src/test/common.spec.ts @@ -0,0 +1,54 @@ +import {getBrowser, isBrowser} from './common'; + +const sampleAgents = { + ie9: 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 7.1; Trident/5.0)', + ie10: 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)', + ie11: 'Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; AS; rv:11.0) like Gecko', + firefox: 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1', + edge: + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246', + chrome: 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36', + safari: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_3) AppleWebKit/537.75.14 (KHTML, like Gecko) Version/7.0.3 Safari/7046A194A', + unknown: 'Something that wont match at all' +}; + +describe('test-tools', () => { + + describe('getBrowser()', () => { + + it('should detect browsers', () => { + expect(getBrowser(sampleAgents.ie11)).toBe('ie11'); + expect(getBrowser(sampleAgents.ie10)).toBe('ie10'); + expect(getBrowser(sampleAgents.ie9)).toBe('ie9'); + expect(getBrowser(sampleAgents.edge)).toBe('edge'); + expect(getBrowser(sampleAgents.chrome)).toBe('chrome'); + expect(getBrowser(sampleAgents.safari)).toBe('safari'); + expect(getBrowser(sampleAgents.firefox)).toBe('firefox'); + }); + + it('should crash for an unknown browser', () => { expect(() => { getBrowser(sampleAgents.unknown); }).toThrow(); }); + }); + + describe('isBrowser()', () => { + + it('should match browser to the current one', () => { + expect(isBrowser('ie9', sampleAgents.ie9)).toBeTruthy(); + expect(isBrowser('ie9', sampleAgents.ie10)).toBeFalsy(); + }); + + it('should match an array of browsers to the current one', () => { + expect(isBrowser(['ie10', 'ie11'], sampleAgents.ie9)).toBeFalsy(); + expect(isBrowser(['ie9', 'ie11'], sampleAgents.ie9)).toBeTruthy(); + }); + + it('should match all ie browsers as one', () => { + expect(isBrowser('ie', sampleAgents.ie9)).toBeTruthy(); + expect(isBrowser(['ie'], sampleAgents.ie10)).toBeTruthy(); + expect(isBrowser(['ie', 'edge'], sampleAgents.ie11)).toBeTruthy(); + expect(isBrowser('edge', sampleAgents.ie11)).toBeFalsy(); + }); + }); + + +}); diff --git a/src/test/common.ts b/src/test/common.ts new file mode 100644 index 0000000..fb47ce0 --- /dev/null +++ b/src/test/common.ts @@ -0,0 +1,82 @@ +import {DebugElement} from '@angular/core'; +import {TestBed, ComponentFixture} from '@angular/core/testing'; +import {Key} from '../util/key'; + + + +export function createGenericTestComponent(html: string, type: {new (...args: any[]): T}): ComponentFixture { + TestBed.overrideComponent(type, {set: {template: html}}); + const fixture = TestBed.createComponent(type); + fixture.detectChanges(); + return fixture as ComponentFixture; +} + +export type Browser = 'ie9' | 'ie10' | 'ie11' | 'ie' | 'edge' | 'chrome' | 'safari' | 'firefox'; + +export function getBrowser(ua = window.navigator.userAgent) { + let browser = 'unknown'; + + // IE < 11 + const msie = ua.indexOf('MSIE '); + if (msie > 0) { + return 'ie' + parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10); + } + + // IE 11 + if (ua.indexOf('Trident/') > 0) { + let rv = ua.indexOf('rv:'); + return 'ie' + parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10); + } + + // Edge + if (ua.indexOf('Edge/') > 0) { + return 'edge'; + } + + // Chrome + if (ua.indexOf('Chrome/') > 0) { + return 'chrome'; + } + + // Safari + if (ua.indexOf('Safari/') > 0) { + return 'safari'; + } + + // Firefox + if (ua.indexOf('Firefox/') > 0) { + return 'firefox'; + } + + if (browser === 'unknown') { + throw new Error('Browser detection failed for: ' + ua); + } +} + +export function isBrowser(browsers: Browser | Browser[], ua = window.navigator.userAgent) { + let browsersStr = Array.isArray(browsers) ? (browsers as Browser[]).map(x => x.toString()) : [browsers.toString()]; + let browser = getBrowser(ua); + + if (browsersStr.indexOf('ie') > -1 && browser.startsWith('ie')) { + return true; + } else { + return browsersStr.indexOf(browser) > -1; + } +} + +export function createKeyEvent(key: Key, options: {type: 'keyup' | 'keydown'} = { + type: 'keyup' +}) { + const event = document.createEvent('KeyboardEvent') as any; + let initEvent = (event.initKeyEvent || event.initKeyboardEvent).bind(event); + initEvent(options.type, true, true, window, 0, 0, 0, 0, 0, key); + Object.defineProperties(event, {which: {get: () => key}}); + + return event; +} + +export function triggerEvent(element: DebugElement | HTMLElement, eventName: string) { + const evt = document.createEvent('Event'); + evt.initEvent(eventName, true, false); + (element instanceof DebugElement ? element.nativeElement : element).dispatchEvent(evt); +} diff --git a/src/test/datepicker/common.ts b/src/test/datepicker/common.ts new file mode 100644 index 0000000..678f51f --- /dev/null +++ b/src/test/datepicker/common.ts @@ -0,0 +1,11 @@ +export function getNavigationLinks(element: HTMLElement): HTMLElement[] { + return Array.from(element.querySelectorAll('button')); +} + +export function getMonthSelect(element: HTMLElement): HTMLSelectElement { + return element.querySelectorAll('select')[0] as HTMLSelectElement; +} + +export function getYearSelect(element: HTMLElement): HTMLSelectElement { + return element.querySelectorAll('select')[1] as HTMLSelectElement; +} diff --git a/src/test/global.spec.ts b/src/test/global.spec.ts new file mode 100644 index 0000000..100a65a --- /dev/null +++ b/src/test/global.spec.ts @@ -0,0 +1,11 @@ +afterAll(() => { + // Check that only the last test element is here, all previous ones must have been removed + const divs = Array.from(document.body.children).filter((element: HTMLElement) => { + return element.tagName.toLocaleLowerCase() === 'div' && element.id !== 'ngb-live'; + }); + + if (divs.length > 1) { + console.warn('DOM nodes left:', divs); + fail(`Found ${divs.length - 1} orphan node(s) left in DOM.`); + } +}); diff --git a/src/test/jasmine.config.ts b/src/test/jasmine.config.ts new file mode 100644 index 0000000..7c8ca02 --- /dev/null +++ b/src/test/jasmine.config.ts @@ -0,0 +1,20 @@ +// Timeouts +jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000; + +// Matchers +beforeEach(() => { + jasmine.addMatchers({ + toHaveCssClass: function(util, customEqualityTests) { + return {compare: buildError(false), negativeCompare: buildError(true)}; + + function buildError(isNot: boolean) { + return function(actual: HTMLElement, className: string) { + return { + pass: actual.classList.contains(className) === !isNot, + message: `Expected ${actual.outerHTML} ${isNot ? 'not ' : ''}to contain the CSS class "${className}"` + }; + }; + } + } + }); +}); diff --git a/src/test/typeahead/common.ts b/src/test/typeahead/common.ts new file mode 100644 index 0000000..8538134 --- /dev/null +++ b/src/test/typeahead/common.ts @@ -0,0 +1,29 @@ +import {DebugElement} from '@angular/core'; +import {By} from '@angular/platform-browser'; + +function normalizeText(txt: string): string { + return txt.trim().replace(/\s+/g, ' '); +} + +export function getWindowLinks(element: DebugElement): DebugElement[] { + return Array.from(element.queryAll(By.css('button.dropdown-item'))); +} + +export function expectResults(nativeEl: HTMLElement, resultsDef: string[]): void { + const pages = nativeEl.querySelectorAll('button.dropdown-item'); + + expect(pages.length).toEqual(resultsDef.length); + + for (let i = 0; i < resultsDef.length; i++) { + let resultDef = resultsDef[i]; + let classIndicator = resultDef.charAt(0); + + if (classIndicator === '+') { + expect(pages[i]).toHaveCssClass('active'); + expect(normalizeText(pages[i].textContent)).toEqual(resultDef.substr(1)); + } else { + expect(pages[i]).not.toHaveCssClass('active'); + expect(normalizeText(pages[i].textContent)).toEqual(resultDef); + } + } +} diff --git a/src/test/typings/custom-jasmine.d.ts b/src/test/typings/custom-jasmine.d.ts new file mode 100644 index 0000000..e0da4c8 --- /dev/null +++ b/src/test/typings/custom-jasmine.d.ts @@ -0,0 +1,9 @@ +declare module jasmine { + interface Matchers { + toHaveToast(content?: string | string[]): boolean; + toHaveCssClass(expected: any): boolean; + toHaveModal(content?: string | string[], selector?: string): boolean; + toHaveBackdrop(): boolean; + toBeShown(): boolean; + } +} diff --git a/src/timepicker/ngb-time-adapter.spec.ts b/src/timepicker/ngb-time-adapter.spec.ts new file mode 100644 index 0000000..dc69b0b --- /dev/null +++ b/src/timepicker/ngb-time-adapter.spec.ts @@ -0,0 +1,48 @@ +import {NgbTimeStructAdapter} from './ngb-time-adapter'; + +describe('ngb-time model adapter', () => { + let adapter: NgbTimeStructAdapter; + + beforeEach(() => { adapter = new NgbTimeStructAdapter(); }); + + describe('fromModel', () => { + + it('should convert invalid and incomplete values to null', () => { + expect(adapter.fromModel(null)).toBeNull(); + expect(adapter.fromModel(undefined)).toBeNull(); + expect(adapter.fromModel('')).toBeNull(); + expect(adapter.fromModel('s')).toBeNull(); + expect(adapter.fromModel(2)).toBeNull(); + expect(adapter.fromModel({})).toBeNull(); + expect(adapter.fromModel(new Date())).toBeNull(); + expect(adapter.fromModel({hour: 20})).toBeNull(); + }); + + it('should convert valid time', () => { + expect(adapter.fromModel({hour: 19, minute: 5, second: 1})).toEqual({hour: 19, minute: 5, second: 1}); + expect(adapter.fromModel({hour: 19, minute: 5})).toEqual({hour: 19, minute: 5, second: null}); + expect(adapter.fromModel({hour: 19, minute: 5, second: null})).toEqual({hour: 19, minute: 5, second: null}); + }); + }); + + describe('toModel', () => { + + it('should convert invalid and incomplete values to null', () => { + expect(adapter.toModel(null)).toBeNull(); + expect(adapter.toModel(undefined)).toBeNull(); + expect(adapter.toModel('')).toBeNull(); + expect(adapter.toModel('s')).toBeNull(); + expect(adapter.toModel(2)).toBeNull(); + expect(adapter.toModel({})).toBeNull(); + expect(adapter.toModel(new Date())).toBeNull(); + expect(adapter.toModel({hour: 20})).toBeNull(); + }); + + it('should convert a valid time', () => { + expect(adapter.toModel({hour: 19, minute: 5, second: 1})).toEqual({hour: 19, minute: 5, second: 1}); + expect(adapter.toModel({hour: 19, minute: 5})).toEqual({hour: 19, minute: 5, second: null}); + expect(adapter.toModel({hour: 19, minute: 5, second: null})).toEqual({hour: 19, minute: 5, second: null}); + }); + }); + +}); diff --git a/src/timepicker/ngb-time-adapter.ts b/src/timepicker/ngb-time-adapter.ts new file mode 100644 index 0000000..c9612a5 --- /dev/null +++ b/src/timepicker/ngb-time-adapter.ts @@ -0,0 +1,54 @@ +import {Injectable} from '@angular/core'; +import {NgbTimeStruct} from './ngb-time-struct'; +import {isInteger} from '../util/util'; + +export function NGB_DATEPICKER_TIME_ADAPTER_FACTORY() { + return new NgbTimeStructAdapter(); +} + +/** + * An abstract service that does the conversion between the internal timepicker `NgbTimeStruct` model and + * any provided user time model `T`, ex. a string, a native date, etc. + * + * The adapter is used **only** for conversion when binding timepicker to a form control, + * ex. `[(ngModel)]="userTimeModel"`. Here `userTimeModel` can be of any type. + * + * The default timepicker implementation assumes we use `NgbTimeStruct` as a user model. + * + * See the [custom time adapter demo](#/components/timepicker/examples#adapter) for an example. + * + * @since 2.2.0 + */ +@Injectable({providedIn: 'root', useFactory: NGB_DATEPICKER_TIME_ADAPTER_FACTORY}) +export abstract class NgbTimeAdapter { + /** + * Converts a user-model time of type `T` to an `NgbTimeStruct` for internal use. + */ + abstract fromModel(value: T): NgbTimeStruct; + + /** + * Converts an internal `NgbTimeStruct` time to a user-model time of type `T`. + */ + abstract toModel(time: NgbTimeStruct): T; +} + +@Injectable() +export class NgbTimeStructAdapter extends NgbTimeAdapter { + /** + * Converts a NgbTimeStruct value into NgbTimeStruct value + */ + fromModel(time: NgbTimeStruct): NgbTimeStruct { + return (time && isInteger(time.hour) && isInteger(time.minute)) ? + {hour: time.hour, minute: time.minute, second: isInteger(time.second) ? time.second : null} : + null; + } + + /** + * Converts a NgbTimeStruct value into NgbTimeStruct value + */ + toModel(time: NgbTimeStruct): NgbTimeStruct { + return (time && isInteger(time.hour) && isInteger(time.minute)) ? + {hour: time.hour, minute: time.minute, second: isInteger(time.second) ? time.second : null} : + null; + } +} diff --git a/src/timepicker/ngb-time-struct.ts b/src/timepicker/ngb-time-struct.ts new file mode 100644 index 0000000..8e16ed6 --- /dev/null +++ b/src/timepicker/ngb-time-struct.ts @@ -0,0 +1,19 @@ +/** + * An interface for the time model used by the timepicker. + */ +export interface NgbTimeStruct { + /** + * The hour in the `[0, 23]` range. + */ + hour: number; + + /** + * The minute in the `[0, 59]` range. + */ + minute: number; + + /** + * The second in the `[0, 59]` range. + */ + second: number; +} diff --git a/src/timepicker/ngb-time.spec.ts b/src/timepicker/ngb-time.spec.ts new file mode 100644 index 0000000..37169d6 --- /dev/null +++ b/src/timepicker/ngb-time.spec.ts @@ -0,0 +1,222 @@ +import {NgbTime} from './ngb-time'; + +describe('NgbTime', () => { + + it('should allow constructing new objects', () => { + expect(new NgbTime(undefined, undefined).toString()).toBe('0:0:0'); + expect(new NgbTime(12, 31, 45).toString()).toBe('12:31:45'); + }); + + it('should allow changing hours', () => { + const t = new NgbTime(10, 30); + expect(t.toString()).toBe('10:30:0'); + + t.changeHour(1); + expect(t.toString()).toBe('11:30:0'); + + t.changeHour(5); + expect(t.toString()).toBe('16:30:0'); + + t.changeHour(-2); + expect(t.toString()).toBe('14:30:0'); + }); + + it('should properly change undefined hours', () => { + const t = new NgbTime(undefined, 30); + + t.changeHour(1); + expect(t.toString()).toBe('1:30:0'); + }); + + it('should allow changing hours with rolling', () => { + const t = new NgbTime(23, 30); + expect(t.toString()).toBe('23:30:0'); + + t.changeHour(1); + expect(t.toString()).toBe('0:30:0'); + + t.changeHour(-5); + expect(t.toString()).toBe('19:30:0'); + + t.changeHour(6); + expect(t.toString()).toBe('1:30:0'); + + t.changeHour(26); + expect(t.toString()).toBe('3:30:0'); + }); + + it('should allow changing hours with rolling around 0', () => { + const t = new NgbTime(0, 30); + expect(t.toString()).toBe('0:30:0'); + + t.changeHour(-48); + expect(t.toString()).toBe('0:30:0'); + }); + + it('should allow changing minutes', () => { + const t = new NgbTime(10, 30); + expect(t.toString()).toBe('10:30:0'); + + t.changeMinute(1); + expect(t.toString()).toBe('10:31:0'); + + t.changeMinute(5); + expect(t.toString()).toBe('10:36:0'); + + t.changeMinute(-2); + expect(t.toString()).toBe('10:34:0'); + }); + + it('should properly change undefined minutes', () => { + const t = new NgbTime(1, undefined); + + t.changeMinute(0); + expect(t.toString()).toBe('1:0:0'); + }); + + it('should allow changing minutes with rolling', () => { + const t = new NgbTime(10, 30); + expect(t.toString()).toBe('10:30:0'); + + t.changeMinute(41); + expect(t.toString()).toBe('11:11:0'); + + t.changeMinute(121); + expect(t.toString()).toBe('13:12:0'); + + t.changeMinute(-122); + expect(t.toString()).toBe('11:10:0'); + }); + + it('should allow changing minutes with rolling around zero boundaries', () => { + const t = new NgbTime(0, 30); + expect(t.toString()).toBe('0:30:0'); + + t.changeMinute(-40); + expect(t.toString()).toBe('23:50:0'); + + t.changeMinute(50); + expect(t.toString()).toBe('0:40:0'); + + t.changeMinute(24 * 60); + expect(t.toString()).toBe('0:40:0'); + + t.changeMinute(-48 * 60); + expect(t.toString()).toBe('0:40:0'); + }); + + it('should allow changing seconds', () => { + const t = new NgbTime(10, 30, 30); + expect(t.toString()).toBe('10:30:30'); + + t.changeSecond(1); + expect(t.toString()).toBe('10:30:31'); + + t.changeSecond(5); + expect(t.toString()).toBe('10:30:36'); + + t.changeSecond(-6); + expect(t.toString()).toBe('10:30:30'); + }); + + it('should properly change undefined seconds', () => { + const t = new NgbTime(1, 20, undefined); + + t.changeSecond(30); + expect(t.toString()).toBe('1:20:30'); + }); + + it('should allow changing seconds with rolling', () => { + const t = new NgbTime(10, 30, 30); + expect(t.toString()).toBe('10:30:30'); + + t.changeSecond(60); + expect(t.toString()).toBe('10:31:30'); + + t.changeSecond(60 * 60); + expect(t.toString()).toBe('11:31:30'); + + t.changeSecond(-60 * 60); + expect(t.toString()).toBe('10:31:30'); + }); + + it('should allow changing seconds with rolling around zero boundaries', () => { + const t = new NgbTime(0, 0, 30); + expect(t.toString()).toBe('0:0:30'); + + t.changeSecond(-40); + expect(t.toString()).toBe('23:59:50'); + + t.changeSecond(110); + expect(t.toString()).toBe('0:1:40'); + + t.changeMinute(24 * 3600); + expect(t.toString()).toBe('0:1:40'); + + t.changeMinute(-24 * 3600); + expect(t.toString()).toBe('0:1:40'); + }); + + it('should allow updating hours', () => { + const t = new NgbTime(0, 0, 30); + expect(t.toString()).toBe('0:0:30'); + + t.updateHour(11); + expect(t.toString()).toBe('11:0:30'); + }); + + it('should allow updating hours with rolling', () => { + const t = new NgbTime(0, 0, 30); + expect(t.toString()).toBe('0:0:30'); + + t.updateHour(25); + expect(t.toString()).toBe('1:0:30'); + }); + + it('should allow updating minutes', () => { + const t = new NgbTime(11, 0, 30); + expect(t.toString()).toBe('11:0:30'); + + t.updateMinute(40); + expect(t.toString()).toBe('11:40:30'); + }); + + it('should allow updating minutes with rolling', () => { + const t = new NgbTime(11, 30, 30); + expect(t.toString()).toBe('11:30:30'); + + t.updateMinute(90); + expect(t.toString()).toBe('12:30:30'); + + t.updateMinute(-120); + expect(t.toString()).toBe('10:0:30'); + }); + + it('should allow updating seconds', () => { + const t = new NgbTime(11, 0, 30); + expect(t.toString()).toBe('11:0:30'); + + t.updateSecond(40); + expect(t.toString()).toBe('11:0:40'); + }); + + it('should allow updating seconds with rolling', () => { + const t = new NgbTime(11, 0, 30); + expect(t.toString()).toBe('11:0:30'); + + t.updateSecond(70); + expect(t.toString()).toBe('11:1:10'); + }); + + it('should have a validity flag', () => { + expect(new NgbTime(11, 0, 30).isValid()).toBeTruthy(); + expect(new NgbTime(null, 0, 30).isValid()).toBeFalsy(); + expect(new NgbTime(11, null, 30).isValid()).toBeFalsy(); + expect(new NgbTime(11, 0, null).isValid()).toBeFalsy(); + expect(new NgbTime(null, 0, null).isValid()).toBeFalsy(); + expect(new NgbTime(null, null, null).isValid()).toBeFalsy(); + }); + + it('should have a validity flag with optional seconds checking', + () => { expect(new NgbTime(11, 0).isValid(false)).toBeTruthy(); }); +}); diff --git a/src/timepicker/ngb-time.ts b/src/timepicker/ngb-time.ts new file mode 100644 index 0000000..e6911ae --- /dev/null +++ b/src/timepicker/ngb-time.ts @@ -0,0 +1,51 @@ +import {isNumber, toInteger} from '../util/util'; + +export class NgbTime { + hour: number; + minute: number; + second: number; + + constructor(hour?: number, minute?: number, second?: number) { + this.hour = toInteger(hour); + this.minute = toInteger(minute); + this.second = toInteger(second); + } + + changeHour(step = 1) { this.updateHour((isNaN(this.hour) ? 0 : this.hour) + step); } + + updateHour(hour: number) { + if (isNumber(hour)) { + this.hour = (hour < 0 ? 24 + hour : hour) % 24; + } else { + this.hour = NaN; + } + } + + changeMinute(step = 1) { this.updateMinute((isNaN(this.minute) ? 0 : this.minute) + step); } + + updateMinute(minute: number) { + if (isNumber(minute)) { + this.minute = minute % 60 < 0 ? 60 + minute % 60 : minute % 60; + this.changeHour(Math.floor(minute / 60)); + } else { + this.minute = NaN; + } + } + + changeSecond(step = 1) { this.updateSecond((isNaN(this.second) ? 0 : this.second) + step); } + + updateSecond(second: number) { + if (isNumber(second)) { + this.second = second < 0 ? 60 + second % 60 : second % 60; + this.changeMinute(Math.floor(second / 60)); + } else { + this.second = NaN; + } + } + + isValid(checkSecs = true) { + return isNumber(this.hour) && isNumber(this.minute) && (checkSecs ? isNumber(this.second) : true); + } + + toString() { return `${this.hour || 0}:${this.minute || 0}:${this.second || 0}`; } +} diff --git a/src/timepicker/timepicker-config.spec.ts b/src/timepicker/timepicker-config.spec.ts new file mode 100644 index 0000000..9d40498 --- /dev/null +++ b/src/timepicker/timepicker-config.spec.ts @@ -0,0 +1,17 @@ +import {NgbTimepickerConfig} from './timepicker-config'; + +describe('ngb-timepicker-config', () => { + it('should have sensible default values', () => { + const config = new NgbTimepickerConfig(); + + expect(config.meridian).toBe(false); + expect(config.spinners).toBe(true); + expect(config.seconds).toBe(false); + expect(config.hourStep).toBe(1); + expect(config.minuteStep).toBe(1); + expect(config.secondStep).toBe(1); + expect(config.disabled).toBe(false); + expect(config.readonlyInputs).toBe(false); + expect(config.size).toBe('medium'); + }); +}); diff --git a/src/timepicker/timepicker-config.ts b/src/timepicker/timepicker-config.ts new file mode 100644 index 0000000..f5e1685 --- /dev/null +++ b/src/timepicker/timepicker-config.ts @@ -0,0 +1,20 @@ +import {Injectable} from '@angular/core'; + +/** + * A configuration service for the [`NgbTimepicker`](#/components/timepicker/api#NgbTimepicker) component. + * + * You can inject this service, typically in your root component, and customize the values of its properties in + * order to provide default values for all the timepickers used in the application. + */ +@Injectable({providedIn: 'root'}) +export class NgbTimepickerConfig { + meridian = false; + spinners = true; + seconds = false; + hourStep = 1; + minuteStep = 1; + secondStep = 1; + disabled = false; + readonlyInputs = false; + size: 'small' | 'medium' | 'large' = 'medium'; +} diff --git a/src/timepicker/timepicker-i18n.spec.ts b/src/timepicker/timepicker-i18n.spec.ts new file mode 100644 index 0000000..30e5e58 --- /dev/null +++ b/src/timepicker/timepicker-i18n.spec.ts @@ -0,0 +1,16 @@ +import {NgbTimepickerI18nDefault} from './timepicker-i18n'; +import {TestBed} from '@angular/core/testing'; + +describe('ngb-timepicker-i18n-default', () => { + + let i18n: NgbTimepickerI18nDefault; + + beforeEach(() => { + TestBed.configureTestingModule({providers: [NgbTimepickerI18nDefault]}); + i18n = TestBed.get(NgbTimepickerI18nDefault); + }); + + it('should return morning period', () => { expect(i18n.getMorningPeriod()).toBe('AM'); }); + + it('should return afternoon period', () => { expect(i18n.getAfternoonPeriod()).toBe('PM'); }); +}); diff --git a/src/timepicker/timepicker-i18n.ts b/src/timepicker/timepicker-i18n.ts new file mode 100644 index 0000000..4d5e435 --- /dev/null +++ b/src/timepicker/timepicker-i18n.ts @@ -0,0 +1,39 @@ +import {Inject, Injectable, LOCALE_ID} from '@angular/core'; +import {FormStyle, getLocaleDayPeriods, TranslationWidth} from '@angular/common'; + +export function NGB_TIMEPICKER_I18N_FACTORY(locale) { + return new NgbTimepickerI18nDefault(locale); +} + +/** + * Type of the service supplying day periods (for example, 'AM' and 'PM') to NgbTimepicker component. + * The default implementation of this service honors the Angular locale, and uses the registered locale data, + * as explained in the Angular i18n guide. + */ +@Injectable({providedIn: 'root', useFactory: NGB_TIMEPICKER_I18N_FACTORY, deps: [LOCALE_ID]}) +export abstract class NgbTimepickerI18n { + /** + * Returns the name for the period before midday. + */ + abstract getMorningPeriod(): string; + + /** + * Returns the name for the period after midday. + */ + abstract getAfternoonPeriod(): string; +} + +@Injectable() +export class NgbTimepickerI18nDefault extends NgbTimepickerI18n { + private _periods: [string, string]; + + constructor(@Inject(LOCALE_ID) locale: string) { + super(); + + this._periods = getLocaleDayPeriods(locale, FormStyle.Standalone, TranslationWidth.Narrow); + } + + getMorningPeriod(): string { return this._periods[0]; } + + getAfternoonPeriod(): string { return this._periods[1]; } +} diff --git a/src/timepicker/timepicker.module.ts b/src/timepicker/timepicker.module.ts new file mode 100644 index 0000000..385a67f --- /dev/null +++ b/src/timepicker/timepicker.module.ts @@ -0,0 +1,14 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; + +import {NgbTimepicker} from './timepicker'; + +export {NgbTimepicker} from './timepicker'; +export {NgbTimepickerConfig} from './timepicker-config'; +export {NgbTimeStruct} from './ngb-time-struct'; +export {NgbTimeAdapter} from './ngb-time-adapter'; +export {NgbTimepickerI18n} from './timepicker-i18n'; + +@NgModule({declarations: [NgbTimepicker], exports: [NgbTimepicker], imports: [CommonModule]}) +export class NgbTimepickerModule { +} diff --git a/src/timepicker/timepicker.scss b/src/timepicker/timepicker.scss new file mode 100644 index 0000000..1661323 --- /dev/null +++ b/src/timepicker/timepicker.scss @@ -0,0 +1,52 @@ +ngb-timepicker { + font-size: 1rem; +} + +.ngb-tp { + display: flex; + align-items: center; + + &-input-container { + width: 4em; + } + + &-chevron { + &::before { + border-style: solid; + border-width: 0.29em 0.29em 0 0; + content: ''; + display: inline-block; + height: 0.69em; + left: 0.05em; + position: relative; + top: 0.15em; + transform: rotate(-45deg); + vertical-align: middle; + width: 0.69em; + } + + &.bottom:before { + top: -.3em; + transform: rotate(135deg); + } + } + + &-input { + text-align: center; + } + + &-hour, + &-minute, + &-second, + &-meridian { + display: flex; + flex-direction: column; + align-items: center; + justify-content: space-around; + } + + &-spacer { + width: 1em; + text-align: center; + } +} diff --git a/src/timepicker/timepicker.spec.ts b/src/timepicker/timepicker.spec.ts new file mode 100644 index 0000000..fc46b9f --- /dev/null +++ b/src/timepicker/timepicker.spec.ts @@ -0,0 +1,1695 @@ +import {async, ComponentFixture, inject, TestBed} from '@angular/core/testing'; +import {createGenericTestComponent} from '../test/common'; + +import {ChangeDetectionStrategy, Component, DebugElement, Injectable} from '@angular/core'; +import {By} from '@angular/platform-browser'; +import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms'; + +import {NgbTimepickerModule} from './timepicker.module'; +import {NgbTimepickerConfig} from './timepicker-config'; +import {NgbTimepicker} from './timepicker'; +import {NgbTimepickerI18n} from './timepicker-i18n'; +import {NgbTimeAdapter, NgbTimeStructAdapter} from './ngb-time-adapter'; +import {NgbTimeStruct} from './ngb-time-struct'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +const createOnPushTestComponent = (html: string) => + createGenericTestComponent(html, TestComponentOnPush) as ComponentFixture; + +function getTimepicker(el: HTMLElement) { + return el.querySelector('ngb-timepicker'); +} + +function getInputs(el: HTMLElement) { + return el.querySelectorAll('input'); +} + +function getButtons(nativeEl: HTMLElement) { + return nativeEl.querySelectorAll('button.btn-link'); +} + +function getFieldsetElement(element: HTMLElement): HTMLFieldSetElement { + return element.querySelector('fieldset'); +} + +function getMeridianButton(nativeEl: HTMLElement) { + return nativeEl.querySelector('button.btn-outline-primary'); +} + +function createChangeEvent(value: string) { + return {target: {value: value}}; +} + +function expectToDisplayTime(el: HTMLElement, time: string) { + const inputs = getInputs(el); + const timeParts = time.split(':'); + let timeInInputs = []; + + expect(inputs.length).toBe(timeParts.length); + + for (let i = 0; i < inputs.length; i++) { + timeInInputs.push((inputs[i]).value); + } + + expect(timeInInputs.join(':')).toBe(time); +} + +function expectSameValues(timepicker: NgbTimepicker, config: NgbTimepickerConfig) { + expect(timepicker.meridian).toBe(config.meridian); + expect(timepicker.spinners).toBe(config.spinners); + expect(timepicker.seconds).toBe(config.seconds); + expect(timepicker.hourStep).toBe(config.hourStep); + expect(timepicker.minuteStep).toBe(config.minuteStep); + expect(timepicker.secondStep).toBe(config.secondStep); + expect(timepicker.disabled).toBe(config.disabled); + expect(timepicker.readonlyInputs).toBe(config.readonlyInputs); + expect(timepicker.size).toBe(config.size); +} + +function customizeConfig(config: NgbTimepickerConfig) { + config.meridian = true; + config.spinners = false; + config.seconds = true; + config.hourStep = 2; + config.minuteStep = 3; + config.secondStep = 4; + config.disabled = true; + config.readonlyInputs = true; +} + +describe('ngb-timepicker', () => { + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestComponent, TestComponentOnPush], + imports: [NgbTimepickerModule, FormsModule, ReactiveFormsModule] + }); + }); + + describe('initialization', () => { + it('should initialize inputs with provided config', () => { + const defaultConfig = new NgbTimepickerConfig(); + const timepicker = new NgbTimepicker(new NgbTimepickerConfig(), new NgbTimeStructAdapter(), null, new TestI18n()); + expectSameValues(timepicker, defaultConfig); + }); + }); + + describe('rendering based on model', () => { + + it('should render hour and minute inputs', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 13, minute: 30}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { expectToDisplayTime(fixture.nativeElement, '13:30'); }); + })); + + it('should update inputs value on model change', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 13, minute: 30}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectToDisplayTime(fixture.nativeElement, '13:30'); + + fixture.componentInstance.model = {hour: 14, minute: 40}; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { expectToDisplayTime(fixture.nativeElement, '14:40'); }); + })); + + it('should render hour and minute inputs with padding', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 1, minute: 3}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { expectToDisplayTime(fixture.nativeElement, '01:03'); }); + })); + + it('should render hour, minute and seconds inputs with padding', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 3, second: 4}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { expectToDisplayTime(fixture.nativeElement, '10:03:04'); }); + })); + + it('should render invalid or empty hour and minute as blank string', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: undefined, minute: 'aaa'}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { expectToDisplayTime(fixture.nativeElement, ':'); }); + })); + + it('should render invalid or empty second as blank string', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 20, second: false}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { expectToDisplayTime(fixture.nativeElement, '10:20:'); }); + })); + + it('should render empty fields on null model', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = null; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { expectToDisplayTime(fixture.nativeElement, '::'); }); + })); + }); + + + describe('model updates in response to increment / decrement button clicks', () => { + + it('should increment / decrement hours', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 0}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + + const buttons = getButtons(fixture.nativeElement); + + expectToDisplayTime(fixture.nativeElement, '10:30'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + + (buttons[0]).click(); // H+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '11:30'); + expect(fixture.componentInstance.model).toEqual({hour: 11, minute: 30, second: 0}); + + + (buttons[1]).click(); // H- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + }); + })); + + it('should wrap hours', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 23, minute: 30, second: 0}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + + const buttons = getButtons(fixture.nativeElement); + + expectToDisplayTime(fixture.nativeElement, '23:30'); + expect(fixture.componentInstance.model).toEqual({hour: 23, minute: 30, second: 0}); + + (buttons[0]).click(); // H+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '00:30'); + expect(fixture.componentInstance.model).toEqual({hour: 0, minute: 30, second: 0}); + + (buttons[1]).click(); // H- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '23:30'); + expect(fixture.componentInstance.model).toEqual({hour: 23, minute: 30, second: 0}); + }); + })); + + it('should increment / decrement minutes', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 0}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + + const buttons = getButtons(fixture.nativeElement); + + expectToDisplayTime(fixture.nativeElement, '10:30'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + + (buttons[2]).click(); // M+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:31'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 31, second: 0}); + + (buttons[3]).click(); // M- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + }); + })); + + it('should wrap minutes', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 22, minute: 59, second: 0}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + const buttons = getButtons(fixture.nativeElement); + + expectToDisplayTime(fixture.nativeElement, '22:59'); + expect(fixture.componentInstance.model).toEqual({hour: 22, minute: 59, second: 0}); + + (buttons[2]).click(); // M+ + fixture.detectChanges(); + expect(fixture.componentInstance.model).toEqual({hour: 23, minute: 0, second: 0}); + + (buttons[3]).click(); // M- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '22:59'); + expect(fixture.componentInstance.model).toEqual({hour: 22, minute: 59, second: 0}); + }); + })); + + it('should increment / decrement seconds', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 0}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + const buttons = getButtons(fixture.nativeElement); + + expectToDisplayTime(fixture.nativeElement, '10:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + + (buttons[4]).click(); // S+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30:01'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 1}); + + (buttons[5]).click(); // S- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + }); + })); + + it('should wrap seconds', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 59}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + + const buttons = getButtons(fixture.nativeElement); + + expectToDisplayTime(fixture.nativeElement, '10:30:59'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 59}); + + (buttons[4]).click(); // S+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:31:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 31, second: 0}); + + (buttons[5]).click(); // S- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30:59'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 59}); + }); + })); + }); + + describe('increment/decrement keyboard bindings', () => { + + function getDebugInputs(fixture: ComponentFixture): Array { + return fixture.debugElement.queryAll(By.css('input')); + } + + it('should increment / decrement hours', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 0}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectToDisplayTime(fixture.nativeElement, '10:30'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + + const hourInput = getDebugInputs(fixture)[0]; + + hourInput.triggerEventHandler('keydown.ArrowUp', {preventDefault: () => {}}); // H+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '11:30'); + expect(fixture.componentInstance.model).toEqual({hour: 11, minute: 30, second: 0}); + + hourInput.triggerEventHandler('keydown.ArrowDown', {preventDefault: () => {}}); // H- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + }); + })); + + it('should increment / decrement minutes', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 0}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectToDisplayTime(fixture.nativeElement, '10:30'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + + const minuteInput = getDebugInputs(fixture)[1]; + + minuteInput.triggerEventHandler('keydown.ArrowUp', {preventDefault: () => {}}); // M+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:31'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 31, second: 0}); + + minuteInput.triggerEventHandler('keydown.ArrowDown', {preventDefault: () => {}}); // M- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + }); + })); + + it('should increment / decrement seconds', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 0}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectToDisplayTime(fixture.nativeElement, '10:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + + const secondInput = getDebugInputs(fixture)[2]; + + secondInput.triggerEventHandler('keydown.ArrowUp', {preventDefault: () => {}}); // S+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30:01'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 1}); + + secondInput.triggerEventHandler('keydown.ArrowDown', {preventDefault: () => {}}); // S- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + }); + })); + }); + + describe('model updates in response to input field changes', () => { + + it('should update hours', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 0}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + + const inputs = fixture.debugElement.queryAll(By.css('input')); + + expectToDisplayTime(fixture.nativeElement, '10:30'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + + inputs[0].triggerEventHandler('change', createChangeEvent('11')); + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '11:30'); + expect(fixture.componentInstance.model).toEqual({hour: 11, minute: 30, second: 0}); + + inputs[0].triggerEventHandler('change', createChangeEvent(`${24 + 11}`)); + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '11:30'); + expect(fixture.componentInstance.model).toEqual({hour: 11, minute: 30, second: 0}); + + inputs[0].triggerEventHandler('change', createChangeEvent('aa')); + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, ':30'); + expect(fixture.componentInstance.model).toEqual(null); + }); + })); + + it('should update minutes', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 0}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + + const inputs = fixture.debugElement.queryAll(By.css('input')); + + expectToDisplayTime(fixture.nativeElement, '10:30'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + + inputs[1].triggerEventHandler('change', createChangeEvent('40')); + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:40'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 40, second: 0}); + + inputs[1].triggerEventHandler('change', createChangeEvent('70')); + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '11:10'); + expect(fixture.componentInstance.model).toEqual({hour: 11, minute: 10, second: 0}); + + inputs[1].triggerEventHandler('change', createChangeEvent('aa')); + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '11:'); + expect(fixture.componentInstance.model).toEqual(null); + }); + })); + + it('should update seconds', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 0}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + + const inputs = fixture.debugElement.queryAll(By.css('input')); + + expectToDisplayTime(fixture.nativeElement, '10:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + + inputs[2].triggerEventHandler('change', createChangeEvent('40')); + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30:40'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 40}); + + inputs[2].triggerEventHandler('change', createChangeEvent('70')); + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:31:10'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 31, second: 10}); + + inputs[2].triggerEventHandler('change', createChangeEvent('aa')); + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:31:'); + expect(fixture.componentInstance.model).toEqual(null); + }); + })); + }); + + describe('meridian', () => { + + beforeEach( + () => { TestBed.configureTestingModule({providers: [{provide: NgbTimepickerI18n, useClass: TestI18n}]}); }); + + it('should render meridian button with proper value', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 13, minute: 30, second: 0}; + const meridianButton = getMeridianButton(fixture.nativeElement); + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectToDisplayTime(fixture.nativeElement, '01:30:00'); + expect(meridianButton.textContent).toBe('afternoon'); + + fixture.componentInstance.model = {hour: 1, minute: 30, second: 0}; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectToDisplayTime(fixture.nativeElement, '01:30:00'); + expect(meridianButton.textContent).toBe('morning'); + }); + })); + + it('should render 12 PM/AM as 12:mm and meridian button with proper value', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 12, minute: 30, second: 0}; + const meridianButton = getMeridianButton(fixture.nativeElement); + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectToDisplayTime(fixture.nativeElement, '12:30:00'); + expect(meridianButton.textContent).toBe('afternoon'); + + fixture.componentInstance.model = {hour: 0, minute: 30, second: 0}; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectToDisplayTime(fixture.nativeElement, '12:30:00'); + expect(meridianButton.textContent).toBe('morning'); + }); + })); + + it('should update model on meridian click', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 13, minute: 30, second: 0}; + const meridianButton = getMeridianButton(fixture.nativeElement); + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectToDisplayTime(fixture.nativeElement, '01:30:00'); + expect(meridianButton.textContent).toBe('afternoon'); + + meridianButton.click(); + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectToDisplayTime(fixture.nativeElement, '01:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 1, minute: 30, second: 0}); + expect(meridianButton.textContent).toBe('morning'); + }); + })); + + + it('should respect meridian when propagating model (PM)', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 14, minute: 30}; + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + + fixture.whenStable() + .then(() => { + inputs[0].triggerEventHandler('change', createChangeEvent('3')); + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { expect(fixture.componentInstance.model).toEqual({hour: 15, minute: 30, second: 0}); }); + })); + + it('should respect meridian when propagating model (AM)', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 9, minute: 30}; + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + + fixture.whenStable() + .then(() => { + inputs[0].triggerEventHandler('change', createChangeEvent('10')); + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); }); + })); + + it('should interpret 12 as midnight (00:00) when meridian is set to AM', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 9, minute: 0}; + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + + fixture.whenStable() + .then(() => { + inputs[0].triggerEventHandler('change', createChangeEvent('12')); + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { expect(fixture.componentInstance.model).toEqual({hour: 0, minute: 0, second: 0}); }); + })); + + it('should interpret 12 as noon (12:00) when meridian is set to PM', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 18, minute: 0}; + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + + fixture.whenStable() + .then(() => { + inputs[0].triggerEventHandler('change', createChangeEvent('12')); + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { expect(fixture.componentInstance.model).toEqual({hour: 12, minute: 0, second: 0}); }); + })); + + it('should interpret hour more than 12 as 24h value (AM)', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 7, minute: 30, second: 0}; + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + const meridianButton = getMeridianButton(fixture.nativeElement); + + fixture.whenStable() + .then(() => { + inputs[0].triggerEventHandler('change', createChangeEvent('22')); + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectToDisplayTime(fixture.nativeElement, '10:30'); + expect(meridianButton.textContent).toBe('afternoon'); + expect(fixture.componentInstance.model).toEqual({hour: 22, minute: 30, second: 0}); + }); + })); + + it('should interpret hour more than 12 as 24h value (PM)', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 15, minute: 30, second: 0}; + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + const meridianButton = getMeridianButton(fixture.nativeElement); + + fixture.whenStable() + .then(() => { + inputs[0].triggerEventHandler('change', createChangeEvent('22')); + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectToDisplayTime(fixture.nativeElement, '10:30'); + expect(meridianButton.textContent).toBe('afternoon'); + expect(fixture.componentInstance.model).toEqual({hour: 22, minute: 30, second: 0}); + }); + })); + + it('should use remainder of division by 24 as a value in 24h format when hour > 24 (AM)', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 7, minute: 30, second: 0}; + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + const meridianButton = getMeridianButton(fixture.nativeElement); + + fixture.whenStable() + .then(() => { + inputs[0].triggerEventHandler('change', createChangeEvent(`${24 + 9}`)); + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectToDisplayTime(fixture.nativeElement, '09:30'); + expect(meridianButton.textContent).toBe('morning'); + expect(fixture.componentInstance.model).toEqual({hour: 9, minute: 30, second: 0}); + }); + })); + + it('should use remainder of division by 24 as a value in 24h format when hour > 24 (PM)', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 15, minute: 30, second: 0}; + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + const meridianButton = getMeridianButton(fixture.nativeElement); + + fixture.whenStable() + .then(() => { + inputs[0].triggerEventHandler('change', createChangeEvent(`${24 + 9}`)); + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectToDisplayTime(fixture.nativeElement, '09:30'); + expect(meridianButton.textContent).toBe('morning'); + expect(fixture.componentInstance.model).toEqual({hour: 9, minute: 30, second: 0}); + }); + })); + + }); + + describe('forms', () => { + + it('should work with template-driven form validation', async(() => { + const html = ` +
+ +
`; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expect(getTimepicker(compiled)).toHaveCssClass('ng-invalid'); + expect(getTimepicker(compiled)).not.toHaveCssClass('ng-valid'); + + fixture.componentInstance.model = {hour: 12, minute: 0, second: 0}; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expect(getTimepicker(compiled)).toHaveCssClass('ng-valid'); + expect(getTimepicker(compiled)).not.toHaveCssClass('ng-invalid'); + }); + })); + + it('should work with template-driven form validation when meridian is true', async(() => { + const html = ` +
+ +
`; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expect(getTimepicker(compiled)).toHaveCssClass('ng-valid'); + expect(getTimepicker(compiled)).not.toHaveCssClass('ng-invalid'); + + fixture.componentInstance.model = {hour: 11, minute: 0, second: 0}; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expect(getTimepicker(compiled)).toHaveCssClass('ng-valid'); + expect(getTimepicker(compiled)).not.toHaveCssClass('ng-invalid'); + }); + })); + + it('should work with model-driven form validation', async(() => { + const html = ` +
+ +
`; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + const inputs = fixture.debugElement.queryAll(By.css('input')); + + expect(getTimepicker(compiled)).toHaveCssClass('ng-invalid'); + expect(getTimepicker(compiled)).not.toHaveCssClass('ng-valid'); + + inputs[0].triggerEventHandler('change', createChangeEvent('12')); + inputs[1].triggerEventHandler('change', createChangeEvent('15')); + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expect(getTimepicker(compiled)).toHaveCssClass('ng-valid'); + expect(getTimepicker(compiled)).not.toHaveCssClass('ng-invalid'); + }); + })); + + it('should propagate model changes only if valid - no seconds', () => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 12, minute: 0}; + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + inputs[0].triggerEventHandler('change', createChangeEvent('aa')); + fixture.detectChanges(); + + expect(fixture.componentInstance.model).toBeNull(); + }); + + it('should propagate model changes only if valid - with seconds', () => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 12, minute: 0, second: 0}; + fixture.detectChanges(); + + const inputs = fixture.debugElement.queryAll(By.css('input')); + inputs[2].triggerEventHandler('change', createChangeEvent('aa')); + fixture.detectChanges(); + + expect(fixture.componentInstance.model).toBeNull(); + }); + + it('should not submit form when spinners clicked', async(() => { + const html = `
+ +
`; + + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + const buttons = getButtons(compiled); + const button = buttons[0] as HTMLButtonElement; + + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + button.click(); + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { expect(fixture.componentInstance.submitted).toBeFalsy(); }); + })); + }); + + describe('disabled', () => { + + it('should not change the value on button click, when it is disabled', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 13, minute: 30, second: 0}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + + const buttons = getButtons(fixture.nativeElement); + + expectToDisplayTime(fixture.nativeElement, '13:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 13, minute: 30, second: 0}); + + (buttons[0]).click(); // H+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '13:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 13, minute: 30, second: 0}); + + (buttons[1]).click(); // H- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '13:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 13, minute: 30, second: 0}); + + (buttons[2]).click(); // M+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '13:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 13, minute: 30, second: 0}); + + (buttons[3]).click(); // M- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '13:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 13, minute: 30, second: 0}); + + (buttons[4]).click(); // S+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '13:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 13, minute: 30, second: 0}); + + (buttons[5]).click(); // S- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '13:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 13, minute: 30, second: 0}); + }); + })); + + it('should have disabled class, when it is disabled', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + let fieldset = getFieldsetElement(fixture.nativeElement); + expect(fieldset.hasAttribute('disabled')).toBeTruthy(); + + fixture.componentInstance.disabled = false; + fixture.detectChanges(); + fixture.whenStable().then(() => { + fixture.detectChanges(); + fieldset = getFieldsetElement(fixture.nativeElement); + expect(fieldset.hasAttribute('disabled')).toBeFalsy(); + }); + }); + })); + + it('should have disabled attribute when it is disabled using reactive forms', async(() => { + const html = `
`; + + const fixture = createTestComponent(html); + fixture.detectChanges(); + let fieldset = getFieldsetElement(fixture.nativeElement); + expect(fieldset.hasAttribute('disabled')).toBeTruthy(); + })); + }); + + describe('readonly', () => { + + it('should change the value on button click, when it is readonly', async(() => { + const html = + ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 13, minute: 30, second: 0}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + + const buttons = getButtons(fixture.nativeElement); + + expectToDisplayTime(fixture.nativeElement, '13:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 13, minute: 30, second: 0}); + + (buttons[0]).click(); // H+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '14:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 14, minute: 30, second: 0}); + + (buttons[1]).click(); // H- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '13:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 13, minute: 30, second: 0}); + + (buttons[2]).click(); // M+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '13:31:00'); + expect(fixture.componentInstance.model).toEqual({hour: 13, minute: 31, second: 0}); + + (buttons[3]).click(); // M- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '13:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 13, minute: 30, second: 0}); + + (buttons[4]).click(); // S+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '13:30:01'); + expect(fixture.componentInstance.model).toEqual({hour: 13, minute: 30, second: 1}); + + (buttons[5]).click(); // S- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '13:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 13, minute: 30, second: 0}); + }); + })); + + it('should not change value on input change, when it is readonly', () => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.detectChanges(); + + let inputs = getInputs(fixture.nativeElement); + expect(inputs[0].hasAttribute('readonly')).toBeTruthy(); + expect(inputs[1].hasAttribute('readonly')).toBeTruthy(); + expect(inputs[2].hasAttribute('readonly')).toBeTruthy(); + + fixture.componentInstance.readonly = false; + fixture.detectChanges(); + inputs = getInputs(fixture.nativeElement); + expect(inputs[0].hasAttribute('readonly')).toBeFalsy(); + expect(inputs[1].hasAttribute('readonly')).toBeFalsy(); + expect(inputs[2].hasAttribute('readonly')).toBeFalsy(); + }); + }); + + describe('spinners', () => { + + it('should not have spinners if configured so', () => { + const html = ``; + + const fixture = createTestComponent(html); + const buttons = getButtons(fixture.nativeElement); + expect(buttons.length).toBe(0); + }); + }); + + describe('size', () => { + + it('should add appropriate CSS classes to buttons and inputs when size is small', () => { + const html = ``; + + const fixture = createTestComponent(html); + const buttons = getButtons(fixture.nativeElement); + const inputs = getInputs(fixture.nativeElement); + for (let i = 0; i < buttons.length; i++) { + expect(buttons[i]).toHaveCssClass('btn-sm'); + } + for (let i = 0; i < inputs.length; i++) { + expect(inputs[i]).toHaveCssClass('form-control-sm'); + } + }); + + it('should add appropriate CSS classes to buttons and inputs when size is large', () => { + const html = ``; + + const fixture = createTestComponent(html); + const buttons = getButtons(fixture.nativeElement); + const inputs = getInputs(fixture.nativeElement); + for (let i = 0; i < buttons.length; i++) { + expect(buttons[i]).toHaveCssClass('btn-lg'); + } + for (let i = 0; i < inputs.length; i++) { + expect(inputs[i]).toHaveCssClass('form-control-lg'); + } + }); + + it('should not add special CSS classes to buttons and inputs when size is medium', () => { + const html = ``; + + const fixture = createTestComponent(html); + const buttons = getButtons(fixture.nativeElement); + const inputs = getInputs(fixture.nativeElement); + for (let i = 0; i < buttons.length; i++) { + expect(buttons[i]).not.toHaveCssClass('btn-lg'); + } + for (let i = 0; i < inputs.length; i++) { + expect(inputs[i]).not.toHaveCssClass('form-control-lg'); + } + }); + + it('should not add special CSS classes to buttons and inputs when no size is specified', () => { + const html = ``; + + const fixture = createTestComponent(html); + const buttons = getButtons(fixture.nativeElement); + const inputs = getInputs(fixture.nativeElement); + for (let i = 0; i < buttons.length; i++) { + expect(buttons[i]).not.toHaveCssClass('btn-lg'); + } + for (let i = 0; i < inputs.length; i++) { + expect(inputs[i]).not.toHaveCssClass('form-control-lg'); + } + }); + }); + + describe('Custom config', () => { + let config: NgbTimepickerConfig; + + beforeEach(() => { + TestBed.configureTestingModule({imports: [NgbTimepickerModule]}); + TestBed.overrideComponent(NgbTimepicker, {set: {template: ''}}); + }); + + beforeEach(inject([NgbTimepickerConfig], (c: NgbTimepickerConfig) => { + config = c; + customizeConfig(config); + })); + + it('should initialize inputs with provided config', () => { + const fixture = TestBed.createComponent(NgbTimepicker); + + const timepicker = fixture.componentInstance; + expectSameValues(timepicker, config); + }); + }); + + describe('Custom config as provider', () => { + const config = new NgbTimepickerConfig(); + customizeConfig(config); + + beforeEach(() => { + TestBed.configureTestingModule( + {imports: [NgbTimepickerModule], providers: [{provide: NgbTimepickerConfig, useValue: config}]}); + }); + + it('should initialize inputs with provided config as provider', () => { + const fixture = createGenericTestComponent('', NgbTimepicker); + + const timepicker = fixture.componentInstance; + expectSameValues(timepicker, config); + }); + }); + + describe('accessibility', () => { + + it('should have text for screen readers on buttons', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 0}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + const buttons = getButtons(fixture.nativeElement); + + expect((buttons[0]).querySelector('.sr-only').textContent).toBe('Increment hours'); + expect((buttons[1]).querySelector('.sr-only').textContent).toBe('Decrement hours'); + expect((buttons[2]).querySelector('.sr-only').textContent).toBe('Increment minutes'); + expect((buttons[3]).querySelector('.sr-only').textContent).toBe('Decrement minutes'); + expect((buttons[4]).querySelector('.sr-only').textContent).toBe('Increment seconds'); + expect((buttons[5]).querySelector('.sr-only').textContent).toBe('Decrement seconds'); + }); + })); + + it('should have aria-label for inputs', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 0}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + const inputs = getInputs(fixture.nativeElement); + + expect(inputs[0].getAttribute('aria-label')).toBe('Hours'); + expect(inputs[1].getAttribute('aria-label')).toBe('Minutes'); + expect(inputs[2].getAttribute('aria-label')).toBe('Seconds'); + }); + })); + }); + + describe('Custom steps', () => { + const config = new NgbTimepickerConfig(); + config.seconds = true; + config.hourStep = 2; + config.minuteStep = 3; + config.secondStep = 4; + + beforeEach(() => { + TestBed.configureTestingModule( + {imports: [NgbTimepickerModule], providers: [{provide: NgbTimepickerConfig, useValue: config}]}); + }); + + it('should increment / decrement hours by 6', async(async() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 0}; + fixture.detectChanges(); + await fixture.whenStable(); + + fixture.detectChanges(); + await fixture.whenStable(); + const buttons = getButtons(fixture.nativeElement); + + expectToDisplayTime(fixture.nativeElement, '10:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + + (buttons[0]).click(); // H+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '16:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 16, minute: 30, second: 0}); + + (buttons[1]).click(); // H- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + })); + + it('should increment / decrement hours to default value if step set to undefined', async(async() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 0}; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + await fixture.whenStable(); + + const buttons = getButtons(fixture.nativeElement); + + expectToDisplayTime(fixture.nativeElement, '10:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + + (buttons[0]).click(); // H+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '12:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 12, minute: 30, second: 0}); + + (buttons[1]).click(); // H- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + })); + + it('should increment / decrement minutes by 7', async(async() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 0}; + fixture.detectChanges(); + await fixture.whenStable(); + + fixture.detectChanges(); + await fixture.whenStable(); + const buttons = getButtons(fixture.nativeElement); + + expectToDisplayTime(fixture.nativeElement, '10:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + + (buttons[2]).click(); // M+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:37:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 37, second: 0}); + + (buttons[3]).click(); // M- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + })); + + it('should increment / decrement minutes to default value if step set to undefined', async(async() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 0}; + fixture.detectChanges(); + await fixture.whenStable(); + + fixture.detectChanges(); + await fixture.whenStable(); + + const buttons = getButtons(fixture.nativeElement); + + expectToDisplayTime(fixture.nativeElement, '10:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + + (buttons[2]).click(); // M+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:33:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 33, second: 0}); + + (buttons[3]).click(); // M- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + })); + + it('should increment / decrement seconds by 8', async(async() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 0}; + fixture.detectChanges(); + await fixture.whenStable(); + + fixture.detectChanges(); + await fixture.whenStable(); + const buttons = getButtons(fixture.nativeElement); + + expectToDisplayTime(fixture.nativeElement, '10:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + + (buttons[4]).click(); // S+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30:08'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 8}); + + (buttons[5]).click(); // S- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + })); + + it('should increment / decrement seconds to default value if step set to undefined', async(async() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 0}; + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + await fixture.whenStable(); + const buttons = getButtons(fixture.nativeElement); + + expectToDisplayTime(fixture.nativeElement, '10:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + + (buttons[4]).click(); // S+ + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30:04'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 4}); + + (buttons[5]).click(); // S- + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:30:00'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + })); + }); + + describe('Seconds handling', () => { + it('should propagate seconds to 0 in model if seconds not shown and no second in initial model', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + const inputs = fixture.debugElement.queryAll(By.css('input')); + + inputs[1].triggerEventHandler('change', createChangeEvent('40')); + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:40'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 40, second: 0}); + }); + })); + + it('should propagate second as 0 in model if seconds not shown and null initial model', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + const inputs = fixture.debugElement.queryAll(By.css('input')); + + inputs[0].triggerEventHandler('change', createChangeEvent('10')); + inputs[1].triggerEventHandler('change', createChangeEvent('40')); + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:40'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 40, second: 0}); + }); + })); + + it('should leave second as is in model if seconds not shown and second present in initial model', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: 30}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + const inputs = fixture.debugElement.queryAll(By.css('input')); + + inputs[1].triggerEventHandler('change', createChangeEvent('40')); + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '10:40'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 40, second: 30}); + }); + })); + + it('should reset the second to 0 if invalid when seconds are hidden', async(() => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = {hour: 10, minute: 30, second: null}; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectToDisplayTime(fixture.nativeElement, '10:30:'); + + fixture.componentInstance.showSeconds = false; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectToDisplayTime(fixture.nativeElement, '10:30'); + expect(fixture.componentInstance.model).toEqual({hour: 10, minute: 30, second: 0}); + }); + })); + }); + + describe('Custom adapter', () => { + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestComponent], + imports: [NgbTimepickerModule, FormsModule], + providers: [{provide: NgbTimeAdapter, useClass: StringTimeAdapter}] + }); + }); + + it('should display the right time when model is a string parsed by a custom time adapter', async(() => { + const html = ``; + const fixture = createTestComponent(html); + + fixture.componentInstance.model = null; + fixture.detectChanges(); + + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { expectToDisplayTime(fixture.nativeElement, ':'); }) + .then(() => { + fixture.componentInstance.model = '09:25:00'; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { expectToDisplayTime(fixture.nativeElement, '09:25'); }); + })); + + it('should write the entered value as a string formatted by a custom time adapter', () => { + const html = ``; + + const fixture = createTestComponent(html); + fixture.componentInstance.model = null; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + + const inputs = fixture.debugElement.queryAll(By.css('input')); + inputs[0].triggerEventHandler('change', createChangeEvent('11')); + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '11:'); + expect(fixture.componentInstance.model).toBeNull(); + + inputs[1].triggerEventHandler('change', createChangeEvent('5')); + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '11:05'); + expect(fixture.componentInstance.model).toEqual('11:05:00'); + + inputs[0].triggerEventHandler('change', createChangeEvent('aa')); + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, ':05'); + expect(fixture.componentInstance.model).toBeNull(); + }); + }); + }); + + describe('on push', () => { + + it('should render initial model value', async(async() => { + const fixture = + createOnPushTestComponent(``); + fixture.detectChanges(); + await fixture.whenStable(); + fixture.detectChanges(); + expectToDisplayTime(fixture.nativeElement, '13:30'); + })); + }); +}); + + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + model; + disabled = true; + readonly = true; + form = new FormGroup({control: new FormControl('', Validators.required)}); + disabledForm = new FormGroup({control: new FormControl({value: '', disabled: true})}); + submitted = false; + + showSeconds = true; + + onSubmit() { this.submitted = true; } +} + +@Component({selector: 'test-cmp-on-push', template: '', changeDetection: ChangeDetectionStrategy.OnPush}) +class TestComponentOnPush { +} + +@Injectable() +class StringTimeAdapter extends NgbTimeAdapter { + fromModel(value: string): NgbTimeStruct { + if (!value) { + return null; + } + const split = value.split(':'); + return {hour: parseInt(split[0], 10), minute: parseInt(split[1], 10), second: parseInt(split[2], 10)}; + } + + toModel(time: NgbTimeStruct): string { + if (!time) { + return null; + } + return `${this.pad(time.hour)}:${this.pad(time.minute)}:${this.pad(time.second)}`; + } + + private pad(i: number): string { return i < 10 ? `0${i}` : `${i}`; } +} + +@Injectable() +class TestI18n extends NgbTimepickerI18n { + getMorningPeriod(): string { return 'morning'; } + getAfternoonPeriod(): string { return 'afternoon'; } +} diff --git a/src/timepicker/timepicker.ts b/src/timepicker/timepicker.ts new file mode 100644 index 0000000..2ff2ccd --- /dev/null +++ b/src/timepicker/timepicker.ts @@ -0,0 +1,284 @@ +import { + ChangeDetectorRef, + Component, + forwardRef, + Input, + OnChanges, + SimpleChanges, + ViewEncapsulation +} from '@angular/core'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; + +import {isInteger, isNumber, padNumber, toInteger} from '../util/util'; +import {NgbTime} from './ngb-time'; +import {NgbTimepickerConfig} from './timepicker-config'; +import {NgbTimeAdapter} from './ngb-time-adapter'; +import {NgbTimepickerI18n} from './timepicker-i18n'; + +const NGB_TIMEPICKER_VALUE_ACCESSOR = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NgbTimepicker), + multi: true +}; + +/** + * A directive that helps with wth picking hours, minutes and seconds. + */ +@Component({ + selector: 'ngb-timepicker', + encapsulation: ViewEncapsulation.None, + styleUrls: ['./timepicker.scss'], + template: ` +
+
+
+ + + +
+
:
+
+ + + +
+
:
+
+ + + +
+
+
+ +
+
+
+ `, + providers: [NGB_TIMEPICKER_VALUE_ACCESSOR] +}) +export class NgbTimepicker implements ControlValueAccessor, + OnChanges { + disabled: boolean; + model: NgbTime; + + private _hourStep: number; + private _minuteStep: number; + private _secondStep: number; + + /** + * Whether to display 12H or 24H mode. + */ + @Input() meridian: boolean; + + /** + * If `true`, the spinners above and below inputs are visible. + */ + @Input() spinners: boolean; + + /** + * If `true`, it is possible to select seconds. + */ + @Input() seconds: boolean; + + /** + * The number of hours to add/subtract when clicking hour spinners. + */ + @Input() + set hourStep(step: number) { + this._hourStep = isInteger(step) ? step : this._config.hourStep; + } + + get hourStep(): number { return this._hourStep; } + + /** + * The number of minutes to add/subtract when clicking minute spinners. + */ + @Input() + set minuteStep(step: number) { + this._minuteStep = isInteger(step) ? step : this._config.minuteStep; + } + + get minuteStep(): number { return this._minuteStep; } + + /** + * The number of seconds to add/subtract when clicking second spinners. + */ + @Input() + set secondStep(step: number) { + this._secondStep = isInteger(step) ? step : this._config.secondStep; + } + + get secondStep(): number { return this._secondStep; } + + /** + * If `true`, the timepicker is readonly and can't be changed. + */ + @Input() readonlyInputs: boolean; + + /** + * The size of inputs and buttons. + */ + @Input() size: 'small' | 'medium' | 'large'; + + constructor( + private readonly _config: NgbTimepickerConfig, private _ngbTimeAdapter: NgbTimeAdapter, + private _cd: ChangeDetectorRef, public i18n: NgbTimepickerI18n) { + this.meridian = _config.meridian; + this.spinners = _config.spinners; + this.seconds = _config.seconds; + this.hourStep = _config.hourStep; + this.minuteStep = _config.minuteStep; + this.secondStep = _config.secondStep; + this.disabled = _config.disabled; + this.readonlyInputs = _config.readonlyInputs; + this.size = _config.size; + } + + onChange = (_: any) => {}; + onTouched = () => {}; + + writeValue(value) { + const structValue = this._ngbTimeAdapter.fromModel(value); + this.model = structValue ? new NgbTime(structValue.hour, structValue.minute, structValue.second) : new NgbTime(); + if (!this.seconds && (!structValue || !isNumber(structValue.second))) { + this.model.second = 0; + } + this._cd.markForCheck(); + } + + registerOnChange(fn: (value: any) => any): void { this.onChange = fn; } + + registerOnTouched(fn: () => any): void { this.onTouched = fn; } + + setDisabledState(isDisabled: boolean) { this.disabled = isDisabled; } + + changeHour(step: number) { + this.model.changeHour(step); + this.propagateModelChange(); + } + + changeMinute(step: number) { + this.model.changeMinute(step); + this.propagateModelChange(); + } + + changeSecond(step: number) { + this.model.changeSecond(step); + this.propagateModelChange(); + } + + updateHour(newVal: string) { + const isPM = this.model.hour >= 12; + const enteredHour = toInteger(newVal); + if (this.meridian && (isPM && enteredHour < 12 || !isPM && enteredHour === 12)) { + this.model.updateHour(enteredHour + 12); + } else { + this.model.updateHour(enteredHour); + } + this.propagateModelChange(); + } + + updateMinute(newVal: string) { + this.model.updateMinute(toInteger(newVal)); + this.propagateModelChange(); + } + + updateSecond(newVal: string) { + this.model.updateSecond(toInteger(newVal)); + this.propagateModelChange(); + } + + toggleMeridian() { + if (this.meridian) { + this.changeHour(12); + } + } + + formatHour(value: number) { + if (isNumber(value)) { + if (this.meridian) { + return padNumber(value % 12 === 0 ? 12 : value % 12); + } else { + return padNumber(value % 24); + } + } else { + return padNumber(NaN); + } + } + + formatMinSec(value: number) { return padNumber(value); } + + get isSmallSize(): boolean { return this.size === 'small'; } + + get isLargeSize(): boolean { return this.size === 'large'; } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['seconds'] && !this.seconds && this.model && !isNumber(this.model.second)) { + this.model.second = 0; + this.propagateModelChange(false); + } + } + + private propagateModelChange(touched = true) { + if (touched) { + this.onTouched(); + } + if (this.model.isValid(this.seconds)) { + this.onChange( + this._ngbTimeAdapter.toModel({hour: this.model.hour, minute: this.model.minute, second: this.model.second})); + } else { + this.onChange(this._ngbTimeAdapter.toModel(null)); + } + } +} diff --git a/src/toast/toast-config.spec.ts b/src/toast/toast-config.spec.ts new file mode 100644 index 0000000..4f67a5f --- /dev/null +++ b/src/toast/toast-config.spec.ts @@ -0,0 +1,11 @@ +import {NgbToastConfig} from './toast-config'; + +describe('NgbToastConfig', () => { + it('should have sensible default values', () => { + const config = new NgbToastConfig(); + + expect(config.delay).toBe(500); + expect(config.autohide).toBe(true); + expect(config.ariaLive).toBe('polite'); + }); +}); diff --git a/src/toast/toast-config.ts b/src/toast/toast-config.ts new file mode 100644 index 0000000..2029b82 --- /dev/null +++ b/src/toast/toast-config.ts @@ -0,0 +1,42 @@ +import {Injectable} from '@angular/core'; + +/** + * Interface used to type all toast config options. See `NgbToastConfig`. + * + * @since 5.0.0 + */ +export interface NgbToastOptions { + /** + * Specify if the toast component should emit the `hide()` output + * after a certain `delay` in ms. + */ + autohide?: boolean; + + /** + * Delay in ms after which the `hide()` output should be emitted. + */ + delay?: number; + + /** + * Type of aria-live attribute to be used. + * + * Could be one of these 2 values (as string): + * - `polite` (default) + * - `alert` + */ + ariaLive?: 'polite' | 'alert'; +} + +/** + * Configuration service for the NgbToast component. You can inject this service, typically in your root component, + * and customize the values of its properties in order to provide default values for all the toasts used in the + * application. + * + * @since 5.0.0 + */ +@Injectable({providedIn: 'root'}) +export class NgbToastConfig implements NgbToastOptions { + autohide = true; + delay = 500; + ariaLive: 'polite' | 'alert' = 'polite'; +} diff --git a/src/toast/toast.module.ts b/src/toast/toast.module.ts new file mode 100644 index 0000000..ccc8d17 --- /dev/null +++ b/src/toast/toast.module.ts @@ -0,0 +1,11 @@ +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; + +import {NgbToast, NgbToastHeader} from './toast'; + +export {NgbToast, NgbToastHeader} from './toast'; +export {NgbToastConfig, NgbToastOptions} from './toast-config'; + +@NgModule({declarations: [NgbToast, NgbToastHeader], imports: [CommonModule], exports: [NgbToast, NgbToastHeader]}) +export class NgbToastModule { +} diff --git a/src/toast/toast.scss b/src/toast/toast.scss new file mode 100644 index 0000000..63d8adc --- /dev/null +++ b/src/toast/toast.scss @@ -0,0 +1,14 @@ +.ngb-toasts { + position: fixed; + top: 0; + right: 0; + margin: 0.5em; + z-index: 1200; +} + +ngb-toast { + .toast-header .close { + margin-left: auto; + margin-bottom: 0.25rem; + } +} diff --git a/src/toast/toast.spec.ts b/src/toast/toast.spec.ts new file mode 100644 index 0000000..6cab45e --- /dev/null +++ b/src/toast/toast.spec.ts @@ -0,0 +1,119 @@ +import {Component} from '@angular/core'; +import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; + +import {createGenericTestComponent} from '../test/common'; +import {NgbToastModule} from './toast.module'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +const getElementWithSelector = (fixture: ComponentFixture, className) => + fixture.nativeElement.querySelector(className); + +const getToastElement = (fixture: ComponentFixture): Element => + getElementWithSelector(fixture, 'ngb-toast'); +const getToastHeaderElement = (fixture: ComponentFixture): Element => + getElementWithSelector(fixture, 'ngb-toast .toast-header'); +const getToastBodyElement = (fixture: ComponentFixture): Element => + getElementWithSelector(fixture, 'ngb-toast .toast-body'); + +describe('ngb-toast', () => { + + beforeEach(() => { TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbToastModule]}); }); + + describe('via declarative usage', () => { + it('should be instantiable declaratively', () => { + const fixture = createTestComponent(`body`); + expect(fixture.componentInstance).toBeTruthy(); + }); + + it('should have default classnames', () => { + const fixture = createTestComponent(`body`); + // Below getter are using Bootstrap classnames + const toast = getToastElement(fixture); + const header = getToastHeaderElement(fixture); + const body = getToastBodyElement(fixture); + + expect(toast).toBeDefined(); + expect(header).toBeDefined(); + expect(body).toBeDefined(); + }); + + it('should not generate a header element when header input is not specified', () => { + const fixture = createTestComponent(`body`); + const toastHeader = getToastHeaderElement(fixture); + expect(toastHeader).toBeNull(); + }); + + it('should contain a close button when header is specified', () => { + const fixture = createTestComponent(`body`); + const toastHeader = getToastHeaderElement(fixture); + expect(toastHeader.querySelector('button.close')).toBeDefined(); + }); + + it('should contain a close button when ngbToastHeader is used', () => { + const fixture = createTestComponent(` + {{header}} + body + `); + const toastHeader = getToastHeaderElement(fixture); + expect(toastHeader.querySelector('button.close')).toBeDefined(); + }); + + it('should emit hide output when close is clicked', () => { + const fixture = + createTestComponent(`body`); + + const toast = getToastElement(fixture); + const closeButton = toast.querySelector('button.close') as HTMLElement; + closeButton.click(); + fixture.detectChanges(); + expect(fixture.componentInstance.hide).toHaveBeenCalled(); + }); + + it('should emit hide output after default delay (500ms)', fakeAsync(() => { + const fixture = createTestComponent(`body`); + tick(499); + fixture.detectChanges(); + expect(fixture.componentInstance.hide).not.toHaveBeenCalled(); + tick(500); + fixture.detectChanges(); + expect(fixture.componentInstance.hide).toHaveBeenCalledTimes(1); + })); + + it('should emit hide output after a custom delay in ms', fakeAsync(() => { + const fixture = + createTestComponent(`body`); + tick(9999); + fixture.detectChanges(); + expect(fixture.componentInstance.hide).not.toHaveBeenCalled(); + tick(10000); + fixture.detectChanges(); + expect(fixture.componentInstance.hide).toHaveBeenCalledTimes(1); + })); + + it('should emit hide only one time regardless of autohide toggling', fakeAsync(() => { + const fixture = + createTestComponent(`body`); + tick(250); + fixture.componentInstance.autohide = false; + fixture.detectChanges(); + tick(250); + fixture.detectChanges(); + expect(fixture.componentInstance.hide).not.toHaveBeenCalled(); + fixture.componentInstance.autohide = true; + fixture.detectChanges(); + tick(500); + fixture.detectChanges(); + expect(fixture.componentInstance.hide).toHaveBeenCalledTimes(1); + })); + }); +}); + + +@Component({selector: 'test-cmp', template: ''}) +export class TestComponent { + visible = true; + autohide = true; + hide = jasmine.createSpy('hideSpy'); +} diff --git a/src/toast/toast.ts b/src/toast/toast.ts new file mode 100644 index 0000000..49b3853 --- /dev/null +++ b/src/toast/toast.ts @@ -0,0 +1,137 @@ +import { + AfterContentInit, + Attribute, + Component, + ContentChild, + Directive, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, + TemplateRef, + ViewEncapsulation, +} from '@angular/core'; + +import {NgbToastConfig} from './toast-config'; + +/** + * This directive allows the usage of HTML markup or other directives + * inside of the toast's header. + * + * @since 5.0.0 + */ +@Directive({selector: '[ngbToastHeader]'}) +export class NgbToastHeader { +} + +/** + * Toasts provide feedback messages as notifications to the user. + * Goal is to mimic the push notifications available both on mobile and desktop operating systems. + * + * @since 5.0.0 + */ +@Component({ + selector: 'ngb-toast', + exportAs: 'ngbToast', + encapsulation: ViewEncapsulation.None, + host: { + 'role': 'alert', + '[attr.aria-live]': 'ariaLive', + 'aria-atomic': 'true', + '[class.toast]': 'true', + '[class.show]': 'true', + '[class.autohide]': 'autohide', + }, + template: ` + + {{header}} + + +
+ + +
+
+
+ +
+ `, + styleUrls: ['./toast.scss'] +}) +export class NgbToast implements AfterContentInit, + OnChanges { + private _timeoutID; + + /** + * Delay after which the toast will hide (ms). + * default: `500` (ms) (inherited from NgbToastConfig) + */ + @Input() delay: number; + + /** + * Auto hide the toast after a delay in ms. + * default: `true` (inherited from NgbToastConfig) + */ + @Input() autohide: boolean; + + /** + * Text to be used as toast's header. + * Ignored if a ContentChild template is specified at the same time. + */ + @Input() header: string; + + /** + * A template like `` can be + * used in the projected content to allow markup usage. + */ + @ContentChild(NgbToastHeader, {read: TemplateRef, static: true}) contentHeaderTpl: TemplateRef| null = null; + + /** + * An event fired immediately when toast's `hide()` method has been called. + * It can only occur in 2 different scenarios: + * - `autohide` timeout fires + * - user clicks on a closing cross (×) + * + * Additionally this output is purely informative. The toast won't disappear. It's up to the user to take care of + * that. + */ + @Output('hide') hideOutput = new EventEmitter(); + + constructor(@Attribute('aria-live') public ariaLive: string, config: NgbToastConfig) { + if (this.ariaLive == null) { + this.ariaLive = config.ariaLive; + } + this.delay = config.delay; + this.autohide = config.autohide; + } + + ngAfterContentInit() { this._init(); } + + ngOnChanges(changes: SimpleChanges) { + if ('autohide' in changes) { + this._clearTimeout(); + this._init(); + } + } + + hide() { + this._clearTimeout(); + this.hideOutput.emit(); + } + + private _init() { + if (this.autohide && !this._timeoutID) { + this._timeoutID = setTimeout(() => this.hide(), this.delay); + } + } + + private _clearTimeout() { + if (this._timeoutID) { + clearTimeout(this._timeoutID); + this._timeoutID = null; + } + } +} diff --git a/src/tooltip/tooltip-config.spec.ts b/src/tooltip/tooltip-config.spec.ts new file mode 100644 index 0000000..08f6c3e --- /dev/null +++ b/src/tooltip/tooltip-config.spec.ts @@ -0,0 +1,16 @@ +import {NgbTooltipConfig} from './tooltip-config'; + +describe('ngb-tooltip-config', () => { + it('should have sensible default values', () => { + const config = new NgbTooltipConfig(); + + expect(config.autoClose).toBe(true); + expect(config.placement).toBe('auto'); + expect(config.triggers).toBe('hover focus'); + expect(config.container).toBeUndefined(); + expect(config.disableTooltip).toBe(false); + expect(config.tooltipClass).toBeUndefined(); + expect(config.openDelay).toBe(0); + expect(config.closeDelay).toBe(0); + }); +}); diff --git a/src/tooltip/tooltip-config.ts b/src/tooltip/tooltip-config.ts new file mode 100644 index 0000000..400e2ed --- /dev/null +++ b/src/tooltip/tooltip-config.ts @@ -0,0 +1,20 @@ +import {Injectable} from '@angular/core'; +import {PlacementArray} from '../util/positioning'; + +/** + * A configuration service for the [`NgbTooltip`](#/components/tooltip/api#NgbTooltip) component. + * + * You can inject this service, typically in your root component, and customize the values of its properties in + * order to provide default values for all the tooltips used in the application. + */ +@Injectable({providedIn: 'root'}) +export class NgbTooltipConfig { + autoClose: boolean | 'inside' | 'outside' = true; + placement: PlacementArray = 'auto'; + triggers = 'hover focus'; + container: string; + disableTooltip = false; + tooltipClass: string; + openDelay = 0; + closeDelay = 0; +} diff --git a/src/tooltip/tooltip.module.ts b/src/tooltip/tooltip.module.ts new file mode 100644 index 0000000..9e345c0 --- /dev/null +++ b/src/tooltip/tooltip.module.ts @@ -0,0 +1,11 @@ +import {NgModule} from '@angular/core'; + +import {NgbTooltip, NgbTooltipWindow} from './tooltip'; + +export {NgbTooltipConfig} from './tooltip-config'; +export {NgbTooltip} from './tooltip'; +export {Placement} from '../util/positioning'; + +@NgModule({declarations: [NgbTooltip, NgbTooltipWindow], exports: [NgbTooltip], entryComponents: [NgbTooltipWindow]}) +export class NgbTooltipModule { +} diff --git a/src/tooltip/tooltip.scss b/src/tooltip/tooltip.scss new file mode 100644 index 0000000..d4ecc6d --- /dev/null +++ b/src/tooltip/tooltip.scss @@ -0,0 +1,36 @@ + +$arrow-size: 0.8rem; + +ngb-tooltip-window { + &.bs-tooltip-top .arrow, + &.bs-tooltip-bottom .arrow { + left: calc(50% - #{$arrow-size / 2}); + } + + &.bs-tooltip-top-left .arrow, + &.bs-tooltip-bottom-left .arrow { + left: 1em; + } + + &.bs-tooltip-top-right .arrow, + &.bs-tooltip-bottom-right .arrow { + left: auto; + right: 0.8rem; + } + + &.bs-tooltip-left .arrow, + &.bs-tooltip-right .arrow { + top: calc(50% - #{$arrow-size / 2}); + } + + &.bs-tooltip-left-top .arrow, + &.bs-tooltip-right-top .arrow { + top: 0.4rem; + } + + &.bs-tooltip-left-bottom .arrow, + &.bs-tooltip-right-bottom .arrow { + top: auto; + bottom: 0.4rem; + } +} diff --git a/src/tooltip/tooltip.spec.ts b/src/tooltip/tooltip.spec.ts new file mode 100644 index 0000000..b028860 --- /dev/null +++ b/src/tooltip/tooltip.spec.ts @@ -0,0 +1,664 @@ +import {TestBed, ComponentFixture, inject, fakeAsync, tick} from '@angular/core/testing'; +import {createGenericTestComponent, createKeyEvent, triggerEvent} from '../test/common'; + +import {By} from '@angular/platform-browser'; +import { + Component, + ViewChild, + ChangeDetectionStrategy, + TemplateRef, + ViewContainerRef, + AfterViewInit +} from '@angular/core'; + +import {Key} from '../util/key'; + +import {NgbTooltipModule} from './tooltip.module'; +import {NgbTooltipWindow, NgbTooltip} from './tooltip'; +import {NgbTooltipConfig} from './tooltip-config'; + +function dispatchEscapeKeyUpEvent() { + document.dispatchEvent(createKeyEvent(Key.Escape)); +} + +const createTestComponent = + (html: string) => >createGenericTestComponent(html, TestComponent); + +const createOnPushTestComponent = + (html: string) => >createGenericTestComponent(html, TestOnPushComponent); + +describe('ngb-tooltip-window', () => { + beforeEach(() => { TestBed.configureTestingModule({imports: [NgbTooltipModule]}); }); + + afterEach(() => { + // Cleaning elements, because of a TestBed issue with the id attribute + Array.from(document.body.children).map((element: HTMLElement) => { + if (element.tagName.toLocaleLowerCase() === 'div') { + element.parentNode.removeChild(element); + } + }); + }); + + it('should render tooltip on top by default', () => { + const fixture = TestBed.createComponent(NgbTooltipWindow); + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveCssClass('tooltip'); + expect(fixture.nativeElement).not.toHaveCssClass('bs-tooltip-top'); + expect(fixture.nativeElement.getAttribute('role')).toBe('tooltip'); + }); + + it('should optionally have a custom class', () => { + const fixture = TestBed.createComponent(NgbTooltipWindow); + fixture.detectChanges(); + + expect(fixture.nativeElement).not.toHaveCssClass('my-custom-class'); + + fixture.componentInstance.tooltipClass = 'my-custom-class'; + fixture.detectChanges(); + + expect(fixture.nativeElement).toHaveCssClass('my-custom-class'); + }); + +}); + +describe('ngb-tooltip', () => { + + beforeEach(() => { + TestBed.configureTestingModule( + {declarations: [TestComponent, TestOnPushComponent, TestHooksComponent], imports: [NgbTooltipModule]}); + }); + + function getWindow(element) { return element.querySelector('ngb-tooltip-window'); } + + describe('basic functionality', () => { + + it('should open and close a tooltip - default settings and content as string', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + const id = windowEl.getAttribute('id'); + + expect(windowEl).toHaveCssClass('tooltip'); + expect(windowEl).toHaveCssClass('bs-tooltip-top'); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + expect(windowEl.getAttribute('role')).toBe('tooltip'); + expect(windowEl.parentNode).toBe(fixture.nativeElement); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBe(id); + + triggerEvent(directive, 'mouseleave'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBeNull(); + }); + + it('should open and close a tooltip - default settings and content from a template', () => { + const fixture = createTestComponent(` + Hello, {{name}}!
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + const id = windowEl.getAttribute('id'); + + expect(windowEl).toHaveCssClass('tooltip'); + expect(windowEl).toHaveCssClass('bs-tooltip-top'); + expect(windowEl.textContent.trim()).toBe('Hello, World!'); + expect(windowEl.getAttribute('role')).toBe('tooltip'); + expect(windowEl.parentNode).toBe(fixture.nativeElement); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBe(id); + + triggerEvent(directive, 'mouseleave'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBeNull(); + }); + + it('should open and close a tooltip - default settings, content from a template and context supplied', () => { + const fixture = createTestComponent(` + Hello, {{name}}!
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + directive.context.tooltip.open({name: 'John'}); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + const id = windowEl.getAttribute('id'); + + expect(windowEl).toHaveCssClass('tooltip'); + expect(windowEl).toHaveCssClass('bs-tooltip-top'); + expect(windowEl.textContent.trim()).toBe('Hello, John!'); + expect(windowEl.getAttribute('role')).toBe('tooltip'); + expect(windowEl.parentNode).toBe(fixture.nativeElement); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBe(id); + + triggerEvent(directive, 'mouseleave'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBeNull(); + }); + + it('should open and close a tooltip - default settings and custom class', () => { + const fixture = createTestComponent(` +
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + const id = windowEl.getAttribute('id'); + + expect(windowEl).toHaveCssClass('tooltip'); + expect(windowEl).toHaveCssClass('bs-tooltip-top'); + expect(windowEl).toHaveCssClass('my-custom-class'); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + expect(windowEl.getAttribute('role')).toBe('tooltip'); + expect(windowEl.parentNode).toBe(fixture.nativeElement); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBe(id); + + triggerEvent(directive, 'mouseleave'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(directive.nativeElement.getAttribute('aria-describedby')).toBeNull(); + }); + + it('should not open a tooltip if content is falsy', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toBeNull(); + }); + + it('should close the tooltip tooltip if content becomes falsy', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + fixture.componentInstance.name = null; + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should not open a tooltip if [disableTooltip] flag', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toBeNull(); + }); + + it('should allow re-opening previously closed tooltips', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + triggerEvent(directive, 'mouseleave'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + }); + + it('should not leave dangling tooltips in the DOM', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should properly cleanup tooltips with manual triggers', () => { + const fixture = createTestComponent(` + +
+
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should open tooltip from hooks', () => { + const fixture = TestBed.createComponent(TestHooksComponent); + fixture.detectChanges(); + + const tooltipWindow = fixture.debugElement.query(By.directive(NgbTooltipWindow)); + expect(tooltipWindow.nativeElement).toHaveCssClass('tooltip'); + expect(tooltipWindow.nativeElement).toHaveCssClass('show'); + }); + + describe('positioning', () => { + + it('should use requested position', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('tooltip'); + expect(windowEl).toHaveCssClass('bs-tooltip-left'); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + }); + + it('should properly position tooltips when a component is using the OnPush strategy', () => { + const fixture = createOnPushTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('tooltip'); + expect(windowEl).toHaveCssClass('bs-tooltip-left'); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + }); + + it('should have proper arrow placement', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('tooltip'); + expect(windowEl).toHaveCssClass('bs-tooltip-right'); + expect(windowEl).toHaveCssClass('bs-tooltip-right-top'); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + }); + + it('should accept placement in array (second value of the array should be applied)', () => { + const fixture = createTestComponent( + `
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('tooltip'); + expect(windowEl).toHaveCssClass('bs-tooltip-top'); + expect(windowEl).toHaveCssClass('bs-tooltip-top-left'); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + }); + + it('should accept placement with space separated values (second value should be applied)', () => { + const fixture = createTestComponent( + `
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('tooltip'); + expect(windowEl).toHaveCssClass('bs-tooltip-top'); + expect(windowEl).toHaveCssClass('bs-tooltip-top-left'); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + }); + + it('should apply auto placement', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + const windowEl = getWindow(fixture.nativeElement); + + expect(windowEl).toHaveCssClass('tooltip'); + // actual placement with auto is not known in advance, so use regex to check it + expect(windowEl.getAttribute('class')).toMatch('bs-tooltip-\.'); + expect(windowEl.textContent.trim()).toBe('Great tip!'); + }); + + }); + + describe('triggers', () => { + + it('should support focus triggers', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'focusin'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + triggerEvent(directive, 'focusout'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should support toggle triggers', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should non-default toggle triggers', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should support multiple triggers', () => { + const fixture = + createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should not use default for manual triggers', () => { + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should allow toggling for manual triggers', () => { + const fixture = createTestComponent(` +
+ `); + const button = fixture.nativeElement.querySelector('button'); + + button.click(); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + button.click(); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should allow open / close for manual triggers', () => { + const fixture = createTestComponent(` +
+ + `); + + const buttons = fixture.nativeElement.querySelectorAll('button'); + + buttons[0].click(); // open + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + buttons[1].click(); // close + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + + it('should not throw when open called for manual triggers and open tooltip', () => { + const fixture = createTestComponent(` +
+ `); + const button = fixture.nativeElement.querySelector('button'); + + button.click(); // open + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + + button.click(); // open + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + }); + + it('should not throw when closed called for manual triggers and closed tooltip', () => { + const fixture = createTestComponent(` +
+ `); + + const button = fixture.nativeElement.querySelector('button'); + + button.click(); // close + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + }); + }); + }); + + describe('container', () => { + + it('should be appended to the element matching the selector passed to "container"', () => { + const selector = 'body'; + const fixture = createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(getWindow(document.querySelector(selector))).not.toBeNull(); + }); + + it('should properly destroy tooltips when the "container" option is used', () => { + const selector = 'body'; + const fixture = + createTestComponent(`
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + triggerEvent(directive, 'mouseenter'); + fixture.detectChanges(); + + expect(getWindow(document.querySelector(selector))).not.toBeNull(); + fixture.componentRef.instance.show = false; + fixture.detectChanges(); + expect(getWindow(document.querySelector(selector))).toBeNull(); + }); + }); + + describe('visibility', () => { + it('should emit events when showing and hiding tooltip', () => { + const fixture = createTestComponent( + `
`); + const directive = fixture.debugElement.query(By.directive(NgbTooltip)); + + let shownSpy = spyOn(fixture.componentInstance, 'shown'); + let hiddenSpy = spyOn(fixture.componentInstance, 'hidden'); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + expect(shownSpy).toHaveBeenCalled(); + + triggerEvent(directive, 'click'); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(hiddenSpy).toHaveBeenCalled(); + }); + + it('should not emit close event when already closed', () => { + const fixture = createTestComponent( + `
`); + + let shownSpy = spyOn(fixture.componentInstance, 'shown'); + let hiddenSpy = spyOn(fixture.componentInstance, 'hidden'); + + fixture.componentInstance.tooltip.open(); + fixture.detectChanges(); + + fixture.componentInstance.tooltip.open(); + fixture.detectChanges(); + + expect(getWindow(fixture.nativeElement)).not.toBeNull(); + expect(shownSpy).toHaveBeenCalled(); + expect(shownSpy.calls.count()).toEqual(1); + expect(hiddenSpy).not.toHaveBeenCalled(); + }); + + it('should not emit open event when already opened', () => { + const fixture = createTestComponent( + `
`); + + let shownSpy = spyOn(fixture.componentInstance, 'shown'); + let hiddenSpy = spyOn(fixture.componentInstance, 'hidden'); + + fixture.componentInstance.tooltip.close(); + fixture.detectChanges(); + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(shownSpy).not.toHaveBeenCalled(); + expect(hiddenSpy).not.toHaveBeenCalled(); + }); + + it('should report correct visibility', () => { + const fixture = createTestComponent(`
`); + fixture.detectChanges(); + + expect(fixture.componentInstance.tooltip.isOpen()).toBeFalsy(); + + fixture.componentInstance.tooltip.open(); + fixture.detectChanges(); + expect(fixture.componentInstance.tooltip.isOpen()).toBeTruthy(); + + fixture.componentInstance.tooltip.close(); + fixture.detectChanges(); + expect(fixture.componentInstance.tooltip.isOpen()).toBeFalsy(); + }); + }); + + describe('Custom config', () => { + let config: NgbTooltipConfig; + + beforeEach(() => { + TestBed.configureTestingModule({imports: [NgbTooltipModule]}); + TestBed.overrideComponent(TestComponent, {set: {template: `
`}}); + }); + + beforeEach(inject([NgbTooltipConfig], (c: NgbTooltipConfig) => { + config = c; + config.placement = 'bottom'; + config.triggers = 'click'; + config.container = 'body'; + config.tooltipClass = 'my-custom-class'; + })); + + it('should initialize inputs with provided config', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + const tooltip = fixture.componentInstance.tooltip; + + expect(tooltip.placement).toBe(config.placement); + expect(tooltip.triggers).toBe(config.triggers); + expect(tooltip.container).toBe(config.container); + expect(tooltip.tooltipClass).toBe(config.tooltipClass); + }); + }); + + describe('Custom config as provider', () => { + let config = new NgbTooltipConfig(); + config.placement = 'bottom'; + config.triggers = 'click'; + config.container = 'body'; + config.tooltipClass = 'my-custom-class'; + + beforeEach(() => { + TestBed.configureTestingModule( + {imports: [NgbTooltipModule], providers: [{provide: NgbTooltipConfig, useValue: config}]}); + }); + + it('should initialize inputs with provided config as provider', () => { + const fixture = createTestComponent(`
`); + const tooltip = fixture.componentInstance.tooltip; + + expect(tooltip.placement).toBe(config.placement); + expect(tooltip.triggers).toBe(config.triggers); + expect(tooltip.container).toBe(config.container); + expect(tooltip.tooltipClass).toBe(config.tooltipClass); + }); + }); + + describe('non-regression', () => { + + /** + * Under very specific conditions ngOnDestroy can be invoked without calling ngOnInit first. + * See discussion in https://github.com/ng-bootstrap/ng-bootstrap/issues/2199 for more details. + */ + it('should not try to call listener cleanup function when no listeners registered', () => { + const fixture = createTestComponent(` +
+ + `); + const buttonEl = fixture.debugElement.query(By.css('button')); + triggerEvent(buttonEl, 'click'); + }); + }); +}); + +@Component({selector: 'test-cmpt', template: ``}) +export class TestComponent { + name = 'World'; + show = true; + + @ViewChild(NgbTooltip, {static: true}) tooltip: NgbTooltip; + + shown() {} + hidden() {} + + constructor(private _vcRef: ViewContainerRef) {} + + createAndDestroyTplWithATooltip(tpl: TemplateRef) { + this._vcRef.createEmbeddedView(tpl, {}, 0); + this._vcRef.remove(0); + } +} + +@Component({selector: 'test-onpush-cmpt', changeDetection: ChangeDetectionStrategy.OnPush, template: ``}) +export class TestOnPushComponent { +} + +@Component({selector: 'test-hooks', template: `
`}) +export class TestHooksComponent implements AfterViewInit { + @ViewChild(NgbTooltip, {static: true}) tooltip; + + ngAfterViewInit() { this.tooltip.open(); } +} diff --git a/src/tooltip/tooltip.ts b/src/tooltip/tooltip.ts new file mode 100644 index 0000000..093d6af --- /dev/null +++ b/src/tooltip/tooltip.ts @@ -0,0 +1,265 @@ +import { + Component, + Directive, + Input, + Output, + EventEmitter, + ChangeDetectionStrategy, + OnInit, + OnDestroy, + Inject, + Injector, + Renderer2, + ComponentRef, + ElementRef, + TemplateRef, + ViewContainerRef, + ComponentFactoryResolver, + NgZone, + ViewEncapsulation, + ChangeDetectorRef, + ApplicationRef +} from '@angular/core'; +import {DOCUMENT} from '@angular/common'; + +import {listenToTriggers} from '../util/triggers'; +import {ngbAutoClose} from '../util/autoclose'; +import {positionElements, PlacementArray} from '../util/positioning'; +import {PopupService} from '../util/popup'; + +import {NgbTooltipConfig} from './tooltip-config'; + +let nextId = 0; + +@Component({ + selector: 'ngb-tooltip-window', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + host: {'[class]': '"tooltip show" + (tooltipClass ? " " + tooltipClass : "")', 'role': 'tooltip', '[id]': 'id'}, + template: `
`, + styleUrls: ['./tooltip.scss'] +}) +export class NgbTooltipWindow { + @Input() id: string; + @Input() tooltipClass: string; +} + +/** + * A lightweight and extensible directive for fancy tooltip creation. + */ +@Directive({selector: '[ngbTooltip]', exportAs: 'ngbTooltip'}) +export class NgbTooltip implements OnInit, OnDestroy { + /** + * Indicates whether the tooltip should be closed on `Escape` key and inside/outside clicks: + * + * * `true` - closes on both outside and inside clicks as well as `Escape` presses + * * `false` - disables the autoClose feature (NB: triggers still apply) + * * `"inside"` - closes on inside clicks as well as Escape presses + * * `"outside"` - closes on outside clicks (sometimes also achievable through triggers) + * as well as `Escape` presses + * + * @since 3.0.0 + */ + @Input() autoClose: boolean | 'inside' | 'outside'; + + /** + * The preferred placement of the tooltip. + * + * Possible values are `"top"`, `"top-left"`, `"top-right"`, `"bottom"`, `"bottom-left"`, + * `"bottom-right"`, `"left"`, `"left-top"`, `"left-bottom"`, `"right"`, `"right-top"`, + * `"right-bottom"` + * + * Accepts an array of strings or a string with space separated possible values. + * + * The default order of preference is `"auto"` (same as the sequence above). + * + * Please see the [positioning overview](#/positioning) for more details. + */ + @Input() placement: PlacementArray; + + /** + * Specifies events that should trigger the tooltip. + * + * Supports a space separated list of event names. + * For more details see the [triggers demo](#/components/tooltip/examples#triggers). + */ + @Input() triggers: string; + + /** + * A selector specifying the element the tooltip should be appended to. + * + * Currently only supports `"body"`. + */ + @Input() container: string; + + /** + * If `true`, tooltip is disabled and won't be displayed. + * + * @since 1.1.0 + */ + @Input() disableTooltip: boolean; + + /** + * An optional class applied to the tooltip window element. + * + * @since 3.2.0 + */ + @Input() tooltipClass: string; + + /** + * The opening delay in ms. Works only for "non-manual" opening triggers defined by the `triggers` input. + * + * @since 4.1.0 + */ + @Input() openDelay: number; + + /** + * The closing delay in ms. Works only for "non-manual" opening triggers defined by the `triggers` input. + * + * @since 4.1.0 + */ + @Input() closeDelay: number; + + /** + * An event emitted when the tooltip is shown. Contains no payload. + */ + @Output() shown = new EventEmitter(); + /** + * An event emitted when the popover is hidden. Contains no payload. + */ + @Output() hidden = new EventEmitter(); + + private _ngbTooltip: string | TemplateRef; + private _ngbTooltipWindowId = `ngb-tooltip-${nextId++}`; + private _popupService: PopupService; + private _windowRef: ComponentRef; + private _unregisterListenersFn; + private _zoneSubscription: any; + + constructor( + private _elementRef: ElementRef, private _renderer: Renderer2, injector: Injector, + componentFactoryResolver: ComponentFactoryResolver, viewContainerRef: ViewContainerRef, config: NgbTooltipConfig, + private _ngZone: NgZone, @Inject(DOCUMENT) private _document: any, private _changeDetector: ChangeDetectorRef, + private _applicationRef: ApplicationRef) { + this.autoClose = config.autoClose; + this.placement = config.placement; + this.triggers = config.triggers; + this.container = config.container; + this.disableTooltip = config.disableTooltip; + this.tooltipClass = config.tooltipClass; + this.openDelay = config.openDelay; + this.closeDelay = config.closeDelay; + this._popupService = new PopupService( + NgbTooltipWindow, injector, viewContainerRef, _renderer, componentFactoryResolver, _applicationRef); + + this._zoneSubscription = _ngZone.onStable.subscribe(() => { + if (this._windowRef) { + positionElements( + this._elementRef.nativeElement, this._windowRef.location.nativeElement, this.placement, + this.container === 'body', 'bs-tooltip'); + } + }); + } + + /** + * The string content or a `TemplateRef` for the content to be displayed in the tooltip. + * + * If the content if falsy, the tooltip won't open. + */ + @Input() + set ngbTooltip(value: string | TemplateRef) { + this._ngbTooltip = value; + if (!value && this._windowRef) { + this.close(); + } + } + + get ngbTooltip() { return this._ngbTooltip; } + + /** + * Opens the tooltip. + * + * This is considered to be a "manual" triggering. + * The `context` is an optional value to be injected into the tooltip template when it is created. + */ + open(context?: any) { + if (!this._windowRef && this._ngbTooltip && !this.disableTooltip) { + this._windowRef = this._popupService.open(this._ngbTooltip, context); + this._windowRef.instance.tooltipClass = this.tooltipClass; + this._windowRef.instance.id = this._ngbTooltipWindowId; + + this._renderer.setAttribute(this._elementRef.nativeElement, 'aria-describedby', this._ngbTooltipWindowId); + + if (this.container === 'body') { + this._document.querySelector(this.container).appendChild(this._windowRef.location.nativeElement); + } + + // We need to detect changes, because we don't know where .open() might be called from. + // Ex. opening tooltip from one of lifecycle hooks that run after the CD + // (say from ngAfterViewInit) will result in 'ExpressionHasChanged' exception + this._windowRef.changeDetectorRef.detectChanges(); + + // We need to mark for check, because tooltip won't work inside the OnPush component. + // Ex. when we use expression like `{{ tooltip.isOpen() : 'opened' : 'closed' }}` + // inside the template of an OnPush component and we change the tooltip from + // open -> closed, the expression in question won't be updated unless we explicitly + // mark the parent component to be checked. + this._windowRef.changeDetectorRef.markForCheck(); + + ngbAutoClose( + this._ngZone, this._document, this.autoClose, () => this.close(), this.hidden, + [this._windowRef.location.nativeElement]); + + this.shown.emit(); + } + } + + /** + * Closes the tooltip. + * + * This is considered to be a "manual" triggering of the tooltip. + */ + close(): void { + if (this._windowRef != null) { + this._renderer.removeAttribute(this._elementRef.nativeElement, 'aria-describedby'); + this._popupService.close(); + this._windowRef = null; + this.hidden.emit(); + this._changeDetector.markForCheck(); + } + } + + /** + * Toggles the tooltip. + * + * This is considered to be a "manual" triggering of the tooltip. + */ + toggle(): void { + if (this._windowRef) { + this.close(); + } else { + this.open(); + } + } + + /** + * Returns `true`, if the popover is currently shown. + */ + isOpen(): boolean { return this._windowRef != null; } + + ngOnInit() { + this._unregisterListenersFn = listenToTriggers( + this._renderer, this._elementRef.nativeElement, this.triggers, this.isOpen.bind(this), this.open.bind(this), + this.close.bind(this), +this.openDelay, +this.closeDelay); + } + + ngOnDestroy() { + this.close(); + // This check is needed as it might happen that ngOnDestroy is called before ngOnInit + // under certain conditions, see: https://github.com/ng-bootstrap/ng-bootstrap/issues/2199 + if (this._unregisterListenersFn) { + this._unregisterListenersFn(); + } + this._zoneSubscription.unsubscribe(); + } +} diff --git a/src/tsconfig-ie.spec.json b/src/tsconfig-ie.spec.json new file mode 100644 index 0000000..be2b7b4 --- /dev/null +++ b/src/tsconfig-ie.spec.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.spec.json", + "compilerOptions": { + "target": "es5", + } +} diff --git a/src/tsconfig.json b/src/tsconfig.json new file mode 100644 index 0000000..ede5f4b --- /dev/null +++ b/src/tsconfig.json @@ -0,0 +1,17 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "inlineSources": true, + "importHelpers": true + }, + "angularCompilerOptions": { + "annotateForClosureCompiler": true, + "skipTemplateCodegen": true, + "strictMetadataEmit": true, + "fullTemplateTypeCheck": true, + "strictInjectionParameters": true + }, + "include": [ + "./**/*.ts" + ] +} diff --git a/src/tsconfig.spec.json b/src/tsconfig.spec.json new file mode 100644 index 0000000..bce3803 --- /dev/null +++ b/src/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "./test.ts" + ], + "include": [ + "./**/*.spec.ts", + "./**/*.d.ts" + ] +} diff --git a/src/tslint.json b/src/tslint.json new file mode 100644 index 0000000..ec365f1 --- /dev/null +++ b/src/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": "../tslint.json" +} diff --git a/src/typeahead/highlight.scss b/src/typeahead/highlight.scss new file mode 100644 index 0000000..bf328f1 --- /dev/null +++ b/src/typeahead/highlight.scss @@ -0,0 +1,3 @@ +.ngb-highlight { + font-weight: bold; +} diff --git a/src/typeahead/highlight.spec.ts b/src/typeahead/highlight.spec.ts new file mode 100644 index 0000000..20df7e2 --- /dev/null +++ b/src/typeahead/highlight.spec.ts @@ -0,0 +1,165 @@ +import {TestBed, ComponentFixture} from '@angular/core/testing'; +import {createGenericTestComponent} from '../test/common'; + +import {Component} from '@angular/core'; + +import {NgbHighlight} from './highlight'; +import {NgbTypeaheadModule} from './typeahead.module'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +/** + * Get generated innerHtml without HTML comments and Angular debug attributes + */ +function highlightHtml(fixture) { + const elms = fixture.nativeElement.children[0].childNodes; + let elm; + let result = ''; + let nodeName; + + for (let i = 0; i < elms.length; i++) { + elm = elms[i]; + + if (elm.nodeType === Node.ELEMENT_NODE) { + nodeName = elm.nodeName.toLowerCase(); + result += `<${nodeName} class="${elm.className}">${elm.textContent}`; + } else if (elm.nodeType === Node.TEXT_NODE) { + result += elm.wholeText; + } + } + + return result; +} + +describe('ngb-highlight', () => { + + beforeEach(() => { + TestBed.overrideModule(NgbTypeaheadModule, {set: {exports: [NgbHighlight]}}); + TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbTypeaheadModule]}); + }); + + it('should render highlighted text when there is one match', () => { + const fixture = createTestComponent(''); + + expect(highlightHtml(fixture)).toBe('foo bar baz'); + }); + + it('should render highlighted text when there are multiple matches', () => { + const fixture = createTestComponent(''); + + expect(highlightHtml(fixture)) + .toBe('foo bar baz bar foo'); + }); + + it('should render highlighted text when there is a match at the beginning', () => { + const fixture = createTestComponent(''); + + expect(highlightHtml(fixture)).toBe('bar baz'); + }); + + it('should render highlighted text when there is a match at the end', () => { + const fixture = createTestComponent(''); + + expect(highlightHtml(fixture)).toBe('bar baz'); + }); + + it('should render highlighted text when there is a case-insensitive match', () => { + const fixture = createTestComponent(''); + + expect(highlightHtml(fixture)).toBe('foo bAR baz'); + }); + + it('should render highlighted text with special characters', () => { + const fixture = createTestComponent(''); + + expect(highlightHtml(fixture)).toBe('foo (bAR baz'); + }); + + it('should render highlighted text for stringified non-string args', () => { + const fixture = createTestComponent(''); + fixture.detectChanges(); + expect(highlightHtml(fixture)).toBe('123'); + }); + + it('should behave reasonably for blank result', () => { + const fixture = createTestComponent(''); + + expect(highlightHtml(fixture)).toBe(''); + }); + + it('should not convert null result to string', () => { + const fixture = createTestComponent(''); + + expect(highlightHtml(fixture)).toBe(''); + }); + + it('should properly detect matches in 0 result', () => { + const fixture = createTestComponent(''); + + expect(highlightHtml(fixture)).toBe(`0`); + }); + + it('should not highlight anything for blank term', () => { + const fixture = createTestComponent(''); + + expect(highlightHtml(fixture)).toBe('1null23'); + }); + + it('should not highlight anything for blank term', () => { + const fixture = createTestComponent(``); + + expect(highlightHtml(fixture)).toBe('123'); + }); + + it('should properly highlight zeros', () => { + const fixture = createTestComponent(``); + + expect(highlightHtml(fixture)).toBe('0123'); + }); + + it('should support custom highlight class', () => { + const fixture = createTestComponent(''); + + expect(highlightHtml(fixture)).toBe('123'); + }); + + it('should highlight when term contains array with 1 item', () => { + const fixture = createTestComponent(``); + + expect(highlightHtml(fixture)).toBe('foo bar baz'); + }); + + it('should highlight when term contains array with several items', () => { + const fixture = createTestComponent(``); + + expect(highlightHtml(fixture)) + .toBe('foo bar baz'); + }); + + it('should highlight when term contains array with several items and the terms in text stand together', () => { + const fixture = createTestComponent(``); + + expect(highlightHtml(fixture)) + .toBe('foobar baz'); + }); + + it('should not fail when term contains null element', () => { + const fixture = createTestComponent(``); + + expect(highlightHtml(fixture)).toBe('foo bar baz'); + }); + + it('should highlight when term contains mix of strings and numbers', () => { + const fixture = + createTestComponent(``); + + expect(highlightHtml(fixture)) + .toBe('1123456789'); + }); +}); + + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { +} diff --git a/src/typeahead/highlight.ts b/src/typeahead/highlight.ts new file mode 100644 index 0000000..6484b2b --- /dev/null +++ b/src/typeahead/highlight.ts @@ -0,0 +1,51 @@ +import {Component, Input, OnChanges, ChangeDetectionStrategy, SimpleChanges, ViewEncapsulation} from '@angular/core'; +import {regExpEscape, toString} from '../util/util'; + +/** + * A component that helps with text highlighting. + * + * If splits the `result` text into parts that contain the searched `term` and generates the HTML markup to simplify + * highlighting: + * + * Ex. `result="Alaska"` and `term="as"` will produce `Alaska`. + */ +@Component({ + selector: 'ngb-highlight', + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None, + template: `` + + `{{part}}{{part}}` + + ``, // template needs to be formatted in a certain way so we don't add empty text nodes + styleUrls: ['./highlight.scss'] +}) +export class NgbHighlight implements OnChanges { + parts: string[]; + + /** + * The CSS class for `` elements wrapping the `term` inside the `result`. + */ + @Input() highlightClass = 'ngb-highlight'; + + /** + * The text highlighting is added to. + * + * If the `term` is found inside this text, it will be highlighted. + * If the `term` contains array then all the items from it will be highlighted inside the text. + */ + @Input() result: string; + + /** + * The term or array of terms to be highlighted. + * Since version `v4.2.0` term could be a `string[]` + */ + @Input() term: string | string[]; + + ngOnChanges(changes: SimpleChanges) { + const result = toString(this.result); + + const terms = Array.isArray(this.term) ? this.term : [this.term]; + const escapedTerms = terms.map(term => regExpEscape(toString(term))).filter(term => term); + + this.parts = escapedTerms.length ? result.split(new RegExp(`(${escapedTerms.join('|')})`, 'gmi')) : [result]; + } +} diff --git a/src/typeahead/typeahead-config.spec.ts b/src/typeahead/typeahead-config.spec.ts new file mode 100644 index 0000000..0d4cf46 --- /dev/null +++ b/src/typeahead/typeahead-config.spec.ts @@ -0,0 +1,12 @@ +import {NgbTypeaheadConfig} from './typeahead-config'; + +describe('ngb-typeahead-config', () => { + it('should have sensible default values', () => { + const config = new NgbTypeaheadConfig(); + + expect(config.container).toBeUndefined(); + expect(config.editable).toBeTruthy(); + expect(config.focusFirst).toBeTruthy(); + expect(config.showHint).toBeFalsy(); + }); +}); diff --git a/src/typeahead/typeahead-config.ts b/src/typeahead/typeahead-config.ts new file mode 100644 index 0000000..1f7f3ff --- /dev/null +++ b/src/typeahead/typeahead-config.ts @@ -0,0 +1,17 @@ +import {Injectable} from '@angular/core'; +import {PlacementArray} from '../util/positioning'; + +/** + * A configuration service for the [`NgbTypeahead`](#/components/typeahead/api#NgbTypeahead) component. + * + * You can inject this service, typically in your root component, and customize the values of its properties in + * order to provide default values for all the typeaheads used in the application. + */ +@Injectable({providedIn: 'root'}) +export class NgbTypeaheadConfig { + container; + editable = true; + focusFirst = true; + showHint = false; + placement: PlacementArray = ['bottom-left', 'bottom-right', 'top-left', 'top-right']; +} diff --git a/src/typeahead/typeahead-window.spec.ts b/src/typeahead/typeahead-window.spec.ts new file mode 100644 index 0000000..9d9953c --- /dev/null +++ b/src/typeahead/typeahead-window.spec.ts @@ -0,0 +1,216 @@ +import {TestBed, ComponentFixture} from '@angular/core/testing'; +import {createGenericTestComponent} from '../test/common'; + +import {Component, ViewChild} from '@angular/core'; + +import {NgbTypeaheadWindow} from './typeahead-window'; +import {expectResults, getWindowLinks} from '../test/typeahead/common'; +import {NgbTypeaheadModule} from './typeahead.module'; + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +describe('ngb-typeahead-window', () => { + + beforeEach(() => { + TestBed.overrideModule(NgbTypeaheadModule, {set: {exports: [NgbTypeaheadWindow]}}); + TestBed.configureTestingModule({declarations: [TestComponent], imports: [NgbTypeaheadModule]}); + }); + + describe('display', () => { + + it('should display results with the first row active', () => { + const fixture = + createTestComponent(''); + + expectResults(fixture.nativeElement, ['+bar', 'baz']); + }); + + it('should use a formatting function to display results', () => { + const fixture = createTestComponent( + ''); + + expectResults(fixture.nativeElement, ['+BAR', 'BAZ']); + }); + + it('should use a custom template if provided', () => { + const fixture = createTestComponent(` + {{r.toUpperCase()}}-{{t}} + `); + + expectResults(fixture.nativeElement, ['+BAR-ba', 'BAZ-ba']); + }); + }); + + describe('active row', () => { + + it('should change active row on prev / next method call', () => { + const html = ` + + + `; + const fixture = createTestComponent(html); + const buttons = fixture.nativeElement.querySelectorAll('button'); + + expectResults(fixture.nativeElement, ['+bar', 'baz']); + + buttons[0].click(); + fixture.detectChanges(); + expectResults(fixture.nativeElement, ['bar', '+baz']); + + buttons[1].click(); + fixture.detectChanges(); + expectResults(fixture.nativeElement, ['+bar', 'baz']); + }); + + it('should wrap active row on prev / next method call', () => { + const html = ` + + + `; + const fixture = createTestComponent(html); + const buttons = fixture.nativeElement.querySelectorAll('button'); + + expectResults(fixture.nativeElement, ['+bar', 'baz']); + + buttons[1].click(); + fixture.detectChanges(); + expectResults(fixture.nativeElement, ['bar', '+baz']); + + buttons[0].click(); + fixture.detectChanges(); + expectResults(fixture.nativeElement, ['+bar', 'baz']); + }); + + it('should wrap active row on prev / next method call for [focusFirst]="false"', () => { + const html = ` + + + `; + const fixture = createTestComponent(html); + const buttons = fixture.nativeElement.querySelectorAll('button'); + + expectResults(fixture.nativeElement, ['bar', 'baz']); + + buttons[0].click(); // next + fixture.detectChanges(); + expectResults(fixture.nativeElement, ['+bar', 'baz']); + + buttons[0].click(); // next + fixture.detectChanges(); + expectResults(fixture.nativeElement, ['bar', '+baz']); + + buttons[0].click(); // next + fixture.detectChanges(); + expectResults(fixture.nativeElement, ['bar', 'baz']); + + buttons[1].click(); // prev + fixture.detectChanges(); + expectResults(fixture.nativeElement, ['bar', '+baz']); + + buttons[1].click(); // prev + fixture.detectChanges(); + expectResults(fixture.nativeElement, ['+bar', 'baz']); + + buttons[1].click(); // prev + fixture.detectChanges(); + expectResults(fixture.nativeElement, ['bar', 'baz']); + }); + + it('should change active row on mouseenter', () => { + const fixture = + createTestComponent(``); + const links = getWindowLinks(fixture.debugElement); + + expectResults(fixture.nativeElement, ['+bar', 'baz']); + + links[1].triggerEventHandler('mouseenter', {}); + fixture.detectChanges(); + expectResults(fixture.nativeElement, ['bar', '+baz']); + }); + }); + + describe('result selection', () => { + it('should select a given row on click', () => { + const fixture = createTestComponent( + ''); + const links = getWindowLinks(fixture.debugElement); + + expectResults(fixture.nativeElement, ['+bar', 'baz']); + + links[1].triggerEventHandler('click', {}); + fixture.detectChanges(); + expect(fixture.componentInstance.selected).toBe('baz'); + }); + + it('should return selected row via getActive()', () => { + const html = ` + + + `; + const fixture = createTestComponent(html); + + const buttons = fixture.nativeElement.querySelectorAll('button'); + const activeBtn = buttons[0]; + const nextBtn = buttons[1]; + + activeBtn.click(); + expectResults(fixture.nativeElement, ['+bar', 'baz']); + expect(fixture.componentInstance.active).toBe('bar'); + + nextBtn.click(); + activeBtn.click(); + fixture.detectChanges(); + expectResults(fixture.nativeElement, ['bar', '+baz']); + expect(fixture.componentInstance.active).toBe('baz'); + }); + + it('should have buttons of type button', () => { + const html = ` + `; + const fixture = createTestComponent(html); + const buttons = fixture.nativeElement.querySelectorAll('button'); + expect(buttons.length).toBeGreaterThan(0); + for (let i = 0; i < buttons.length; i++) { + expect(buttons[i].getAttribute('type')).toBe('button'); + } + }); + }); + + describe('accessibility', () => { + + function getWindow(element): HTMLDivElement { + return element.querySelector('ngb-typeahead-window.dropdown-menu'); + } + + it('should add correct ARIA attributes', () => { + const fixture = createTestComponent( + ''); + const compiled = fixture.nativeElement.querySelector('ngb-typeahead-window.dropdown-menu'); + + expect(compiled.getAttribute('role')).toBe('listbox'); + expect(compiled.getAttribute('id')).toBe('test-typeahead'); + + const buttons = fixture.nativeElement.querySelectorAll('button'); + expect(buttons.length).toBeGreaterThan(0); + for (let i = 0; i < buttons.length; i++) { + expect(buttons[i].getAttribute('id')).toBe('test-typeahead-' + i); + expect(buttons[i].getAttribute('role')).toBe('option'); + } + }); + + }); + +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + active: string; + results = ['bar', 'baz']; + term = 'ba'; + selected: string; + + @ViewChild(NgbTypeaheadWindow, {static: true}) popup: NgbTypeaheadWindow; + + formatterFn = (result) => { return result.toUpperCase(); }; +} diff --git a/src/typeahead/typeahead-window.ts b/src/typeahead/typeahead-window.ts new file mode 100644 index 0000000..6a0776a --- /dev/null +++ b/src/typeahead/typeahead-window.ts @@ -0,0 +1,123 @@ +import {Component, Input, Output, EventEmitter, TemplateRef, OnInit} from '@angular/core'; + +import {toString} from '../util/util'; + +/** + * The context for the typeahead result template in case you want to override the default one. + */ +export interface ResultTemplateContext { + /** + * Your typeahead result item. + */ + result: any; + + /** + * Search term from the `` used to get current result. + */ + term: string; +} + +@Component({ + selector: 'ngb-typeahead-window', + exportAs: 'ngbTypeaheadWindow', + host: {'(mousedown)': '$event.preventDefault()', 'class': 'dropdown-menu show', 'role': 'listbox', '[id]': 'id'}, + template: ` + + + + + + + ` +}) +export class NgbTypeaheadWindow implements OnInit { + activeIdx = 0; + + /** + * The id for the typeahead window. The id should be unique and the same + * as the associated typeahead's id. + */ + @Input() id: string; + + /** + * Flag indicating if the first row should be active initially + */ + @Input() focusFirst = true; + + /** + * Typeahead match results to be displayed + */ + @Input() results; + + /** + * Search term used to get current results + */ + @Input() term: string; + + /** + * A function used to format a given result before display. This function should return a formatted string without any + * HTML markup + */ + @Input() formatter = toString; + + /** + * A template to override a matching result default display + */ + @Input() resultTemplate: TemplateRef; + + /** + * Event raised when user selects a particular result row + */ + @Output('select') selectEvent = new EventEmitter(); + + @Output('activeChange') activeChangeEvent = new EventEmitter(); + + hasActive() { return this.activeIdx > -1 && this.activeIdx < this.results.length; } + + getActive() { return this.results[this.activeIdx]; } + + markActive(activeIdx: number) { + this.activeIdx = activeIdx; + this._activeChanged(); + } + + next() { + if (this.activeIdx === this.results.length - 1) { + this.activeIdx = this.focusFirst ? (this.activeIdx + 1) % this.results.length : -1; + } else { + this.activeIdx++; + } + this._activeChanged(); + } + + prev() { + if (this.activeIdx < 0) { + this.activeIdx = this.results.length - 1; + } else if (this.activeIdx === 0) { + this.activeIdx = this.focusFirst ? this.results.length - 1 : -1; + } else { + this.activeIdx--; + } + this._activeChanged(); + } + + resetActive() { + this.activeIdx = this.focusFirst ? 0 : -1; + this._activeChanged(); + } + + select(item) { this.selectEvent.emit(item); } + + ngOnInit() { this.resetActive(); } + + private _activeChanged() { + this.activeChangeEvent.emit(this.activeIdx >= 0 ? this.id + '-' + this.activeIdx : undefined); + } +} diff --git a/src/typeahead/typeahead.module.ts b/src/typeahead/typeahead.module.ts new file mode 100644 index 0000000..7bb764d --- /dev/null +++ b/src/typeahead/typeahead.module.ts @@ -0,0 +1,20 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; + +import {NgbHighlight} from './highlight'; +import {NgbTypeaheadWindow} from './typeahead-window'; +import {NgbTypeahead} from './typeahead'; + +export {NgbHighlight} from './highlight'; +export {NgbTypeaheadWindow} from './typeahead-window'; +export {NgbTypeaheadConfig} from './typeahead-config'; +export {NgbTypeahead, NgbTypeaheadSelectItemEvent} from './typeahead'; + +@NgModule({ + declarations: [NgbTypeahead, NgbHighlight, NgbTypeaheadWindow], + exports: [NgbTypeahead, NgbHighlight], + imports: [CommonModule], + entryComponents: [NgbTypeaheadWindow] +}) +export class NgbTypeaheadModule { +} diff --git a/src/typeahead/typeahead.spec.ts b/src/typeahead/typeahead.spec.ts new file mode 100644 index 0000000..16d9448 --- /dev/null +++ b/src/typeahead/typeahead.spec.ts @@ -0,0 +1,990 @@ +import {ChangeDetectionStrategy, Component, DebugElement, ViewChild} from '@angular/core'; +import {async, ComponentFixture, fakeAsync, inject, TestBed, tick} from '@angular/core/testing'; +import {FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators} from '@angular/forms'; +import {By} from '@angular/platform-browser'; +import {merge, Observable, Subject} from 'rxjs'; +import {debounceTime, filter, map} from 'rxjs/operators'; + +import {createGenericTestComponent, isBrowser} from '../test/common'; +import {expectResults, getWindowLinks} from '../test/typeahead/common'; +import {ARIA_LIVE_DELAY} from '../util/accessibility/live'; +import {Key} from '../util/key'; +import {NgbTypeahead} from './typeahead'; +import {NgbTypeaheadConfig} from './typeahead-config'; +import {NgbTypeaheadModule} from './typeahead.module'; + + + +const createTestComponent = (html: string) => + createGenericTestComponent(html, TestComponent) as ComponentFixture; + +const createOnPushTestComponent = (html: string) => + createGenericTestComponent(html, TestOnPushComponent) as ComponentFixture; + +const createAsyncTestComponent = (html: string) => + createGenericTestComponent(html, TestAsyncComponent) as ComponentFixture; + +function createKeyDownEvent(key: number) { + const event = {which: key, preventDefault: () => {}, stopPropagation: () => {}}; + spyOn(event, 'preventDefault'); + spyOn(event, 'stopPropagation'); + return event; +} + +function getWindow(element): HTMLDivElement { + return element.querySelector('ngb-typeahead-window.dropdown-menu'); +} + +function getDebugInput(element: DebugElement): DebugElement { + return element.query(By.directive(NgbTypeahead)); +} + +function getNativeInput(element: HTMLElement): HTMLInputElement { + return element.querySelector('input'); +} + +function changeInput(element: any, value: string) { + const input = getNativeInput(element); + input.value = value; + const evt = document.createEvent('MouseEvent'); + evt.initEvent('input', true, true); + input.dispatchEvent(evt); +} + +function blurInput(element: any) { + const input = getNativeInput(element); + const evt = document.createEvent('HTMLEvents'); + evt.initEvent('blur', false, false); + input.dispatchEvent(evt); +} + +function expectInputValue(element: HTMLElement, value: string) { + expect(getNativeInput(element).value).toBe(value); +} + +function expectWindowResults(element, expectedResults: string[]) { + const window = getWindow(element); + expect(window).not.toBeNull(); + expectResults(window, expectedResults); +} + +describe('ngb-typeahead', () => { + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TestComponent, TestOnPushComponent, TestAsyncComponent], + imports: [NgbTypeaheadModule, FormsModule, ReactiveFormsModule], + providers: [{provide: ARIA_LIVE_DELAY, useValue: null}] + }); + }); + + describe('valueaccessor', () => { + + it('should format values when no formatter provided', async(() => { + const fixture = createTestComponent(''); + + const el = fixture.nativeElement; + const comp = fixture.componentInstance; + expectInputValue(el, ''); + + comp.model = 'text'; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + expectInputValue(el, 'text'); + + comp.model = null; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectInputValue(el, ''); + + comp.model = {}; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { expectInputValue(el, '[object Object]'); }); + })); + + it('should format values with custom formatter provided', async(() => { + const html = + ''; + const fixture = createTestComponent(html); + const el = fixture.nativeElement; + const comp = fixture.componentInstance; + expectInputValue(el, ''); + + comp.model = null; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + expectInputValue(el, ''); + + comp.model = {value: 'text'}; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { expectInputValue(el, 'TEXT'); }); + })); + + it('should use custom input formatter with falsy values', async(() => { + const html = ''; + const fixture = createTestComponent(html); + const el = fixture.nativeElement; + const comp = fixture.componentInstance; + expectInputValue(el, ''); + + comp.model = null; + fixture.detectChanges(); + fixture.whenStable() + .then(() => { + expectInputValue(el, ''); + + comp.model = 0; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { + expectInputValue(el, '0'); + + comp.model = false; + fixture.detectChanges(); + return fixture.whenStable(); + }) + .then(() => { expectInputValue(el, 'FALSE'); }); + })); + }); + + describe('window', () => { + + it('should be closed by default', () => { + const fixture = createTestComponent(``); + const compiled = fixture.nativeElement; + expect(getWindow(compiled)).toBeNull(); + }); + + it('should not be opened when the model changes', async(() => { + const fixture = createTestComponent(``); + const compiled = fixture.nativeElement; + + fixture.componentInstance.model = 'one'; + fixture.detectChanges(); + fixture.whenStable().then(() => { expect(getWindow(compiled)).toBeNull(); }); + })); + + it('should be opened when there are results', async(() => { + const fixture = createTestComponent(``); + const compiled = fixture.nativeElement; + + fixture.whenStable().then(() => { + changeInput(compiled, 'one'); + fixture.detectChanges(); + expectWindowResults(compiled, ['+one', 'one more']); + expect(fixture.componentInstance.model).toBe('one'); + }); + })); + + it('should be closed when there no results', () => { + const fixture = createTestComponent(``); + const compiled = fixture.nativeElement; + + expect(getWindow(compiled)).toBeNull(); + }); + + it('should work when returning null as results', async(() => { + const fixture = createTestComponent(``); + const compiled = fixture.nativeElement; + + fixture.whenStable().then(() => { + changeInput(compiled, 'one'); + fixture.detectChanges(); + expect(getWindow(compiled)).toBeNull(); + }); + })); + + it('should select the result on click, close window and fill the input', async(() => { + const fixture = createTestComponent(``); + const compiled = fixture.nativeElement; + + fixture.whenStable().then(() => { + // clicking selected + changeInput(compiled, 'o'); + fixture.detectChanges(); + expectWindowResults(compiled, ['+one', 'one more']); + + getWindowLinks(fixture.debugElement)[0].triggerEventHandler('click', {}); + fixture.detectChanges(); + expect(getWindow(compiled)).toBeNull(); + expectInputValue(compiled, 'one'); + expect(fixture.componentInstance.model).toBe('one'); + + // clicking not selected + changeInput(compiled, 'o'); + fixture.detectChanges(); + expectWindowResults(compiled, ['+one', 'one more']); + expectInputValue(compiled, 'o'); + + getWindowLinks(fixture.debugElement)[0].triggerEventHandler('click', {}); + fixture.detectChanges(); + expect(getWindow(compiled)).toBeNull(); + expectInputValue(compiled, 'one'); + }); + })); + + it('should select the result on ENTER, close window and fill the input', async(() => { + const fixture = createTestComponent(``); + const compiled = fixture.nativeElement; + + fixture.whenStable().then(() => { + changeInput(compiled, 'o'); + fixture.detectChanges(); + expectWindowResults(compiled, ['+one', 'one more']); + + const event = createKeyDownEvent(Key.Enter); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expect(getWindow(compiled)).toBeNull(); + expectInputValue(compiled, 'one'); + expect(fixture.componentInstance.model).toBe('one'); + expect(event.preventDefault).toHaveBeenCalled(); + expect(event.stopPropagation).toHaveBeenCalled(); + }); + })); + + it('should select the result on TAB, close window and fill the input', () => { + const fixture = createTestComponent(``); + const compiled = fixture.nativeElement; + + changeInput(compiled, 'o'); + fixture.detectChanges(); + expect(getWindow(compiled)).not.toBeNull(); + + const event = createKeyDownEvent(Key.Tab); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expect(getWindow(compiled)).toBeNull(); + expectInputValue(compiled, 'one'); + expect(fixture.componentInstance.model).toBe('one'); + expect(event.preventDefault).toHaveBeenCalled(); + expect(event.stopPropagation).toHaveBeenCalled(); + }); + + it('should make previous/next results active with up/down arrow keys', () => { + const fixture = createTestComponent(``); + const compiled = fixture.nativeElement; + + changeInput(compiled, 'o'); + fixture.detectChanges(); + expectWindowResults(compiled, ['+one', 'one more']); + + // down + let event = createKeyDownEvent(Key.ArrowDown); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expectWindowResults(compiled, ['one', '+one more']); + expect(event.preventDefault).toHaveBeenCalled(); + + event = createKeyDownEvent(Key.ArrowDown); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expectWindowResults(compiled, ['+one', 'one more']); + expect(event.preventDefault).toHaveBeenCalled(); + + // up + event = createKeyDownEvent(Key.ArrowUp); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expectWindowResults(compiled, ['one', '+one more']); + expect(event.preventDefault).toHaveBeenCalled(); + + event = createKeyDownEvent(Key.ArrowUp); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expectWindowResults(compiled, ['+one', 'one more']); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should use provided result formatter function', () => { + const fixture = + createTestComponent(``); + const compiled = fixture.nativeElement; + + changeInput(compiled, 'o'); + fixture.detectChanges(); + expectWindowResults(compiled, ['+ONE', 'ONE MORE']); + }); + + it('should not mark first result as active when focusFirst is false', () => { + const fixture = createTestComponent(``); + const compiled = fixture.nativeElement; + + changeInput(compiled, 'o'); + fixture.detectChanges(); + expectWindowResults(compiled, ['one', 'one more']); + }); + + it('should reset active index when result changes', () => { + const fixture = createTestComponent(``); + const compiled = fixture.nativeElement; + + changeInput(compiled, 'o'); + fixture.detectChanges(); + expectWindowResults(compiled, ['+one', 'one more']); + + // move down to highlight the second item + let event = createKeyDownEvent(Key.ArrowDown); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expectWindowResults(compiled, ['one', '+one more']); + expect(event.preventDefault).toHaveBeenCalled(); + + // change search criteria to reset results while the popup stays open + changeInput(compiled, 't'); + fixture.detectChanges(); + expectWindowResults(compiled, ['+two', 'three']); + }); + + + it('should properly make previous/next results active with down arrow keys when focusFirst is false', () => { + const fixture = createTestComponent(``); + const compiled = fixture.nativeElement; + + changeInput(compiled, 'o'); + fixture.detectChanges(); + expectWindowResults(compiled, ['one', 'one more']); + + // down + let event = createKeyDownEvent(Key.ArrowDown); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expectWindowResults(compiled, ['+one', 'one more']); + expect(event.preventDefault).toHaveBeenCalled(); + + event = createKeyDownEvent(Key.ArrowDown); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expectWindowResults(compiled, ['one', '+one more']); + expect(event.preventDefault).toHaveBeenCalled(); + + event = createKeyDownEvent(Key.ArrowDown); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expectWindowResults(compiled, ['one', 'one more']); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should properly make previous/next results active with up arrow keys when focusFirst is false', () => { + const fixture = createTestComponent(``); + const compiled = fixture.nativeElement; + + changeInput(compiled, 'o'); + fixture.detectChanges(); + expectWindowResults(compiled, ['one', 'one more']); + + // up + let event = createKeyDownEvent(Key.ArrowUp); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expectWindowResults(compiled, ['one', '+one more']); + expect(event.preventDefault).toHaveBeenCalled(); + + event = createKeyDownEvent(Key.ArrowUp); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expectWindowResults(compiled, ['+one', 'one more']); + expect(event.preventDefault).toHaveBeenCalled(); + }); + + it('should not select the result on TAB, close window and not write to the input when focusFirst is false', () => { + const fixture = + createTestComponent(``); + const compiled = fixture.nativeElement; + + changeInput(compiled, 'o'); + fixture.detectChanges(); + expect(getWindow(compiled)).not.toBeNull(); + + const event = createKeyDownEvent(Key.Tab); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expect(getWindow(compiled)).toBeNull(); + expectInputValue(compiled, 'o'); + expect(fixture.componentInstance.model).toBe('o'); + expect(event.preventDefault).not.toHaveBeenCalled(); + }); + + it('should properly display results when an owning components using OnPush strategy', fakeAsync(() => { + const fixture = createOnPushTestComponent(``); + const compiled = fixture.nativeElement; + + changeInput(compiled, 'o'); + fixture.detectChanges(); + tick(250); + expectWindowResults(compiled, ['+one', 'one more']); + })); + }); + + describe('with async typeahead function', () => { + it('should not display results when input is "blured"', fakeAsync(() => { + const fixture = createAsyncTestComponent(``); + const compiled = fixture.nativeElement; + + changeInput(compiled, 'one'); + fixture.detectChanges(); + + tick(50); + + blurInput(compiled); + fixture.detectChanges(); + + tick(250); + expect(getWindow(compiled)).toBeNull(); + + // Make sure that it is resubscribed again + changeInput(compiled, 'two'); + fixture.detectChanges(); + tick(250); + expect(getWindow(compiled)).not.toBeNull(); + })); + + it('should not display results when value selected while new results are been loading', fakeAsync(() => { + const fixture = createAsyncTestComponent(``); + const compiled = fixture.nativeElement; + + // Change input first time + changeInput(compiled, 'one'); + fixture.detectChanges(); + + // Results for first input are loaded + tick(250); + expect(getWindow(compiled)).not.toBeNull(); + + // Change input second time + changeInput(compiled, 'two'); + fixture.detectChanges(); + tick(50); + + // Select a value from first results list while second is still in progress + getWindowLinks(fixture.debugElement)[0].triggerEventHandler('click', {}); + fixture.detectChanges(); + expect(getWindow(compiled)).toBeNull(); + + // Results for second input are loaded (window shouldn't be opened in this case) + tick(250); + expect(getWindow(compiled)).toBeNull(); + + // Make sure that it is resubscribed again + changeInput(compiled, 'three'); + fixture.detectChanges(); + tick(250); + expect(getWindow(compiled)).not.toBeNull(); + })); + }); + + describe('objects', () => { + + it('should work with custom objects as values', async(() => { + const fixture = createTestComponent(` + `); + const compiled = fixture.nativeElement; + + fixture.whenStable().then(() => { + changeInput(compiled, 'o'); + fixture.detectChanges(); + expectWindowResults(compiled, ['+ONE', 'ONE MORE']); + + const event = createKeyDownEvent(Key.Enter); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expect(getWindow(compiled)).toBeNull(); + expect(getNativeInput(compiled).value).toBe('1 one'); + expect(fixture.componentInstance.model).toEqual({id: 1, value: 'one'}); + }); + })); + + it('should allow to assign ngModel custom objects', async(() => { + const fixture = createTestComponent(` + `); + const compiled = fixture.nativeElement; + + fixture.componentInstance.model = {id: 1, value: 'one'}; + fixture.detectChanges(); + fixture.whenStable().then(() => { + expect(getWindow(compiled)).toBeNull(); + expect(getNativeInput(compiled).value).toBe('1 one'); + }); + })); + }); + + describe('forms', () => { + + it('should work with template-driven form validation', async(() => { + const html = ` +
+ +
`; + const fixture = createTestComponent(html); + fixture.whenStable().then(() => { + fixture.detectChanges(); + const compiled = fixture.nativeElement; + expect(getNativeInput(compiled)).toHaveCssClass('ng-invalid'); + expect(getNativeInput(compiled)).not.toHaveCssClass('ng-valid'); + + changeInput(compiled, 'o'); + fixture.detectChanges(); + expect(getNativeInput(compiled)).toHaveCssClass('ng-valid'); + expect(getNativeInput(compiled)).not.toHaveCssClass('ng-invalid'); + }); + })); + + it('should work with model-driven form validation', () => { + const html = ` +
+ +
`; + const fixture = createTestComponent(html); + const compiled = fixture.nativeElement; + + expect(getNativeInput(compiled)).toHaveCssClass('ng-invalid'); + expect(getNativeInput(compiled)).not.toHaveCssClass('ng-valid'); + + changeInput(compiled, 'o'); + fixture.detectChanges(); + expect(getNativeInput(compiled)).toHaveCssClass('ng-valid'); + expect(getNativeInput(compiled)).not.toHaveCssClass('ng-invalid'); + }); + + + it('should support disabled state', async(() => { + const html = ` +
+ +
`; + const fixture = createTestComponent(html); + fixture.whenStable().then(() => { + fixture.detectChanges(); + const compiled = fixture.nativeElement; + expect(getNativeInput(compiled).disabled).toBeTruthy(); + }); + })); + + it('should only propagate model changes on select when the editable option is on', async(() => { + const html = ` +
+ +
`; + const fixture = createTestComponent(html); + fixture.whenStable().then(() => { + fixture.detectChanges(); + const compiled = fixture.nativeElement; + expect(getNativeInput(compiled)).toHaveCssClass('ng-invalid'); + expect(getNativeInput(compiled)).not.toHaveCssClass('ng-valid'); + + changeInput(compiled, 'o'); + fixture.detectChanges(); + expect(getNativeInput(compiled)).toHaveCssClass('ng-invalid'); + expect(getNativeInput(compiled)).not.toHaveCssClass('ng-valid'); + expect(fixture.componentInstance.model).toBeUndefined(); + + const event = createKeyDownEvent(Key.Enter); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expect(getNativeInput(compiled)).not.toHaveCssClass('ng-invalid'); + expect(getNativeInput(compiled)).toHaveCssClass('ng-valid'); + expect(fixture.componentInstance.model).toBe('one'); + }); + })); + + it('should clear model on user input when the editable option is on', async(() => { + const html = ` +
+ +
`; + const fixture = createTestComponent(html); + fixture.whenStable().then(() => { + fixture.detectChanges(); + const compiled = fixture.nativeElement; + expect(getNativeInput(compiled)).toHaveCssClass('ng-invalid'); + expect(getNativeInput(compiled)).not.toHaveCssClass('ng-valid'); + + changeInput(compiled, 'o'); + fixture.detectChanges(); + expect(getNativeInput(compiled)).toHaveCssClass('ng-invalid'); + expect(getNativeInput(compiled)).not.toHaveCssClass('ng-valid'); + expect(fixture.componentInstance.model).toBeUndefined(); + + const event = createKeyDownEvent(Key.Enter); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expect(getNativeInput(compiled)).not.toHaveCssClass('ng-invalid'); + expect(getNativeInput(compiled)).toHaveCssClass('ng-valid'); + expect(fixture.componentInstance.model).toBe('one'); + + changeInput(compiled, 'tw'); + fixture.detectChanges(); + expect(getNativeInput(compiled)).toHaveCssClass('ng-invalid'); + expect(getNativeInput(compiled)).not.toHaveCssClass('ng-valid'); + expect(fixture.componentInstance.model).toBeUndefined(); + }); + })); + }); + + describe('select event', () => { + + it('should raise select event when a result is selected', () => { + const fixture = createTestComponent(''); + const input = getNativeInput(fixture.nativeElement); + + // clicking selected + changeInput(fixture.nativeElement, 'o'); + fixture.detectChanges(); + getWindowLinks(fixture.debugElement)[0].triggerEventHandler('click', {}); + fixture.detectChanges(); + + expect(fixture.componentInstance.selectEventValue).toBe('one'); + }); + + it('should not propagate model when preventDefault() is called on selectEvent', async(() => { + const fixture = createTestComponent( + ''); + const input = getNativeInput(fixture.nativeElement); + + // clicking selected + changeInput(fixture.nativeElement, 'o'); + fixture.detectChanges(); + getWindowLinks(fixture.debugElement)[0].triggerEventHandler('click', {}); + fixture.detectChanges(); + fixture.whenStable().then(() => { expect(fixture.componentInstance.model).toBe('o'); }); + })); + }); + + describe('container', () => { + + it('should be appended to the element matching the selector passed to "container"', () => { + const selector = 'body'; + const fixture = createTestComponent(``); + + changeInput(fixture.nativeElement, 'one'); + fixture.detectChanges(); + + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(getWindow(document.querySelector(selector))).not.toBeNull(); + }); + + it('should properly destroy typeahead window when the "container" option is used', () => { + const selector = 'body'; + const fixture = createTestComponent(``); + + changeInput(fixture.nativeElement, 'one'); + fixture.detectChanges(); + + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(getWindow(document.querySelector(selector))).not.toBeNull(); + + fixture.componentInstance.show = false; + fixture.detectChanges(); + + expect(getWindow(fixture.nativeElement)).toBeNull(); + expect(getWindow(document.querySelector(selector))).toBeNull(); + }); + }); + + describe('auto attributes', () => { + + it('should have autocomplete, autocapitalize and autocorrect attributes set to off', () => { + const fixture = createTestComponent(''); + const input = getNativeInput(fixture.nativeElement); + + expect(input.getAttribute('autocomplete')).toBe('off'); + expect(input.getAttribute('autocapitalize')).toBe('off'); + expect(input.getAttribute('autocorrect')).toBe('off'); + }); + + it('should have configurable autocomplete attribute', () => { + const fixture = + createTestComponent(''); + const input = getNativeInput(fixture.nativeElement); + + expect(input.getAttribute('autocomplete')).toBe('ignored-123456'); + }); + }); + + describe('accessibility', () => { + + it('should have correct role, aria-autocomplete, aria-expanded set by default', () => { + const fixture = createTestComponent(''); + const input = getNativeInput(fixture.nativeElement); + + fixture.detectChanges(); + + expect(input.getAttribute('role')).toBe('combobox'); + expect(input.getAttribute('aria-multiline')).toBe('false'); + expect(input.getAttribute('aria-autocomplete')).toBe('list'); + expect(input.getAttribute('aria-expanded')).toBe('false'); + expect(input.getAttribute('aria-owns')).toBeNull(); + expect(input.getAttribute('aria-autocomplete')).toBe('list'); + expect(input.getAttribute('aria-activedescendant')).toBeNull(); + }); + + it('should correctly set aria-autocomplete depending on showHint', () => { + const fixture = createTestComponent(''); + const input = getNativeInput(fixture.nativeElement); + + fixture.detectChanges(); + + expect(input.getAttribute('aria-autocomplete')).toBe('both'); + }); + + it('should have the correct ARIA attributes when interacting with input', async(() => { + const fixture = createTestComponent(``); + const compiled = fixture.nativeElement; + const input = getNativeInput(compiled); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + changeInput(compiled, 'o'); + fixture.detectChanges(); + expectWindowResults(compiled, ['+one', 'one more']); + expect(input.getAttribute('aria-expanded')).toBe('true'); + expect(input.getAttribute('aria-owns')).toMatch(/ngb-typeahead-[0-9]+/); + expect(input.getAttribute('aria-activedescendant')).toMatch(/ngb-typeahead-[0-9]+-0/); + + let event = createKeyDownEvent(Key.ArrowDown); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expect(input.getAttribute('aria-activedescendant')).toMatch(/ngb-typeahead-[0-9]+-1/); + + event = createKeyDownEvent(Key.Enter); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expect(input.getAttribute('aria-expanded')).toBe('false'); + expect(input.getAttribute('aria-owns')).toBeNull(); + expect(input.getAttribute('aria-activedescendant')).toBeNull(); + }); + })); + }); + + if (!isBrowser(['ie', 'edge'])) { + describe('hint', () => { + + it('should show hint when an item starts with user input', async(() => { + const fixture = createTestComponent( + ``); + const compiled = fixture.nativeElement; + const inputEl = getNativeInput(compiled); + + fixture.whenStable().then(() => { + changeInput(compiled, 'on'); + fixture.detectChanges(); + expectWindowResults(compiled, ['+one', 'one more']); + expect(inputEl.value).toBe('one'); + expect(inputEl.selectionStart).toBe(2); + expect(inputEl.selectionEnd).toBe(3); + + const event = createKeyDownEvent(Key.ArrowDown); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expect(inputEl.value).toBe('one more'); + expect(inputEl.selectionStart).toBe(2); + expect(inputEl.selectionEnd).toBe(8); + }); + })); + + it('should show hint with no selection when an item does not starts with user input', async(() => { + const fixture = createTestComponent( + ``); + const compiled = fixture.nativeElement; + const inputEl = getNativeInput(compiled); + + fixture.whenStable().then(() => { + changeInput(compiled, 'ne'); + fixture.detectChanges(); + expectWindowResults(compiled, ['+one', 'one more']); + expect(inputEl.value).toBe('one'); + expect(inputEl.selectionStart).toBe(inputEl.selectionEnd); + + const event = createKeyDownEvent(Key.ArrowDown); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expect(inputEl.value).toBe('one more'); + expect(inputEl.selectionStart).toBe(inputEl.selectionEnd); + }); + })); + + it('should take input formatter into account when displaying hints', async(() => { + const fixture = createTestComponent(``); + const compiled = fixture.nativeElement; + const inputEl = getNativeInput(compiled); + + fixture.whenStable().then(() => { + changeInput(compiled, 'on'); + fixture.detectChanges(); + expectWindowResults(compiled, ['+one', 'one more']); + expect(inputEl.value).toBe('onE'); + expect(inputEl.selectionStart).toBe(2); + expect(inputEl.selectionEnd).toBe(3); + + const event = createKeyDownEvent(Key.ArrowDown); + getDebugInput(fixture.debugElement).triggerEventHandler('keydown', event); + fixture.detectChanges(); + expect(inputEl.value).toBe('onE MORE'); + expect(inputEl.selectionStart).toBe(2); + expect(inputEl.selectionEnd).toBe(8); + }); + })); + + it('should not show hint when there is no result selected', async(() => { + const fixture = createTestComponent( + ``); + fixture.detectChanges(); + const compiled = fixture.nativeElement; + const inputEl = getNativeInput(compiled); + + fixture.whenStable().then(() => { + changeInput(compiled, 'on'); + fixture.detectChanges(); + expectWindowResults(compiled, ['one', 'one more']); + expect(inputEl.value).toBe('on'); + }); + })); + + describe('should clear input properly when model get reset to empty string', () => { + [``, + ``] + .forEach((html, index) => { + const showHint = index === 1; + it(`${index === 0 ? 'without' : 'with'} showHint activated`, async(async() => { + const fixture = createTestComponent(html); + fixture.detectChanges(); + await fixture.whenStable(); + + const compiled = fixture.nativeElement; + changeInput(compiled, 'on'); + fixture.detectChanges(); + + expectInputValue(compiled, showHint ? 'one' : 'on'); + + fixture.componentInstance.model = ''; + fixture.detectChanges(); + await fixture.whenStable(); + + document.body.click(); + fixture.detectChanges(); + + expectInputValue(compiled, ''); + })); + }); + }); + + }); + + describe('Custom config', () => { + beforeEach(() => { + TestBed.overrideComponent( + TestComponent, {set: {template: ''}}); + }); + + beforeEach(inject([NgbTypeaheadConfig], (c: NgbTypeaheadConfig) => { c.showHint = true; })); + + it('should initialize inputs with provided config', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const typeahead = fixture.componentInstance.typeahead; + expect(typeahead.showHint).toBe(true); + }); + }); + + describe('Custom config as provider', () => { + beforeEach(() => { + const config = new NgbTypeaheadConfig(); + config.showHint = true; + TestBed.configureTestingModule({providers: [{provide: NgbTypeaheadConfig, useValue: config}]}); + + TestBed.overrideComponent( + TestComponent, {set: {template: ''}}); + }); + + it('should initialize inputs with provided config as provider', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + const typeahead = fixture.componentInstance.typeahead; + expect(typeahead.showHint).toBe(true); + }); + }); + } +}); + +@Component({selector: 'test-cmp', template: ''}) +class TestComponent { + private _strings = ['one', 'one more', 'two', 'three']; + private _objects = + [{id: 1, value: 'one'}, {id: 10, value: 'one more'}, {id: 2, value: 'two'}, {id: 3, value: 'three'}]; + + model: any; + selectEventValue: any; + show = true; + + form = new FormGroup({control: new FormControl('', Validators.required)}); + + findOutput$: Observable; + + @ViewChild(NgbTypeahead, {static: true}) typeahead: NgbTypeahead; + focus$ = new Subject(); + click$ = new Subject(); + + find = + (text$: Observable) => { + const clicks$ = this.click$.pipe(filter(() => !this.typeahead.isPopupOpen())); + this.findOutput$ = + merge(text$, this.focus$, clicks$).pipe(map(text => this._strings.filter(v => v.startsWith(text)))); + return this.findOutput$; + } + + findAnywhere = + (text$: Observable) => { + return text$.pipe(map(text => this._strings.filter(v => v.indexOf(text) > -1))); + } + + findNothing = (text$: Observable) => { return text$.pipe(map(text => [])); }; + + findNull = (text$: Observable) => { return text$.pipe(map(text => null)); }; + + findObjects = + (text$: Observable) => { + return text$.pipe(map(text => this._objects.filter(v => v.value.startsWith(text)))); + } + + formatter = (obj: {id: number, value: string}) => { return `${obj.id} ${obj.value}`; }; + + uppercaseFormatter = s => `${s}`.toUpperCase(); + + uppercaseObjFormatter = (obj: {value: string}) => { return `${obj.value}`.toUpperCase(); }; + + + onSelect($event) { this.selectEventValue = $event; } +} + +@Component({selector: 'test-onpush-cmp', changeDetection: ChangeDetectionStrategy.OnPush, template: ''}) +class TestOnPushComponent { + private _strings = ['one', 'one more', 'two', 'three']; + + find = (text$: Observable) => { + return text$.pipe(debounceTime(200), map(text => this._strings.filter(v => v.startsWith(text)))); + } +} + +@Component({selector: 'test-async-cmp', template: ''}) +class TestAsyncComponent { + private _strings = ['one', 'one more', 'two', 'three']; + + find = (text$: Observable) => { + return text$.pipe(debounceTime(200), map(text => this._strings.filter(v => v.startsWith(text)))); + } +} diff --git a/src/typeahead/typeahead.ts b/src/typeahead/typeahead.ts new file mode 100644 index 0000000..c53543b --- /dev/null +++ b/src/typeahead/typeahead.ts @@ -0,0 +1,414 @@ +import { + ChangeDetectorRef, + ComponentFactoryResolver, + ComponentRef, + Directive, + ElementRef, + EventEmitter, + forwardRef, + Inject, + Injector, + Input, + NgZone, + OnDestroy, + OnInit, + Output, + Renderer2, + TemplateRef, + ViewContainerRef, + ApplicationRef +} from '@angular/core'; +import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms'; +import {DOCUMENT} from '@angular/common'; +import {BehaviorSubject, fromEvent, Observable, Subject, Subscription} from 'rxjs'; +import {map, switchMap, tap} from 'rxjs/operators'; + +import {Live} from '../util/accessibility/live'; +import {ngbAutoClose} from '../util/autoclose'; +import {Key} from '../util/key'; +import {PopupService} from '../util/popup'; +import {PlacementArray, positionElements} from '../util/positioning'; +import {isDefined, toString} from '../util/util'; + +import {NgbTypeaheadConfig} from './typeahead-config'; +import {NgbTypeaheadWindow, ResultTemplateContext} from './typeahead-window'; + + +const NGB_TYPEAHEAD_VALUE_ACCESSOR = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NgbTypeahead), + multi: true +}; + +/** + * An event emitted right before an item is selected from the result list. + */ +export interface NgbTypeaheadSelectItemEvent { + /** + * The item from the result list about to be selected. + */ + item: any; + + /** + * Calling this function will prevent item selection from happening. + */ + preventDefault: () => void; +} + +let nextWindowId = 0; + +/** + * A directive providing a simple way of creating powerful typeaheads from any text input. + */ +@Directive({ + selector: 'input[ngbTypeahead]', + exportAs: 'ngbTypeahead', + host: { + '(blur)': 'handleBlur()', + '[class.open]': 'isPopupOpen()', + '(keydown)': 'handleKeyDown($event)', + '[autocomplete]': 'autocomplete', + 'autocapitalize': 'off', + 'autocorrect': 'off', + 'role': 'combobox', + 'aria-multiline': 'false', + '[attr.aria-autocomplete]': 'showHint ? "both" : "list"', + '[attr.aria-activedescendant]': 'activeDescendant', + '[attr.aria-owns]': 'isPopupOpen() ? popupId : null', + '[attr.aria-expanded]': 'isPopupOpen()' + }, + providers: [NGB_TYPEAHEAD_VALUE_ACCESSOR] +}) +export class NgbTypeahead implements ControlValueAccessor, + OnInit, OnDestroy { + private _popupService: PopupService; + private _subscription: Subscription; + private _closed$ = new Subject(); + private _inputValueBackup: string; + private _valueChanges: Observable; + private _resubscribeTypeahead: BehaviorSubject; + private _windowRef: ComponentRef; + private _zoneSubscription: any; + + /** + * The value for the `autocomplete` attribute for the `` element. + * + * Defaults to `"off"` to disable the native browser autocomplete, but you can override it if necessary. + * + * @since 2.1.0 + */ + @Input() autocomplete = 'off'; + + /** + * A selector specifying the element the typeahead popup will be appended to. + * + * Currently only supports `"body"`. + */ + @Input() container: string; + + /** + * If `true`, model values will not be restricted only to items selected from the popup. + */ + @Input() editable: boolean; + + /** + * If `true`, the first item in the result list will always stay focused while typing. + */ + @Input() focusFirst: boolean; + + /** + * The function that converts an item from the result list to a `string` to display in the `` field. + * + * It is called when the user selects something in the popup or the model value changes, so the input needs to + * be updated. + */ + @Input() inputFormatter: (item: any) => string; + + /** + * The function that converts a stream of text values from the `` element to the stream of the array of items + * to display in the typeahead popup. + * + * If the resulting observable emits a non-empty array - the popup will be shown. If it emits an empty array - the + * popup will be closed. + * + * See the [basic example](#/components/typeahead/examples#basic) for more details. + * + * Note that the `this` argument is `undefined` so you need to explicitly bind it to a desired "this" target. + */ + @Input() ngbTypeahead: (text: Observable) => Observable; + + /** + * The function that converts an item from the result list to a `string` to display in the popup. + * + * Must be provided, if your `ngbTypeahead` returns something other than `Observable`. + * + * Alternatively for more complex markup in the popup you should use `resultTemplate`. + */ + @Input() resultFormatter: (item: any) => string; + + /** + * The template to override the way resulting items are displayed in the popup. + * + * See the [ResultTemplateContext](#/components/typeahead/api#ResultTemplateContext) for the template context. + * + * Also see the [template for results demo](#/components/typeahead/examples#template) for more details. + */ + @Input() resultTemplate: TemplateRef; + + /** + * If `true`, will show the hint in the `` when an item in the result list matches. + */ + @Input() showHint: boolean; + + /** + * The preferred placement of the typeahead. + * + * Possible values are `"top"`, `"top-left"`, `"top-right"`, `"bottom"`, `"bottom-left"`, + * `"bottom-right"`, `"left"`, `"left-top"`, `"left-bottom"`, `"right"`, `"right-top"`, + * `"right-bottom"` + * + * Accepts an array of strings or a string with space separated possible values. + * + * The default order of preference is `"bottom-left bottom-right top-left top-right"` + * + * Please see the [positioning overview](#/positioning) for more details. + */ + @Input() placement: PlacementArray = 'bottom-left'; + + /** + * An event emitted right before an item is selected from the result list. + * + * Event payload is of type [`NgbTypeaheadSelectItemEvent`](#/components/typeahead/api#NgbTypeaheadSelectItemEvent). + */ + @Output() selectItem = new EventEmitter(); + + activeDescendant: string; + popupId = `ngb-typeahead-${nextWindowId++}`; + + private _onTouched = () => {}; + private _onChange = (_: any) => {}; + + constructor( + private _elementRef: ElementRef, private _viewContainerRef: ViewContainerRef, + private _renderer: Renderer2, private _injector: Injector, componentFactoryResolver: ComponentFactoryResolver, + config: NgbTypeaheadConfig, ngZone: NgZone, private _live: Live, @Inject(DOCUMENT) private _document: any, + private _ngZone: NgZone, private _changeDetector: ChangeDetectorRef, private _applicationRef: ApplicationRef) { + this.container = config.container; + this.editable = config.editable; + this.focusFirst = config.focusFirst; + this.showHint = config.showHint; + this.placement = config.placement; + + this._valueChanges = fromEvent(_elementRef.nativeElement, 'input') + .pipe(map($event => ($event.target as HTMLInputElement).value)); + + this._resubscribeTypeahead = new BehaviorSubject(null); + + this._popupService = new PopupService( + NgbTypeaheadWindow, _injector, _viewContainerRef, _renderer, componentFactoryResolver, _applicationRef); + + this._zoneSubscription = ngZone.onStable.subscribe(() => { + if (this.isPopupOpen()) { + positionElements( + this._elementRef.nativeElement, this._windowRef.location.nativeElement, this.placement, + this.container === 'body'); + } + }); + } + + ngOnInit(): void { + const inputValues$ = this._valueChanges.pipe(tap(value => { + this._inputValueBackup = this.showHint ? value : null; + if (this.editable) { + this._onChange(value); + } + })); + const results$ = inputValues$.pipe(this.ngbTypeahead); + const processedResults$ = results$.pipe(tap(() => { + if (!this.editable) { + this._onChange(undefined); + } + })); + const userInput$ = this._resubscribeTypeahead.pipe(switchMap(() => processedResults$)); + this._subscription = this._subscribeToUserInput(userInput$); + } + + ngOnDestroy(): void { + this._closePopup(); + this._unsubscribeFromUserInput(); + this._zoneSubscription.unsubscribe(); + } + + registerOnChange(fn: (value: any) => any): void { this._onChange = fn; } + + registerOnTouched(fn: () => any): void { this._onTouched = fn; } + + writeValue(value) { + this._writeInputValue(this._formatItemForInput(value)); + if (this.showHint) { + this._inputValueBackup = value; + } + } + + setDisabledState(isDisabled: boolean): void { + this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled); + } + + /** + * Dismisses typeahead popup window + */ + dismissPopup() { + if (this.isPopupOpen()) { + this._resubscribeTypeahead.next(null); + this._closePopup(); + if (this.showHint && this._inputValueBackup !== null) { + this._writeInputValue(this._inputValueBackup); + } + this._changeDetector.markForCheck(); + } + } + + /** + * Returns true if the typeahead popup window is displayed + */ + isPopupOpen() { return this._windowRef != null; } + + handleBlur() { + this._resubscribeTypeahead.next(null); + this._onTouched(); + } + + handleKeyDown(event: KeyboardEvent) { + if (!this.isPopupOpen()) { + return; + } + + // tslint:disable-next-line:deprecation + switch (event.which) { + case Key.ArrowDown: + event.preventDefault(); + this._windowRef.instance.next(); + this._showHint(); + break; + case Key.ArrowUp: + event.preventDefault(); + this._windowRef.instance.prev(); + this._showHint(); + break; + case Key.Enter: + case Key.Tab: + const result = this._windowRef.instance.getActive(); + if (isDefined(result)) { + event.preventDefault(); + event.stopPropagation(); + this._selectResult(result); + } + this._closePopup(); + break; + } + } + + private _openPopup() { + if (!this.isPopupOpen()) { + this._inputValueBackup = this._elementRef.nativeElement.value; + this._windowRef = this._popupService.open(); + this._windowRef.instance.id = this.popupId; + this._windowRef.instance.selectEvent.subscribe((result: any) => this._selectResultClosePopup(result)); + this._windowRef.instance.activeChangeEvent.subscribe((activeId: string) => this.activeDescendant = activeId); + + if (this.container === 'body') { + window.document.querySelector(this.container).appendChild(this._windowRef.location.nativeElement); + } + + this._changeDetector.markForCheck(); + + ngbAutoClose( + this._ngZone, this._document, 'outside', () => this.dismissPopup(), this._closed$, + [this._elementRef.nativeElement, this._windowRef.location.nativeElement]); + } + } + + private _closePopup() { + this._closed$.next(); + this._popupService.close(); + this._windowRef = null; + this.activeDescendant = undefined; + } + + private _selectResult(result: any) { + let defaultPrevented = false; + this.selectItem.emit({item: result, preventDefault: () => { defaultPrevented = true; }}); + this._resubscribeTypeahead.next(null); + + if (!defaultPrevented) { + this.writeValue(result); + this._onChange(result); + } + } + + private _selectResultClosePopup(result: any) { + this._selectResult(result); + this._closePopup(); + } + + private _showHint() { + if (this.showHint && this._windowRef.instance.hasActive() && this._inputValueBackup != null) { + const userInputLowerCase = this._inputValueBackup.toLowerCase(); + const formattedVal = this._formatItemForInput(this._windowRef.instance.getActive()); + + if (userInputLowerCase === formattedVal.substr(0, this._inputValueBackup.length).toLowerCase()) { + this._writeInputValue(this._inputValueBackup + formattedVal.substr(this._inputValueBackup.length)); + this._elementRef.nativeElement['setSelectionRange'].apply( + this._elementRef.nativeElement, [this._inputValueBackup.length, formattedVal.length]); + } else { + this._writeInputValue(formattedVal); + } + } + } + + private _formatItemForInput(item: any): string { + return item != null && this.inputFormatter ? this.inputFormatter(item) : toString(item); + } + + private _writeInputValue(value: string): void { + this._renderer.setProperty(this._elementRef.nativeElement, 'value', toString(value)); + } + + private _subscribeToUserInput(userInput$: Observable): Subscription { + return userInput$.subscribe((results) => { + if (!results || results.length === 0) { + this._closePopup(); + } else { + this._openPopup(); + this._windowRef.instance.focusFirst = this.focusFirst; + this._windowRef.instance.results = results; + this._windowRef.instance.term = this._elementRef.nativeElement.value; + if (this.resultFormatter) { + this._windowRef.instance.formatter = this.resultFormatter; + } + if (this.resultTemplate) { + this._windowRef.instance.resultTemplate = this.resultTemplate; + } + this._windowRef.instance.resetActive(); + + // The observable stream we are subscribing to might have async steps + // and if a component containing typeahead is using the OnPush strategy + // the change detection turn wouldn't be invoked automatically. + this._windowRef.changeDetectorRef.detectChanges(); + + this._showHint(); + } + + // live announcer + const count = results ? results.length : 0; + this._live.say(count === 0 ? 'No results available' : `${count} result${count === 1 ? '' : 's'} available`); + }); + } + + private _unsubscribeFromUserInput() { + if (this._subscription) { + this._subscription.unsubscribe(); + } + this._subscription = null; + } +} diff --git a/src/util/accessibility/live.spec.ts b/src/util/accessibility/live.spec.ts new file mode 100644 index 0000000..19179ac --- /dev/null +++ b/src/util/accessibility/live.spec.ts @@ -0,0 +1,53 @@ +import {TestBed, ComponentFixture, inject} from '@angular/core/testing'; +import {Component} from '@angular/core'; +import {By} from '@angular/platform-browser'; +import {Live, ARIA_LIVE_DELAY} from './live'; + + + +function getLiveElement(): Element | null { + return document.body.querySelector('#ngb-live'); +} + + + +describe('LiveAnnouncer', () => { + let live: Live; + let fixture: ComponentFixture; + + const say = () => { fixture.debugElement.query(By.css('button')).nativeElement.click(); }; + + describe('live announcer', () => { + beforeEach(() => TestBed.configureTestingModule({ + providers: [Live, {provide: ARIA_LIVE_DELAY, useValue: null}], + declarations: [TestComponent], + })); + + beforeEach(inject([Live], (_live: Live) => { + live = _live; + fixture = TestBed.createComponent(TestComponent); + })); + + it('should correctly update the text message', () => { + say(); + const liveElement = getLiveElement(); + expect(liveElement.textContent).toBe('test'); + expect(liveElement.id).toBe('ngb-live'); + }); + + it('should remove the used element from the DOM on destroy', () => { + say(); + live.ngOnDestroy(); + + expect(getLiveElement()).toBeFalsy(); + }); + }); +}); + + + +@Component({template: ``}) +class TestComponent { + constructor(public live: Live) {} + say() { this.live.say('test'); } +} diff --git a/src/util/accessibility/live.ts b/src/util/accessibility/live.ts new file mode 100644 index 0000000..8bc11b5 --- /dev/null +++ b/src/util/accessibility/live.ts @@ -0,0 +1,59 @@ +import {Injectable, Inject, InjectionToken, OnDestroy} from '@angular/core'; +import {DOCUMENT} from '@angular/common'; + + + +// usefulness (and default value) of delay documented in Material's CDK +// https://github.com/angular/material2/blob/6405da9b8e8532a7e5c854c920ee1815c275d734/src/cdk/a11y/live-announcer/live-announcer.ts#L50 +export type ARIA_LIVE_DELAY_TYPE = number | null; +export const ARIA_LIVE_DELAY = new InjectionToken( + 'live announcer delay', {providedIn: 'root', factory: ARIA_LIVE_DELAY_FACTORY}); +export function ARIA_LIVE_DELAY_FACTORY(): number { + return 100; +} + + +function getLiveElement(document: any, lazyCreate = false): HTMLElement | null { + let element = document.body.querySelector('#ngb-live') as HTMLElement; + + if (element == null && lazyCreate) { + element = document.createElement('div'); + + element.setAttribute('id', 'ngb-live'); + element.setAttribute('aria-live', 'polite'); + element.setAttribute('aria-atomic', 'true'); + + element.classList.add('sr-only'); + + document.body.appendChild(element); + } + + return element; +} + + + +@Injectable({providedIn: 'root'}) +export class Live implements OnDestroy { + constructor(@Inject(DOCUMENT) private _document: any, @Inject(ARIA_LIVE_DELAY) private _delay: any) {} + + ngOnDestroy() { + const element = getLiveElement(this._document); + if (element) { + element.parentElement.removeChild(element); + } + } + + say(message: string) { + const element = getLiveElement(this._document, true); + const delay = this._delay; + + element.textContent = ''; + const setText = () => element.textContent = message; + if (delay === null) { + setText(); + } else { + setTimeout(setText, delay); + } + } +} diff --git a/src/util/autoclose.ts b/src/util/autoclose.ts new file mode 100644 index 0000000..7d72fe5 --- /dev/null +++ b/src/util/autoclose.ts @@ -0,0 +1,63 @@ +import {NgZone} from '@angular/core'; +import {fromEvent, Observable, race} from 'rxjs'; +import {delay, filter, map, takeUntil, withLatestFrom} from 'rxjs/operators'; +import {Key} from './key'; +import {closest} from './util'; + +const isContainedIn = (element: HTMLElement, array?: HTMLElement[]) => + array ? array.some(item => item.contains(element)) : false; + +const matchesSelectorIfAny = (element: HTMLElement, selector?: string) => + !selector || closest(element, selector) != null; + +// we'll have to use 'touch' events instead of 'mouse' events on iOS and add a more significant delay +// to avoid re-opening when handling (click) on a toggling element +// TODO: use proper Angular platform detection when NgbAutoClose becomes a service and we can inject PLATFORM_ID +let iOS = false; +if (typeof navigator !== 'undefined') { + iOS = !!navigator.userAgent && /iPad|iPhone|iPod/.test(navigator.userAgent); +} + +export function ngbAutoClose( + zone: NgZone, document: any, type: boolean | 'inside' | 'outside', close: () => void, closed$: Observable, + insideElements: HTMLElement[], ignoreElements?: HTMLElement[], insideSelector?: string) { + // closing on ESC and outside clicks + if (type) { + zone.runOutsideAngular(() => { + + const shouldCloseOnClick = (event: MouseEvent | TouchEvent) => { + const element = event.target as HTMLElement; + if ((event instanceof MouseEvent && event.button === 2) || isContainedIn(element, ignoreElements)) { + return false; + } + if (type === 'inside') { + return isContainedIn(element, insideElements) && matchesSelectorIfAny(element, insideSelector); + } else if (type === 'outside') { + return !isContainedIn(element, insideElements); + } else /* if (type === true) */ { + return matchesSelectorIfAny(element, insideSelector) || !isContainedIn(element, insideElements); + } + }; + + const escapes$ = fromEvent(document, 'keydown') + .pipe( + takeUntil(closed$), + // tslint:disable-next-line:deprecation + filter(e => e.which === Key.Escape)); + + + // we have to pre-calculate 'shouldCloseOnClick' on 'mousedown/touchstart', + // because on 'mouseup/touchend' DOM nodes might be detached + const mouseDowns$ = fromEvent(document, iOS ? 'touchstart' : 'mousedown') + .pipe(map(shouldCloseOnClick), takeUntil(closed$)); + + const closeableClicks$ = fromEvent(document, iOS ? 'touchend' : 'mouseup') + .pipe( + withLatestFrom(mouseDowns$), filter(([_, shouldClose]) => shouldClose), + delay(iOS ? 16 : 0), takeUntil(closed$)) as Observable; + + + race([escapes$, closeableClicks$]).subscribe(() => zone.run(close)); + }); + } +} diff --git a/src/util/focus-trap.ts b/src/util/focus-trap.ts new file mode 100644 index 0000000..caef923 --- /dev/null +++ b/src/util/focus-trap.ts @@ -0,0 +1,66 @@ +import {fromEvent, Observable} from 'rxjs'; +import {filter, map, takeUntil, withLatestFrom} from 'rxjs/operators'; + +import {Key} from '../util/key'; + + +const FOCUSABLE_ELEMENTS_SELECTOR = [ + 'a[href]', 'button:not([disabled])', 'input:not([disabled]):not([type="hidden"])', 'select:not([disabled])', + 'textarea:not([disabled])', '[contenteditable]', '[tabindex]:not([tabindex="-1"])' +].join(', '); + +/** + * Returns first and last focusable elements inside of a given element based on specific CSS selector + */ +export function getFocusableBoundaryElements(element: HTMLElement): HTMLElement[] { + const list: HTMLElement[] = + Array.from(element.querySelectorAll(FOCUSABLE_ELEMENTS_SELECTOR) as NodeListOf) + .filter(el => el.tabIndex !== -1); + return [list[0], list[list.length - 1]]; +} + +/** + * Function that enforces browser focus to be trapped inside a DOM element. + * + * Works only for clicks inside the element and navigation with 'Tab', ignoring clicks outside of the element + * + * @param element The element around which focus will be trapped inside + * @param stopFocusTrap$ The observable stream. When completed the focus trap will clean up listeners + * and free internal resources + * @param refocusOnClick Put the focus back to the last focused element whenever a click occurs on element (default to + * false) + */ +export const ngbFocusTrap = (element: HTMLElement, stopFocusTrap$: Observable, refocusOnClick = false) => { + // last focused element + const lastFocusedElement$ = + fromEvent(element, 'focusin').pipe(takeUntil(stopFocusTrap$), map(e => e.target)); + + // 'tab' / 'shift+tab' stream + fromEvent(element, 'keydown') + .pipe( + takeUntil(stopFocusTrap$), + // tslint:disable:deprecation + filter(e => e.which === Key.Tab), + // tslint:enable:deprecation + withLatestFrom(lastFocusedElement$)) + .subscribe(([tabEvent, focusedElement]) => { + const[first, last] = getFocusableBoundaryElements(element); + + if ((focusedElement === first || focusedElement === element) && tabEvent.shiftKey) { + last.focus(); + tabEvent.preventDefault(); + } + + if (focusedElement === last && !tabEvent.shiftKey) { + first.focus(); + tabEvent.preventDefault(); + } + }); + + // inside click + if (refocusOnClick) { + fromEvent(element, 'click') + .pipe(takeUntil(stopFocusTrap$), withLatestFrom(lastFocusedElement$), map(arr => arr[1] as HTMLElement)) + .subscribe(lastFocusedElement => lastFocusedElement.focus()); + } +}; diff --git a/src/util/key.ts b/src/util/key.ts new file mode 100644 index 0000000..e102d9b --- /dev/null +++ b/src/util/key.ts @@ -0,0 +1,14 @@ +export enum Key { + Tab = 9, + Enter = 13, + Escape = 27, + Space = 32, + PageUp = 33, + PageDown = 34, + End = 35, + Home = 36, + ArrowLeft = 37, + ArrowUp = 38, + ArrowRight = 39, + ArrowDown = 40 +} diff --git a/src/util/popup.ts b/src/util/popup.ts new file mode 100644 index 0000000..8f9d61e --- /dev/null +++ b/src/util/popup.ts @@ -0,0 +1,60 @@ +import { + Injector, + TemplateRef, + ViewRef, + ViewContainerRef, + Renderer2, + ComponentRef, + ComponentFactoryResolver, + ApplicationRef +} from '@angular/core'; + +export class ContentRef { + constructor(public nodes: any[], public viewRef?: ViewRef, public componentRef?: ComponentRef) {} +} + +export class PopupService { + private _windowRef: ComponentRef; + private _contentRef: ContentRef; + + constructor( + private _type: any, private _injector: Injector, private _viewContainerRef: ViewContainerRef, + private _renderer: Renderer2, private _componentFactoryResolver: ComponentFactoryResolver, + private _applicationRef: ApplicationRef) {} + + open(content?: string | TemplateRef, context?: any): ComponentRef { + if (!this._windowRef) { + this._contentRef = this._getContentRef(content, context); + this._windowRef = this._viewContainerRef.createComponent( + this._componentFactoryResolver.resolveComponentFactory(this._type), 0, this._injector, + this._contentRef.nodes); + } + + return this._windowRef; + } + + close() { + if (this._windowRef) { + this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._windowRef.hostView)); + this._windowRef = null; + + if (this._contentRef.viewRef) { + this._applicationRef.detachView(this._contentRef.viewRef); + this._contentRef.viewRef.destroy(); + this._contentRef = null; + } + } + } + + private _getContentRef(content: string | TemplateRef, context?: any): ContentRef { + if (!content) { + return new ContentRef([]); + } else if (content instanceof TemplateRef) { + const viewRef = content.createEmbeddedView(context); + this._applicationRef.attachView(viewRef); + return new ContentRef([viewRef.rootNodes], viewRef); + } else { + return new ContentRef([[this._renderer.createText(`${content}`)]]); + } + } +} diff --git a/src/util/positioning.spec.ts b/src/util/positioning.spec.ts new file mode 100644 index 0000000..d85a46f --- /dev/null +++ b/src/util/positioning.spec.ts @@ -0,0 +1,224 @@ +import {Positioning} from './positioning'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {createGenericTestComponent} from 'src/test/common'; +import {Component} from '@angular/core'; + +describe('Positioning', () => { + + function createElement( + height: number, width: number, marginTop: number, marginLeft: number, isAbsolute = false): HTMLElement { + let el = document.createElement('div'); + if (isAbsolute) { + el.style.position = 'absolute'; + el.style.top = '0'; + el.style.left = '0'; + } + el.style.display = 'inline-block'; + el.style.height = height + 'px'; + el.style.width = width + 'px'; + el.style.marginTop = marginTop + 'px'; + el.style.marginLeft = marginLeft + 'px'; + + return el; + } + + function checkPosition(el: HTMLElement, top: number, left: number) { + const transform = el.style.transform; + expect(transform).toBe(`translate(${left}px, ${top}px)`); + } + + let element, targetElement, positioning, documentMargin, bodyMargin, bodyHeight, bodyWidth; + beforeAll(() => { + positioning = new Positioning(); + documentMargin = document.documentElement.style.margin; + bodyMargin = document.body.style.margin; + bodyHeight = document.body.style.height; + bodyWidth = document.body.style.width; + + document.documentElement.style.margin = '0'; + document.body.style.margin = '0'; + }); + + afterAll(() => { + document.documentElement.style.margin = documentMargin; + document.body.style.margin = bodyMargin; + }); + + beforeEach(() => { + TestBed.configureTestingModule({declarations: [TestComponent]}); + const fixture = TestBed.createComponent(TestComponent); + + element = fixture.nativeElement.querySelector('#element'); + targetElement = fixture.nativeElement.querySelector('#targetElement'); + }); + + it('should calculate the element offset', () => { + let position = positioning.offset(element); + + expect(position.height).toBe(200); + expect(position.width).toBe(300); + expect(position.top).toBe(100); + expect(position.bottom).toBe(300); + expect(position.left).toBe(150); + expect(position.right).toBe(450); + }); + + it('should calculate the element offset when scrolled', () => { + document.documentElement.scrollTop = 1000; + document.documentElement.scrollLeft = 1000; + + let position = positioning.offset(element); + + expect(position.top).toBe(100); + expect(position.bottom).toBe(300); + expect(position.left).toBe(150); + expect(position.right).toBe(450); + + document.documentElement.scrollTop = 0; + document.documentElement.scrollLeft = 0; + }); + + it('should calculate the element position', () => { + let position = positioning.position(element); + + expect(position.height).toBe(200); + expect(position.width).toBe(300); + expect(position.top).toBe(100); + expect(position.bottom).toBe(300); + expect(position.left).toBe(150); + expect(position.right).toBe(450); + }); + + it('should calculate the element position when scrolled', () => { + document.documentElement.scrollTop = 1000; + document.documentElement.scrollLeft = 1000; + + let position = positioning.position(element); + + expect(position.top).toBe(100); + expect(position.bottom).toBe(300); + expect(position.left).toBe(150); + expect(position.right).toBe(450); + + document.documentElement.scrollTop = 0; + document.documentElement.scrollLeft = 0; + }); + + it('should calculate the element position on positioned ancestor', () => { + let childElement = createElement(100, 150, 50, 75); + + element.style.position = 'relative'; + element.appendChild(childElement); + + let position = positioning.position(childElement); + + expect(position.top).toBe(50); + expect(position.bottom).toBe(150); + expect(position.left).toBe(75); + expect(position.right).toBe(225); + + element.style.position = ''; + element.removeChild(childElement); + }); + + it('should position the element top-left', () => { + + let isInViewport = positioning.positionElements(element, targetElement, 'top-left'); + + expect(isInViewport).toBe(true); + checkPosition(targetElement, 40, 150); + }); + + it('should position the element top-center', () => { + let isInViewport = positioning.positionElements(element, targetElement, 'top'); + + expect(isInViewport).toBe(true); + checkPosition(targetElement, 40, 250); + }); + + it('should position the element top-right', () => { + let isInViewport = positioning.positionElements(element, targetElement, 'top-right'); + + expect(isInViewport).toBe(true); + checkPosition(targetElement, 40, 350); + }); + + it('should position the element bottom-left', () => { + let isInViewport = positioning.positionElements(element, targetElement, 'bottom-left'); + + expect(isInViewport).toBe(true); + checkPosition(targetElement, 300, 150); + }); + + it('should position the element bottom-center', () => { + let isInViewport = positioning.positionElements(element, targetElement, 'bottom'); + + expect(isInViewport).toBe(true); + checkPosition(targetElement, 300, 250); + }); + + it('should position the element bottom-right', () => { + let isInViewport = positioning.positionElements(element, targetElement, 'bottom-right'); + + expect(isInViewport).toBe(true); + checkPosition(targetElement, 300, 350); + }); + + it('should position the element left-top', () => { + let isInViewport = positioning.positionElements(element, targetElement, 'left-top'); + + expect(isInViewport).toBe(true); + checkPosition(targetElement, 100, 30); + }); + + it('should position the element left-center', () => { + let isInViewport = positioning.positionElements(element, targetElement, 'left'); + + expect(isInViewport).toBe(true); + checkPosition(targetElement, 175, 30); + }); + + it('should position the element left-bottom', () => { + let isInViewport = positioning.positionElements(element, targetElement, 'left-bottom'); + + expect(isInViewport).toBe(true); + checkPosition(targetElement, 250, 30); + }); + + it('should position the element right-top', () => { + let isInViewport = positioning.positionElements(element, targetElement, 'right-top'); + + expect(isInViewport).toBe(true); + checkPosition(targetElement, 100, 450); + }); + + it('should position the element right-center', () => { + let isInViewport = positioning.positionElements(element, targetElement, 'right'); + + expect(isInViewport).toBe(true); + checkPosition(targetElement, 175, 450); + }); + + it('should position the element right-bottom', () => { + let isInViewport = positioning.positionElements(element, targetElement, 'right-bottom'); + + expect(isInViewport).toBe(true); + checkPosition(targetElement, 250, 450); + }); + +}); + +@Component({ + template: ` +
+
+` +}) +export class TestComponent { +} diff --git a/src/util/positioning.ts b/src/util/positioning.ts new file mode 100644 index 0000000..6205e50 --- /dev/null +++ b/src/util/positioning.ts @@ -0,0 +1,254 @@ +// previous version: +// https://github.com/angular-ui/bootstrap/blob/07c31d0731f7cb068a1932b8e01d2312b796b4ec/src/position/position.js +export class Positioning { + private getAllStyles(element: HTMLElement) { return window.getComputedStyle(element); } + + private getStyle(element: HTMLElement, prop: string): string { return this.getAllStyles(element)[prop]; } + + private isStaticPositioned(element: HTMLElement): boolean { + return (this.getStyle(element, 'position') || 'static') === 'static'; + } + + private offsetParent(element: HTMLElement): HTMLElement { + let offsetParentEl = element.offsetParent || document.documentElement; + + while (offsetParentEl && offsetParentEl !== document.documentElement && this.isStaticPositioned(offsetParentEl)) { + offsetParentEl = offsetParentEl.offsetParent; + } + + return offsetParentEl || document.documentElement; + } + + position(element: HTMLElement, round = true): ClientRect { + let elPosition: ClientRect; + let parentOffset: ClientRect = {width: 0, height: 0, top: 0, bottom: 0, left: 0, right: 0}; + + if (this.getStyle(element, 'position') === 'fixed') { + elPosition = element.getBoundingClientRect(); + elPosition = { + top: elPosition.top, + bottom: elPosition.bottom, + left: elPosition.left, + right: elPosition.right, + height: elPosition.height, + width: elPosition.width + }; + } else { + const offsetParentEl = this.offsetParent(element); + + elPosition = this.offset(element, false); + + if (offsetParentEl !== document.documentElement) { + parentOffset = this.offset(offsetParentEl, false); + } + + parentOffset.top += offsetParentEl.clientTop; + parentOffset.left += offsetParentEl.clientLeft; + } + + elPosition.top -= parentOffset.top; + elPosition.bottom -= parentOffset.top; + elPosition.left -= parentOffset.left; + elPosition.right -= parentOffset.left; + + if (round) { + elPosition.top = Math.round(elPosition.top); + elPosition.bottom = Math.round(elPosition.bottom); + elPosition.left = Math.round(elPosition.left); + elPosition.right = Math.round(elPosition.right); + } + + return elPosition; + } + + offset(element: HTMLElement, round = true): ClientRect { + const elBcr = element.getBoundingClientRect(); + const viewportOffset = { + top: window.pageYOffset - document.documentElement.clientTop, + left: window.pageXOffset - document.documentElement.clientLeft + }; + + let elOffset = { + height: elBcr.height || element.offsetHeight, + width: elBcr.width || element.offsetWidth, + top: elBcr.top + viewportOffset.top, + bottom: elBcr.bottom + viewportOffset.top, + left: elBcr.left + viewportOffset.left, + right: elBcr.right + viewportOffset.left + }; + + if (round) { + elOffset.height = Math.round(elOffset.height); + elOffset.width = Math.round(elOffset.width); + elOffset.top = Math.round(elOffset.top); + elOffset.bottom = Math.round(elOffset.bottom); + elOffset.left = Math.round(elOffset.left); + elOffset.right = Math.round(elOffset.right); + } + + return elOffset; + } + + /* + Return false if the element to position is outside the viewport + */ + positionElements(hostElement: HTMLElement, targetElement: HTMLElement, placement: string, appendToBody?: boolean): + boolean { + const[placementPrimary = 'top', placementSecondary = 'center'] = placement.split('-'); + + const hostElPosition = appendToBody ? this.offset(hostElement, false) : this.position(hostElement, false); + const targetElStyles = this.getAllStyles(targetElement); + + const marginTop = parseFloat(targetElStyles.marginTop); + const marginBottom = parseFloat(targetElStyles.marginBottom); + const marginLeft = parseFloat(targetElStyles.marginLeft); + const marginRight = parseFloat(targetElStyles.marginRight); + + let topPosition = 0; + let leftPosition = 0; + + switch (placementPrimary) { + case 'top': + topPosition = (hostElPosition.top - (targetElement.offsetHeight + marginTop + marginBottom)); + break; + case 'bottom': + topPosition = (hostElPosition.top + hostElPosition.height); + break; + case 'left': + leftPosition = (hostElPosition.left - (targetElement.offsetWidth + marginLeft + marginRight)); + break; + case 'right': + leftPosition = (hostElPosition.left + hostElPosition.width); + break; + } + + switch (placementSecondary) { + case 'top': + topPosition = hostElPosition.top; + break; + case 'bottom': + topPosition = hostElPosition.top + hostElPosition.height - targetElement.offsetHeight; + break; + case 'left': + leftPosition = hostElPosition.left; + break; + case 'right': + leftPosition = hostElPosition.left + hostElPosition.width - targetElement.offsetWidth; + break; + case 'center': + if (placementPrimary === 'top' || placementPrimary === 'bottom') { + leftPosition = (hostElPosition.left + hostElPosition.width / 2 - targetElement.offsetWidth / 2); + } else { + topPosition = (hostElPosition.top + hostElPosition.height / 2 - targetElement.offsetHeight / 2); + } + break; + } + + /// The translate3d/gpu acceleration render a blurry text on chrome, the next line is commented until a browser fix + // targetElement.style.transform = `translate3d(${Math.round(leftPosition)}px, ${Math.floor(topPosition)}px, 0px)`; + targetElement.style.transform = `translate(${Math.round(leftPosition)}px, ${Math.round(topPosition)}px)`; + + // Check if the targetElement is inside the viewport + const targetElBCR = targetElement.getBoundingClientRect(); + const html = document.documentElement; + const windowHeight = window.innerHeight || html.clientHeight; + const windowWidth = window.innerWidth || html.clientWidth; + + return targetElBCR.left >= 0 && targetElBCR.top >= 0 && targetElBCR.right <= windowWidth && + targetElBCR.bottom <= windowHeight; + } +} + +const placementSeparator = /\s+/; +const positionService = new Positioning(); + +/* + * Accept the placement array and applies the appropriate placement dependent on the viewport. + * Returns the applied placement. + * In case of auto placement, placements are selected in order + * 'top', 'bottom', 'left', 'right', + * 'top-left', 'top-right', + * 'bottom-left', 'bottom-right', + * 'left-top', 'left-bottom', + * 'right-top', 'right-bottom'. + * */ +export function positionElements( + hostElement: HTMLElement, targetElement: HTMLElement, placement: string | Placement | PlacementArray, + appendToBody?: boolean, baseClass?: string): Placement { + let placementVals: Array = + Array.isArray(placement) ? placement : placement.split(placementSeparator) as Array; + + const allowedPlacements = [ + 'top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right', 'left-top', 'left-bottom', + 'right-top', 'right-bottom' + ]; + + const classList = targetElement.classList; + const addClassesToTarget = (targetPlacement: Placement): Array => { + const[primary, secondary] = targetPlacement.split('-'); + const classes = []; + if (baseClass) { + classes.push(`${baseClass}-${primary}`); + if (secondary) { + classes.push(`${baseClass}-${primary}-${secondary}`); + } + + classes.forEach((classname) => { classList.add(classname); }); + } + return classes; + }; + + // Remove old placement classes to avoid issues + if (baseClass) { + allowedPlacements.forEach((placementToRemove) => { classList.remove(`${baseClass}-${placementToRemove}`); }); + } + + // replace auto placement with other placements + let hasAuto = placementVals.findIndex(val => val === 'auto'); + if (hasAuto >= 0) { + allowedPlacements.forEach(function(obj) { + if (placementVals.find(val => val.search('^' + obj) !== -1) == null) { + placementVals.splice(hasAuto++, 1, obj as Placement); + } + }); + } + + // coordinates where to position + + // Required for transform: + const style = targetElement.style; + style.position = 'absolute'; + style.top = '0'; + style.left = '0'; + style['will-change'] = 'transform'; + + let testPlacement: Placement; + let isInViewport = false; + for (testPlacement of placementVals) { + let addedClasses = addClassesToTarget(testPlacement); + + if (positionService.positionElements(hostElement, targetElement, testPlacement, appendToBody)) { + isInViewport = true; + break; + } + + // Remove the baseClasses for further calculation + if (baseClass) { + addedClasses.forEach((classname) => { classList.remove(classname); }); + } + } + + if (!isInViewport) { + // If nothing match, the first placement is the default one + testPlacement = placementVals[0]; + addClassesToTarget(testPlacement); + positionService.positionElements(hostElement, targetElement, testPlacement, appendToBody); + } + + return testPlacement; +} + +export type Placement = 'auto' | 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | + 'bottom-right' | 'left-top' | 'left-bottom' | 'right-top' | 'right-bottom'; + +export type PlacementArray = Placement | Array| string; diff --git a/src/util/scrollbar.ts b/src/util/scrollbar.ts new file mode 100644 index 0000000..90a4e3a --- /dev/null +++ b/src/util/scrollbar.ts @@ -0,0 +1,72 @@ +import {Injectable, Inject} from '@angular/core'; +import {DOCUMENT} from '@angular/common'; + + +const noop = () => {}; + + + +/** Type for the callback used to revert the scrollbar compensation. */ +export type CompensationReverter = () => void; + + + +/** + * Utility to handle the scrollbar. + * + * It allows to compensate the lack of a vertical scrollbar by adding an + * equivalent padding on the right of the body, and to remove this compensation. + */ +@Injectable({providedIn: 'root'}) +export class ScrollBar { + constructor(@Inject(DOCUMENT) private _document: any) {} + + /** + * Detects if a scrollbar is present and if yes, already compensates for its + * removal by adding an equivalent padding on the right of the body. + * + * @return a callback used to revert the compensation (noop if there was none, + * otherwise a function removing the padding) + */ + compensate(): CompensationReverter { return !this._isPresent() ? noop : this._adjustBody(this._getWidth()); } + + /** + * Adds a padding of the given width on the right of the body. + * + * @return a callback used to revert the padding to its previous value + */ + private _adjustBody(width: number): CompensationReverter { + const body = this._document.body; + const userSetPadding = body.style.paddingRight; + const paddingAmount = parseFloat(window.getComputedStyle(body)['padding-right']); + body.style['padding-right'] = `${paddingAmount + width}px`; + return () => body.style['padding-right'] = userSetPadding; + } + + /** + * Tells whether a scrollbar is currently present on the body. + * + * @return true if scrollbar is present, false otherwise + */ + private _isPresent(): boolean { + const rect = this._document.body.getBoundingClientRect(); + return rect.left + rect.right < window.innerWidth; + } + + /** + * Calculates and returns the width of a scrollbar. + * + * @return the width of a scrollbar on this page + */ + private _getWidth(): number { + const measurer = this._document.createElement('div'); + measurer.className = 'modal-scrollbar-measure'; + + const body = this._document.body; + body.appendChild(measurer); + const width = measurer.getBoundingClientRect().width - measurer.clientWidth; + body.removeChild(measurer); + + return width; + } +} diff --git a/src/util/triggers.spec.ts b/src/util/triggers.spec.ts new file mode 100644 index 0000000..f0e1e5a --- /dev/null +++ b/src/util/triggers.spec.ts @@ -0,0 +1,236 @@ +import {fakeAsync, tick} from '@angular/core/testing'; +import {Subject, Subscription, Observable} from 'rxjs'; +import {parseTriggers, triggerDelay} from './triggers'; + +describe('triggers', () => { + + describe('parseTriggers', () => { + + it('should parse single trigger', () => { + const t = parseTriggers('foo'); + + expect(t.length).toBe(1); + expect(t[0].open).toBe('foo'); + expect(t[0].close).toBe('foo'); + }); + + it('should parse open:close form', () => { + const t = parseTriggers('foo:bar'); + + expect(t.length).toBe(1); + expect(t[0].open).toBe('foo'); + expect(t[0].close).toBe('bar'); + }); + + it('should parse multiple triggers', () => { + const t = parseTriggers('foo:bar bar:baz'); + + expect(t.length).toBe(2); + expect(t[0].open).toBe('foo'); + expect(t[0].close).toBe('bar'); + expect(t[1].open).toBe('bar'); + expect(t[1].close).toBe('baz'); + }); + + it('should parse multiple triggers with mixed forms', () => { + const t = parseTriggers('foo bar:baz'); + + expect(t.length).toBe(2); + expect(t[0].open).toBe('foo'); + expect(t[0].close).toBe('foo'); + expect(t[1].open).toBe('bar'); + expect(t[1].close).toBe('baz'); + }); + + it('should properly trim excessive white-spaces', () => { + const t = parseTriggers('foo bar \n baz '); + + expect(t.length).toBe(3); + expect(t[0].open).toBe('foo'); + expect(t[0].close).toBe('foo'); + expect(t[1].open).toBe('bar'); + expect(t[1].close).toBe('bar'); + expect(t[2].open).toBe('baz'); + expect(t[2].close).toBe('baz'); + }); + + it('should lookup and translate special aliases', () => { + const t = parseTriggers('hover'); + + expect(t.length).toBe(1); + expect(t[0].open).toBe('mouseenter'); + expect(t[0].close).toBe('mouseleave'); + }); + + it('should detect manual triggers', () => { + const t = parseTriggers('manual'); + + expect(t[0].isManual).toBeTruthy(); + }); + + it('should ignore empty inputs', () => { + expect(parseTriggers(null).length).toBe(0); + expect(parseTriggers(undefined).length).toBe(0); + expect(parseTriggers('').length).toBe(0); + }); + + it('should throw when more than one manual trigger detected', () => { + expect(() => { + parseTriggers('manual click manual'); + }).toThrow('Triggers parse error: only one manual trigger is allowed'); + }); + + it('should throw when manual trigger is mixed with other triggers', () => { + expect(() => { + parseTriggers('click manual'); + }).toThrow(`Triggers parse error: manual trigger can\'t be mixed with other triggers`); + }); + + }); + + describe('triggerDelay', () => { + let subject$: Subject; + let delayed$: Observable; + let open: boolean; + let subscription: Subscription; + let spy: jasmine.Spy; + + beforeEach(() => { + subject$ = new Subject(); + spy = jasmine.createSpy('listener', (newValue) => open = newValue).and.callThrough(); + delayed$ = subject$.asObservable().pipe(triggerDelay(5000, 1000, () => open)); + subscription = delayed$.subscribe(spy); + }); + + afterEach(() => { + if (subscription) { + subscription.unsubscribe(); + subscription = null; + } + }); + + it('delays open', fakeAsync(() => { + open = false; + subject$.next(true); + tick(4999); + expect(spy).not.toHaveBeenCalled(); + tick(2); + expect(spy).toHaveBeenCalledWith(true); + tick(100000); + expect(spy.calls.count()).toBe(1); + })); + + it('cancels open if it is already done through another way', fakeAsync(() => { + open = false; + subject$.next(true); + tick(4999); + expect(spy).not.toHaveBeenCalled(); + open = true; + tick(2); + expect(spy).not.toHaveBeenCalled(); + tick(100000); + expect(spy.calls.count()).toBe(0); + })); + + it('delays close', fakeAsync(() => { + open = true; + subject$.next(false); + tick(999); + expect(spy).not.toHaveBeenCalled(); + tick(2); + expect(spy).toHaveBeenCalledWith(false); + tick(100000); + expect(spy.calls.count()).toBe(1); + })); + + it('cancels close if it is already done through another way', fakeAsync(() => { + open = true; + subject$.next(false); + tick(999); + expect(spy).not.toHaveBeenCalled(); + open = false; + tick(2); + expect(spy).not.toHaveBeenCalled(); + tick(100000); + expect(spy.calls.count()).toBe(0); + })); + + it('ignores extra open during openDelay', fakeAsync(() => { + open = false; + subject$.next(true); + tick(200); + subject$.next(true); + tick(100); + subject$.next(true); + tick(200); + tick(4499); + expect(spy).not.toHaveBeenCalled(); + tick(2); + expect(spy).toHaveBeenCalledWith(true); + tick(100000); + expect(spy.calls.count()).toBe(1); + })); + + it('ignores extra close during closeDelay', fakeAsync(() => { + open = true; + subject$.next(false); + tick(200); + subject$.next(false); + tick(100); + subject$.next(false); + tick(200); + tick(499); + expect(spy).not.toHaveBeenCalled(); + tick(2); + expect(spy).toHaveBeenCalledWith(false); + tick(100000); + expect(spy.calls.count()).toBe(1); + })); + + it('cancels open when receiving close during openDelay', fakeAsync(() => { + open = false; + subject$.next(true); + tick(4999); + subject$.next(false); + tick(100000); + expect(spy).not.toHaveBeenCalled(); + })); + + it('cancels close when receiving open during closeDelay', fakeAsync(() => { + open = true; + subject$.next(false); + tick(999); + subject$.next(true); + tick(100000); + expect(spy).not.toHaveBeenCalled(); + })); + + it('closes during openDelay if opened through another way', fakeAsync(() => { + open = false; + subject$.next(true); + tick(4999); + open = true; + subject$.next(false); + tick(999); + expect(spy).not.toHaveBeenCalled(); + tick(2); + expect(spy).toHaveBeenCalledWith(false); + tick(100000); + expect(spy.calls.count()).toBe(1); + })); + + it('opens during closeDelay if closed through another way', fakeAsync(() => { + open = true; + subject$.next(false); + tick(999); + open = false; + subject$.next(true); + tick(4999); + expect(spy).not.toHaveBeenCalled(); + tick(2); + expect(spy).toHaveBeenCalledWith(true); + tick(100000); + expect(spy.calls.count()).toBe(1); + })); + }); +}); diff --git a/src/util/triggers.ts b/src/util/triggers.ts new file mode 100644 index 0000000..582d54f --- /dev/null +++ b/src/util/triggers.ts @@ -0,0 +1,112 @@ +import {Observable, merge} from 'rxjs'; +import {share, filter, delay, map} from 'rxjs/operators'; + +export class Trigger { + constructor(public open: string, public close?: string) { + if (!close) { + this.close = open; + } + } + + isManual() { return this.open === 'manual' || this.close === 'manual'; } +} + +const DEFAULT_ALIASES = { + 'hover': ['mouseenter', 'mouseleave'], + 'focus': ['focusin', 'focusout'], +}; + +export function parseTriggers(triggers: string, aliases = DEFAULT_ALIASES): Trigger[] { + const trimmedTriggers = (triggers || '').trim(); + + if (trimmedTriggers.length === 0) { + return []; + } + + const parsedTriggers = trimmedTriggers.split(/\s+/).map(trigger => trigger.split(':')).map((triggerPair) => { + let alias = aliases[triggerPair[0]] || triggerPair; + return new Trigger(alias[0], alias[1]); + }); + + const manualTriggers = parsedTriggers.filter(triggerPair => triggerPair.isManual()); + + if (manualTriggers.length > 1) { + throw 'Triggers parse error: only one manual trigger is allowed'; + } + + if (manualTriggers.length === 1 && parsedTriggers.length > 1) { + throw 'Triggers parse error: manual trigger can\'t be mixed with other triggers'; + } + + return parsedTriggers; +} + +export function observeTriggers(renderer: any, nativeElement: any, triggers: Trigger[], isOpenedFn: () => boolean) { + return new Observable(subscriber => { + const listeners = []; + const openFn = () => subscriber.next(true); + const closeFn = () => subscriber.next(false); + const toggleFn = () => subscriber.next(!isOpenedFn()); + + triggers.forEach((trigger: Trigger) => { + if (trigger.open === trigger.close) { + listeners.push(renderer.listen(nativeElement, trigger.open, toggleFn)); + } else { + listeners.push( + renderer.listen(nativeElement, trigger.open, openFn), + renderer.listen(nativeElement, trigger.close, closeFn)); + } + }); + + return () => { listeners.forEach(unsubscribeFn => unsubscribeFn()); }; + }); +} + +const delayOrNoop = (time: number) => time > 0 ? delay(time) : (a: Observable) => a; + +export function triggerDelay(openDelay: number, closeDelay: number, isOpenedFn: () => boolean) { + return (input$: Observable) => { + let pending = null; + const filteredInput$ = input$.pipe( + map(open => ({open})), filter(event => { + const currentlyOpen = isOpenedFn(); + if (currentlyOpen !== event.open && (!pending || pending.open === currentlyOpen)) { + pending = event; + return true; + } + if (pending && pending.open !== event.open) { + pending = null; + } + return false; + }), + share()); + const delayedOpen$ = filteredInput$.pipe(filter(event => event.open), delayOrNoop(openDelay)); + const delayedClose$ = filteredInput$.pipe(filter(event => !event.open), delayOrNoop(closeDelay)); + return merge(delayedOpen$, delayedClose$) + .pipe( + filter(event => { + if (event === pending) { + pending = null; + return event.open !== isOpenedFn(); + } + return false; + }), + map(event => event.open)); + }; +} + +export function listenToTriggers( + renderer: any, nativeElement: any, triggers: string, isOpenedFn: () => boolean, openFn, closeFn, openDelay = 0, + closeDelay = 0) { + const parsedTriggers = parseTriggers(triggers); + + if (parsedTriggers.length === 1 && parsedTriggers[0].isManual()) { + return () => {}; + } + + const subscription = observeTriggers(renderer, nativeElement, parsedTriggers, isOpenedFn) + .pipe(triggerDelay(openDelay, closeDelay, isOpenedFn)) + .subscribe(open => (open ? openFn() : closeFn())); + + return () => subscription.unsubscribe(); +} diff --git a/src/util/util.spec.ts b/src/util/util.spec.ts new file mode 100644 index 0000000..6e49baa --- /dev/null +++ b/src/util/util.spec.ts @@ -0,0 +1,112 @@ +import {toInteger, toString, getValueInRange, isInteger, isString, hasClassName} from './util'; + +describe('util', () => { + + describe('toInteger', () => { + + it('should be noop for integers', () => { + expect(toInteger(0)).toBe(0); + expect(toInteger(10)).toBe(10); + }); + + it('should act as Math.floor for numbers', () => { + expect(toInteger(0.1)).toBe(0); + expect(toInteger(0.9)).toBe(0); + }); + + it('should parse strings', () => { + expect(toInteger('0')).toBe(0); + expect(toInteger('10')).toBe(10); + expect(toInteger('10.1')).toBe(10); + expect(toInteger('10.9')).toBe(10); + }); + + }); + + describe('toString', () => { + + it('should be noop for strings', () => { expect(toString('foo')).toBe('foo'); }); + + it('should return empty string for undefined values', () => { + expect(toString(null)).toBe(''); + expect(toString(undefined)).toBe(''); + }); + + it('should stringify non-string values', () => { + expect(toString(10)).toBe('10'); + expect(toString(false)).toBe('false'); + }); + + }); + + describe('getValueInRange', () => { + + it('should be noop for numbers in range', () => { expect(getValueInRange(5, 10, 0)).toBe(5); }); + + it('should do corrections in range', () => { + expect(getValueInRange(11, 10, 0)).toBe(10); + expect(getValueInRange(-1, 10, 0)).toBe(0); + }); + + it('should take 0 as a default min bound', () => { + expect(getValueInRange(11, 10)).toBe(10); + expect(getValueInRange(-1, 10)).toBe(0); + }); + + }); + + describe('isInteger', () => { + + it('should recognize integers', () => { + expect(isInteger(0)).toBeTruthy(); + expect(isInteger(10)).toBeTruthy(); + expect(isInteger(-110)).toBeTruthy(); + }); + + it('should recognize non-integers', () => { + expect(isInteger(null)).toBeFalsy(); + expect(isString([])).toBeFalsy(); + expect(isString(undefined)).toBeFalsy(); + expect(isInteger('2048')).toBeFalsy(); + expect(isInteger(14.1)).toBeFalsy(); + expect(isInteger(-14.1)).toBeFalsy(); + }); + + }); + + describe('isString', () => { + + it('should recognize strings', () => { + expect(isString('string')).toBeTruthy(); + expect(isString('')).toBeTruthy(); + }); + + it('should recognize non-strings', () => { + expect(isString(null)).toBeFalsy(); + expect(isString(2048)).toBeFalsy(); + expect(isString([])).toBeFalsy(); + expect(isString(undefined)).toBeFalsy(); + }); + + }); + + describe('hasClassName', () => { + + it('should find classes correctly', () => { + const element = {className: 'foo bar baz'}; + + expect(hasClassName(element, 'foo')).toBeTruthy(); + expect(hasClassName(element, 'bar')).toBeTruthy(); + expect(hasClassName(element, 'baz')).toBeTruthy(); + expect(hasClassName(element, 'fo')).toBeFalsy(); + expect(hasClassName(element, ' ')).toBeFalsy(); + }); + + it('should work with incorrect values', () => { + expect(hasClassName(null, 'foo')).toBeFalsy(); + expect(hasClassName({}, 'foo')).toBeFalsy(); + expect(hasClassName({className: null}, 'foo')).toBeFalsy(); + }); + }); + +}); diff --git a/src/util/util.ts b/src/util/util.ts new file mode 100644 index 0000000..eb6470d --- /dev/null +++ b/src/util/util.ts @@ -0,0 +1,75 @@ +export function toInteger(value: any): number { + return parseInt(`${value}`, 10); +} + +export function toString(value: any): string { + return (value !== undefined && value !== null) ? `${value}` : ''; +} + +export function getValueInRange(value: number, max: number, min = 0): number { + return Math.max(Math.min(value, max), min); +} + +export function isString(value: any): value is string { + return typeof value === 'string'; +} + +export function isNumber(value: any): value is number { + return !isNaN(toInteger(value)); +} + +export function isInteger(value: any): value is number { + return typeof value === 'number' && isFinite(value) && Math.floor(value) === value; +} + +export function isDefined(value: any): boolean { + return value !== undefined && value !== null; +} + +export function padNumber(value: number) { + if (isNumber(value)) { + return `0${value}`.slice(-2); + } else { + return ''; + } +} + +export function regExpEscape(text) { + return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); +} + +export function hasClassName(element: any, className: string): boolean { + return element && element.className && element.className.split && + element.className.split(/\s+/).indexOf(className) >= 0; +} + +if (typeof Element !== 'undefined' && !Element.prototype.closest) { + // Polyfill for ie10+ + + if (!Element.prototype.matches) { + // IE uses the non-standard name: msMatchesSelector + Element.prototype.matches = (Element.prototype as any).msMatchesSelector || Element.prototype.webkitMatchesSelector; + } + + Element.prototype.closest = function(s: string) { + let el = this; + if (!document.documentElement.contains(el)) { + return null; + } + do { + if (el.matches(s)) { + return el; + } + el = el.parentElement || el.parentNode; + } while (el !== null && el.nodeType === 1); + return null; + }; +} + +export function closest(element: HTMLElement, selector): HTMLElement { + if (!selector) { + return null; + } + + return element.closest(selector); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7376695 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "target": "es2015", + "lib": ["es2015", "dom"], + "module": "esnext", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "noEmitOnError": true, + "outDir": "temp", + "declaration": false, + "sourceMap": true + } +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..29e1923 --- /dev/null +++ b/tslint.json @@ -0,0 +1,109 @@ +{ + "rulesDirectory": [ + "node_modules/tslint-jasmine-rules/dist" + ], + "rules": { + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "deprecation": { + "severity": "warning" + }, + "eofline": true, + "forin": true, + "import-blacklist": [ + true, + "rxjs/Rx", + "rxjs/index", + "util" + ], + "indent": [ + true, + "spaces" + ], + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + { + "order": [ + "static-field", + "instance-field", + "static-method", + "instance-method" + ] + } + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-variable": true, + "no-empty": false, + "no-eval": true, + "no-inferrable-types": true, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unused-expression": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + "no-focused-tests": true, + "no-disabled-tests": true + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..38b5f4d --- /dev/null +++ b/yarn.lock @@ -0,0 +1,10078 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@angular-devkit/architect@0.800.6": + version "0.800.6" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.800.6.tgz#24d65f1e079f294312fe76dc11eaabf5e5d43eff" + integrity sha512-946ceRci/1yx09g8iRvULLoVihcB2RW9nhpCCMum4L9wheip8t4FWso3pd3JtPQGJV9dmsnwPzR9s12bncmj3g== + dependencies: + "@angular-devkit/core" "8.0.6" + rxjs "6.4.0" + +"@angular-devkit/architect@0.802.0": + version "0.802.0" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.802.0.tgz#5e346e5f7192a95a7270b8c713f9fe9d127f5602" + integrity sha512-Zd/ao7uE8ctV4n6drKl35cK5xrRsmgva7lsiBRc4J09vDWaRrCsxTKr6nw1gkFBDuSGZc9OmvtEFFPg2I/YHwQ== + dependencies: + "@angular-devkit/core" "8.2.0" + rxjs "6.4.0" + +"@angular-devkit/build-angular@~0.800.0": + version "0.800.6" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-angular/-/build-angular-0.800.6.tgz#96515da8cb83f28b3afae8b3fd73392f2b54925d" + integrity sha512-b6WPGN8PReRizeTe5sR3XS2sqTqfCeFIDXI4sPy3T3XdmO1dB/UP8trsHXifuNTNSVIID4X0hDwXuz36Lk+4Jw== + dependencies: + "@angular-devkit/architect" "0.800.6" + "@angular-devkit/build-optimizer" "0.800.6" + "@angular-devkit/build-webpack" "0.800.6" + "@angular-devkit/core" "8.0.6" + "@ngtools/webpack" "8.0.6" + ajv "6.10.0" + autoprefixer "9.5.1" + browserslist "4.5.5" + caniuse-lite "1.0.30000974" + circular-dependency-plugin "5.0.2" + clean-css "4.2.1" + copy-webpack-plugin "5.0.2" + core-js "3.0.1" + file-loader "3.0.1" + glob "7.1.3" + istanbul-instrumenter-loader "3.0.1" + karma-source-map-support "1.4.0" + less "3.9.0" + less-loader "4.1.0" + license-webpack-plugin "2.1.1" + loader-utils "1.2.3" + mini-css-extract-plugin "0.6.0" + minimatch "3.0.4" + open "6.2.0" + parse5 "4.0.0" + postcss "7.0.14" + postcss-import "12.0.1" + postcss-loader "3.0.0" + raw-loader "1.0.0" + rxjs "6.4.0" + sass "1.19.0" + sass-loader "7.1.0" + semver "6.0.0" + source-map-loader "0.2.4" + source-map-support "0.5.12" + speed-measure-webpack-plugin "1.3.1" + stats-webpack-plugin "0.7.0" + style-loader "0.23.1" + stylus "0.54.5" + stylus-loader "3.0.2" + terser-webpack-plugin "1.2.3" + tree-kill "1.2.1" + webpack "4.30.0" + webpack-dev-middleware "3.6.2" + webpack-dev-server "3.3.1" + webpack-merge "4.2.1" + webpack-sources "1.3.0" + webpack-subresource-integrity "1.1.0-rc.6" + worker-plugin "3.1.0" + +"@angular-devkit/build-ng-packagr@~0.800.0": + version "0.800.6" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-ng-packagr/-/build-ng-packagr-0.800.6.tgz#2d6201a129100bb66b93da0e6bd754d158dddaa6" + integrity sha512-Ahu4Q9zNtgulD9d96vpKkqg5wGpeYpnq8VBZ/wKCA1M1DLNQAdGegDMfpPErzVKYBRc85ZItVh0yffyFIe6waQ== + dependencies: + "@angular-devkit/architect" "0.800.6" + rxjs "6.4.0" + +"@angular-devkit/build-optimizer@0.800.6": + version "0.800.6" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-optimizer/-/build-optimizer-0.800.6.tgz#2a801d0bb03296cd34e866d783a86b0e6ac0250b" + integrity sha512-f8u9c5VA+bxbYREKX6EY8QsbIT8ziDRHlhJ1n6H2nUTaQi+THtbPfrDsf3S3aVACfkkY+LEGGl135XEPr5PoxA== + dependencies: + loader-utils "1.2.3" + source-map "0.5.6" + typescript "3.4.4" + webpack-sources "1.3.0" + +"@angular-devkit/build-webpack@0.800.6": + version "0.800.6" + resolved "https://registry.yarnpkg.com/@angular-devkit/build-webpack/-/build-webpack-0.800.6.tgz#74a22b4928b73e3106408977a813ad76a6354c9b" + integrity sha512-FwNGa99dxL9dACv/eLTP6u50tlPLG01yqp/JFAgxS0OmDkEMjSBLNgS8b8qhTo8XMhMsMWzb8yIUwV1PcSj6qg== + dependencies: + "@angular-devkit/architect" "0.800.6" + "@angular-devkit/core" "8.0.6" + rxjs "6.4.0" + webpack-merge "4.2.1" + +"@angular-devkit/core@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-8.0.0.tgz#a0ca65d8d0f928db9288316b1f3346d21f722213" + integrity sha512-wYf4zzpYj5Y673DG8iteK0GsDDuXBKN/TOXm4lUwmXcz8QHTD+BfR6qA5TBDqlMGpU7CP1/0vgbv2px17CDETQ== + dependencies: + ajv "6.10.0" + fast-json-stable-stringify "2.0.0" + magic-string "0.25.2" + rxjs "6.4.0" + source-map "0.7.3" + +"@angular-devkit/core@8.0.6": + version "8.0.6" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-8.0.6.tgz#540ff673f1808fc09538c8aa6c01f08750921301" + integrity sha512-gbKEVsQuYqBJPzgaxEitvs0aN9NwmUHhTkum28mRyPbS3witay/q8+3ls48M2W+98Da/PQbfndxFY4OCa+qHEA== + dependencies: + ajv "6.10.0" + fast-json-stable-stringify "2.0.0" + magic-string "0.25.2" + rxjs "6.4.0" + source-map "0.7.3" + +"@angular-devkit/core@8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-8.2.0.tgz#aaeb8d05e3a45fcff6309159fa6aa7a20665d53f" + integrity sha512-jZQn5hQ84++00+yuD/Ak303/Q06keFVyd+QbSfVrpHTFyOwPeNNSPLbN6A0S7X3bKOuoZhUHg+eQBa5BljVC2g== + dependencies: + ajv "6.10.2" + fast-json-stable-stringify "2.0.0" + magic-string "0.25.3" + rxjs "6.4.0" + source-map "0.7.3" + +"@angular-devkit/schematics@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-8.0.0.tgz#53d14646c6286b0397417990fc83e3e9a6ecf233" + integrity sha512-IXJOs/DkDqNbfG76sNNY5ePZ37rjkMUopmtvhN6/U1hQFwTpGa9N0bCHFphcKraXeS6Jfox5XwFEStc/1xyhfw== + dependencies: + "@angular-devkit/core" "8.0.0" + rxjs "6.4.0" + +"@angular-devkit/schematics@8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-8.2.0.tgz#cd7e1fc0bbe2f18f9eb86d65afd942c9b3858d2a" + integrity sha512-/XUWJijLXzhtWdjoQ5ioLo5r5V5+sJ0SSnSP0N8MQyLOgTd1FDGtBMsAMJ3n2/uwUl2/O9WTlV1xNLlg7neYVQ== + dependencies: + "@angular-devkit/core" "8.2.0" + rxjs "6.4.0" + +"@angular/animations@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-8.0.0.tgz#6286094babdb3879f7aefcd73aa31772469e50b4" + integrity sha512-hggSRi83rmocLwzrKZtmFcqPdivKSJqp2yiYaiNmJ2yQWJ1JW/Lurypv9H347RWxmwCCwC2kV8embTGbOXIFDQ== + dependencies: + tslib "^1.9.0" + +"@angular/cli@^8.0.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-8.2.0.tgz#c012c3f2474b081861960a43c3c1cf7bf8a57fa6" + integrity sha512-KtjC5Mge93YjPQXxEKnXzQ7pmryizfVunrcKHSwhnzfNdwqSjcfL2evl4oBT07b6RfT0nF8HWn0ATWpiLWwrXQ== + dependencies: + "@angular-devkit/architect" "0.802.0" + "@angular-devkit/core" "8.2.0" + "@angular-devkit/schematics" "8.2.0" + "@schematics/angular" "8.2.0" + "@schematics/update" "0.802.0" + "@yarnpkg/lockfile" "1.1.0" + ansi-colors "4.1.1" + debug "^4.1.1" + ini "1.3.5" + inquirer "6.5.0" + npm-package-arg "6.1.0" + open "6.4.0" + pacote "9.5.4" + read-package-tree "5.3.1" + semver "6.3.0" + symbol-observable "1.2.0" + universal-analytics "^0.4.20" + uuid "^3.3.2" + +"@angular/common@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-8.0.0.tgz#700aeda9be8af96692fce0ea6bf6157f7c874c0e" + integrity sha512-iOAJZ0+1zTRHnHE/5G30+4Q66W1pfZkSkxZIXvgijZ+wtuNloYdWNy/IdZ/m7ayBI7A6FsYEhyMUoWz2HVEJNw== + dependencies: + tslib "^1.9.0" + +"@angular/compiler-cli@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-8.0.0.tgz#b53ebb5accc34a68bf7a63d16130ca7c568f8a51" + integrity sha512-Z0U0Ih8A7V3J1gq7AXnXbrGAD2ERmz7JbREJJRHDWiUNxIqGQiV3Odo1V8FL5n/cKvLwSYM2Ubvk10gb0+3njA== + dependencies: + canonical-path "1.0.0" + chokidar "^2.1.1" + convert-source-map "^1.5.1" + dependency-graph "^0.7.2" + magic-string "^0.25.0" + minimist "^1.2.0" + reflect-metadata "^0.1.2" + shelljs "^0.8.1" + source-map "^0.6.1" + tslib "^1.9.0" + yargs "13.1.0" + +"@angular/compiler@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-8.0.0.tgz#302c987737e1473db3a113ff70fbbb315aa41b58" + integrity sha512-4rKsVFMNykF83tPL1VE1+j9kZ3cWHUsLOAB/VqmF64EcR/GsbjKog2v23rSso5kqUtPiVq/FWGYllW6qMdxtJA== + dependencies: + tslib "^1.9.0" + +"@angular/core@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-8.0.0.tgz#bf7a582b818e9181d830219907470e2b865ba32f" + integrity sha512-mrkP1PTzqCmZGLYll+TDyawLXHzi+FcRPqSuRxCmDMthUUE93SLXT2yISDkx9aMPtFKgFr6KfrIkKuCz16BP/g== + dependencies: + tslib "^1.9.0" + +"@angular/forms@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-8.0.0.tgz#6d636c4f83004290e1a5732a05e87148aaf6ed64" + integrity sha512-T6XdG3mALWzvnrN3fA1hAmfwvraiF1SPMWNXgPk2riuMf8CFdoro+tQZ4eo1islHrTTw5QzmqN8JJALfhAG6bg== + dependencies: + tslib "^1.9.0" + +"@angular/platform-browser-dynamic@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-8.0.0.tgz#c15f394579ff44f3752033de58edc1afa5065d59" + integrity sha512-dx7W7JoSFbsveexjZ/BPlsXbMDLWVLmRCo7IqLvibMrTbdpaaOCNJIXJk1X+f7JJrQ7SwlZaVkoLCMoDWw6fmA== + dependencies: + tslib "^1.9.0" + +"@angular/platform-browser@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-8.0.0.tgz#fc7c55a0483e67e5606e499c129fda60ae8d4363" + integrity sha512-fTD+pTMbq+On9Uv3VXiei2lfuX7GX31dngm/Y4yWTFeW6eXy0+7kkfflzpLOb0hykCZvcXzarqCuEBBYNLrrOg== + dependencies: + tslib "^1.9.0" + +"@angular/platform-server@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-8.0.0.tgz#87e80acba6b09955046dc0a9da7cd6b2e005061a" + integrity sha512-pA6m1okOfyy2qH5A6jUxrhx6z7eAG+ne7IM+j/6JUBDjp4KO9BC84aa/xfpZq5dsskl8E8II9c4hUKocMyeRjA== + dependencies: + domino "^2.1.2" + tslib "^1.9.0" + xhr2 "^0.1.4" + +"@angular/router@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-8.0.0.tgz#26094fd473e17441b0ae8af4883ec1b4ea3ad569" + integrity sha512-DGUTb8qpndE5m716xh00GxuC8o7qamlqbUruGB+SQD6ynU7s5yLGxtKffxqb1BT63+YewpsVxc2Koruvb1qjDw== + dependencies: + tslib "^1.9.0" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" + integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw== + dependencies: + "@babel/highlight" "^7.0.0" + +"@babel/generator@^7.4.0", "@babel/generator@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.5.5.tgz#873a7f936a3c89491b43536d12245b626664e3cf" + integrity sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ== + dependencies: + "@babel/types" "^7.5.5" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + trim-right "^1.0.1" + +"@babel/helper-function-name@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz#a0ceb01685f73355d4360c1247f582bfafc8ff53" + integrity sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw== + dependencies: + "@babel/helper-get-function-arity" "^7.0.0" + "@babel/template" "^7.1.0" + "@babel/types" "^7.0.0" + +"@babel/helper-get-function-arity@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz#83572d4320e2a4657263734113c42868b64e49c3" + integrity sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ== + dependencies: + "@babel/types" "^7.0.0" + +"@babel/helper-split-export-declaration@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz#ff94894a340be78f53f06af038b205c49d993677" + integrity sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q== + dependencies: + "@babel/types" "^7.4.4" + +"@babel/highlight@^7.0.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540" + integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.4.3", "@babel/parser@^7.4.4", "@babel/parser@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b" + integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g== + +"@babel/template@^7.1.0", "@babel/template@^7.4.0": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.4.4.tgz#f4b88d1225689a08f5bc3a17483545be9e4ed237" + integrity sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@babel/parser" "^7.4.4" + "@babel/types" "^7.4.4" + +"@babel/traverse@^7.4.3": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.5.5.tgz#f664f8f368ed32988cd648da9f72d5ca70f165bb" + integrity sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ== + dependencies: + "@babel/code-frame" "^7.5.5" + "@babel/generator" "^7.5.5" + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/parser" "^7.5.5" + "@babel/types" "^7.5.5" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + +"@babel/types@^7.0.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.5.5": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.5.5.tgz#97b9f728e182785909aa4ab56264f090a028d18a" + integrity sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw== + dependencies: + esutils "^2.0.2" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + +"@ngtools/webpack@8.0.6": + version "8.0.6" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-8.0.6.tgz#c6b5416710913b24b741a17c8f255e54f42c9abf" + integrity sha512-ulu+5lLt4RjmcCXbmaGCjqjuOWt18DVek/Sq4HFE9E7zP+n7HercsU6h+9PrtaZThj9NB0B7A+afRB5aAQN/bQ== + dependencies: + "@angular-devkit/core" "8.0.6" + enhanced-resolve "4.1.0" + rxjs "6.4.0" + tree-kill "1.2.1" + webpack-sources "1.3.0" + +"@nguniversal/express-engine@8.0.0-rc.1": + version "8.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@nguniversal/express-engine/-/express-engine-8.0.0-rc.1.tgz#95f05b6fdac036f0deda7813e8ee08fa4b4edfd2" + integrity sha512-WGJZTxkCMgDHK3UQHi6h7AL9Us7Vroz+pAS60lKJ0oNUCxoJ9S+i4jXfb6rtR5DtOTBqUy8O8fQ0U0HZOhhoWA== + +"@nguniversal/module-map-ngfactory-loader@8.0.0-rc.1": + version "8.0.0-rc.1" + resolved "https://registry.yarnpkg.com/@nguniversal/module-map-ngfactory-loader/-/module-map-ngfactory-loader-8.0.0-rc.1.tgz#ca82a170fe72057b2379a55147580803c951319e" + integrity sha512-dPac8uahg4XHSvrXP0/XkU/LaFhAHJ8N9h93ttXfrEXNMukarOmbyKzAuX9DVjcE6+lll1UCZtsEweRvQBZPbw== + +"@schematics/angular@8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-8.0.0.tgz#47954888fb8acbc3600235db7a46229c47fe5d9c" + integrity sha512-c/cFpe+u7Xh4xX3/kn9BSRY4YhdO0OsDbRK0pGLDJFFs5JGvwoURtNXn4/4dVlsj3PWyNhxK0Ljl3dyw3NQBHA== + dependencies: + "@angular-devkit/core" "8.0.0" + "@angular-devkit/schematics" "8.0.0" + +"@schematics/angular@8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-8.2.0.tgz#1ac24ed3708a197088119ca419e421a161ad2fd9" + integrity sha512-DOo2wtk9fk0kHCDA/I+/mRrGKirgeqVhDbgOV4d2gbYSAiTl0s1Gb4eFAkJeovQTlARfaL2PIqDDkNeYjc7xpw== + dependencies: + "@angular-devkit/core" "8.2.0" + "@angular-devkit/schematics" "8.2.0" + +"@schematics/update@0.802.0": + version "0.802.0" + resolved "https://registry.yarnpkg.com/@schematics/update/-/update-0.802.0.tgz#8ebdc4ace6372a6aed824abb78ef4b56f2bb5d72" + integrity sha512-vMcFLTuw9jSlWQq6nNgMQi2fT/wGyaucvjkxFAs7pC+lyRwYws3IkOukbET7WeJ3ix0ZBEhMbPJ8EibUNDITjw== + dependencies: + "@angular-devkit/core" "8.2.0" + "@angular-devkit/schematics" "8.2.0" + "@yarnpkg/lockfile" "1.1.0" + ini "1.3.5" + pacote "9.5.4" + rxjs "6.4.0" + semver "6.3.0" + semver-intersect "1.4.0" + +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-1.1.2.tgz#b1665e2c461a2cd92f4c1bbf50d5454de0d4b421" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + +"@types/body-parser@*": + version "1.17.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.0.tgz#9f5c9d9bd04bb54be32d5eb9fc0d8c974e6cf58c" + integrity sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.32" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28" + integrity sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg== + dependencies: + "@types/node" "*" + +"@types/estree@0.0.39": + version "0.0.39" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== + +"@types/events@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" + integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== + +"@types/express-serve-static-core@*": + version "4.16.7" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.7.tgz#50ba6f8a691c08a3dd9fa7fba25ef3133d298049" + integrity sha512-847KvL8Q1y3TtFLRTXcVakErLJQgdpFSaq+k043xefz9raEf0C7HalpSY7OW5PyjCnY8P7bPW5t/Co9qqp+USg== + dependencies: + "@types/node" "*" + "@types/range-parser" "*" + +"@types/express@^4.16.1": + version "4.17.0" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.0.tgz#49eaedb209582a86f12ed9b725160f12d04ef287" + integrity sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/serve-static" "*" + +"@types/fs-extra@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-7.0.0.tgz#9c4ad9e1339e7448a76698829def1f159c1b636c" + integrity sha512-ndoMMbGyuToTy4qB6Lex/inR98nPiNHacsgMPvy+zqMLgSxbt8VtWpDArpGp69h1fEDQHn1KB+9DWD++wgbwYA== + dependencies: + "@types/node" "*" + +"@types/glob@^7.1.1": + version "7.1.1" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" + integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w== + dependencies: + "@types/events" "*" + "@types/minimatch" "*" + "@types/node" "*" + +"@types/he@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@types/he/-/he-1.1.0.tgz#0ddc2ae80f0814f729f0f7e5aa77b191ab4a9598" + integrity sha512-HyiLOiJhclRBPzcbYrNThdi0JOdq7bT4hq9jFBPQk4HGjzkwYVQnMj9IDi7qvYkg9QTly2oZ9kjm4j7d8Ic9eA== + +"@types/jasmine@*", "@types/jasmine@~3.3.8": + version "3.3.16" + resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-3.3.16.tgz#7c84074f5d7f84da9a14f816ccfb9aeb4da13f27" + integrity sha512-Nveep4zKGby8uIvG2AEUyYOwZS8uVeHK9TgbuWYSawUDDdIgfhCKz28QzamTo//Jk7Ztt9PO3f+vzlB6a4GV1Q== + +"@types/jasminewd2@~2.0.3": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/jasminewd2/-/jasminewd2-2.0.6.tgz#2f57a8d9875a6c9ef328a14bd070ba14a055ac39" + integrity sha512-2ZOKrxb8bKRmP/po5ObYnRDgFE4i+lQiEB27bAMmtMWLgJSqlIDqlLx6S0IRorpOmOPRQ6O80NujTmQAtBkeNw== + dependencies: + "@types/jasmine" "*" + +"@types/marked@^0.6.1": + version "0.6.5" + resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.6.5.tgz#3cf2a56ef615dad24aaf99784ef90a9eba4e29d8" + integrity sha512-6kBKf64aVfx93UJrcyEZ+OBM5nGv4RLsI6sR1Ar34bpgvGVRoyTgpxn4ZmtxOM5aDTAaaznYuYUH8bUX3Nk3YA== + +"@types/mime@*": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" + integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== + +"@types/minimatch@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" + integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== + +"@types/node@*", "@types/node@^12.6.9": + version "12.7.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.0.tgz#545dde2a1a5c27d281cfb8308d6736e0708f5d6c" + integrity sha512-vqcj1MVm2Sla4PpMfYKh1MyDN4D2f/mPIZD7RdAGqEsbE+JxfeqQHHVbRDQ0Nqn8i73gJa1HQ1Pu3+nH4Q0Yiw== + +"@types/node@~10.9.0": + version "10.9.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.9.4.tgz#0f4cb2dc7c1de6096055357f70179043c33e9897" + integrity sha512-fCHV45gS+m3hH17zgkgADUSi2RR1Vht6wOZ0jyHP8rjiQra9f+mIcgwPQHllmDocYOstIEbKlxbFDYlgrTPYqw== + +"@types/normalize-package-data@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" + integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== + +"@types/prismjs@1.16.0": + version "1.16.0" + resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.16.0.tgz#4328c9f65698e59f4feade8f4e5d928c748fd643" + integrity sha512-mEyuziLrfDCQ4juQP1k706BUU/c8OGn/ZFl69AXXY6dStHClKX4P+N8+rhqpul1vRDA2VOygzMRSJJZHyDEOfw== + +"@types/q@^0.0.32": + version "0.0.32" + resolved "https://registry.yarnpkg.com/@types/q/-/q-0.0.32.tgz#bd284e57c84f1325da702babfc82a5328190c0c5" + integrity sha1-vShOV8hPEyXacCur/IKlMoGQwMU= + +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + +"@types/resolve@0.0.8": + version "0.0.8" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-0.0.8.tgz#f26074d238e02659e323ce1a13d041eee280e194" + integrity sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ== + dependencies: + "@types/node" "*" + +"@types/selenium-webdriver@^3.0.0": + version "3.0.16" + resolved "https://registry.yarnpkg.com/@types/selenium-webdriver/-/selenium-webdriver-3.0.16.tgz#50a4755f8e33edacd9c406729e9b930d2451902a" + integrity sha512-lMC2G0ItF2xv4UCiwbJGbnJlIuUixHrioOhNGHSCsYCJ8l4t9hMCUimCytvFv7qy6AfSzRxhRHoGa+UqaqwyeA== + +"@types/serve-static@*": + version "1.13.2" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.2.tgz#f5ac4d7a6420a99a6a45af4719f4dcd8cd907a48" + integrity sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q== + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + +"@types/source-list-map@*": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" + integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== + +"@types/strip-bom@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" + integrity sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I= + +"@types/strip-json-comments@0.0.30": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" + integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== + +"@types/webpack-sources@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.5.tgz#be47c10f783d3d6efe1471ff7f042611bd464a92" + integrity sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w== + dependencies: + "@types/node" "*" + "@types/source-list-map" "*" + source-map "^0.6.1" + +"@webassemblyjs/ast@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.8.5.tgz#51b1c5fe6576a34953bf4b253df9f0d490d9e359" + integrity sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ== + dependencies: + "@webassemblyjs/helper-module-context" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/wast-parser" "1.8.5" + +"@webassemblyjs/floating-point-hex-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz#1ba926a2923613edce496fd5b02e8ce8a5f49721" + integrity sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ== + +"@webassemblyjs/helper-api-error@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz#c49dad22f645227c5edb610bdb9697f1aab721f7" + integrity sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA== + +"@webassemblyjs/helper-buffer@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz#fea93e429863dd5e4338555f42292385a653f204" + integrity sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q== + +"@webassemblyjs/helper-code-frame@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz#9a740ff48e3faa3022b1dff54423df9aa293c25e" + integrity sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ== + dependencies: + "@webassemblyjs/wast-printer" "1.8.5" + +"@webassemblyjs/helper-fsm@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz#ba0b7d3b3f7e4733da6059c9332275d860702452" + integrity sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow== + +"@webassemblyjs/helper-module-context@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz#def4b9927b0101dc8cbbd8d1edb5b7b9c82eb245" + integrity sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g== + dependencies: + "@webassemblyjs/ast" "1.8.5" + mamacro "^0.0.3" + +"@webassemblyjs/helper-wasm-bytecode@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz#537a750eddf5c1e932f3744206551c91c1b93e61" + integrity sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ== + +"@webassemblyjs/helper-wasm-section@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz#74ca6a6bcbe19e50a3b6b462847e69503e6bfcbf" + integrity sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" + +"@webassemblyjs/ieee754@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz#712329dbef240f36bf57bd2f7b8fb9bf4154421e" + integrity sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.8.5.tgz#044edeb34ea679f3e04cd4fd9824d5e35767ae10" + integrity sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.8.5.tgz#a8bf3b5d8ffe986c7c1e373ccbdc2a0915f0cedc" + integrity sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw== + +"@webassemblyjs/wasm-edit@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz#962da12aa5acc1c131c81c4232991c82ce56e01a" + integrity sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/helper-wasm-section" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" + "@webassemblyjs/wasm-opt" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" + "@webassemblyjs/wast-printer" "1.8.5" + +"@webassemblyjs/wasm-gen@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz#54840766c2c1002eb64ed1abe720aded714f98bc" + integrity sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/ieee754" "1.8.5" + "@webassemblyjs/leb128" "1.8.5" + "@webassemblyjs/utf8" "1.8.5" + +"@webassemblyjs/wasm-opt@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz#b24d9f6ba50394af1349f510afa8ffcb8a63d264" + integrity sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-buffer" "1.8.5" + "@webassemblyjs/wasm-gen" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" + +"@webassemblyjs/wasm-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz#21576f0ec88b91427357b8536383668ef7c66b8d" + integrity sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-api-error" "1.8.5" + "@webassemblyjs/helper-wasm-bytecode" "1.8.5" + "@webassemblyjs/ieee754" "1.8.5" + "@webassemblyjs/leb128" "1.8.5" + "@webassemblyjs/utf8" "1.8.5" + +"@webassemblyjs/wast-parser@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz#e10eecd542d0e7bd394f6827c49f3df6d4eefb8c" + integrity sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/floating-point-hex-parser" "1.8.5" + "@webassemblyjs/helper-api-error" "1.8.5" + "@webassemblyjs/helper-code-frame" "1.8.5" + "@webassemblyjs/helper-fsm" "1.8.5" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/wast-printer@1.8.5": + version "1.8.5" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz#114bbc481fd10ca0e23b3560fa812748b0bae5bc" + integrity sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/wast-parser" "1.8.5" + "@xtuc/long" "4.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +"@yarnpkg/lockfile@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" + integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== + +JSONStream@^1.0.4, JSONStream@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" + integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +acorn-dynamic-import@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz#482210140582a36b83c3e342e1cfebcaa9240948" + integrity sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw== + +acorn@^6.0.5, acorn@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.2.1.tgz#3ed8422d6dec09e6121cc7a843ca86a330a86b51" + integrity sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q== + +add-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" + integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo= + +adm-zip@^0.4.9, adm-zip@~0.4.3: + version "0.4.13" + resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.13.tgz#597e2f8cc3672151e1307d3e95cddbc75672314a" + integrity sha512-fERNJX8sOXfel6qCBCMPvZLzENBEhZTzKqg6vrOW5pvoEaQuJhRU4ndTAh6lHOxn1I6jnz2NHra56ZODM751uw== + +after@0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" + integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= + +agent-base@4, agent-base@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" + integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== + dependencies: + es6-promisify "^5.0.0" + +agent-base@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" + integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== + dependencies: + es6-promisify "^5.0.0" + +agentkeepalive@^3.4.1: + version "3.5.2" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67" + integrity sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ== + dependencies: + humanize-ms "^1.2.1" + +ajv-errors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ajv-errors/-/ajv-errors-1.0.1.tgz#f35986aceb91afadec4102fbd85014950cefa64d" + integrity sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ== + +ajv-keywords@^3.1.0, ajv-keywords@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.4.1.tgz#ef916e271c64ac12171fd8384eaae6b2345854da" + integrity sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ== + +ajv@6.10.0: + version "6.10.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" + integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@6.10.2, ajv@^6.1.0, ajv@^6.10.2, ajv@^6.5.5: + version "6.10.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" + integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^5.0.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + integrity sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU= + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +amdefine@>=0.0.4: + version "1.0.1" + resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" + integrity sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU= + +ansi-align@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb" + integrity sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw== + dependencies: + string-width "^3.0.0" + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-colors@^3.0.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" + integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== + +ansi-escapes@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== + +ansi-gray@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-gray/-/ansi-gray-0.1.1.tgz#2962cf54ec9792c48510a3deb524436861ef7251" + integrity sha1-KWLPVOyXksSFEKPetSRDaGHvclE= + dependencies: + ansi-wrap "0.1.0" + +ansi-html@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" + integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4= + +ansi-regex@^2.0.0, ansi-regex@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-wrap@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" + integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +anymatch@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.0.3.tgz#2fb624fe0e84bccab00afee3d0006ed310f22f09" + integrity sha512-c6IvoeBECQlMVuYUjSwimnhmztImpErfxJzWZhIQinIvQWoGOnB0dLIgifbPHQt5heS6mNlaZG16f06H3C8t1g== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +append-transform@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" + integrity sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw== + dependencies: + default-require-extensions "^2.0.0" + +aproba@^1.0.3, aproba@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +archy@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" + integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +arg@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.1.tgz#485f8e7c390ce4c5f78257dbea80d4be11feda4c" + integrity sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-differ@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" + integrity sha1-7/UuN1gknTO+QCuLuOVkuytdQDE= + +array-each@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f" + integrity sha1-p5SvDAWrF1KEbudTofIRoFugxE8= + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +array-flatten@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + +array-ify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" + integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= + +array-slice@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4" + integrity sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w== + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1, array-uniq@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +arraybuffer.slice@~0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" + integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== + +arrify@^1.0.0, arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + +asap@^2.0.0, asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +asn1.js@^4.0.0: + version "4.10.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" + integrity sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assert@^1.1.1: + version "1.5.0" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb" + integrity sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA== + dependencies: + object-assign "^4.1.1" + util "0.10.3" + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +async-each@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== + +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +async@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" + integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= + +async@^2.1.2, async@^2.5.0, async@^2.6.1, async@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" + integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + dependencies: + lodash "^4.17.14" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +atob@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +autoprefixer@9.5.1: + version "9.5.1" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.5.1.tgz#243b1267b67e7e947f28919d786b50d3bb0fb357" + integrity sha512-KJSzkStUl3wP0D5sdMlP82Q52JLy5+atf2MHAre48+ckWkXgixmfHyWmA77wFDy6jTHU6mIgXv6hAQ2mf1PjJQ== + dependencies: + browserslist "^4.5.4" + caniuse-lite "^1.0.30000957" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^7.0.14" + postcss-value-parser "^3.3.1" + +autoprefixer@^9.6.0: + version "9.6.1" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.6.1.tgz#51967a02d2d2300bb01866c1611ec8348d355a47" + integrity sha512-aVo5WxR3VyvyJxcJC3h4FKfwCQvQWb1tSI5VHNibddCVWrcD1NvlxEweg3TSgiPztMnWfjpy2FURKA2kvDE+Tw== + dependencies: + browserslist "^4.6.3" + caniuse-lite "^1.0.30000980" + chalk "^2.4.2" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^7.0.17" + postcss-value-parser "^4.0.0" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== + +babel-code-frame@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-generator@^6.18.0: + version "6.26.1" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" + integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA== + dependencies: + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.17.4" + source-map "^0.5.7" + trim-right "^1.0.1" + +babel-messages@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" + integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4= + dependencies: + babel-runtime "^6.22.0" + +babel-runtime@^6.22.0, babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +babel-template@^6.16.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" + integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= + dependencies: + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" + +babel-traverse@^6.18.0, babel-traverse@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" + integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= + dependencies: + babel-code-frame "^6.26.0" + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" + +babel-types@^6.18.0, babel-types@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + +babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== + +backo2@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base64-arraybuffer@0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" + integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= + +base64-js@^1.0.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + +base64id@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-1.0.0.tgz#47688cb99bb6804f0e06d3e763b1c32e57d8e6b6" + integrity sha1-R2iMuZu2gE8OBtPnY7HDLlfY5rY= + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +beeper@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809" + integrity sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak= + +better-assert@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" + integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI= + dependencies: + callsite "1.0.0" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^1.0.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" + integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== + +binary-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" + integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== + +blob@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" + integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== + +blocking-proxy@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/blocking-proxy/-/blocking-proxy-1.0.1.tgz#81d6fd1fe13a4c0d6957df7f91b75e98dac40cb2" + integrity sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA== + dependencies: + minimist "^1.2.0" + +bluebird@^3.3.0, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5: + version "3.5.5" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f" + integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w== + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: + version "4.11.8" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" + integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== + +body-parser@1.19.0, body-parser@^1.16.1: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5" + integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + +bootstrap@4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.3.1.tgz#280ca8f610504d99d7b6b4bfc4b68cec601704ac" + integrity sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag== + +boxen@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-3.2.0.tgz#fbdff0de93636ab4450886b6ff45b92d098f45eb" + integrity sha512-cU4J/+NodM3IHdSL2yN8bqYqnmlBTidDR4RC7nJs61ZmtGz8VZzM3HLQX0zY5mrSmPtR3xWwsq2jOUQqFZN8+A== + dependencies: + ansi-align "^3.0.0" + camelcase "^5.3.1" + chalk "^2.4.2" + cli-boxes "^2.2.0" + string-width "^3.0.0" + term-size "^1.2.0" + type-fest "^0.3.0" + widest-line "^2.0.0" + +brace-expansion@^1.0.0, brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.1, braces@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +braces@^3.0.1, braces@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" + integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" + integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +browserify-rsa@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" + integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= + dependencies: + bn.js "^4.1.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" + integrity sha1-qk62jl17ZYuqa/alfmMMvXqT0pg= + dependencies: + bn.js "^4.1.1" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.2" + elliptic "^6.0.0" + inherits "^2.0.1" + parse-asn1 "^5.0.0" + +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== + dependencies: + pako "~1.0.5" + +browserslist@4.5.5: + version "4.5.5" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.5.5.tgz#fe1a352330d2490d5735574c149a85bc18ef9b82" + integrity sha512-0QFO1r/2c792Ohkit5XI8Cm8pDtZxgNl2H6HU4mHrpYz7314pEYcsAVVatM0l/YmxPnEzh9VygXouj4gkFUTKA== + dependencies: + caniuse-lite "^1.0.30000960" + electron-to-chromium "^1.3.124" + node-releases "^1.1.14" + +browserslist@^4.0.0, browserslist@^4.5.4, browserslist@^4.6.3: + version "4.6.6" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.6.6.tgz#6e4bf467cde520bc9dbdf3747dafa03531cec453" + integrity sha512-D2Nk3W9JL9Fp/gIcWei8LrERCS+eXu9AM5cfXA8WEZ84lFks+ARnZ0q/R69m2SV3Wjma83QDDPxsNKXUwdIsyA== + dependencies: + caniuse-lite "^1.0.30000984" + electron-to-chromium "^1.3.191" + node-releases "^1.1.25" + +browserstack@^1.5.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/browserstack/-/browserstack-1.5.2.tgz#17d8bb76127a1cc0ea416424df80d218f803673f" + integrity sha512-+6AFt9HzhKykcPF79W6yjEUJcdvZOV0lIXdkORXMJftGrDl0OKWqRF4GHqpDNkxiceDT/uB7Fb/aDwktvXX7dg== + dependencies: + https-proxy-agent "^2.2.1" + +buffer-alloc-unsafe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" + integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== + +buffer-alloc@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" + integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== + dependencies: + buffer-alloc-unsafe "^1.1.0" + buffer-fill "^1.0.0" + +buffer-fill@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" + integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +buffer-indexof@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" + integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + integrity sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk= + +buffer@^4.3.0: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg= + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-modules@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= + +builtin-modules@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.1.0.tgz#aad97c15131eb76b65b50ef208e7584cd76a7484" + integrity sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw== + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug= + +builtins@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" + integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +cacache@^11.0.2, cacache@^11.3.1: + version "11.3.3" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-11.3.3.tgz#8bd29df8c6a718a6ebd2d010da4d7972ae3bbadc" + integrity sha512-p8WcneCytvzPxhDvYp31PD039vi77I12W+/KfR9S8AZbaiARFBCpsPJS+9uhWfeBfeAtW7o/4vt3MUqLkbY6nA== + dependencies: + bluebird "^3.5.5" + chownr "^1.1.1" + figgy-pudding "^3.5.1" + glob "^7.1.4" + graceful-fs "^4.1.15" + lru-cache "^5.1.1" + mississippi "^3.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.3" + ssri "^6.0.1" + unique-filename "^1.1.1" + y18n "^4.0.0" + +cacache@^12.0.0, cacache@^12.0.2: + version "12.0.2" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-12.0.2.tgz#8db03205e36089a3df6954c66ce92541441ac46c" + integrity sha512-ifKgxH2CKhJEg6tNdAwziu6Q33EvuG26tYcda6PT3WKisZcYDXsnEdnRv67Po3yCzFfaSoMjGZzJyD2c3DT1dg== + dependencies: + bluebird "^3.5.5" + chownr "^1.1.1" + figgy-pudding "^3.5.1" + glob "^7.1.4" + graceful-fs "^4.1.15" + infer-owner "^1.0.3" + lru-cache "^5.1.1" + mississippi "^3.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.3" + ssri "^6.0.1" + unique-filename "^1.1.1" + y18n "^4.0.0" + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-6.1.0.tgz#20ffb8bd162ba4be11e9567d823db651052ca912" + integrity sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg== + dependencies: + clone-response "^1.0.2" + get-stream "^5.1.0" + http-cache-semantics "^4.0.0" + keyv "^3.0.0" + lowercase-keys "^2.0.0" + normalize-url "^4.1.0" + responselike "^1.0.2" + +caching-transform@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/caching-transform/-/caching-transform-3.0.2.tgz#601d46b91eca87687a281e71cef99791b0efca70" + integrity sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w== + dependencies: + hasha "^3.0.0" + make-dir "^2.0.0" + package-hash "^3.0.0" + write-file-atomic "^2.4.2" + +caller-callsite@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" + integrity sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ= + dependencies: + callsites "^2.0.0" + +caller-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-2.0.0.tgz#468f83044e369ab2010fac5f06ceee15bb2cb1f4" + integrity sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ= + dependencies: + caller-callsite "^2.0.0" + +callsite@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" + integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= + +callsites@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase-keys@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77" + integrity sha1-oqpfsa9oh1glnDLBQUJteJI7m3c= + dependencies: + camelcase "^4.1.0" + map-obj "^2.0.0" + quick-lru "^1.0.0" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= + +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +caniuse-lite@1.0.30000974: + version "1.0.30000974" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000974.tgz#b7afe14ee004e97ce6dc73e3f878290a12928ad8" + integrity sha512-xc3rkNS/Zc3CmpMKuczWEdY2sZgx09BkAxfvkxlAEBTqcMHeL8QnPqhKse+5sRTi3nrw2pJwToD2WvKn1Uhvww== + +caniuse-lite@^1.0.30000957, caniuse-lite@^1.0.30000960, caniuse-lite@^1.0.30000980, caniuse-lite@^1.0.30000984: + version "1.0.30000989" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000989.tgz#b9193e293ccf7e4426c5245134b8f2a56c0ac4b9" + integrity sha512-vrMcvSuMz16YY6GSVZ0dWDTJP8jqk3iFQ/Aq5iqblPwxSVVZI+zxDyTX0VPqtQsDnfdrBDcsmhgTEOh5R8Lbpw== + +canonical-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/canonical-path/-/canonical-path-1.0.0.tgz#fcb470c23958def85081856be7a86e904f180d1d" + integrity sha512-feylzsbDxi1gPZ1IjystzIQZagYYLvfKrSuygUCgf7z6x790VEzze5QEkdSV1U58RA7Hi0+v6fv4K54atOzATg== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +"chokidar@>=2.0.0 <4.0.0", chokidar@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.0.2.tgz#0d1cd6d04eb2df0327446188cd13736a3367d681" + integrity sha512-c4PR2egjNjI1um6bamCQ6bUNPDiyofNQruHvKgHQ4gDUP/ITSVSzNsiI5OWtHOsX323i5ha/kk4YmOZ1Ktg7KA== + dependencies: + anymatch "^3.0.1" + braces "^3.0.2" + glob-parent "^5.0.0" + is-binary-path "^2.1.0" + is-glob "^4.0.1" + normalize-path "^3.0.0" + readdirp "^3.1.1" + optionalDependencies: + fsevents "^2.0.6" + +chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.3, chokidar@^2.1.1, chokidar@^2.1.5, chokidar@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.6.tgz#b6cad653a929e244ce8a834244164d241fa954c5" + integrity sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g== + dependencies: + anymatch "^2.0.0" + async-each "^1.0.1" + braces "^2.3.2" + glob-parent "^3.1.0" + inherits "^2.0.3" + is-binary-path "^1.0.0" + is-glob "^4.0.0" + normalize-path "^3.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.2.1" + upath "^1.1.1" + optionalDependencies: + fsevents "^1.2.7" + +chownr@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.2.tgz#a18f1e0b269c8a6a5d3c86eb298beb14c3dd7bf6" + integrity sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A== + +chrome-trace-event@^1.0.0, chrome-trace-event@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz#234090ee97c7d4ad1a2c4beae27505deffc608a4" + integrity sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ== + dependencies: + tslib "^1.9.0" + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +circular-dependency-plugin@5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/circular-dependency-plugin/-/circular-dependency-plugin-5.0.2.tgz#da168c0b37e7b43563fb9f912c1c007c213389ef" + integrity sha512-oC7/DVAyfcY3UWKm0sN/oVoDedQDQiw/vIiAnuTWTpE5s0zWf7l3WY417Xw/Fbi/QbAjctAkxgMiS9P0s3zkmA== + +clang-format@1.0.35: + version "1.0.35" + resolved "https://registry.yarnpkg.com/clang-format/-/clang-format-1.0.35.tgz#611c719ac4bb4db9b6a3bedb6d64d4aba02f1113" + integrity sha1-YRxxmsS7Tbm2o77bbWTUq6AvERM= + dependencies: + async "^1.5.2" + glob "^7.0.0" + resolve "^1.1.6" + +clang-format@^1.0.32: + version "1.2.4" + resolved "https://registry.yarnpkg.com/clang-format/-/clang-format-1.2.4.tgz#4bb4b0a98180428deb093cf20982e9fc1af20b6c" + integrity sha512-sw+nrGUp3hvmANd1qF8vZPuezSYQAiXgGBiEtkXTtJnnu6b00fCqkkDIsnRKrNgg4nv6NYZE92ejvOMIXZoejw== + dependencies: + async "^1.5.2" + glob "^7.0.0" + resolve "^1.1.6" + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +clean-css@4.2.1, clean-css@^4.1.11: + version "4.2.1" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17" + integrity sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g== + dependencies: + source-map "~0.6.0" + +cli-boxes@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.0.tgz#538ecae8f9c6ca508e3c3c95b453fe93cb4c168d" + integrity sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w== + +cli-color@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/cli-color/-/cli-color-1.4.0.tgz#7d10738f48526824f8fe7da51857cb0f572fe01f" + integrity sha512-xu6RvQqqrWEo6MPR1eixqGPywhYBHRs653F9jfXB2Hx4jdM/3WxiNE1vppRmxtMIfl16SFYTpYlrnqH/HsK/2w== + dependencies: + ansi-regex "^2.1.1" + d "1" + es5-ext "^0.10.46" + es6-iterator "^2.0.3" + memoizee "^0.4.14" + timers-ext "^0.1.5" + +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= + dependencies: + restore-cursor "^2.0.0" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= + +clipboard@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.4.tgz#836dafd66cf0fea5d71ce5d5b0bf6e958009112d" + integrity sha512-Vw26VSLRpJfBofiVaFb/I8PVfdI1OxKcYShe6fm0sP/DtmiWQNCjhM/okTvdCo0G+lMMm1rMYbk4IK4x1X+kgQ== + dependencies: + good-listener "^1.2.2" + select "^1.1.2" + tiny-emitter "^2.0.0" + +cliui@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" + integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== + dependencies: + string-width "^2.1.1" + strip-ansi "^4.0.0" + wrap-ansi "^2.0.0" + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +clone-deep@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713" + integrity sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ== + dependencies: + for-own "^1.0.0" + is-plain-object "^2.0.4" + kind-of "^6.0.0" + shallow-clone "^1.0.0" + +clone-response@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" + integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= + dependencies: + mimic-response "^1.0.0" + +clone-stats@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" + integrity sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE= + +clone@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/clone/-/clone-0.2.0.tgz#c6126a90ad4f72dbf5acdb243cc37724fe93fc1f" + integrity sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8= + +clone@^1.0.0, clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= + +clone@^2.1.1, clone@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +colors@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" + integrity sha1-FopHAXVran9RoSzgyXv6KMCE7WM= + +colors@^1.1.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d" + integrity sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg== + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.12.0, commander@^2.12.1, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@~2.20.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" + integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + +compare-func@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-1.3.2.tgz#99dd0ba457e1f9bc722b12c08ec33eeab31fa648" + integrity sha1-md0LpFfh+bxyKxLAjsM+6rMfpkg= + dependencies: + array-ify "^1.0.0" + dot-prop "^3.0.0" + +compare-versions@^3.4.0: + version "3.5.1" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.5.1.tgz#26e1f5cf0d48a77eced5046b9f67b6b61075a393" + integrity sha512-9fGPIB7C6AyM18CJJBHt5EnCZDG3oiTJYy0NjfIAGjKpzv0tkxWko7TNQHF5ymqm7IH03tqmeuBxtvD+Izh6mg== + +component-bind@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" + integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E= + +component-emitter@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= + +component-emitter@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +component-inherit@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" + integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= + +compressible@~2.0.16: + version "2.0.17" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.17.tgz#6e8c108a16ad58384a977f3a482ca20bff2f38c1" + integrity sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw== + dependencies: + mime-db ">= 1.40.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +concat-stream@^1.5.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +concurrently@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-4.1.1.tgz#42cf84d625163f3f5b2e2262568211ad76e1dbe8" + integrity sha512-48+FE5RJ0qc8azwKv4keVQWlni1hZeSjcWr8shBelOBtBHcKj1aJFM9lHRiSc1x7lq416pkvsqfBMhSRja+Lhw== + dependencies: + chalk "^2.4.1" + date-fns "^1.23.0" + lodash "^4.17.10" + read-pkg "^4.0.1" + rxjs "^6.3.3" + spawn-command "^0.0.2-1" + supports-color "^4.5.0" + tree-kill "^1.1.0" + yargs "^12.0.1" + +configstore@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-4.0.0.tgz#5933311e95d3687efb592c528b922d9262d227e7" + integrity sha512-CmquAXFBocrzaSM8mtGPMM/HiWmyIpr4CcJl/rgY2uCObZ/S7cKU0silxslqJejl+t/T9HS8E0PUNQD81JGUEQ== + dependencies: + dot-prop "^4.1.0" + graceful-fs "^4.1.2" + make-dir "^1.0.0" + unique-string "^1.0.0" + write-file-atomic "^2.0.0" + xdg-basedir "^3.0.0" + +connect-history-api-fallback@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz#8b32089359308d111115d81cad3fceab888f97bc" + integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== + +connect@^3.6.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/connect/-/connect-3.7.0.tgz#5d49348910caa5e07a01800b030d0c35f20484f8" + integrity sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ== + dependencies: + debug "2.6.9" + finalhandler "1.1.2" + parseurl "~1.3.3" + utils-merge "1.0.1" + +console-browserify@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + integrity sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA= + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +conventional-changelog-angular@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.3.tgz#299fdd43df5a1f095283ac16aeedfb0a682ecab0" + integrity sha512-YD1xzH7r9yXQte/HF9JBuEDfvjxxwDGGwZU1+ndanbY0oFgA+Po1T9JDSpPLdP0pZT6MhCAsdvFKC4TJ4MTJTA== + dependencies: + compare-func "^1.3.1" + q "^1.5.1" + +conventional-changelog-atom@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/conventional-changelog-atom/-/conventional-changelog-atom-2.0.1.tgz#dc88ce650ffa9ceace805cbe70f88bfd0cb2c13a" + integrity sha512-9BniJa4gLwL20Sm7HWSNXd0gd9c5qo49gCi8nylLFpqAHhkFTj7NQfROq3f1VpffRtzfTQp4VKU5nxbe2v+eZQ== + dependencies: + q "^1.5.1" + +conventional-changelog-cli@^2.0.12: + version "2.0.23" + resolved "https://registry.yarnpkg.com/conventional-changelog-cli/-/conventional-changelog-cli-2.0.23.tgz#3f6b2bb3e1e6a6f520f7fa514fe8fba2d92faab0" + integrity sha512-a/jDZHEUpSHQMAqeDrmrFhz9CKHBKhBGpJyc38BCfNjFA1RKchpq/Qqbo1BZwRLWrW/PX7IGsUicTyhniqUH9g== + dependencies: + add-stream "^1.0.0" + conventional-changelog "^3.1.10" + lodash "^4.14.14" + meow "^4.0.0" + tempfile "^1.1.1" + +conventional-changelog-codemirror@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/conventional-changelog-codemirror/-/conventional-changelog-codemirror-2.0.1.tgz#acc046bc0971460939a0cc2d390e5eafc5eb30da" + integrity sha512-23kT5IZWa+oNoUaDUzVXMYn60MCdOygTA2I+UjnOMiYVhZgmVwNd6ri/yDlmQGXHqbKhNR5NoXdBzSOSGxsgIQ== + dependencies: + q "^1.5.1" + +conventional-changelog-conventionalcommits@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.1.0.tgz#eb7d47a9c5f1a6f9846a649482294e4ac50d7683" + integrity sha512-J3xolGrH8PTxpCqueHOuZtv3Cp73SQOWiBQzlsaugZAZ+hZgcJBonmC+1bQbfGs2neC2S18p2L1Gx+nTEglJTQ== + dependencies: + compare-func "^1.3.1" + q "^1.5.1" + +conventional-changelog-core@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-4.0.0.tgz#a9e83889e43a21b05fa098a507cad475905a0439" + integrity sha512-+bZMeBUdjKxfyX2w6EST9U7zb85wxrGS3IV4H7SqPya44osNQbm3P+vyqfLs6s57FkoEamC93ioDEiguVLWmSQ== + dependencies: + conventional-changelog-writer "^4.0.7" + conventional-commits-parser "^3.0.3" + dateformat "^3.0.0" + get-pkg-repo "^1.0.0" + git-raw-commits "2.0.0" + git-remote-origin-url "^2.0.0" + git-semver-tags "^3.0.0" + lodash "^4.2.1" + normalize-package-data "^2.3.5" + q "^1.5.1" + read-pkg "^3.0.0" + read-pkg-up "^3.0.0" + through2 "^3.0.0" + +conventional-changelog-ember@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/conventional-changelog-ember/-/conventional-changelog-ember-2.0.2.tgz#284ffdea8c83ea8c210b65c5b4eb3e5cc0f4f51a" + integrity sha512-qtZbA3XefO/n6DDmkYywDYi6wDKNNc98MMl2F9PKSaheJ25Trpi3336W8fDlBhq0X+EJRuseceAdKLEMmuX2tg== + dependencies: + q "^1.5.1" + +conventional-changelog-eslint@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/conventional-changelog-eslint/-/conventional-changelog-eslint-3.0.2.tgz#e9eb088cda6be3e58b2de6a5aac63df0277f3cbe" + integrity sha512-Yi7tOnxjZLXlCYBHArbIAm8vZ68QUSygFS7PgumPRiEk+9NPUeucy5Wg9AAyKoBprSV3o6P7Oghh4IZSLtKCvQ== + dependencies: + q "^1.5.1" + +conventional-changelog-express@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/conventional-changelog-express/-/conventional-changelog-express-2.0.1.tgz#fea2231d99a5381b4e6badb0c1c40a41fcacb755" + integrity sha512-G6uCuCaQhLxdb4eEfAIHpcfcJ2+ao3hJkbLrw/jSK/eROeNfnxCJasaWdDAfFkxsbpzvQT4W01iSynU3OoPLIw== + dependencies: + q "^1.5.1" + +conventional-changelog-jquery@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/conventional-changelog-jquery/-/conventional-changelog-jquery-3.0.4.tgz#7eb598467b83db96742178e1e8d68598bffcd7ae" + integrity sha512-IVJGI3MseYoY6eybknnTf9WzeQIKZv7aNTm2KQsiFVJH21bfP2q7XVjfoMibdCg95GmgeFlaygMdeoDDa+ZbEQ== + dependencies: + q "^1.5.1" + +conventional-changelog-jshint@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/conventional-changelog-jshint/-/conventional-changelog-jshint-2.0.1.tgz#11c0e8283abf156a4ff78e89be6fdedf9bd72202" + integrity sha512-kRFJsCOZzPFm2tzRHULWP4tauGMvccOlXYf3zGeuSW4U0mZhk5NsjnRZ7xFWrTFPlCLV+PNmHMuXp5atdoZmEg== + dependencies: + compare-func "^1.3.1" + q "^1.5.1" + +conventional-changelog-preset-loader@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.2.0.tgz#571e2b3d7b53d65587bea9eedf6e37faa5db4fcc" + integrity sha512-zXB+5vF7D5Y3Cb/rJfSyCCvFphCVmF8mFqOdncX3BmjZwAtGAPfYrBcT225udilCKvBbHgyzgxqz2GWDB5xShQ== + +conventional-changelog-writer@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-4.0.7.tgz#e4b7d9cbea902394ad671f67108a71fa90c7095f" + integrity sha512-p/wzs9eYaxhFbrmX/mCJNwJuvvHR+j4Fd0SQa2xyAhYed6KBiZ780LvoqUUvsayP4R1DtC27czalGUhKV2oabw== + dependencies: + compare-func "^1.3.1" + conventional-commits-filter "^2.0.2" + dateformat "^3.0.0" + handlebars "^4.1.2" + json-stringify-safe "^5.0.1" + lodash "^4.2.1" + meow "^4.0.0" + semver "^6.0.0" + split "^1.0.0" + through2 "^3.0.0" + +conventional-changelog@^3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/conventional-changelog/-/conventional-changelog-3.1.10.tgz#889a8daa4b7673a1dc1605746f9ae66546b373c1" + integrity sha512-6RDj31hL39HUkpqvPjRlOxAwJRwur8O2qu9m6R0FBNDGwCJyy4SYH9NfyshozxYSeklrauKRf3oSbyoEZVzu9Q== + dependencies: + conventional-changelog-angular "^5.0.3" + conventional-changelog-atom "^2.0.1" + conventional-changelog-codemirror "^2.0.1" + conventional-changelog-conventionalcommits "^4.1.0" + conventional-changelog-core "^4.0.0" + conventional-changelog-ember "^2.0.2" + conventional-changelog-eslint "^3.0.2" + conventional-changelog-express "^2.0.1" + conventional-changelog-jquery "^3.0.4" + conventional-changelog-jshint "^2.0.1" + conventional-changelog-preset-loader "^2.2.0" + +conventional-commits-filter@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/conventional-commits-filter/-/conventional-commits-filter-2.0.2.tgz#f122f89fbcd5bb81e2af2fcac0254d062d1039c1" + integrity sha512-WpGKsMeXfs21m1zIw4s9H5sys2+9JccTzpN6toXtxhpw2VNF2JUXwIakthKBy+LN4DvJm+TzWhxOMWOs1OFCFQ== + dependencies: + lodash.ismatch "^4.4.0" + modify-values "^1.0.0" + +conventional-commits-parser@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.0.3.tgz#c3f972fd4e056aa8b9b4f5f3d0e540da18bf396d" + integrity sha512-KaA/2EeUkO4bKjinNfGUyqPTX/6w9JGshuQRik4r/wJz7rUw3+D3fDG6sZSEqJvKILzKXFQuFkpPLclcsAuZcg== + dependencies: + JSONStream "^1.0.4" + is-text-path "^2.0.0" + lodash "^4.2.1" + meow "^4.0.0" + split2 "^2.0.0" + through2 "^3.0.0" + trim-off-newlines "^1.0.0" + +convert-source-map@^1.5.0, convert-source-map@^1.5.1, convert-source-map@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" + integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== + dependencies: + safe-buffer "~5.1.1" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +copy-concurrently@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + integrity sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A== + dependencies: + aproba "^1.1.1" + fs-write-stream-atomic "^1.0.8" + iferr "^0.1.5" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.0" + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +copy-webpack-plugin@5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-5.0.2.tgz#56186dfddbf9aa1b29c97fa4c796c1be98870da4" + integrity sha512-7nC7EynPrnBTtBwwbG1aTqrfNS1aTb9eEjSmQDqFtKAsJrR3uDb+pCDIFT2LzhW+SgGJxQcYzThrmXzzZ720uw== + dependencies: + cacache "^11.3.1" + find-cache-dir "^2.0.0" + glob-parent "^3.1.0" + globby "^7.1.1" + is-glob "^4.0.0" + loader-utils "^1.1.0" + minimatch "^3.0.4" + normalize-path "^3.0.0" + p-limit "^2.1.0" + serialize-javascript "^1.4.0" + webpack-log "^2.0.0" + +core-js@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.1.tgz#1343182634298f7f38622f95e73f54e48ddf4738" + integrity sha512-sco40rF+2KlE0ROMvydjkrVMMG1vYilP2ALoRXcYR4obqbYIuV3Bg+51GEDW+HF8n7NRA+iaA4qD0nD9lo9mew== + +core-js@^2, core-js@^2.2.0, core-js@^2.4.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" + integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cosmiconfig@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" + integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== + dependencies: + import-fresh "^2.0.0" + is-directory "^0.3.1" + js-yaml "^3.13.1" + parse-json "^4.0.0" + +cp-file@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/cp-file/-/cp-file-6.2.0.tgz#40d5ea4a1def2a9acdd07ba5c0b0246ef73dc10d" + integrity sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA== + dependencies: + graceful-fs "^4.1.2" + make-dir "^2.0.0" + nested-error-stacks "^2.0.0" + pify "^4.0.1" + safe-buffer "^5.0.1" + +create-ecdh@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.3.tgz#c9111b6f33045c4697f144787f9254cdc77c45ff" + integrity sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw== + dependencies: + bn.js "^4.1.0" + elliptic "^6.0.0" + +create-hash@^1.1.0, create-hash@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^4: + version "4.0.2" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" + integrity sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE= + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= + dependencies: + lru-cache "^4.0.1" + shebang-command "^1.2.0" + which "^1.2.9" + +crypto-browserify@^3.11.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + +crypto-random-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" + integrity sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4= + +css-parse@1.7.x: + version "1.7.0" + resolved "https://registry.yarnpkg.com/css-parse/-/css-parse-1.7.0.tgz#321f6cf73782a6ff751111390fc05e2c657d8c9b" + integrity sha1-Mh9s9zeCpv91ERE5D8BeLGV9jJs= + +cuint@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" + integrity sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs= + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= + dependencies: + array-find-index "^1.0.1" + +custom-event@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" + integrity sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU= + +cyclist@~0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" + integrity sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA= + +d@1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + +dargs@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/dargs/-/dargs-4.1.0.tgz#03a9dbb4b5c2f139bf14ae53f0b8a2a6a86f4e17" + integrity sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc= + dependencies: + number-is-nan "^1.0.0" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +date-fns@^1.23.0: + version "1.30.1" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" + integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== + +date-format@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/date-format/-/date-format-2.1.0.tgz#31d5b5ea211cf5fd764cd38baf9d033df7e125cf" + integrity sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA== + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + integrity sha1-6vQ5/U1ISK105cx9vvIAZyueNFs= + +dateformat@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062" + integrity sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI= + +dateformat@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" + integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== + +dateformat@~1.0.4-1.2.3: + version "1.0.12" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" + integrity sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk= + dependencies: + get-stdin "^4.0.1" + meow "^3.3.0" + +debounce@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131" + integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg== + +debug@*, debug@^4.1.0, debug@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@3.1.0, debug@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@^3.0.0, debug@^3.1.0, debug@^3.2.5, debug@^3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +debuglog@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" + integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= + +decamelize-keys@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" + integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= + dependencies: + decamelize "^1.1.0" + map-obj "^1.0.0" + +decamelize@^1.1.0, decamelize@^1.1.2, decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= + dependencies: + mimic-response "^1.0.0" + +deep-equal@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" + integrity sha1-9dJgKStmDghO/0zbyfCK0yR0SLU= + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +default-gateway@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-4.2.0.tgz#167104c7500c2115f6dd69b0a536bb8ed720552b" + integrity sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA== + dependencies: + execa "^1.0.0" + ip-regex "^2.1.0" + +default-require-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-2.0.0.tgz#f5f8fbb18a7d6d50b21f641f649ebb522cfe24f7" + integrity sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc= + dependencies: + strip-bom "^3.0.0" + +defaults@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" + integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= + dependencies: + clone "^1.0.2" + +defer-to-connect@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.0.2.tgz#4bae758a314b034ae33902b5aac25a8dd6a8633e" + integrity sha512-k09hcQcTDY+cwgiwa6PYKLm3jlagNzQ+RSvhjzESOGOx+MNOuXkxTfEvPrO1IOQ81tArCFYQgi631clB70RpQw== + +define-properties@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +del@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" + integrity sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag= + dependencies: + globby "^5.0.0" + is-path-cwd "^1.0.0" + is-path-in-cwd "^1.0.0" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + rimraf "^2.2.8" + +del@^4.1.0, del@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/del/-/del-4.1.1.tgz#9e8f117222ea44a31ff3a156c049b99052a9f0b4" + integrity sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ== + dependencies: + "@types/glob" "^7.1.1" + globby "^6.1.0" + is-path-cwd "^2.0.0" + is-path-in-cwd "^2.0.0" + p-map "^2.0.0" + pify "^4.0.1" + rimraf "^2.6.3" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +delegate@^3.1.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" + integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +dependency-graph@^0.7.2: + version "0.7.2" + resolved "https://registry.yarnpkg.com/dependency-graph/-/dependency-graph-0.7.2.tgz#91db9de6eb72699209d88aea4c1fd5221cac1c49" + integrity sha512-KqtH4/EZdtdfWX0p6MGP9jljvxSY6msy/pRUD4jgNwVpv3v1QmNLlsB3LDSSUg79BRVSn7jI1QPRtArGABovAQ== + +deprecated@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/deprecated/-/deprecated-0.0.1.tgz#f9c9af5464afa1e7a971458a8bdef2aa94d5bb19" + integrity sha1-+cmvVGSvoeepcUWKi97yqpTVuxk= + +des.js@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc" + integrity sha1-wHTS4qpqipoH29YfmhXCzYPsjsw= + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +detect-file@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7" + integrity sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc= + +detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg= + dependencies: + repeating "^2.0.0" + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha1-+hN8S9aY7fVc1c0CrFWfkaTEups= + +detect-node@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.0.4.tgz#014ee8f8f669c5c58023da64b8179c083a28c46c" + integrity sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw== + +dezalgo@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456" + integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= + dependencies: + asap "^2.0.0" + wrappy "1" + +di@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c" + integrity sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw= + +diff@^2.0.2: + version "2.2.3" + resolved "https://registry.yarnpkg.com/diff/-/diff-2.2.3.tgz#60eafd0d28ee906e4e8ff0a52c1229521033bf99" + integrity sha1-YOr9DSjukG5Oj/ClLBIpUhAzv5k= + +diff@^3.2.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +diff@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff" + integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q== + +diffie-hellman@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" + integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dir-glob@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" + integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw== + dependencies: + path-type "^3.0.0" + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= + +dns-packet@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-1.3.1.tgz#12aa426981075be500b910eedcd0b47dd7deda5a" + integrity sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg== + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= + dependencies: + buffer-indexof "^1.0.0" + +dom-serialize@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" + integrity sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs= + dependencies: + custom-event "~1.0.0" + ent "~2.2.0" + extend "^3.0.0" + void-elements "^2.0.0" + +domain-browser@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" + integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== + +domino@^2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/domino/-/domino-2.1.3.tgz#0ca1ad02cbd316ebe2e99e0ac9fb0010407d4601" + integrity sha512-EwjTbUv1Q/RLQOdn9k7ClHutrQcWGsfXaRQNOnM/KgK4xDBoLFEcIRFuBSxAx13Vfa63X029gXYrNFrSy+DOSg== + +dot-prop@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177" + integrity sha1-G3CK8JSknJoOfbyteQq6U52sEXc= + dependencies: + is-obj "^1.0.0" + +dot-prop@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" + integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ== + dependencies: + is-obj "^1.0.0" + +duplexer2@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.0.2.tgz#c614dcf67e2fb14995a91711e5a617e8a60a31db" + integrity sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds= + dependencies: + readable-stream "~1.1.9" + +duplexer2@~0.1.0: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME= + dependencies: + readable-stream "^2.0.2" + +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" + integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= + +duplexer@^0.1.1, duplexer@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= + +duplexify@^3.4.2, duplexify@^3.6.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +dynamic-dedupe@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz#06e44c223f5e4e94d78ef9db23a6515ce2f962a1" + integrity sha1-BuRMIj9eTpTXjvnbI6ZRXOL5YqE= + dependencies: + xtend "^4.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +ejs@2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0" + integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ== + +electron-to-chromium@^1.3.124, electron-to-chromium@^1.3.191: + version "1.3.217" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.217.tgz#a13a91f8f7cd3a52f0bb6957cbced8628bfcdbbd" + integrity sha512-e8enF2CtMZnOtpKIktgGMk1eWH7qUSrpISJE/BSHh+GZFLAt3P2/4KrjR0kZDO4WVsm9hfN5fWCoGXo/3Telpg== + +elliptic@^6.0.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.0.tgz#2b8ed4c891b7de3200e14412a5b8248c7af505ca" + integrity sha512-eFOJTMyCYb7xtE/caJ6JJu+bhi67WCYNbkGSknu20pmM8Ke/bqOfdnZWxyoGN26JgfxTbXrsCkEw4KheCT/KGg== + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + +email-addresses@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/email-addresses/-/email-addresses-3.0.3.tgz#fc3c6952f68da24239914e982c8a7783bc2ed96d" + integrity sha512-kUlSC06PVvvjlMRpNIl3kR1NRXLEe86VQ7N0bQeaCZb2g+InShCeHQp/JvyYNTugMnRN2NvJhHlc3q12MWbbpg== + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +emojis-list@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" + integrity sha1-TapNnbAPmBmIDHn6RXrlsJof04k= + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s= + dependencies: + iconv-lite "~0.4.13" + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== + dependencies: + once "^1.4.0" + +end-of-stream@~0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-0.1.5.tgz#8e177206c3c80837d85632e8b9359dfe8b2f6eaf" + integrity sha1-jhdyBsPICDfYVjLouTWd/osvbq8= + dependencies: + once "~1.3.0" + +engine.io-client@~3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.2.1.tgz#6f54c0475de487158a1a7c77d10178708b6add36" + integrity sha512-y5AbkytWeM4jQr7m/koQLc5AxpRKC1hEVUb/s1FUAWEJq5AzJJ4NLvzuKPuxtDi5Mq755WuDvZ6Iv2rXj4PTzw== + dependencies: + component-emitter "1.2.1" + component-inherit "0.0.3" + debug "~3.1.0" + engine.io-parser "~2.1.1" + has-cors "1.1.0" + indexof "0.0.1" + parseqs "0.0.5" + parseuri "0.0.5" + ws "~3.3.1" + xmlhttprequest-ssl "~1.5.4" + yeast "0.1.2" + +engine.io-parser@~2.1.0, engine.io-parser@~2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.1.3.tgz#757ab970fbf2dfb32c7b74b033216d5739ef79a6" + integrity sha512-6HXPre2O4Houl7c4g7Ic/XzPnHBvaEmN90vtRO9uLmwtRqQmTOw0QMevL1TOfL2Cpu1VzsaTmMotQgMdkzGkVA== + dependencies: + after "0.8.2" + arraybuffer.slice "~0.0.7" + base64-arraybuffer "0.1.5" + blob "0.0.5" + has-binary2 "~1.0.2" + +engine.io@~3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.2.1.tgz#b60281c35484a70ee0351ea0ebff83ec8c9522a2" + integrity sha512-+VlKzHzMhaU+GsCIg4AoXF1UdDFjHHwMmMKqMJNDNLlUlejz58FCy4LBqB2YVJskHGYl06BatYWKP2TVdVXE5w== + dependencies: + accepts "~1.3.4" + base64id "1.0.0" + cookie "0.3.1" + debug "~3.1.0" + engine.io-parser "~2.1.0" + ws "~3.3.1" + +enhanced-resolve@4.1.0, enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f" + integrity sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng== + dependencies: + graceful-fs "^4.1.2" + memory-fs "^0.4.0" + tapable "^1.0.0" + +ent@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= + +err-code@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" + integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA= + +errno@^0.1.1, errno@^0.1.3, errno@~0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" + integrity sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg== + dependencies: + prr "~1.0.1" + +error-ex@^1.2.0, error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.5.1: + version "1.13.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" + integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== + dependencies: + es-to-primitive "^1.2.0" + function-bind "^1.1.1" + has "^1.0.3" + is-callable "^1.1.4" + is-regex "^1.0.4" + object-keys "^1.0.12" + +es-to-primitive@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" + integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +es5-ext@^0.10.35, es5-ext@^0.10.45, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: + version "0.10.50" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.50.tgz#6d0e23a0abdb27018e5ac4fd09b412bc5517a778" + integrity sha512-KMzZTPBkeQV/JcSQhI5/z6d9VWJ3EnQ194USTUwIYZ2ZbpN8+SGXQKt1h68EX44+qt+Fzr8DO17vnxrw7c3agw== + dependencies: + es6-iterator "~2.0.3" + es6-symbol "~3.1.1" + next-tick "^1.0.0" + +es6-error@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" + integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== + +es6-iterator@^2.0.3, es6-iterator@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha1-p96IkUGgWpSwhUQDstCg+/qY87c= + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-promise@^4.0.3: + version "4.2.8" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" + integrity sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w== + +es6-promisify@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" + integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= + dependencies: + es6-promise "^4.0.3" + +es6-symbol@^3.1.1, es6-symbol@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77" + integrity sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc= + dependencies: + d "1" + es5-ext "~0.10.14" + +es6-weak-map@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-weak-map/-/es6-weak-map-2.0.3.tgz#b6da1f16cc2cc0d9be43e6bdbfc5e7dfcdf31d53" + integrity sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA== + dependencies: + d "1" + es5-ext "^0.10.46" + es6-iterator "^2.0.3" + es6-symbol "^3.1.1" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +eslint-scope@^4.0.0, eslint-scope@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.3.tgz#ca03833310f6889a3264781aa82e63eb9cfe7848" + integrity sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== + dependencies: + estraverse "^4.1.0" + +estraverse@^4.1.0, estraverse@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" + integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= + +estree-walker@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" + integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk= + dependencies: + d "1" + es5-ext "~0.10.14" + +event-stream@^3.1.5: + version "3.3.5" + resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.5.tgz#e5dd8989543630d94c6cf4d657120341fa31636b" + integrity sha512-vyibDcu5JL20Me1fP734QBH/kenBGLZap2n0+XXM7mvuUPzJ20Ydqj1aKcIeMdri1p+PU+4yAKugjN8KCVst+g== + dependencies: + duplexer "^0.1.1" + from "^0.1.7" + map-stream "0.0.7" + pause-stream "^0.0.11" + split "^1.0.1" + stream-combiner "^0.2.2" + through "^2.3.8" + +eventemitter3@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" + integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== + +events@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.0.0.tgz#9a0a0dfaf62893d92b875b8f2698ca4114973e88" + integrity sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA== + +eventsource@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-1.0.7.tgz#8fbc72c93fcd34088090bc0a4e64f4b5cee6d8d0" + integrity sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ== + dependencies: + original "^1.0.0" + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +execa@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expand-tilde@^2.0.0, expand-tilde@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502" + integrity sha1-l+gBqgUt8CRU3kawK/YhZCzchQI= + dependencies: + homedir-polyfill "^1.0.1" + +express@^4.16.4, express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@^3.0.0, extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fancy-log@^1.1.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/fancy-log/-/fancy-log-1.3.3.tgz#dbc19154f558690150a23953a0adbd035be45fc7" + integrity sha512-k9oEhlyc0FrVh25qYuSELjr8oxsCoc4/LEZfg2iJJrfEk/tZL9bCoJE47gqAvI2m/AUjluCS4+3I0eTx8n3AEw== + dependencies: + ansi-gray "^0.1.1" + color-support "^1.1.3" + parse-node-version "^1.0.0" + time-stamp "^1.0.0" + +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + integrity sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ= + +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= + +fast-json-stable-stringify@2.0.0, fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + +faye-websocket@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.10.0.tgz#4e492f8d04dfb6f89003507f6edbf2d501e7c6f4" + integrity sha1-TkkvjQTftviQA1B/btvy1QHnxvQ= + dependencies: + websocket-driver ">=0.5.1" + +faye-websocket@~0.11.1: + version "0.11.3" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" + integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== + dependencies: + websocket-driver ">=0.5.1" + +figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" + integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= + dependencies: + escape-string-regexp "^1.0.5" + +file-loader@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-3.0.1.tgz#f8e0ba0b599918b51adfe45d66d1e771ad560faa" + integrity sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw== + dependencies: + loader-utils "^1.0.2" + schema-utils "^1.0.0" + +filename-reserved-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/filename-reserved-regex/-/filename-reserved-regex-1.0.0.tgz#e61cf805f0de1c984567d0386dc5df50ee5af7e4" + integrity sha1-5hz4BfDeHJhFZ9A4bcXfUO5a9+Q= + +filenamify-url@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/filenamify-url/-/filenamify-url-1.0.0.tgz#b32bd81319ef5863b73078bed50f46a4f7975f50" + integrity sha1-syvYExnvWGO3MHi+1Q9GpPeXX1A= + dependencies: + filenamify "^1.0.0" + humanize-url "^1.0.0" + +filenamify@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/filenamify/-/filenamify-1.2.1.tgz#a9f2ffd11c503bed300015029272378f1f1365a5" + integrity sha1-qfL/0RxQO+0wABUCknI3jx8TZaU= + dependencies: + filename-reserved-regex "^1.0.0" + strip-outer "^1.0.0" + trim-repeated "^1.0.0" + +fileset@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0" + integrity sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA= + dependencies: + glob "^7.0.3" + minimatch "^3.0.3" + +filewatcher@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/filewatcher/-/filewatcher-3.0.1.tgz#f4a1957355ddaf443ccd78a895f3d55e23c8a034" + integrity sha1-9KGVc1Xdr0Q8zXiolfPVXiPIoDQ= + dependencies: + debounce "^1.0.0" + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.1.2, finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-2.1.0.tgz#8d0f94cd13fe43c6c7c261a0d86115ca918c05f7" + integrity sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ== + dependencies: + commondir "^1.0.1" + make-dir "^2.0.0" + pkg-dir "^3.0.0" + +find-index@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4" + integrity sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ= + +find-parent-dir@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" + integrity sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ= + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= + dependencies: + locate-path "^2.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +findup-sync@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-3.0.0.tgz#17b108f9ee512dfb7a5c7f3c8b27ea9e1a9c08d1" + integrity sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg== + dependencies: + detect-file "^1.0.0" + is-glob "^4.0.0" + micromatch "^3.0.4" + resolve-dir "^1.0.1" + +findup-sync@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/findup-sync/-/findup-sync-2.0.0.tgz#9326b1488c22d1a6088650a86901b2d9a90a2cbc" + integrity sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw= + dependencies: + detect-file "^1.0.0" + is-glob "^3.1.0" + micromatch "^3.0.4" + resolve-dir "^1.0.1" + +fined@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/fined/-/fined-1.2.0.tgz#d00beccf1aa2b475d16d423b0238b713a2c4a37b" + integrity sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng== + dependencies: + expand-tilde "^2.0.2" + is-plain-object "^2.0.3" + object.defaults "^1.1.0" + object.pick "^1.2.0" + parse-filepath "^1.0.1" + +first-chunk-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz#59bfb50cd905f60d7c394cd3d9acaab4e6ad934e" + integrity sha1-Wb+1DNkF9g18OUzT2ayqtOatk04= + +flagged-respawn@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-1.0.1.tgz#e7de6f1279ddd9ca9aac8a5971d618606b3aab41" + integrity sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q== + +flatted@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" + integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== + +flush-write-stream@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" + integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== + dependencies: + inherits "^2.0.3" + readable-stream "^2.3.6" + +follow-redirects@^1.0.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.7.0.tgz#489ebc198dc0e7f64167bd23b03c4c19b5784c76" + integrity sha512-m/pZQy4Gj287eNy94nivy5wchN3Kp+Q5WgUPNy5lJSZ3sgkVKSYV/ZChMAQVIgx1SqfZ2zBZtPA2YlXIWxxJOQ== + dependencies: + debug "^3.2.6" + +for-in@^0.1.3: + version "0.1.8" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" + integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE= + +for-in@^1.0.1, for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +for-own@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" + integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs= + dependencies: + for-in "^1.0.1" + +foreground-child@^1.5.6: + version "1.5.6" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-1.5.6.tgz#4fd71ad2dfde96789b980a5c0a295937cb2f5ce9" + integrity sha1-T9ca0t/elnibmApcCilZN8svXOk= + dependencies: + cross-spawn "^4" + signal-exit "^3.0.0" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +from2@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + integrity sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8= + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + +from@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" + integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4= + +fs-access@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a" + integrity sha1-1qh/JiJxzv6+wwxVNAf7mV2od3o= + dependencies: + null-check "^1.0.0" + +fs-extra@^7.0.0, fs-extra@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-7.0.1.tgz#4f189c44aa123b895f722804f55ea23eadc348e9" + integrity sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw== + dependencies: + graceful-fs "^4.1.2" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-extra@^8.0.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-minipass@^1.2.5: + version "1.2.6" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.6.tgz#2c5cc30ded81282bfe8a0d7c7c1853ddeb102c07" + integrity sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ== + dependencies: + minipass "^2.2.1" + +fs-write-stream-atomic@^1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" + integrity sha1-tH31NJPvkR33VzHnCp3tAYnbQMk= + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@^1.2.7: + version "1.2.9" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.9.tgz#3f5ed66583ccd6f400b5a00db6f7e861363e388f" + integrity sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw== + dependencies: + nan "^2.12.1" + node-pre-gyp "^0.12.0" + +fsevents@^2.0.6: + version "2.0.7" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.0.7.tgz#382c9b443c6cbac4c57187cdda23aa3bf1ccfc2a" + integrity sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gaze@^0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/gaze/-/gaze-0.5.2.tgz#40b709537d24d1d45767db5a908689dfe69ac44f" + integrity sha1-QLcJU30k0dRXZ9takIaJ3+aaxE8= + dependencies: + globule "~0.1.0" + +genfun@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" + integrity sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA== + +get-caller-file@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" + integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-pkg-repo@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz#c73b489c06d80cc5536c2c853f9e05232056972d" + integrity sha1-xztInAbYDMVTbCyFP54FIyBWly0= + dependencies: + hosted-git-info "^2.1.4" + meow "^3.3.0" + normalize-package-data "^2.3.0" + parse-github-repo-url "^1.3.0" + through2 "^2.0.0" + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= + +get-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= + +get-stream@^4.0.0, get-stream@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.1.0.tgz#01203cdc92597f9b909067c3e656cc1f4d3c4dc9" + integrity sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw== + dependencies: + pump "^3.0.0" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +gh-pages@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/gh-pages/-/gh-pages-2.1.0.tgz#dcf519825d77d3a3ee78763076f4158403fc88c4" + integrity sha512-QmV1fh/2W5GZkfoLsG4g6dRTWiNYuCetMQmm8CL6Us8JVnAufYtS0uJPD8NYogmNB4UZzdRG44uPAL+jcBzEwQ== + dependencies: + async "^2.6.1" + commander "^2.18.0" + email-addresses "^3.0.1" + filenamify-url "^1.0.0" + fs-extra "^7.0.0" + globby "^6.1.0" + graceful-fs "^4.1.11" + rimraf "^2.6.2" + +git-raw-commits@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/git-raw-commits/-/git-raw-commits-2.0.0.tgz#d92addf74440c14bcc5c83ecce3fb7f8a79118b5" + integrity sha512-w4jFEJFgKXMQJ0H0ikBk2S+4KP2VEjhCvLCNqbNRQC8BgGWgLKNCO7a9K9LI+TVT7Gfoloje502sEnctibffgg== + dependencies: + dargs "^4.0.1" + lodash.template "^4.0.2" + meow "^4.0.0" + split2 "^2.0.0" + through2 "^2.0.0" + +git-remote-origin-url@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz#5282659dae2107145a11126112ad3216ec5fa65f" + integrity sha1-UoJlna4hBxRaERJhEq0yFuxfpl8= + dependencies: + gitconfiglocal "^1.0.0" + pify "^2.3.0" + +git-semver-tags@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/git-semver-tags/-/git-semver-tags-3.0.0.tgz#fe10147824657662c82efd9341f0fa59f74ddcba" + integrity sha512-T4C/gJ9k2Bnxz+PubtcyiMtUUKrC+Nh9Q4zaECcnmVMwJgPhrNyP/Rf+YpdRqsJbCV/+kYrCH24Xg+IeAmbOPg== + dependencies: + meow "^4.0.0" + semver "^6.0.0" + +gitconfiglocal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz#41d045f3851a5ea88f03f24ca1c6178114464b9b" + integrity sha1-QdBF84UaXqiPA/JMocYXgRRGS5s= + dependencies: + ini "^1.3.2" + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-parent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.0.0.tgz#1dc99f0f39b006d3e92c2c284068382f0c20e954" + integrity sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg== + dependencies: + is-glob "^4.0.1" + +glob-stream@^3.1.5: + version "3.1.18" + resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-3.1.18.tgz#9170a5f12b790306fdfe598f313f8f7954fd143b" + integrity sha1-kXCl8St5Awb9/lmPMT+PeVT9FDs= + dependencies: + glob "^4.3.1" + glob2base "^0.0.12" + minimatch "^2.0.1" + ordered-read-streams "^0.1.0" + through2 "^0.6.1" + unique-stream "^1.0.0" + +glob-watcher@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-0.0.6.tgz#b95b4a8df74b39c83298b0c05c978b4d9a3b710b" + integrity sha1-uVtKjfdLOcgymLDAXJeLTZo7cQs= + dependencies: + gaze "^0.5.1" + +glob2base@^0.0.12: + version "0.0.12" + resolved "https://registry.yarnpkg.com/glob2base/-/glob2base-0.0.12.tgz#9d419b3e28f12e83a362164a277055922c9c0d56" + integrity sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY= + dependencies: + find-index "^0.1.1" + +glob@7.0.x: + version "7.0.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.0.6.tgz#211bafaf49e525b8cd93260d14ab136152b3f57a" + integrity sha1-IRuvr0nlJbjNkyYNFKsTYVKz9Xo= + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.2" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^4.3.1: + version "4.5.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-4.5.3.tgz#c6cb73d3226c1efef04de3c56d012f03377ee15f" + integrity sha1-xstz0yJsHv7wTePFbQEvAzd+4V8= + dependencies: + inflight "^1.0.4" + inherits "2" + minimatch "^2.0.1" + once "^1.3.0" + +glob@^7.0.0, glob@^7.0.3, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: + version "7.1.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" + integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@~3.1.21: + version "3.1.21" + resolved "https://registry.yarnpkg.com/glob/-/glob-3.1.21.tgz#d29e0a055dea5138f4d07ed40e8982e83c2066cd" + integrity sha1-0p4KBV3qUTj00H7UDomC6DwgZs0= + dependencies: + graceful-fs "~1.2.0" + inherits "1" + minimatch "~0.2.11" + +global-dirs@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" + integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU= + dependencies: + ini "^1.3.4" + +global-modules@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-modules@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-1.0.0.tgz#6d770f0eb523ac78164d72b5e71a8877265cc3ea" + integrity sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg== + dependencies: + global-prefix "^1.0.1" + is-windows "^1.0.1" + resolve-dir "^1.0.0" + +global-prefix@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-1.0.2.tgz#dbf743c6c14992593c655568cb66ed32c0122ebe" + integrity sha1-2/dDxsFJklk8ZVVoy2btMsASLr4= + dependencies: + expand-tilde "^2.0.2" + homedir-polyfill "^1.0.1" + ini "^1.3.4" + is-windows "^1.0.1" + which "^1.2.14" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-prefix/-/global-prefix-3.0.0.tgz#fc85f73064df69f50421f47f883fe5b913ba9b97" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^9.18.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== + +globby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" + integrity sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0= + dependencies: + array-union "^1.0.1" + arrify "^1.0.0" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globby@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" + integrity sha1-9abXDoOV4hyFj7BInWTfAkJNUGw= + dependencies: + array-union "^1.0.1" + glob "^7.0.3" + object-assign "^4.0.1" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +globby@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680" + integrity sha1-+yzP+UAfhgCUXfral0QMypcrhoA= + dependencies: + array-union "^1.0.1" + dir-glob "^2.0.0" + glob "^7.1.2" + ignore "^3.3.5" + pify "^3.0.0" + slash "^1.0.0" + +globule@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/globule/-/globule-0.1.0.tgz#d9c8edde1da79d125a151b79533b978676346ae5" + integrity sha1-2cjt3h2nnRJaFRt5UzuXhnY0auU= + dependencies: + glob "~3.1.21" + lodash "~1.0.1" + minimatch "~0.2.11" + +glogg@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/glogg/-/glogg-1.0.2.tgz#2d7dd702beda22eb3bffadf880696da6d846313f" + integrity sha512-5mwUoSuBk44Y4EshyiqcH95ZntbDdTQqA3QYSrxmzj28Ai0vXBGMH1ApSANH14j2sIRtqCEyg6PfsuP7ElOEDA== + dependencies: + sparkles "^1.0.0" + +good-listener@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" + integrity sha1-1TswzfkxPf+33JoNR3CWqm0UXFA= + dependencies: + delegate "^3.1.2" + +got@^9.6.0: + version "9.6.0" + resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" + integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== + dependencies: + "@sindresorhus/is" "^0.14.0" + "@szmarczak/http-timer" "^1.1.2" + cacheable-request "^6.0.0" + decompress-response "^3.3.0" + duplexer3 "^0.1.4" + get-stream "^4.1.0" + lowercase-keys "^1.0.1" + mimic-response "^1.0.1" + p-cancelable "^1.0.0" + to-readable-stream "^1.0.0" + url-parse-lax "^3.0.0" + +graceful-fs@^3.0.0: + version "3.0.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-3.0.11.tgz#7613c778a1afea62f25c630a086d7f3acbbdd818" + integrity sha1-dhPHeKGv6mLyXGMKCG1/Osu92Bg= + dependencies: + natives "^1.1.0" + +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.1.tgz#1c1f0c364882c868f5bff6512146328336a11b1d" + integrity sha512-b9usnbDGnD928gJB3LrCmxoibr3VE4U2SMo5PBuBnokWyDADTqDPXg4YpwKF1trpH+UbGp7QLicO3+aWEy0+mw== + +graceful-fs@~1.2.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-1.2.3.tgz#15a4806a57547cb2d2dbf27f42e89a8c3451b364" + integrity sha1-FaSAaldUfLLS2/J/QuiajDRRs2Q= + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= + +gulp-clang-format@1.0.23: + version "1.0.23" + resolved "https://registry.yarnpkg.com/gulp-clang-format/-/gulp-clang-format-1.0.23.tgz#fe258586b83998491e632fc0c4fc0ecdfa10c89f" + integrity sha1-/iWFhrg5mEkeYy/AxPwOzfoQyJ8= + dependencies: + clang-format "^1.0.32" + gulp-diff "^1.0.0" + gulp-util "^3.0.4" + pkginfo "^0.3.0" + stream-combiner2 "^1.1.1" + stream-equal "0.1.6" + through2 "^0.6.3" + +gulp-diff@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gulp-diff/-/gulp-diff-1.0.0.tgz#101b23712dd6b107bd07d05ab88ea3ac485fed77" + integrity sha1-EBsjcS3WsQe9B9BauI6jrEhf7Xc= + dependencies: + cli-color "^1.0.0" + diff "^2.0.2" + event-stream "^3.1.5" + gulp-util "^3.0.6" + through2 "^2.0.0" + +gulp-util@^3.0.0, gulp-util@^3.0.4, gulp-util@^3.0.6: + version "3.0.8" + resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" + integrity sha1-AFTh50RQLifATBh8PsxQXdVLu08= + dependencies: + array-differ "^1.0.0" + array-uniq "^1.0.2" + beeper "^1.0.0" + chalk "^1.0.0" + dateformat "^2.0.0" + fancy-log "^1.1.0" + gulplog "^1.0.0" + has-gulplog "^0.1.0" + lodash._reescape "^3.0.0" + lodash._reevaluate "^3.0.0" + lodash._reinterpolate "^3.0.0" + lodash.template "^3.0.0" + minimist "^1.1.0" + multipipe "^0.1.2" + object-assign "^3.0.0" + replace-ext "0.0.1" + through2 "^2.0.0" + vinyl "^0.5.0" + +gulp@^3.9.1: + version "3.9.1" + resolved "https://registry.yarnpkg.com/gulp/-/gulp-3.9.1.tgz#571ce45928dd40af6514fc4011866016c13845b4" + integrity sha1-VxzkWSjdQK9lFPxAEYZgFsE4RbQ= + dependencies: + archy "^1.0.0" + chalk "^1.0.0" + deprecated "^0.0.1" + gulp-util "^3.0.0" + interpret "^1.0.0" + liftoff "^2.1.0" + minimist "^1.1.0" + orchestrator "^0.3.0" + pretty-hrtime "^1.0.0" + semver "^4.1.0" + tildify "^1.0.0" + v8flags "^2.0.2" + vinyl-fs "^0.3.0" + +gulplog@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gulplog/-/gulplog-1.0.0.tgz#e28c4d45d05ecbbed818363ce8f9c5926229ffe5" + integrity sha1-4oxNRdBey77YGDY86PnFkmIp/+U= + dependencies: + glogg "^1.0.0" + +handle-thing@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" + integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== + +handlebars@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" + integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw== + dependencies: + neo-async "^2.6.0" + optimist "^0.6.1" + source-map "^0.6.1" + optionalDependencies: + uglify-js "^3.1.4" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + +has-binary2@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" + integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw== + dependencies: + isarray "2.0.1" + +has-cors@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" + integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk= + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + integrity sha1-6CB68cx7MNRGzHC3NLXovhj4jVE= + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-gulplog@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/has-gulplog/-/has-gulplog-0.1.0.tgz#6414c82913697da51590397dafb12f22967811ce" + integrity sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4= + dependencies: + sparkles "^1.0.0" + +has-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" + integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has-yarn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" + integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== + +has@^1.0.1, has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hash-base@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" + integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg= + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +hasha@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/hasha/-/hasha-3.0.0.tgz#52a32fab8569d41ca69a61ff1a214f8eb7c8bd39" + integrity sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk= + dependencies: + is-stream "^1.0.1" + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +homedir-polyfill@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz#743298cef4e5af3e194161fbadcc2151d3a058e8" + integrity sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA== + dependencies: + parse-passwd "^1.0.0" + +hosted-git-info@^2.1.4, hosted-git-info@^2.6.0: + version "2.8.2" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.2.tgz#a35c3f355ac1249f1093c0c2a542ace8818c171a" + integrity sha512-CyjlXII6LMsPMyUzxpTt8fzh5QwzGqPmQXgY/Jyf4Zfp27t/FvfhwoE/8laaMUcMy816CkWF20I7NeQhwwY88w== + dependencies: + lru-cache "^5.1.1" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-entities@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" + integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= + +http-cache-semantics@^3.8.1: + version "3.8.1" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" + integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== + +http-cache-semantics@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz#495704773277eeef6e43f9ab2c2c7d259dda25c5" + integrity sha512-TcIMG3qeVLgDr1TEd2XvHaTnMPwYQUQMIBLy+5pLSDKYFc7UIqj39w8EGzZkaxoLv/l2K8HaI0t5AVA+YYgUew== + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +"http-parser-js@>=0.4.0 <0.4.11": + version "0.4.10" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" + integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q= + +http-proxy-agent@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" + integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== + dependencies: + agent-base "4" + debug "3.1.0" + +http-proxy-middleware@^0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" + integrity sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q== + dependencies: + http-proxy "^1.17.0" + is-glob "^4.0.0" + lodash "^4.17.11" + micromatch "^3.1.10" + +http-proxy@^1.13.0, http-proxy@^1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" + integrity sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g== + dependencies: + eventemitter3 "^3.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= + +https-proxy-agent@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.2.tgz#271ea8e90f836ac9f119daccd39c19ff7dfb0793" + integrity sha512-c8Ndjc9Bkpfx/vCJueCPy0jlP4ccCCSNDp8xwCZzPjKJUm+B+u9WX2x98Qx4n1PiMNTWo3D7KK5ifNV/yJyRzg== + dependencies: + agent-base "^4.3.0" + debug "^3.1.0" + +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0= + dependencies: + ms "^2.0.0" + +humanize-url@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/humanize-url/-/humanize-url-1.0.1.tgz#f4ab99e0d288174ca4e1e50407c55fbae464efff" + integrity sha1-9KuZ4NKIF0yk4eUEB8VfuuRk7/8= + dependencies: + normalize-url "^1.0.0" + strip-url-auth "^1.0.0" + +iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +iferr@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE= + +ignore-walk@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" + integrity sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ== + dependencies: + minimatch "^3.0.4" + +ignore@^3.3.5: + version "3.3.10" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" + integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== + +image-size@~0.5.0: + version "0.5.5" + resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" + integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= + +immediate@~3.0.5: + version "3.0.6" + resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= + +import-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" + integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk= + dependencies: + import-from "^2.1.0" + +import-fresh@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" + integrity sha1-2BNVwVYS04bGH53dOSLUMEgipUY= + dependencies: + caller-path "^2.0.0" + resolve-from "^3.0.0" + +import-from@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" + integrity sha1-M1238qev/VOqpHHUuAId7ja387E= + dependencies: + resolve-from "^3.0.0" + +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" + integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= + +import-local@2.0.0, import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== + dependencies: + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= + dependencies: + repeating "^2.0.0" + +indent-string@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= + +infer-owner@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-1.0.2.tgz#ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b" + integrity sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js= + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1" + integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE= + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@1.3.5, ini@^1.3.2, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + +injection-js@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/injection-js/-/injection-js-2.2.1.tgz#a8d6a085b2f0b8d8650f6f4487f6abb8cc0d67ce" + integrity sha512-zHI+E+dM0PXix5FFTO1Y4/UOyAzE7zG1l/QwAn4jchTThOoBq+UYRFK4AVG7lQgFL+go62SbrzSsjXy9DFEZUg== + +inquirer@6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.0.tgz#2303317efc9a4ea7ec2e2df6f86569b734accf42" + integrity sha512-scfHejeG/lVZSpvCXpsB4j/wQNPM5JC8kiElOI0OUTwmc1RTpXr4H32/HOlQHcZiYl2z2VElwuCVDRG8vFmbnA== + dependencies: + ansi-escapes "^3.2.0" + chalk "^2.4.2" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^3.0.3" + figures "^2.0.0" + lodash "^4.17.12" + mute-stream "0.0.7" + run-async "^2.2.0" + rxjs "^6.4.0" + string-width "^2.1.0" + strip-ansi "^5.1.0" + through "^2.3.6" + +internal-ip@^4.2.0, internal-ip@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" + integrity sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg== + dependencies: + default-gateway "^4.2.0" + ipaddr.js "^1.9.0" + +interpret@1.2.0, interpret@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" + integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== + +invariant@^2.2.2: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +invert-kv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" + integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== + +ip-regex@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" + integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= + +ip@^1.1.0, ip@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + +ipaddr.js@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" + integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== + +ipaddr.js@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-absolute@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" + integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA== + dependencies: + is-relative "^1.0.0" + is-windows "^1.0.1" + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= + dependencies: + binary-extensions "^1.0.0" + +is-binary-path@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" + integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-extglob@^2.1.0, is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= + dependencies: + is-extglob "^2.1.0" + +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-installed-globally@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" + integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA= + dependencies: + global-dirs "^0.1.0" + is-path-inside "^1.0.0" + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= + +is-npm@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-3.0.0.tgz#ec9147bfb629c43f494cf67936a961edec7e8053" + integrity sha512-wsigDr1Kkschp2opC4G3yA6r9EgVA6NjRpWzIi9axXqeIaAATPRJc4uLujXe3Nd9uO8KoDyA4MD6aZSeXTADhA== + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + +is-path-cwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" + integrity sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0= + +is-path-cwd@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" + integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== + +is-path-in-cwd@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz#5ac48b345ef675339bd6c7a48a912110b241cf52" + integrity sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ== + dependencies: + is-path-inside "^1.0.0" + +is-path-in-cwd@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz#bfe2dca26c69f397265a4009963602935a053acb" + integrity sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ== + dependencies: + is-path-inside "^2.1.0" + +is-path-inside@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" + integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= + dependencies: + path-is-inside "^1.0.1" + +is-path-inside@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-2.1.0.tgz#7c9810587d659a40d27bcdb4d5616eab059494b2" + integrity sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg== + dependencies: + path-is-inside "^1.0.2" + +is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" + integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-promise@^2.1, is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= + +is-reference@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.1.3.tgz#e99059204b66fdbe09305cfca715a29caa5c8a51" + integrity sha512-W1iHHv/oyBb2pPxkBxtaewxa1BC58Pn5J0hogyCdefwUIvb6R+TGbAcIa4qPNYLqLhb3EnOgUf2MQkkF76BcKw== + dependencies: + "@types/estree" "0.0.39" + +is-regex@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" + integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= + dependencies: + has "^1.0.1" + +is-relative@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" + integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== + dependencies: + is-unc-path "^1.0.0" + +is-stream@^1.0.1, is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-symbol@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" + integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== + dependencies: + has-symbols "^1.0.0" + +is-text-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-2.0.0.tgz#b2484e2b720a633feb2e85b67dc193ff72c75636" + integrity sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw== + dependencies: + text-extensions "^2.0.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-unc-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" + integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== + dependencies: + unc-path-regex "^0.1.2" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= + +is-windows@^1.0.1, is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + +is-yarn-global@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" + integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isarray@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" + integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= + +isbinaryfile@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.3.tgz#5d6def3edebf6e8ca8cae9c30183a804b5f8be80" + integrity sha512-8cJBL5tTd2OS0dM4jz07wQd5g0dCCqIhUxPIGtZfa5L6hWlvV5MHTITy/DBAsF+Oe2LS1X3krBUhNwaGUWpWxw== + dependencies: + buffer-alloc "^1.2.0" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +istanbul-api@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-2.1.6.tgz#d61702a9d1c66ad89d92e66d401e16b0bda4a35f" + integrity sha512-x0Eicp6KsShG1k1rMgBAi/1GgY7kFGEBwQpw3PXGEmu+rBcBNhqU8g2DgY9mlepAsLPzrzrbqSgCGANnki4POA== + dependencies: + async "^2.6.2" + compare-versions "^3.4.0" + fileset "^2.0.3" + istanbul-lib-coverage "^2.0.5" + istanbul-lib-hook "^2.0.7" + istanbul-lib-instrument "^3.3.0" + istanbul-lib-report "^2.0.8" + istanbul-lib-source-maps "^3.0.6" + istanbul-reports "^2.2.4" + js-yaml "^3.13.1" + make-dir "^2.1.0" + minimatch "^3.0.4" + once "^1.4.0" + +istanbul-instrumenter-loader@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/istanbul-instrumenter-loader/-/istanbul-instrumenter-loader-3.0.1.tgz#9957bd59252b373fae5c52b7b5188e6fde2a0949" + integrity sha512-a5SPObZgS0jB/ixaKSMdn6n/gXSrK2S6q/UfRJBT3e6gQmVjwZROTODQsYW5ZNwOu78hG62Y3fWlebaVOL0C+w== + dependencies: + convert-source-map "^1.5.0" + istanbul-lib-instrument "^1.7.3" + loader-utils "^1.1.0" + schema-utils "^0.3.0" + +istanbul-lib-coverage@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz#ccf7edcd0a0bb9b8f729feeb0930470f9af664f0" + integrity sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ== + +istanbul-lib-coverage@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" + integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== + +istanbul-lib-hook@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz#c95695f383d4f8f60df1f04252a9550e15b5b133" + integrity sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA== + dependencies: + append-transform "^1.0.0" + +istanbul-lib-instrument@^1.7.3: + version "1.10.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz#1f55ed10ac3c47f2bdddd5307935126754d0a9ca" + integrity sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A== + dependencies: + babel-generator "^6.18.0" + babel-template "^6.16.0" + babel-traverse "^6.18.0" + babel-types "^6.18.0" + babylon "^6.18.0" + istanbul-lib-coverage "^1.2.1" + semver "^5.3.0" + +istanbul-lib-instrument@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630" + integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== + dependencies: + "@babel/generator" "^7.4.0" + "@babel/parser" "^7.4.3" + "@babel/template" "^7.4.0" + "@babel/traverse" "^7.4.3" + "@babel/types" "^7.4.0" + istanbul-lib-coverage "^2.0.5" + semver "^6.0.0" + +istanbul-lib-report@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33" + integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== + dependencies: + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + supports-color "^6.1.0" + +istanbul-lib-source-maps@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" + integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + rimraf "^2.6.3" + source-map "^0.6.1" + +istanbul-reports@^2.2.4: + version "2.2.6" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.6.tgz#7b4f2660d82b29303a8fe6091f8ca4bf058da1af" + integrity sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA== + dependencies: + handlebars "^4.1.2" + +jasmine-core@^3.3, jasmine-core@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.4.0.tgz#2a74618e966026530c3518f03e9f845d26473ce3" + integrity sha512-HU/YxV4i6GcmiH4duATwAbJQMlE0MsDIR5XmSVxURxKHn3aGAdbY1/ZJFmVRbKtnLwIxxMJD7gYaPsypcbYimg== + +jasmine-core@~2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" + integrity sha1-vMl5rh+f0FcB5F5S5l06XWPxok4= + +jasmine-spec-reporter@~4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz#1d632aec0341670ad324f92ba84b4b32b35e9e22" + integrity sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg== + dependencies: + colors "1.1.2" + +jasmine@2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-2.8.0.tgz#6b089c0a11576b1f16df11b80146d91d4e8b8a3e" + integrity sha1-awicChFXax8W3xG4AUbZHU6Lij4= + dependencies: + exit "^0.1.2" + glob "^7.0.6" + jasmine-core "~2.8.0" + +jasmine@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/jasmine/-/jasmine-3.4.0.tgz#0fa68903ff0c9697459cd044b44f4dcef5ec8bdc" + integrity sha512-sR9b4n+fnBFDEd7VS2el2DeHgKcPiMVn44rtKFumq9q7P/t8WrxsVIZPob4UDdgcDNCwyDqwxCt4k9TDRmjPoQ== + dependencies: + glob "^7.1.3" + jasmine-core "~3.4.0" + +jasminewd2@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/jasminewd2/-/jasminewd2-2.2.0.tgz#e37cf0b17f199cce23bea71b2039395246b4ec4e" + integrity sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4= + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= + +js-yaml@^3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898" + integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= + +json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + integrity sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A= + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json3@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.3.tgz#7fc10e375fc5ae42c4705a5cc0aa6f62be305b81" + integrity sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA== + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= + optionalDependencies: + graceful-fs "^4.1.6" + +jsonparse@^1.2.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" + integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +jszip@^3.1.3, jszip@^3.1.5: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.2.2.tgz#b143816df7e106a9597a94c77493385adca5bd1d" + integrity sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA== + dependencies: + lie "~3.3.0" + pako "~1.0.2" + readable-stream "~2.3.6" + set-immediate-shim "~1.0.1" + +karma-chrome-launcher@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-2.2.0.tgz#cf1b9d07136cc18fe239327d24654c3dbc368acf" + integrity sha512-uf/ZVpAabDBPvdPdveyk1EPgbnloPvFFGgmRhYLTDH7gEB4nZdSBk8yTU47w1g/drLSx5uMOkjKk7IWKfWg/+w== + dependencies: + fs-access "^1.0.0" + which "^1.2.1" + +karma-coverage-istanbul-reporter@~2.0.1: + version "2.0.6" + resolved "https://registry.yarnpkg.com/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.0.6.tgz#7b6e9c88781447bb87aa6ac24bf74b93e558adc3" + integrity sha512-WFh77RI8bMIKdOvI/1/IBmgnM+Q7NOLhnwG91QJrM8lW+CIXCjTzhhUsT/svLvAkLmR10uWY4RyYbHMLkTglvg== + dependencies: + istanbul-api "^2.1.6" + minimatch "^3.0.4" + +karma-firefox-launcher@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/karma-firefox-launcher/-/karma-firefox-launcher-1.1.0.tgz#2c47030452f04531eb7d13d4fc7669630bb93339" + integrity sha512-LbZ5/XlIXLeQ3cqnCbYLn+rOVhuMIK9aZwlP6eOLGzWdo1UVp7t6CN3DP4SafiRLjexKwHeKHDm0c38Mtd3VxA== + +karma-jasmine@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-2.0.1.tgz#26e3e31f2faf272dd80ebb0e1898914cc3a19763" + integrity sha512-iuC0hmr9b+SNn1DaUD2QEYtUxkS1J+bSJSn7ejdEexs7P8EYvA1CWkEdrDQ+8jVH3AgWlCNwjYsT1chjcNW9lA== + dependencies: + jasmine-core "^3.3" + +karma-sauce-launcher@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/karma-sauce-launcher/-/karma-sauce-launcher-2.0.2.tgz#dbf98e70d86bf287b03a537cf637eb7aefa975c3" + integrity sha512-jLUFaJhHMcKpxFWUesyWYihzM5FvQiJsDwGcCtKeOy2lsWhkVw0V0Byqb1d+wU6myU1mribBtsIcub23HS4kWA== + dependencies: + sauce-connect-launcher "^1.2.4" + saucelabs "^1.5.0" + selenium-webdriver "^4.0.0-alpha.1" + +karma-source-map-support@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz#58526ceccf7e8730e56effd97a4de8d712ac0d6b" + integrity sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A== + dependencies: + source-map-support "^0.5.5" + +karma@~4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/karma/-/karma-4.1.0.tgz#d07387c9743a575b40faf73e8a3eb5421c2193e1" + integrity sha512-xckiDqyNi512U4dXGOOSyLKPwek6X/vUizSy2f3geYevbLj+UIdvNwbn7IwfUIL2g1GXEPWt/87qFD1fBbl/Uw== + dependencies: + bluebird "^3.3.0" + body-parser "^1.16.1" + braces "^2.3.2" + chokidar "^2.0.3" + colors "^1.1.0" + connect "^3.6.0" + core-js "^2.2.0" + di "^0.0.1" + dom-serialize "^2.2.0" + flatted "^2.0.0" + glob "^7.1.1" + graceful-fs "^4.1.2" + http-proxy "^1.13.0" + isbinaryfile "^3.0.0" + lodash "^4.17.11" + log4js "^4.0.0" + mime "^2.3.1" + minimatch "^3.0.2" + optimist "^0.6.1" + qjobs "^1.1.4" + range-parser "^1.2.0" + rimraf "^2.6.0" + safe-buffer "^5.0.1" + socket.io "2.1.1" + source-map "^0.6.1" + tmp "0.0.33" + useragent "2.3.0" + +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + +killable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" + integrity sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg== + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== + +latest-version@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" + integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== + dependencies: + package-json "^6.3.0" + +lcid@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" + integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== + dependencies: + invert-kv "^2.0.0" + +less-loader@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/less-loader/-/less-loader-4.1.0.tgz#2c1352c5b09a4f84101490274fd51674de41363e" + integrity sha512-KNTsgCE9tMOM70+ddxp9yyt9iHqgmSs0yTZc5XH5Wo+g80RWRIYNqE58QJKm/yMud5wZEvz50ugRDuzVIkyahg== + dependencies: + clone "^2.1.1" + loader-utils "^1.1.0" + pify "^3.0.0" + +less-plugin-npm-import@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/less-plugin-npm-import/-/less-plugin-npm-import-2.1.0.tgz#823e6986c93318a98171ca858848b6bead55bf3e" + integrity sha1-gj5phskzGKmBccqFiEi2vq1Vvz4= + dependencies: + promise "~7.0.1" + resolve "~1.1.6" + +less@3.9.0, less@^3.8.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/less/-/less-3.9.0.tgz#b7511c43f37cf57dc87dffd9883ec121289b1474" + integrity sha512-31CmtPEZraNUtuUREYjSqRkeETFdyEHSEPAGq4erDlUXtda7pzNmctdljdIagSb589d/qXGWiiP31R5JVf+v0w== + dependencies: + clone "^2.1.2" + optionalDependencies: + errno "^0.1.1" + graceful-fs "^4.1.2" + image-size "~0.5.0" + mime "^1.4.1" + mkdirp "^0.5.0" + promise "^7.1.1" + request "^2.83.0" + source-map "~0.6.0" + +license-webpack-plugin@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/license-webpack-plugin/-/license-webpack-plugin-2.1.1.tgz#f0ab760f7f301c76f5af52e480f320656b5721bb" + integrity sha512-TiarZIg5vkQ2rGdYJn2+5YxO/zqlqjpK5IVglr7OfmrN1sBCakS+PQrsP2uC5gtve1ZDb9WMSUMlmHDQ0FoW4w== + dependencies: + "@types/webpack-sources" "^0.1.5" + webpack-sources "^1.2.0" + +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== + dependencies: + immediate "~3.0.5" + +liftoff@^2.1.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/liftoff/-/liftoff-2.5.0.tgz#2009291bb31cea861bbf10a7c15a28caf75c31ec" + integrity sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew= + dependencies: + extend "^3.0.0" + findup-sync "^2.0.0" + fined "^1.0.1" + flagged-respawn "^1.0.0" + is-plain-object "^2.0.4" + object.map "^1.0.0" + rechoir "^0.6.2" + resolve "^1.1.7" + +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + +loader-runner@^2.3.0, loader-runner@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" + integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== + +loader-utils@1.2.3, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" + integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA== + dependencies: + big.js "^5.2.2" + emojis-list "^2.0.0" + json5 "^1.0.1" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +lodash._basecopy@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" + integrity sha1-jaDmqHbPNEwK2KVIghEd08XHyjY= + +lodash._basetostring@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz#d1861d877f824a52f669832dcaf3ee15566a07d5" + integrity sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U= + +lodash._basevalues@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz#5b775762802bde3d3297503e26300820fdf661b7" + integrity sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc= + +lodash._getnative@^3.0.0: + version "3.9.1" + resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= + +lodash._isiterateecall@^3.0.0: + version "3.0.9" + resolved "https://registry.yarnpkg.com/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz#5203ad7ba425fae842460e696db9cf3e6aac057c" + integrity sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw= + +lodash._reescape@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reescape/-/lodash._reescape-3.0.0.tgz#2b1d6f5dfe07c8a355753e5f27fac7f1cde1616a" + integrity sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo= + +lodash._reevaluate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz#58bc74c40664953ae0b124d806996daca431e2ed" + integrity sha1-WLx0xAZklTrgsSTYBpltrKQx4u0= + +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= + +lodash._root@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/lodash._root/-/lodash._root-3.0.1.tgz#fba1c4524c19ee9a5f8136b4609f017cf4ded692" + integrity sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI= + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + +lodash.escape@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/lodash.escape/-/lodash.escape-3.2.0.tgz#995ee0dc18c1b48cc92effae71a10aab5b487698" + integrity sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg= + dependencies: + lodash._root "^3.0.0" + +lodash.flattendeep@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" + integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= + +lodash.isarguments@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo= + +lodash.isarray@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + integrity sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U= + +lodash.ismatch@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" + integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= + +lodash.keys@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" + integrity sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo= + dependencies: + lodash._getnative "^3.0.0" + lodash.isarguments "^3.0.0" + lodash.isarray "^3.0.0" + +lodash.restparam@^3.0.0: + version "3.6.1" + resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805" + integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU= + +lodash.tail@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" + integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ= + +lodash.template@^3.0.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-3.6.2.tgz#f8cdecc6169a255be9098ae8b0c53d378931d14f" + integrity sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8= + dependencies: + lodash._basecopy "^3.0.0" + lodash._basetostring "^3.0.0" + lodash._basevalues "^3.0.0" + lodash._isiterateecall "^3.0.0" + lodash._reinterpolate "^3.0.0" + lodash.escape "^3.0.0" + lodash.keys "^3.0.0" + lodash.restparam "^3.0.0" + lodash.templatesettings "^3.0.0" + +lodash.template@^4.0.2: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5" + integrity sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU= + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.escape "^3.0.0" + +lodash.templatesettings@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== + dependencies: + lodash._reinterpolate "^3.0.0" + +lodash@^4.14.14, lodash@^4.16.6, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +lodash@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-1.0.2.tgz#8f57560c83b59fc270bd3d561b690043430e2551" + integrity sha1-j1dWDIO1n8JwvT1WG2kAQ0MOJVE= + +log4js@^4.0.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/log4js/-/log4js-4.5.1.tgz#e543625e97d9e6f3e6e7c9fc196dd6ab2cae30b5" + integrity sha512-EEEgFcE9bLgaYUKuozyFfytQM2wDHtXn4tAN41pkaxpNjAykv11GVdeI4tHtmPWW4Xrgh9R/2d7XYghDVjbKKw== + dependencies: + date-format "^2.0.0" + debug "^4.1.1" + flatted "^2.0.0" + rfdc "^1.1.4" + streamroller "^1.0.6" + +loglevel@^1.6.1, loglevel@^1.6.3: + version "1.6.3" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.3.tgz#77f2eb64be55a404c9fd04ad16d57c1d6d6b1280" + integrity sha512-LoEDv5pgpvWgPF4kNYuIp0qqSJVWak/dML0RY74xlzMZiT9w77teNAwKYKWBTYjlokMirg+o3jBwp+vlLrcfAA== + +loose-envify@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + +lru-cache@2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" + integrity sha1-bUUk6LlV+V1PW1iFHOId1y+06VI= + +lru-cache@4.1.x, lru-cache@^4.0.1: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-queue@0.1: + version "0.1.0" + resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" + integrity sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM= + dependencies: + es5-ext "~0.10.2" + +magic-string@0.25.2: + version "0.25.2" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.2.tgz#139c3a729515ec55e96e69e82a11fe890a293ad9" + integrity sha512-iLs9mPjh9IuTtRsqqhNGYcZXGei0Nh/A4xirrsqW7c+QhKVFL2vm7U09ru6cHRD22azaP/wMDgI+HCqbETMTtg== + dependencies: + sourcemap-codec "^1.4.4" + +magic-string@0.25.3, magic-string@^0.25.0, magic-string@^0.25.2: + version "0.25.3" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.3.tgz#34b8d2a2c7fec9d9bdf9929a3fd81d271ef35be9" + integrity sha512-6QK0OpF/phMz0Q2AxILkX2mFhi7m+WMwTRg0LQKq/WBB0cDP4rYH3Wp4/d3OTXlrPLVJT/RFqj8tFeAR4nk8AA== + dependencies: + sourcemap-codec "^1.4.4" + +make-dir@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== + dependencies: + pify "^3.0.0" + +make-dir@^2.0.0, make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-error@^1.1.1: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== + +make-fetch-happen@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-5.0.0.tgz#a8e3fe41d3415dd656fe7b8e8172e1fb4458b38d" + integrity sha512-nFr/vpL1Jc60etMVKeaLOqfGjMMb3tAHFVJWxHOFCFS04Zmd7kGlMxo0l1tzfhoQje0/UPnd0X8OeGUiXXnfPA== + dependencies: + agentkeepalive "^3.4.1" + cacache "^12.0.0" + http-cache-semantics "^3.8.1" + http-proxy-agent "^2.1.0" + https-proxy-agent "^2.2.1" + lru-cache "^5.1.1" + mississippi "^3.0.0" + node-fetch-npm "^2.0.2" + promise-retry "^1.1.1" + socks-proxy-agent "^4.0.0" + ssri "^6.0.0" + +make-iterator@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" + integrity sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw== + dependencies: + kind-of "^6.0.2" + +mamacro@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/mamacro/-/mamacro-0.0.3.tgz#ad2c9576197c9f1abf308d0787865bd975a3f3e4" + integrity sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA== + +map-age-cleaner@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" + integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== + dependencies: + p-defer "^1.0.0" + +map-cache@^0.2.0, map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= + +map-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" + integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk= + +map-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8" + integrity sha1-ih8HiW2CsQkmvTdEokIACfiJdKg= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +marked@^0.6.1: + version "0.6.3" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.6.3.tgz#79babad78af638ba4d522a9e715cdfdd2429e946" + integrity sha512-Fqa7eq+UaxfMriqzYLayfqAE40WN03jf+zHjT18/uXNuzjq3TY0XTbrAoPeqSJrAmPz11VuUA+kBPYOhHt9oOQ== + +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +mem@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" + integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== + dependencies: + map-age-cleaner "^0.1.1" + mimic-fn "^2.0.0" + p-is-promise "^2.0.0" + +memoizee@^0.4.14: + version "0.4.14" + resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57" + integrity sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg== + dependencies: + d "1" + es5-ext "^0.10.45" + es6-weak-map "^2.0.2" + event-emitter "^0.3.5" + is-promise "^2.1" + lru-queue "0.1" + next-tick "1" + timers-ext "^0.1.5" + +memory-fs@^0.4.0, memory-fs@^0.4.1, memory-fs@~0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" + integrity sha1-OpoguEYlI+RHz7x+i7gO1me/xVI= + dependencies: + errno "^0.1.3" + readable-stream "^2.0.1" + +meow@^3.3.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +meow@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/meow/-/meow-4.0.1.tgz#d48598f6f4b1472f35bf6317a95945ace347f975" + integrity sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A== + dependencies: + camelcase-keys "^4.0.0" + decamelize-keys "^1.0.0" + loud-rejection "^1.0.0" + minimist "^1.1.3" + minimist-options "^3.0.1" + normalize-package-data "^2.3.4" + read-pkg-up "^3.0.0" + redent "^2.0.0" + trim-newlines "^2.0.0" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +merge-source-map@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" + integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== + dependencies: + source-map "^0.6.1" + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +micromatch@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +mime-db@1.40.0, "mime-db@>= 1.40.0 < 2": + version "1.40.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" + integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== + +mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: + version "2.1.24" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" + integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== + dependencies: + mime-db "1.40.0" + +mime@1.6.0, mime@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^2.3.1, mime@^2.4.2: + version "2.4.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" + integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== + +mimic-fn@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== + +mimic-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mimic-response@^1.0.0, mimic-response@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +mini-css-extract-plugin@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.6.0.tgz#a3f13372d6fcde912f3ee4cd039665704801e3b9" + integrity sha512-79q5P7YGI6rdnVyIAV4NXpBQJFWdkzJxCim3Kog4078fM0piAaFlwocqbejdWtLW1cEzCexPrh6EdyFsPgVdAw== + dependencies: + loader-utils "^1.1.0" + normalize-url "^2.0.1" + schema-utils "^1.0.0" + webpack-sources "^1.1.0" + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= + +minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^2.0.1: + version "2.0.10" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-2.0.10.tgz#8d087c39c6b38c001b97fca7ce6d0e1e80afbac7" + integrity sha1-jQh8OcazjAAbl/ynzm0OHoCvusc= + dependencies: + brace-expansion "^1.0.0" + +minimatch@~0.2.11: + version "0.2.14" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.2.14.tgz#c74e780574f63c6f9a090e90efbe6ef53a6a756a" + integrity sha1-x054BXT2PG+aCQ6Q775u9TpqdWo= + dependencies: + lru-cache "2" + sigmund "~1.0.0" + +minimist-options@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954" + integrity sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ== + dependencies: + arrify "^1.0.1" + is-plain-obj "^1.1.0" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.1.0, minimist@^1.1.3, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= + +minipass@^2.2.1, minipass@^2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" + integrity sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minizlib@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" + integrity sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA== + dependencies: + minipass "^2.2.1" + +mississippi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-3.0.0.tgz#ea0a3291f97e0b5e8776b363d5f0a12d94c67022" + integrity sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA== + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^3.0.0" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mixin-object@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" + integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4= + dependencies: + for-in "^0.1.3" + is-extendable "^0.1.1" + +mkdirp@0.5.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +modify-values@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" + integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== + +move-concurrently@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" + integrity sha1-viwAX9oy4LKa8fBdfEszIUxwH5I= + dependencies: + aproba "^1.1.1" + copy-concurrently "^1.0.0" + fs-write-stream-atomic "^1.0.8" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.3" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +ms@^2.0.0, ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= + +multicast-dns@^6.0.1: + version "6.2.3" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-6.2.3.tgz#a0ec7bd9055c4282f790c3c82f4e28db3b31b229" + integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== + dependencies: + dns-packet "^1.3.1" + thunky "^1.0.2" + +multipipe@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-0.1.2.tgz#2a8f2ddf70eed564dff2d57f1e1a137d9f05078b" + integrity sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s= + dependencies: + duplexer2 "0.0.2" + +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= + +nan@^2.12.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +natives@^1.1.0: + version "1.1.6" + resolved "https://registry.yarnpkg.com/natives/-/natives-1.1.6.tgz#a603b4a498ab77173612b9ea1acdec4d980f00bb" + integrity sha512-6+TDFewD4yxY14ptjKaS63GVdtKiES1pTPyxn9Jb0rBqPMZ7VcCiooEhPNsr+mqHtMGxa/5c/HhcC4uPEUw/nA== + +needle@^2.2.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" + integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== + dependencies: + debug "^3.2.6" + iconv-lite "^0.4.4" + sax "^1.2.4" + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" + integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== + +nested-error-stacks@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz#0fbdcf3e13fe4994781280524f8b96b0cdff9c61" + integrity sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug== + +next-tick@1, next-tick@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" + integrity sha1-yobR/ogoFpsBICCOPchCS524NCw= + +ng-packagr@^5.2.0: + version "5.4.3" + resolved "https://registry.yarnpkg.com/ng-packagr/-/ng-packagr-5.4.3.tgz#69816c7f605d6c6af3ba72dbe96053095ff9d3f6" + integrity sha512-hNYtJsQ67xQzCeBCAk+zg/x342ETKgKztoV+P3UL4Ri0Yt4CyJJpCbNTqUsy4HmKw4BjKyJMMtB5V0xmSjN5pw== + dependencies: + ajv "^6.10.2" + autoprefixer "^9.6.0" + browserslist "^4.0.0" + chalk "^2.3.1" + chokidar "^3.0.0" + clean-css "^4.1.11" + commander "^2.12.0" + fs-extra "^8.0.0" + glob "^7.1.2" + injection-js "^2.2.1" + less "^3.8.0" + less-plugin-npm-import "^2.1.0" + node-sass-tilde-importer "^1.0.0" + postcss "^7.0.0" + postcss-url "^8.0.0" + read-pkg-up "^5.0.0" + rimraf "^2.6.1" + rollup "^1.12.1" + rollup-plugin-commonjs "^10.0.0" + rollup-plugin-json "^4.0.0" + rollup-plugin-node-resolve "^5.0.0" + rollup-plugin-sourcemaps "^0.4.2" + rxjs "^6.0.0" + sass "^1.17.3" + stylus "^0.54.5" + terser "^4.1.2" + update-notifier "^3.0.0" + +ngx-build-plus@^8.0.0-rc.3.0.1: + version "8.1.3" + resolved "https://registry.yarnpkg.com/ngx-build-plus/-/ngx-build-plus-8.1.3.tgz#f0f6efa5b0c204f1e3421095112660bff092dd02" + integrity sha512-RU4GUzYYAmok1y1wuqlvIKNkvtoxiPCisSlKW+sz6v0K3RDuBqszUpYuFr21A//hzYDMOpSsxoicpPfzdgkSmA== + dependencies: + "@schematics/angular" "8.0.0" + cross-spawn "^6.0.5" + rxjs "6.4.0" + webpack-dev-server "^3.1.14" + webpack-merge "^4.2.1" + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-fetch-npm@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz#7258c9046182dca345b4208eda918daf33697ff7" + integrity sha512-nJIxm1QmAj4v3nfCvEeCrYSoVwXyxLnaPBK5W1W5DGEJwjlKuC2VEUycGw5oxk+4zZahRrB84PUJJgEmhFTDFw== + dependencies: + encoding "^0.1.11" + json-parse-better-errors "^1.0.0" + safe-buffer "^5.1.1" + +node-forge@0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df" + integrity sha512-MmbQJ2MTESTjt3Gi/3yG1wGpIMhUfcIypUCGtTizFR9IiccFwxSpfp0vtIZlkFclEqERemxfnSdZEMR9VqqEFQ== + +node-libs-browser@^2.0.0, node-libs-browser@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" + integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== + dependencies: + assert "^1.1.1" + browserify-zlib "^0.2.0" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^3.0.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" + path-browserify "0.0.1" + process "^0.11.10" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.3.3" + stream-browserify "^2.0.1" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.11.0" + vm-browserify "^1.0.1" + +node-notifier@^5.4.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.1.tgz#7c0192cc63aedb25cd99619174daa27902b10903" + integrity sha512-p52B+onAEHKW1OF9MGO/S7k/ahGEHfhP5/tvwYzog/5XLYOd8ZuD6vdNZdUuWMONRnKPneXV43v3s6Snx1wsCQ== + dependencies: + growly "^1.3.0" + is-wsl "^1.1.0" + semver "^5.5.0" + shellwords "^0.1.1" + which "^1.3.0" + +node-pre-gyp@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" + integrity sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A== + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + +node-releases@^1.1.14, node-releases@^1.1.25: + version "1.1.26" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.26.tgz#f30563edc5c7dc20cf524cc8652ffa7be0762937" + integrity sha512-fZPsuhhUHMTlfkhDLGtfY80DSJTjOcx+qD1j5pqPkuhUHVS7xHZIg9EE4DHK8O3f0zTxXHX5VIkDG8pu98/wfQ== + dependencies: + semver "^5.3.0" + +node-sass-tilde-importer@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/node-sass-tilde-importer/-/node-sass-tilde-importer-1.0.2.tgz#1a15105c153f648323b4347693fdb0f331bad1ce" + integrity sha512-Swcmr38Y7uB78itQeBm3mThjxBy9/Ah/ykPIaURY/L6Nec9AyRoL/jJ7ECfMR+oZeCTVQNxVMu/aHU+TLRVbdg== + dependencies: + find-parent-dir "^0.3.0" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + integrity sha1-0NRoWv1UFRk8jHUFYC0NF81kR00= + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5, normalize-package-data@^2.4.0, normalize-package-data@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= + +normalize-url@^1.0.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-1.9.1.tgz#2cc0d66b31ea23036458436e3620d85954c66c3c" + integrity sha1-LMDWazHqIwNkWENuNiDYWVTGbDw= + dependencies: + object-assign "^4.0.1" + prepend-http "^1.0.0" + query-string "^4.1.0" + sort-keys "^1.0.0" + +normalize-url@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-2.0.1.tgz#835a9da1551fa26f70e92329069a23aa6574d7e6" + integrity sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw== + dependencies: + prepend-http "^2.0.0" + query-string "^5.0.1" + sort-keys "^2.0.0" + +normalize-url@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.3.0.tgz#9c49e10fc1876aeb76dba88bf1b2b5d9fa57b2ee" + integrity sha512-0NLtR71o4k6GLP+mr6Ty34c5GA6CMoEsncKJxvQd8NzPxaHRJNnb5gZE8R1XF4CPIS7QPHLJ74IFszwtNVAHVQ== + +npm-bundled@^1.0.1: + version "1.0.6" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.6.tgz#e7ba9aadcef962bb61248f91721cd932b3fe6bdd" + integrity sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g== + +npm-package-arg@6.1.0, npm-package-arg@^6.0.0, npm-package-arg@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-6.1.0.tgz#15ae1e2758a5027efb4c250554b85a737db7fcc1" + integrity sha512-zYbhP2k9DbJhA0Z3HKUePUgdB1x7MfIfKssC+WLPFMKTBZKpZh5m13PgexJjCq6KW7j17r0jHWcCpxEqnnncSA== + dependencies: + hosted-git-info "^2.6.0" + osenv "^0.1.5" + semver "^5.5.0" + validate-npm-package-name "^3.0.0" + +npm-packlist@^1.1.12, npm-packlist@^1.1.6: + version "1.4.4" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.4.4.tgz#866224233850ac534b63d1a6e76050092b5d2f44" + integrity sha512-zTLo8UcVYtDU3gdeaFu2Xu0n0EvelfHDGuqtNIn5RO7yQj4H1TqNdBc/yZjxnWA0PVB8D3Woyp0i5B43JwQ6Vw== + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + +npm-pick-manifest@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-2.2.3.tgz#32111d2a9562638bb2c8f2bf27f7f3092c8fae40" + integrity sha512-+IluBC5K201+gRU85vFlUwX3PFShZAbAgDNp2ewJdWMVSppdo/Zih0ul2Ecky/X7b51J7LrrUAP+XOmOCvYZqA== + dependencies: + figgy-pudding "^3.5.1" + npm-package-arg "^6.0.0" + semver "^5.4.1" + +npm-registry-fetch@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/npm-registry-fetch/-/npm-registry-fetch-4.0.0.tgz#5ef75845b605855c7964472542c25da172af8677" + integrity sha512-Jllq35Jag8dtv0M17ue74XtdQTyqKzuAYGiX9mAjOhkmNjib3bBUgK6mUY61+AHnXeSRobQkpY3/xIOS/omptw== + dependencies: + JSONStream "^1.3.4" + bluebird "^3.5.1" + figgy-pudding "^3.4.1" + lru-cache "^5.1.1" + make-fetch-happen "^5.0.0" + npm-package-arg "^6.1.0" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +null-check@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/null-check/-/null-check-1.0.0.tgz#977dffd7176012b9ec30d2a39db5cf72a0439edd" + integrity sha1-l33/1xdgErnsMNKjnbXPcqBDnt0= + +num2fraction@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" + integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +nyc@14.1.1: + version "14.1.1" + resolved "https://registry.yarnpkg.com/nyc/-/nyc-14.1.1.tgz#151d64a6a9f9f5908a1b73233931e4a0a3075eeb" + integrity sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw== + dependencies: + archy "^1.0.0" + caching-transform "^3.0.2" + convert-source-map "^1.6.0" + cp-file "^6.2.0" + find-cache-dir "^2.1.0" + find-up "^3.0.0" + foreground-child "^1.5.6" + glob "^7.1.3" + istanbul-lib-coverage "^2.0.5" + istanbul-lib-hook "^2.0.7" + istanbul-lib-instrument "^3.3.0" + istanbul-lib-report "^2.0.8" + istanbul-lib-source-maps "^3.0.6" + istanbul-reports "^2.2.4" + js-yaml "^3.13.1" + make-dir "^2.1.0" + merge-source-map "^1.1.0" + resolve-from "^4.0.0" + rimraf "^2.6.3" + signal-exit "^3.0.2" + spawn-wrap "^1.4.2" + test-exclude "^5.2.3" + uuid "^3.3.2" + yargs "^13.2.2" + yargs-parser "^13.0.0" + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-3.0.0.tgz#9bedd5ca0897949bca47e7ff408062d549f587f2" + integrity sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I= + +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-component@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" + integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE= + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-keys@^1.0.12: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.defaults@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.defaults/-/object.defaults-1.1.0.tgz#3a7f868334b407dea06da16d88d5cd29e435fecf" + integrity sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8= + dependencies: + array-each "^1.0.1" + array-slice "^1.0.0" + for-own "^1.0.0" + isobject "^3.0.0" + +object.getownpropertydescriptors@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" + integrity sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY= + dependencies: + define-properties "^1.1.2" + es-abstract "^1.5.1" + +object.map@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object.map/-/object.map-1.0.1.tgz#cf83e59dc8fcc0ad5f4250e1f78b3b81bd801d37" + integrity sha1-z4Plncj8wK1fQlDh94s7gb2AHTc= + dependencies: + for-own "^1.0.0" + make-iterator "^1.0.0" + +object.pick@^1.2.0, object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +once@~1.3.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/once/-/once-1.3.3.tgz#b2e261557ce4c314ec8304f3fa82663e4297ca20" + integrity sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA= + dependencies: + wrappy "1" + +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= + dependencies: + mimic-fn "^1.0.0" + +open@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/open/-/open-6.2.0.tgz#7cf92cb961b5d8498b071e64098bf5e27f57230c" + integrity sha512-Vxf6HJkwrqmvh9UAID3MnMYXntbTxKLOSfOnO7LJdzPf3NE3KQYFNV0/Lcz2VAndbRFil58XVCyh8tiX11fiYw== + dependencies: + is-wsl "^1.1.0" + +open@6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/open/-/open-6.4.0.tgz#5c13e96d0dc894686164f18965ecfe889ecfc8a9" + integrity sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg== + dependencies: + is-wsl "^1.1.0" + +opn@^5.5.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" + integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== + dependencies: + is-wsl "^1.1.0" + +optimist@^0.6.1, optimist@~0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + +orchestrator@^0.3.0: + version "0.3.8" + resolved "https://registry.yarnpkg.com/orchestrator/-/orchestrator-0.3.8.tgz#14e7e9e2764f7315fbac184e506c7aa6df94ad7e" + integrity sha1-FOfp4nZPcxX7rBhOUGx6pt+UrX4= + dependencies: + end-of-stream "~0.1.5" + sequencify "~0.0.7" + stream-consume "~0.1.0" + +ordered-read-streams@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-0.1.0.tgz#fd565a9af8eb4473ba69b6ed8a34352cb552f126" + integrity sha1-/VZamvjrRHO6abbtijQ1LLVS8SY= + +original@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/original/-/original-1.0.2.tgz#e442a61cffe1c5fd20a65f3261c26663b303f25f" + integrity sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg== + dependencies: + url-parse "^1.4.3" + +os-browserify@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= + +os-homedir@^1.0.0, os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + +os-locale@^3.0.0, os-locale@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" + integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== + dependencies: + execa "^1.0.0" + lcid "^2.0.0" + mem "^4.0.0" + +os-tmpdir@^1.0.0, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +osenv@^0.1.4, osenv@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + +p-defer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-is-promise@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" + integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-limit@^2.0.0, p-limit@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" + integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ== + dependencies: + p-try "^2.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= + dependencies: + p-limit "^1.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-map@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + +p-retry@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" + integrity sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w== + dependencies: + retry "^0.12.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +package-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/package-hash/-/package-hash-3.0.0.tgz#50183f2d36c9e3e528ea0a8605dff57ce976f88e" + integrity sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA== + dependencies: + graceful-fs "^4.1.15" + hasha "^3.0.0" + lodash.flattendeep "^4.4.0" + release-zalgo "^1.0.0" + +package-json@^6.3.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" + integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== + dependencies: + got "^9.6.0" + registry-auth-token "^4.0.0" + registry-url "^5.0.0" + semver "^6.2.0" + +pacote@9.5.4: + version "9.5.4" + resolved "https://registry.yarnpkg.com/pacote/-/pacote-9.5.4.tgz#8baa26f3d1326d13dc2fe0fe84040a364ae30aad" + integrity sha512-nWr0ari6E+apbdoN0hToTKZElO5h4y8DGFa2pyNA5GQIdcP0imC96bA0bbPw1gpeguVIiUgHHaAlq/6xfPp8Qw== + dependencies: + bluebird "^3.5.3" + cacache "^12.0.0" + figgy-pudding "^3.5.1" + get-stream "^4.1.0" + glob "^7.1.3" + lru-cache "^5.1.1" + make-fetch-happen "^5.0.0" + minimatch "^3.0.4" + minipass "^2.3.5" + mississippi "^3.0.0" + mkdirp "^0.5.1" + normalize-package-data "^2.4.0" + npm-package-arg "^6.1.0" + npm-packlist "^1.1.12" + npm-pick-manifest "^2.2.3" + npm-registry-fetch "^4.0.0" + osenv "^0.1.5" + promise-inflight "^1.0.1" + promise-retry "^1.1.1" + protoduck "^5.0.1" + rimraf "^2.6.2" + safe-buffer "^5.1.2" + semver "^5.6.0" + ssri "^6.0.1" + tar "^4.4.8" + unique-filename "^1.1.1" + which "^1.3.1" + +pako@~1.0.2, pako@~1.0.5: + version "1.0.10" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.10.tgz#4328badb5086a426aa90f541977d4955da5c9732" + integrity sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw== + +parallel-transform@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" + integrity sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY= + dependencies: + cyclist "~0.2.2" + inherits "^2.0.3" + readable-stream "^2.1.5" + +parse-asn1@^5.0.0: + version "5.1.4" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.4.tgz#37f6628f823fbdeb2273b4d540434a22f3ef1fcc" + integrity sha512-Qs5duJcuvNExRfFZ99HDD3z4mAi3r9Wl/FOjEOijlxwCZs7E7mW2vjTpgQ4J8LpTF8x5v+1Vn5UQFejmWT11aw== + dependencies: + asn1.js "^4.0.0" + browserify-aes "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + safe-buffer "^5.1.1" + +parse-filepath@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/parse-filepath/-/parse-filepath-1.0.2.tgz#a632127f53aaf3d15876f5872f3ffac763d6c891" + integrity sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE= + dependencies: + is-absolute "^1.0.0" + map-cache "^0.2.0" + path-root "^0.1.1" + +parse-github-repo-url@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz#9e7d8bb252a6cb6ba42595060b7bf6df3dbc1f50" + integrity sha1-nn2LslKmy2ukJZUGC3v23z28H1A= + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= + dependencies: + error-ex "^1.2.0" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse-json@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.0.0.tgz#73e5114c986d143efa3712d4ea24db9a4266f60f" + integrity sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + lines-and-columns "^1.1.6" + +parse-node-version@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-node-version/-/parse-node-version-1.0.1.tgz#e2b5dbede00e7fa9bc363607f53327e8b073189b" + integrity sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA== + +parse-passwd@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" + integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= + +parse5@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" + integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== + +parseqs@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" + integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0= + dependencies: + better-assert "~1.0.0" + +parseuri@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" + integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo= + dependencies: + better-assert "~1.0.0" + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +path-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" + integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-is-inside@^1.0.1, path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-root-regex@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/path-root-regex/-/path-root-regex-0.1.2.tgz#bfccdc8df5b12dc52c8b43ec38d18d72c04ba96d" + integrity sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0= + +path-root@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/path-root/-/path-root-0.1.1.tgz#9a4a6814cac1c0cd73360a95f32083c8ea4745b7" + integrity sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc= + dependencies: + path-root-regex "^0.1.0" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + +pause-stream@^0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" + integrity sha1-/lo0sMvOErWqaitAPuLnO2AvFEU= + dependencies: + through "~2.3" + +pbkdf2@^3.0.3: + version "3.0.17" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" + integrity sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA== + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +picomatch@^2.0.4, picomatch@^2.0.5: + version "2.0.7" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" + integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== + +pify@^2.0.0, pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + +pkginfo@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21" + integrity sha1-Wyn2qB9wcXFC4J52W76rl7T4HiE= + +portfinder@^1.0.20: + version "1.0.21" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.21.tgz#60e1397b95ac170749db70034ece306b9a27e324" + integrity sha512-ESabpDCzmBS3ekHbmpAIiESq3udRsCBGiBZLsC+HgBKv2ezb0R4oG+7RnYEVZ/ZCfhel5Tx3UzdNWA0Lox2QCA== + dependencies: + async "^1.5.2" + debug "^2.2.0" + mkdirp "0.5.x" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +postcss-import@12.0.1: + version "12.0.1" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-12.0.1.tgz#cf8c7ab0b5ccab5649024536e565f841928b7153" + integrity sha512-3Gti33dmCjyKBgimqGxL3vcV8w9+bsHwO5UrBawp796+jdardbcFl4RP5w/76BwNL7aGzpKstIfF9I+kdE8pTw== + dependencies: + postcss "^7.0.1" + postcss-value-parser "^3.2.3" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-load-config@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-2.1.0.tgz#c84d692b7bb7b41ddced94ee62e8ab31b417b003" + integrity sha512-4pV3JJVPLd5+RueiVVB+gFOAa7GWc25XQcMp86Zexzke69mKf6Nx9LRcQywdz7yZI9n1udOxmLuAwTBypypF8Q== + dependencies: + cosmiconfig "^5.0.0" + import-cwd "^2.0.0" + +postcss-loader@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-3.0.0.tgz#6b97943e47c72d845fa9e03f273773d4e8dd6c2d" + integrity sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA== + dependencies: + loader-utils "^1.1.0" + postcss "^7.0.0" + postcss-load-config "^2.0.0" + schema-utils "^1.0.0" + +postcss-url@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/postcss-url/-/postcss-url-8.0.0.tgz#7b10059bd12929cdbb1971c60f61a0e5af86b4ca" + integrity sha512-E2cbOQ5aii2zNHh8F6fk1cxls7QVFZjLPSrqvmiza8OuXLzIpErij8BDS5Y3STPfJgpIMNCPEr8JlKQWEoozUw== + dependencies: + mime "^2.3.1" + minimatch "^3.0.4" + mkdirp "^0.5.0" + postcss "^7.0.2" + xxhashjs "^0.2.1" + +postcss-value-parser@^3.2.3, postcss-value-parser@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" + integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== + +postcss-value-parser@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.1.tgz#e3f6172cc91302912c89da55a42454025485250f" + integrity sha512-3Jk+/CVH0HBfgSSFWALKm9Hyzf4kumPjZfUxkRYZNcqFztELb2APKxv0nlX8HCdc1/ymePmT/nFf1ST6fjWH2A== + +postcss@7.0.14: + version "7.0.14" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.14.tgz#4527ed6b1ca0d82c53ce5ec1a2041c2346bbd6e5" + integrity sha512-NsbD6XUUMZvBxtQAJuWDJeeC4QFsmWsfozWxCJPWf3M55K9iu2iMDaKqyoOdTJ1R4usBXuxlVFAIo8rZPQD4Bg== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.17, postcss@^7.0.2: + version "7.0.17" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.17.tgz#4da1bdff5322d4a0acaab4d87f3e782436bad31f" + integrity sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ== + dependencies: + chalk "^2.4.2" + source-map "^0.6.1" + supports-color "^6.1.0" + +prepend-http@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= + +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" + integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= + +pretty-hrtime@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" + integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= + +prismjs@1.16.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.16.0.tgz#406eb2c8aacb0f5f0f1167930cb83835d10a4308" + integrity sha512-OA4MKxjFZHSvZcisLGe14THYsug/nF6O1f0pAJc0KN0wTyAcLqmsbE+lTGKSpyh+9pEW57+k6pg2AfYR+coyHA== + optionalDependencies: + clipboard "^2.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= + +promise-retry@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-1.1.1.tgz#6739e968e3051da20ce6497fb2b50f6911df3d6d" + integrity sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0= + dependencies: + err-code "^1.0.0" + retry "^0.10.0" + +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== + dependencies: + asap "~2.0.3" + +promise@~7.0.1: + version "7.0.4" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.0.4.tgz#363e84a4c36c8356b890fed62c91ce85d02ed539" + integrity sha1-Nj6EpMNsg1a4kP7WLJHOhdAu1Tk= + dependencies: + asap "~2.0.3" + +protoduck@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/protoduck/-/protoduck-5.0.1.tgz#03c3659ca18007b69a50fd82a7ebcc516261151f" + integrity sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg== + dependencies: + genfun "^5.0.0" + +protractor@~5.4.0: + version "5.4.2" + resolved "https://registry.yarnpkg.com/protractor/-/protractor-5.4.2.tgz#329efe37f48b2141ab9467799be2d4d12eb48c13" + integrity sha512-zlIj64Cr6IOWP7RwxVeD8O4UskLYPoyIcg0HboWJL9T79F1F0VWtKkGTr/9GN6BKL+/Q/GmM7C9kFVCfDbP5sA== + dependencies: + "@types/q" "^0.0.32" + "@types/selenium-webdriver" "^3.0.0" + blocking-proxy "^1.0.0" + browserstack "^1.5.1" + chalk "^1.1.3" + glob "^7.0.3" + jasmine "2.8.0" + jasminewd2 "^2.1.0" + optimist "~0.6.0" + q "1.4.1" + saucelabs "^1.5.0" + selenium-webdriver "3.6.0" + source-map-support "~0.4.0" + webdriver-js-extender "2.1.0" + webdriver-manager "^12.0.6" + +proxy-addr@~2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" + integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.0" + +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + integrity sha1-0/wRS6BplaRexok/SEzrHXj19HY= + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + +psl@^1.1.24: + version "1.3.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.3.0.tgz#e1ebf6a3b5564fa8376f3da2275da76d875ca1bd" + integrity sha512-avHdspHO+9rQTLbv1RO+MPYeP/SzsCoxofjVnHanETfQhTJrmB0HlDoW+EiN/R+C0BZ+gERab9NY0lPN2TxNag== + +public-encrypt@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" + integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + safe-buffer "^5.1.2" + +pump@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.5.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== + dependencies: + duplexify "^3.6.0" + inherits "^2.0.3" + pump "^2.0.0" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + +punycode@^1.2.4, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +q@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" + integrity sha1-VXBbzZPF82c1MMLCy8DCs63cKG4= + +q@^1.4.1, q@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" + integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= + +qjobs@^1.1.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.2.0.tgz#c45e9c61800bd087ef88d7e256423bdd49e5d071" + integrity sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg== + +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +query-string@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb" + integrity sha1-u7aTucqRXCMlFbIosaArYJBD2+s= + dependencies: + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +query-string@^5.0.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" + integrity sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw== + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM= + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +querystringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" + integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== + +quick-lru@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" + integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" + integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + +range-parser@^1.0.3, range-parser@^1.2.0, range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +raw-loader@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/raw-loader/-/raw-loader-1.0.0.tgz#3f9889e73dadbda9a424bce79809b4133ad46405" + integrity sha512-Uqy5AqELpytJTRxYT4fhltcKPj0TyaEpzJDcGz7DFJi+pQOOi3GjR/DOdxTkTsF+NzhnldIoG6TORaBlInUuqA== + dependencies: + loader-utils "^1.1.0" + schema-utils "^1.0.0" + +rc@^1.2.7, rc@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha1-5mTvMRYRZsl1HNvo28+GtftY93Q= + dependencies: + pify "^2.3.0" + +read-package-json@^2.0.0: + version "2.0.13" + resolved "https://registry.yarnpkg.com/read-package-json/-/read-package-json-2.0.13.tgz#2e82ebd9f613baa6d2ebe3aa72cefe3f68e41f4a" + integrity sha512-/1dZ7TRZvGrYqE0UAfN6qQb5GYBsNcqS1C0tNK601CFOJmtHI7NIGXwetEPU/OtoFHZL3hDxm4rolFFVE9Bnmg== + dependencies: + glob "^7.1.1" + json-parse-better-errors "^1.0.1" + normalize-package-data "^2.0.0" + slash "^1.0.0" + optionalDependencies: + graceful-fs "^4.1.2" + +read-package-tree@5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/read-package-tree/-/read-package-tree-5.3.1.tgz#a32cb64c7f31eb8a6f31ef06f9cedf74068fe636" + integrity sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw== + dependencies: + read-package-json "^2.0.0" + readdir-scoped-modules "^1.0.0" + util-promisify "^2.1.0" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" + integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc= + dependencies: + find-up "^2.0.0" + read-pkg "^3.0.0" + +read-pkg-up@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978" + integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA== + dependencies: + find-up "^3.0.0" + read-pkg "^3.0.0" + +read-pkg-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-5.0.0.tgz#b6a6741cb144ed3610554f40162aa07a6db621b8" + integrity sha512-XBQjqOBtTzyol2CpsQOw8LHV0XbDZVG7xMMjmXAJomlVY03WOBRmYgDJETlvcg0H63AJvPRwT7GFi5rvOzUOKg== + dependencies: + find-up "^3.0.0" + read-pkg "^5.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + +read-pkg@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-4.0.1.tgz#963625378f3e1c4d48c85872b5a6ec7d5d093237" + integrity sha1-ljYlN48+HE1IyFhytabsfV0JMjc= + dependencies: + normalize-package-data "^2.3.2" + parse-json "^4.0.0" + pify "^3.0.0" + +read-pkg@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" + +"readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.6, readable-stream@~2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +"readable-stream@2 || 3", readable-stream@^3.0.6: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" + integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +"readable-stream@>=1.0.33-1 <1.1.0-0": + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@~1.1.9: + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readdir-scoped-modules@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" + integrity sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw== + dependencies: + debuglog "^1.0.1" + dezalgo "^1.0.0" + graceful-fs "^4.1.2" + once "^1.3.0" + +readdirp@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== + dependencies: + graceful-fs "^4.1.11" + micromatch "^3.1.10" + readable-stream "^2.0.2" + +readdirp@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.1.1.tgz#b158123ac343c8b0f31d65680269cc0fc1025db1" + integrity sha512-XXdSXZrQuvqoETj50+JAitxz1UPdt5dupjT6T5nVB+WvjMv2XKYj+s7hPeAVCXvmJrL36O4YYyWlIC3an2ePiQ== + dependencies: + picomatch "^2.0.4" + +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q= + dependencies: + resolve "^1.1.6" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +redent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa" + integrity sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo= + dependencies: + indent-string "^3.0.0" + strip-indent "^2.0.0" + +reflect-metadata@^0.1.2: + version "0.1.13" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" + integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== + +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +registry-auth-token@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.0.0.tgz#30e55961eec77379da551ea5c4cf43cbf03522be" + integrity sha512-lpQkHxd9UL6tb3k/aHAVfnVtn+Bcs9ob5InuFLLEDqSqeq+AljB8GZW9xY0x7F+xYwEcjKe07nyoxzEYz6yvkw== + dependencies: + rc "^1.2.8" + safe-buffer "^5.0.1" + +registry-url@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" + integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== + dependencies: + rc "^1.2.8" + +release-zalgo@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/release-zalgo/-/release-zalgo-1.0.0.tgz#09700b7e5074329739330e535c5a90fb67851730" + integrity sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA= + dependencies: + es6-error "^4.0.1" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= + dependencies: + is-finite "^1.0.0" + +replace-ext@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" + integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ= + +request@^2.83.0, request@^2.87.0, request@^2.88.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= + dependencies: + resolve-from "^3.0.0" + +resolve-dir@^1.0.0, resolve-dir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/resolve-dir/-/resolve-dir-1.0.1.tgz#79a40644c362be82f26effe739c9bb5382046f43" + integrity sha1-eaQGRMNivoLybv/nOcm7U4IEb0M= + dependencies: + expand-tilde "^2.0.0" + global-modules "^1.0.0" + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha1-six699nWiBvItuZTM17rywoYh0g= + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +resolve@^1.0.0, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.11.0, resolve@^1.11.1, resolve@^1.3.2: + version "1.12.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" + integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== + dependencies: + path-parse "^1.0.6" + +resolve@~1.1.6: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= + +responselike@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" + integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= + dependencies: + lowercase-keys "^1.0.0" + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +retry@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" + integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q= + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + +rfdc@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" + integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug== + +rimraf@^2.2.8, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +rollup-plugin-commonjs@^10.0.0: + version "10.0.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-commonjs/-/rollup-plugin-commonjs-10.0.2.tgz#61328f3a29945e2c35f2b2e824c18944fd88a54d" + integrity sha512-DxeR4QXTgTOFseYls1V7vgKbrSJmPYNdEMOs0OvH+7+89C3GiIonU9gFrE0u39Vv1KWm3wepq8KAvKugtoM2Zw== + dependencies: + estree-walker "^0.6.1" + is-reference "^1.1.2" + magic-string "^0.25.2" + resolve "^1.11.0" + rollup-pluginutils "^2.8.1" + +rollup-plugin-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-json/-/rollup-plugin-json-4.0.0.tgz#a18da0a4b30bf5ca1ee76ddb1422afbb84ae2b9e" + integrity sha512-hgb8N7Cgfw5SZAkb3jf0QXii6QX/FOkiIq2M7BAQIEydjHvTyxXHQiIzZaTFgx1GK0cRCHOCBHIyEkkLdWKxow== + dependencies: + rollup-pluginutils "^2.5.0" + +rollup-plugin-node-resolve@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz#730f93d10ed202473b1fb54a5997a7db8c6d8523" + integrity sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw== + dependencies: + "@types/resolve" "0.0.8" + builtin-modules "^3.1.0" + is-module "^1.0.0" + resolve "^1.11.1" + rollup-pluginutils "^2.8.1" + +rollup-plugin-sourcemaps@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/rollup-plugin-sourcemaps/-/rollup-plugin-sourcemaps-0.4.2.tgz#62125aa94087aadf7b83ef4dfaf629b473135e87" + integrity sha1-YhJaqUCHqt97g+9N+vYptHMTXoc= + dependencies: + rollup-pluginutils "^2.0.1" + source-map-resolve "^0.5.0" + +rollup-pluginutils@^2.0.1, rollup-pluginutils@^2.5.0, rollup-pluginutils@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.1.tgz#8fa6dd0697344938ef26c2c09d2488ce9e33ce97" + integrity sha512-J5oAoysWar6GuZo0s+3bZ6sVZAC0pfqKz68De7ZgDi5z63jOVZn1uJL/+z1jeKHNbGII8kAyHF5q8LnxSX5lQg== + dependencies: + estree-walker "^0.6.1" + +rollup@^1.12.1: + version "1.19.4" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-1.19.4.tgz#0cb4e4d6fa127adab59b11d0be50e8dd1c78123a" + integrity sha512-G24w409GNj7i/Yam2cQla6qV2k6Nug8bD2DZg9v63QX/cH/dEdbNJg8H4lUm5M1bRpPKRUC465Rm9H51JTKOfQ== + dependencies: + "@types/estree" "0.0.39" + "@types/node" "^12.6.9" + acorn "^6.2.1" + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= + dependencies: + is-promise "^2.1.0" + +run-queue@^1.0.0, run-queue@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" + integrity sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec= + dependencies: + aproba "^1.1.1" + +rxjs@6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.4.0.tgz#f3bb0fe7bda7fb69deac0c16f17b50b0b8790504" + integrity sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw== + dependencies: + tslib "^1.9.0" + +rxjs@^6.0.0, rxjs@^6.3.3, rxjs@^6.4.0: + version "6.5.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz#2e35ce815cd46d84d02a209fb4e5921e051dbec7" + integrity sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg== + dependencies: + tslib "^1.9.0" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sass-loader@7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.1.0.tgz#16fd5138cb8b424bf8a759528a1972d72aad069d" + integrity sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w== + dependencies: + clone-deep "^2.0.1" + loader-utils "^1.0.1" + lodash.tail "^4.1.1" + neo-async "^2.5.0" + pify "^3.0.0" + semver "^5.5.0" + +sass@1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.19.0.tgz#5de82c713d4299fac57384ef5219534a37fe3e6c" + integrity sha512-8kzKCgxCzh8/zEn3AuRwzLWVSSFj8omkiGwqdJdeOufjM+I88dXxu9LYJ/Gw4rRTHXesN0r1AixBuqM6yLQUJw== + dependencies: + chokidar "^2.0.0" + +sass@^1.17.3: + version "1.22.9" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.22.9.tgz#41a2ed6038027f58be2bd5041293452a29c2cb84" + integrity sha512-FzU1X2V8DlnqabrL4u7OBwD2vcOzNMongEJEx3xMEhWY/v26FFR3aG0hyeu2T965sfR0E9ufJwmG+Qjz78vFPQ== + dependencies: + chokidar ">=2.0.0 <4.0.0" + +sauce-connect-launcher@^1.2.4: + version "1.2.7" + resolved "https://registry.yarnpkg.com/sauce-connect-launcher/-/sauce-connect-launcher-1.2.7.tgz#c7f8b3d4eb354d07a9922b4cd67356f527192556" + integrity sha512-v07+QhFrxgz3seMFuRSonu3gW1s6DbcLQlFhjsRrmKUauzPbbudHdnn91WYgEwhoZVdPNzeZpAEJwcQyd9xnTA== + dependencies: + adm-zip "~0.4.3" + async "^2.1.2" + https-proxy-agent "^2.2.1" + lodash "^4.16.6" + rimraf "^2.5.4" + +saucelabs@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/saucelabs/-/saucelabs-1.5.0.tgz#9405a73c360d449b232839919a86c396d379fd9d" + integrity sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ== + dependencies: + https-proxy-agent "^2.2.1" + +sax@0.5.x: + version "0.5.8" + resolved "https://registry.yarnpkg.com/sax/-/sax-0.5.8.tgz#d472db228eb331c2506b0e8c15524adb939d12c1" + integrity sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE= + +sax@>=0.6.0, sax@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +schema-utils@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.3.0.tgz#f5877222ce3e931edae039f17eb3716e7137f8cf" + integrity sha1-9YdyIs4+kx7a4DnxfrNxbnE3+M8= + dependencies: + ajv "^5.0.0" + +schema-utils@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770" + integrity sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g== + dependencies: + ajv "^6.1.0" + ajv-errors "^1.0.0" + ajv-keywords "^3.1.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= + +select@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" + integrity sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0= + +selenium-webdriver@3.6.0, selenium-webdriver@^3.0.1: + version "3.6.0" + resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz#2ba87a1662c020b8988c981ae62cb2a01298eafc" + integrity sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q== + dependencies: + jszip "^3.1.3" + rimraf "^2.5.4" + tmp "0.0.30" + xml2js "^0.4.17" + +selenium-webdriver@^4.0.0-alpha.1: + version "4.0.0-alpha.4" + resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.4.tgz#73694490e02c941d9d0bf7a36f7c49beb9372512" + integrity sha512-etJt20d8qInkxMAHIm5SEpPBSS+CdxVcybnxzSIB/GlWErb8pIWrArz/VA6VfUW0/6tIcokepXQ5ufvdzqqk1A== + dependencies: + jszip "^3.1.5" + rimraf "^2.6.3" + tmp "0.0.30" + xml2js "^0.4.19" + +selfsigned@^1.10.4: + version "1.10.4" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-1.10.4.tgz#cdd7eccfca4ed7635d47a08bf2d5d3074092e2cd" + integrity sha512-9AukTiDmHXGXWtWjembZ5NDmVvP2695EtpgbCsxCa68w3c88B+alqbmZ4O3hZ4VWGXeGWzEVdvqgAJD8DQPCDw== + dependencies: + node-forge "0.7.5" + +semver-diff@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" + integrity sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY= + dependencies: + semver "^5.0.3" + +semver-intersect@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/semver-intersect/-/semver-intersect-1.4.0.tgz#bdd9c06bedcdd2fedb8cd352c3c43ee8c61321f3" + integrity sha512-d8fvGg5ycKAq0+I6nfWeCx6ffaWJCsBYU0H2Rq56+/zFePYfT8mXkB3tWBSjR5BerkHNZ5eTPIk1/LBYas35xQ== + dependencies: + semver "^5.0.0" + +"semver@2 || 3 || 4 || 5", semver@^5.0.0, semver@^5.0.3, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" + integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== + +semver@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.0.0.tgz#05e359ee571e5ad7ed641a6eec1e547ba52dea65" + integrity sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ== + +semver@6.3.0, semver@^6.0.0, semver@^6.1.1, semver@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^4.1.0: + version "4.3.6" + resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da" + integrity sha1-MAvG4OhjdPe6YQaLWx7NV/xlMto= + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +sequencify@~0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/sequencify/-/sequencify-0.0.7.tgz#90cff19d02e07027fd767f5ead3e7b95d1e7380c" + integrity sha1-kM/xnQLgcCf9dn9erT57ldHnOAw= + +serialize-javascript@^1.4.0, serialize-javascript@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.7.0.tgz#d6e0dfb2a3832a8c94468e6eb1db97e55a192a65" + integrity sha512-ke8UG8ulpFOxO8f8gRYabHQe/ZntKlcig2Mp+8+URDP1D8vJZ0KUt7LYo07q25Z/+JVSgpr/cui9PIp5H6/+nA== + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +set-immediate-shim@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shallow-clone@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571" + integrity sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA== + dependencies: + is-extendable "^0.1.1" + kind-of "^5.0.0" + mixin-object "^2.0.1" + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shelljs@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.3.tgz#a7f3319520ebf09ee81275b2368adb286659b097" + integrity sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + +sigmund@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sigmund/-/sigmund-1.0.1.tgz#3ff21f198cad2175f9f3b781853fd94d0d19b590" + integrity sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA= + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= + +smart-buffer@4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.0.2.tgz#5207858c3815cc69110703c6b94e46c15634395d" + integrity sha512-JDhEpTKzXusOqXZ0BUIdH+CjFdO/CR3tLlf5CN34IypI+xMmXW1uB16OOY8z3cICbJlDAVJzNbwBhNO0wt9OAw== + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +socket.io-adapter@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b" + integrity sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs= + +socket.io-client@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.1.1.tgz#dcb38103436ab4578ddb026638ae2f21b623671f" + integrity sha512-jxnFyhAuFxYfjqIgduQlhzqTcOEQSn+OHKVfAxWaNWa7ecP7xSNk2Dx/3UEsDcY7NcFafxvNvKPmmO7HTwTxGQ== + dependencies: + backo2 "1.0.2" + base64-arraybuffer "0.1.5" + component-bind "1.0.0" + component-emitter "1.2.1" + debug "~3.1.0" + engine.io-client "~3.2.0" + has-binary2 "~1.0.2" + has-cors "1.1.0" + indexof "0.0.1" + object-component "0.0.3" + parseqs "0.0.5" + parseuri "0.0.5" + socket.io-parser "~3.2.0" + to-array "0.1.4" + +socket.io-parser@~3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.2.0.tgz#e7c6228b6aa1f814e6148aea325b51aa9499e077" + integrity sha512-FYiBx7rc/KORMJlgsXysflWx/RIvtqZbyGLlHZvjfmPTPeuD/I8MaW7cfFrj5tRltICJdgwflhfZ3NVVbVLFQA== + dependencies: + component-emitter "1.2.1" + debug "~3.1.0" + isarray "2.0.1" + +socket.io@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.1.1.tgz#a069c5feabee3e6b214a75b40ce0652e1cfb9980" + integrity sha512-rORqq9c+7W0DAK3cleWNSyfv/qKXV99hV4tZe+gGLfBECw3XEhBy7x85F3wypA9688LKjtwO9pX9L33/xQI8yA== + dependencies: + debug "~3.1.0" + engine.io "~3.2.0" + has-binary2 "~1.0.2" + socket.io-adapter "~1.1.0" + socket.io-client "2.1.1" + socket.io-parser "~3.2.0" + +sockjs-client@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/sockjs-client/-/sockjs-client-1.3.0.tgz#12fc9d6cb663da5739d3dc5fb6e8687da95cb177" + integrity sha512-R9jxEzhnnrdxLCNln0xg5uGHqMnkhPSTzUZH2eXcR03S/On9Yvoq2wyUZILRUhZCNVu2PmwWVoyuiPz8th8zbg== + dependencies: + debug "^3.2.5" + eventsource "^1.0.7" + faye-websocket "~0.11.1" + inherits "^2.0.3" + json3 "^3.3.2" + url-parse "^1.4.3" + +sockjs@0.3.19: + version "0.3.19" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d" + integrity sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw== + dependencies: + faye-websocket "^0.10.0" + uuid "^3.0.1" + +socks-proxy-agent@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386" + integrity sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg== + dependencies: + agent-base "~4.2.1" + socks "~2.3.2" + +socks@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.3.2.tgz#ade388e9e6d87fdb11649c15746c578922a5883e" + integrity sha512-pCpjxQgOByDHLlNqlnh/mNSAxIUkyBBuwwhTcV+enZGbDaClPvHdvm6uvOwZfFJkam7cGhBNbb4JxiP8UZkRvQ== + dependencies: + ip "^1.1.5" + smart-buffer "4.0.2" + +sort-keys@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" + integrity sha1-RBttTTRnmPG05J6JIK37oOVD+a0= + dependencies: + is-plain-obj "^1.0.0" + +sort-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-2.0.0.tgz#658535584861ec97d730d6cf41822e1f56684128" + integrity sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg= + dependencies: + is-plain-obj "^1.0.0" + +source-list-map@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== + +source-list-map@~0.1.7: + version "0.1.8" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-0.1.8.tgz#c550b2ab5427f6b3f21f5afead88c4f5587b2106" + integrity sha1-xVCyq1Qn9rPyH1r+rYjE9Vh7IQY= + +source-map-loader@0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-0.2.4.tgz#c18b0dc6e23bf66f6792437557c569a11e072271" + integrity sha512-OU6UJUty+i2JDpTItnizPrlpOIBLmQbWMuBg9q5bVtnHACqw1tn9nNwqJLbv0/00JjnJb/Ee5g5WS5vrRv7zIQ== + dependencies: + async "^2.5.0" + loader-utils "^1.1.0" + +source-map-resolve@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" + integrity sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA== + dependencies: + atob "^2.1.1" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@0.5.12: + version "0.5.12" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" + integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-support@^0.5.12, source-map-support@^0.5.5, source-map-support@^0.5.6, source-map-support@~0.5.10, source-map-support@~0.5.12: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-support@~0.4.0: + version "0.4.18" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" + integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== + dependencies: + source-map "^0.5.6" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + +source-map@0.1.x: + version "0.1.43" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" + integrity sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y= + dependencies: + amdefine ">=0.0.4" + +source-map@0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" + integrity sha1-dc449SvwczxafwwRjYEzSiu19BI= + +source-map@0.7.3, source-map@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@~0.4.1: + version "0.4.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b" + integrity sha1-66T12pwNyZneaAMti092FzZSA2s= + dependencies: + amdefine ">=0.0.4" + +sourcemap-codec@^1.4.4: + version "1.4.6" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.6.tgz#e30a74f0402bad09807640d39e971090a08ce1e9" + integrity sha512-1ZooVLYFxC448piVLBbtOxFcXwnymH9oUF8nRd3CuYDVvkRBxRl6pB4Mtas5a4drtL+E8LDgFkQNcgIw6tc8Hg== + +sparkles@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sparkles/-/sparkles-1.0.1.tgz#008db65edce6c50eec0c5e228e1945061dd0437c" + integrity sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw== + +spawn-command@^0.0.2-1: + version "0.0.2-1" + resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" + integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A= + +spawn-wrap@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/spawn-wrap/-/spawn-wrap-1.4.2.tgz#cff58e73a8224617b6561abdc32586ea0c82248c" + integrity sha512-vMwR3OmmDhnxCVxM8M+xO/FtIp6Ju/mNaDfCMMW7FDcLRTPFWUswec4LXJHTJE2hwTI9O0YBfygu4DalFl7Ylg== + dependencies: + foreground-child "^1.5.6" + mkdirp "^0.5.0" + os-homedir "^1.0.1" + rimraf "^2.6.2" + signal-exit "^3.0.2" + which "^1.3.0" + +spdx-correct@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" + integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.1.tgz#6f12ed1c5db7ea4f24ebb8b89ba58c87c08257f2" + integrity sha512-HeZS3PBdMA+sZSu0qwpCxl3DeALD5ASx8pAX0jZdKXSpPWbQ6SYGnlg3BBmYLx5LtiZrmkAZfErCm2oECBcioA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +speed-measure-webpack-plugin@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/speed-measure-webpack-plugin/-/speed-measure-webpack-plugin-1.3.1.tgz#69840a5cdc08b4638697dac7db037f595d7f36a0" + integrity sha512-qVIkJvbtS9j/UeZumbdfz0vg+QfG/zxonAjzefZrqzkr7xOncLVXkeGbTpzd1gjCBM4PmVNkWlkeTVhgskAGSQ== + dependencies: + chalk "^2.0.1" + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +split2@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493" + integrity sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw== + dependencies: + through2 "^2.0.2" + +split@^1.0.0, split@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" + integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== + dependencies: + through "2" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +ssri@^6.0.0, ssri@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" + integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== + dependencies: + figgy-pudding "^3.5.1" + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +stats-webpack-plugin@0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/stats-webpack-plugin/-/stats-webpack-plugin-0.7.0.tgz#ccffe9b745de8bbb155571e063f8263fc0e2bc06" + integrity sha512-NT0YGhwuQ0EOX+uPhhUcI6/+1Sq/pMzNuSCBVT4GbFl/ac6I/JZefBcjlECNfAb1t3GOx5dEj1Z7x0cAxeeVLQ== + dependencies: + lodash "^4.17.4" + +"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +stream-browserify@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" + integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-combiner2@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stream-combiner2/-/stream-combiner2-1.1.1.tgz#fb4d8a1420ea362764e21ad4780397bebcb41cbe" + integrity sha1-+02KFCDqNidk4hrUeAOXvry0HL4= + dependencies: + duplexer2 "~0.1.0" + readable-stream "^2.0.2" + +stream-combiner@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858" + integrity sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg= + dependencies: + duplexer "~0.1.1" + through "~2.3.4" + +stream-consume@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/stream-consume/-/stream-consume-0.1.1.tgz#d3bdb598c2bd0ae82b8cac7ac50b1107a7996c48" + integrity sha512-tNa3hzgkjEP7XbCkbRXe1jpg+ievoa0O4SCFlMOYEscGSS4JJsckGL8swUyAa/ApGU3Ae4t6Honor4HhL+tRyg== + +stream-each@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" + integrity sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw== + dependencies: + end-of-stream "^1.1.0" + stream-shift "^1.0.0" + +stream-equal@0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/stream-equal/-/stream-equal-0.1.6.tgz#cc522fab38516012e4d4ee47513b147b72359019" + integrity sha1-zFIvqzhRYBLk1O5HUTsUe3I1kBk= + +stream-http@^2.7.2: + version "2.8.3" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" + integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= + +streamroller@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-1.0.6.tgz#8167d8496ed9f19f05ee4b158d9611321b8cacd9" + integrity sha512-3QC47Mhv3/aZNFpDDVO44qQb9gwB9QggMEE0sQmkTAwBVYdBRWISdsywlkfm5II1Q5y/pmrHflti/IgmIzdDBg== + dependencies: + async "^2.6.2" + date-format "^2.0.0" + debug "^3.2.6" + fs-extra "^7.0.1" + lodash "^4.17.14" + +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + integrity sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM= + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string_decoder@^1.0.0, string_decoder@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" + integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w== + dependencies: + safe-buffer "~5.1.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-bom@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-1.0.0.tgz#85b8862f3844b5a6d5ec8467a93598173a36f794" + integrity sha1-hbiGLzhEtabV7IRnqTWYFzo295Q= + dependencies: + first-chunk-stream "^1.0.0" + is-utf8 "^0.2.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= + dependencies: + get-stdin "^4.0.1" + +strip-indent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" + integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= + +strip-json-comments@^2.0.0, strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +strip-outer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-outer/-/strip-outer-1.0.1.tgz#b2fd2abf6604b9d1e6013057195df836b8a9d631" + integrity sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg== + dependencies: + escape-string-regexp "^1.0.2" + +strip-url-auth@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-url-auth/-/strip-url-auth-1.0.1.tgz#22b0fa3a41385b33be3f331551bbb837fa0cd7ae" + integrity sha1-IrD6OkE4WzO+PzMVUbu4N/oM164= + +style-loader@0.23.1: + version "0.23.1" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-0.23.1.tgz#cb9154606f3e771ab6c4ab637026a1049174d925" + integrity sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg== + dependencies: + loader-utils "^1.1.0" + schema-utils "^1.0.0" + +stylus-loader@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/stylus-loader/-/stylus-loader-3.0.2.tgz#27a706420b05a38e038e7cacb153578d450513c6" + integrity sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA== + dependencies: + loader-utils "^1.0.2" + lodash.clonedeep "^4.5.0" + when "~3.6.x" + +stylus@0.54.5, stylus@^0.54.5: + version "0.54.5" + resolved "https://registry.yarnpkg.com/stylus/-/stylus-0.54.5.tgz#42b9560931ca7090ce8515a798ba9e6aa3d6dc79" + integrity sha1-QrlWCTHKcJDOhRWnmLqeaqPW3Hk= + dependencies: + css-parse "1.7.x" + debug "*" + glob "7.0.x" + mkdirp "0.5.x" + sax "0.5.x" + source-map "0.1.x" + +supports-color@6.1.0, supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + +supports-color@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" + integrity sha1-vnoN5ITexcXN34s9WRJQRJEvY1s= + dependencies: + has-flag "^2.0.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +symbol-observable@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== + +tapable@^1.0.0, tapable@^1.1.0, tapable@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + +tar@^4, tar@^4.4.8: + version "4.4.10" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.10.tgz#946b2810b9a5e0b26140cf78bea6b0b0d689eba1" + integrity sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA== + dependencies: + chownr "^1.1.1" + fs-minipass "^1.2.5" + minipass "^2.3.5" + minizlib "^1.2.1" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.3" + +tempfile@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/tempfile/-/tempfile-1.1.1.tgz#5bcc4eaecc4ab2c707d8bc11d99ccc9a2cb287f2" + integrity sha1-W8xOrsxKsscH2LwR2ZzMmiyyh/I= + dependencies: + os-tmpdir "^1.0.0" + uuid "^2.0.1" + +term-size@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" + integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk= + dependencies: + execa "^0.7.0" + +terser-webpack-plugin@1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.2.3.tgz#3f98bc902fac3e5d0de730869f50668561262ec8" + integrity sha512-GOK7q85oAb/5kE12fMuLdn2btOS9OBZn4VsecpHDywoUC/jLhSAKOiYo0ezx7ss2EXPMzyEWFoE0s1WLE+4+oA== + dependencies: + cacache "^11.0.2" + find-cache-dir "^2.0.0" + schema-utils "^1.0.0" + serialize-javascript "^1.4.0" + source-map "^0.6.1" + terser "^3.16.1" + webpack-sources "^1.1.0" + worker-farm "^1.5.2" + +terser-webpack-plugin@^1.1.0, terser-webpack-plugin@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz#61b18e40eaee5be97e771cdbb10ed1280888c2b4" + integrity sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg== + dependencies: + cacache "^12.0.2" + find-cache-dir "^2.1.0" + is-wsl "^1.1.0" + schema-utils "^1.0.0" + serialize-javascript "^1.7.0" + source-map "^0.6.1" + terser "^4.1.2" + webpack-sources "^1.4.0" + worker-farm "^1.7.0" + +terser@^3.16.1: + version "3.17.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-3.17.0.tgz#f88ffbeda0deb5637f9d24b0da66f4e15ab10cb2" + integrity sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ== + dependencies: + commander "^2.19.0" + source-map "~0.6.1" + source-map-support "~0.5.10" + +terser@^4.1.2: + version "4.1.3" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.1.3.tgz#6074fbcf3517561c3272ea885f422c7a8c32d689" + integrity sha512-on13d+cnpn5bMouZu+J8tPYQecsdRJCJuxFJ+FVoPBoLJgk5bCBkp+Uen2hWyi0KIUm6eDarnlAlH+KgIx/PuQ== + dependencies: + commander "^2.20.0" + source-map "~0.6.1" + source-map-support "~0.5.12" + +test-exclude@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0" + integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g== + dependencies: + glob "^7.1.3" + minimatch "^3.0.4" + read-pkg-up "^4.0.0" + require-main-filename "^2.0.0" + +text-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-2.0.0.tgz#43eabd1b495482fae4a2bf65e5f56c29f69220f6" + integrity sha512-F91ZqLgvi1E0PdvmxMgp+gcf6q8fMH7mhdwWfzXnl1k+GbpQDmi8l7DzLC5JTASKbwpY3TfxajAUzAXcv2NmsQ== + +through2@^0.6.1, through2@^0.6.3: + version "0.6.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" + integrity sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg= + dependencies: + readable-stream ">=1.0.33-1 <1.1.0-0" + xtend ">=4.0.0 <4.1.0-0" + +through2@^2.0.0, through2@^2.0.2: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +through2@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a" + integrity sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww== + dependencies: + readable-stream "2 || 3" + +through@2, "through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.4: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +thunky@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.0.3.tgz#f5df732453407b09191dae73e2a8cc73f381a826" + integrity sha512-YwT8pjmNcAXBZqrubu22P4FYsh2D4dxRmnWBOL8Jk8bUcRUtc5326kx32tuTmFDAZtLOGEVNl8POAR8j896Iow== + +tildify@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tildify/-/tildify-1.2.0.tgz#dcec03f55dca9b7aa3e5b04f21817eb56e63588a" + integrity sha1-3OwD9V3Km3qj5bBPIYF+tW5jWIo= + dependencies: + os-homedir "^1.0.0" + +time-stamp@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3" + integrity sha1-dkpaEa9QVhkhsTPztE5hhofg9cM= + +timers-browserify@^2.0.4: + version "2.0.10" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.10.tgz#1d28e3d2aadf1d5a5996c4e9f95601cd053480ae" + integrity sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg== + dependencies: + setimmediate "^1.0.4" + +timers-ext@^0.1.5: + version "0.1.7" + resolved "https://registry.yarnpkg.com/timers-ext/-/timers-ext-0.1.7.tgz#6f57ad8578e07a3fb9f91d9387d65647555e25c6" + integrity sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ== + dependencies: + es5-ext "~0.10.46" + next-tick "1" + +tiny-emitter@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" + integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== + +tmp@0.0.30: + version "0.0.30" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.30.tgz#72419d4a8be7d6ce75148fd8b324e593a711c2ed" + integrity sha1-ckGdSovn1s51FI/YsyTlk6cRwu0= + dependencies: + os-tmpdir "~1.0.1" + +tmp@0.0.33, tmp@0.0.x, tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +to-array@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" + integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA= + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= + +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/to-readable-stream/-/to-readable-stream-1.0.0.tgz#ce0aa0c2f3df6adf852efb404a783e77c0475771" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +tree-kill@1.2.1, tree-kill@^1.1.0, tree-kill@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.1.tgz#5398f374e2f292b9dcc7b2e71e30a5c3bb6c743a" + integrity sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q== + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= + +trim-newlines@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" + integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA= + +trim-off-newlines@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3" + integrity sha1-n5up2e+odkw4dpi8v+sshI8RrbM= + +trim-repeated@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-repeated/-/trim-repeated-1.0.0.tgz#e3646a2ea4e891312bf7eace6cfb05380bc01c21" + integrity sha1-42RqLqTokTEr9+rObPsFOAvAHCE= + dependencies: + escape-string-regexp "^1.0.2" + +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= + +ts-loader@^6.0.1: + version "6.0.4" + resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-6.0.4.tgz#bc331ad91a887a60632d94c9f79448666f2c4b63" + integrity sha512-p2zJYe7OtwR+49kv4gs7v4dMrfYD1IPpOtqiSPCbe8oR+4zEBtdHwzM7A7M91F+suReqgzZrlClk4LRSSp882g== + dependencies: + chalk "^2.3.0" + enhanced-resolve "^4.0.0" + loader-utils "^1.0.2" + micromatch "^4.0.0" + semver "^6.0.0" + +ts-node-dev@^1.0.0-pre.30: + version "1.0.0-pre.40" + resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.0.0-pre.40.tgz#a3a93a6c87993cba8c70c4c92b67d874694b38db" + integrity sha512-78CptStf6oA5wKkRXQPEMBR5zowhnw2bvCETRMhkz2DsuussA56s6lKgUX4EiMMiPkyYdSm8jkJ875j4eo4nkQ== + dependencies: + dateformat "~1.0.4-1.2.3" + dynamic-dedupe "^0.3.0" + filewatcher "~3.0.0" + minimist "^1.1.3" + mkdirp "^0.5.1" + node-notifier "^5.4.0" + resolve "^1.0.0" + rimraf "^2.6.1" + source-map-support "^0.5.12" + tree-kill "^1.2.1" + ts-node "*" + tsconfig "^7.0.0" + +ts-node@*, ts-node@^8.2.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.3.0.tgz#e4059618411371924a1fb5f3b125915f324efb57" + integrity sha512-dyNS/RqyVTDcmNM4NIBAeDMpsAdaQ+ojdf0GOLqE6nwJOgzEkdRNzJywhDfwnuvB10oa6NLVG1rUJQCpRN7qoQ== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.6" + yn "^3.0.0" + +tsconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7" + integrity sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw== + dependencies: + "@types/strip-bom" "^3.0.0" + "@types/strip-json-comments" "0.0.30" + strip-bom "^3.0.0" + strip-json-comments "^2.0.0" + +tsickle@^0.35.0: + version "0.35.0" + resolved "https://registry.yarnpkg.com/tsickle/-/tsickle-0.35.0.tgz#59235df45937c0ec5d072c616c26d2d97fba54b9" + integrity sha512-irsZLX4293YUl9TuwNC5Fy020eLSc4bC3LfKnxnx1oq5wmZD9zSP8qvNNTiwRmf2/rxH+58JINcTARDjuvn+oQ== + dependencies: + minimist "^1.2.0" + mkdirp "^0.5.1" + source-map "^0.7.3" + +tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + +tslint-jasmine-rules@^1.3.2: + version "1.6.0" + resolved "https://registry.yarnpkg.com/tslint-jasmine-rules/-/tslint-jasmine-rules-1.6.0.tgz#6da3e98c12953b3250dbff7df8acc9cd6e2a7e82" + integrity sha512-yswF2tfx0p2eB0/oIY6Q0/7HNIqqNGNYD9yBeqF/DeQwYmpWU6GPGP6dsMPBxCqM6CE0GID88XEvnuBwfedJFQ== + +tslint@^5.16.0: + version "5.18.0" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.18.0.tgz#f61a6ddcf372344ac5e41708095bbf043a147ac6" + integrity sha512-Q3kXkuDEijQ37nXZZLKErssQVnwCV/+23gFEMROi8IlbaBG6tXqLPQJ5Wjcyt/yHPKBC+hD5SzuGaMora+ZS6w== + dependencies: + "@babel/code-frame" "^7.0.0" + builtin-modules "^1.1.1" + chalk "^2.3.0" + commander "^2.12.1" + diff "^3.2.0" + glob "^7.1.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + mkdirp "^0.5.1" + resolve "^1.3.2" + semver "^5.3.0" + tslib "^1.8.0" + tsutils "^2.29.0" + +tsutils@^2.29.0: + version "2.29.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" + integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== + dependencies: + tslib "^1.8.1" + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + integrity sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY= + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-fest@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" + integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== + +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +type@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/type/-/type-1.0.3.tgz#16f5d39f27a2d28d86e48f8981859e9d3296c179" + integrity sha512-51IMtNfVcee8+9GJvj0spSuFcZHe9vSib6Xtgsny1Km9ugyz2mbS08I3rsUIRYgJohFRFU1160sgRodYz378Hg== + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +typescript@3.4.4: + version "3.4.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.4.tgz#aac4a08abecab8091a75f10842ffa0631818f785" + integrity sha512-xt5RsIRCEaf6+j9AyOBgvVuAec0i92rgCaS3S+UVf5Z/vF2Hvtsw08wtUTJqp4djwznoAgjSxeCcU4r+CcDBJA== + +typescript@~3.4.3: + version "3.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.4.5.tgz#2d2618d10bb566572b8d7aad5180d84257d70a99" + integrity sha512-YycBxUb49UUhdNMU5aJ7z5Ej2XGmaIBL0x34vZ82fn3hGvD+bgrMrVDpatgz2f7YxUMJxMkbWxJZeAvDxVe7Vw== + +uglify-js@^3.1.4: + version "3.6.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.6.0.tgz#704681345c53a8b2079fb6cec294b05ead242ff5" + integrity sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg== + dependencies: + commander "~2.20.0" + source-map "~0.6.1" + +ultron@~1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c" + integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og== + +unc-path-regex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" + integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + +unique-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-1.0.0.tgz#d59a4a75427447d9aa6c91e70263f8d26a4b104b" + integrity sha1-1ZpKdUJ0R9mqbJHnAmP40mpLEEs= + +unique-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" + integrity sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo= + dependencies: + crypto-random-string "^1.0.0" + +universal-analytics@^0.4.20: + version "0.4.20" + resolved "https://registry.yarnpkg.com/universal-analytics/-/universal-analytics-0.4.20.tgz#d6b64e5312bf74f7c368e3024a922135dbf24b03" + integrity sha512-gE91dtMvNkjO+kWsPstHRtSwHXz0l2axqptGYp5ceg4MsuurloM0PU3pdOfpb5zBXUvyjT4PwhWK2m39uczZuw== + dependencies: + debug "^3.0.0" + request "^2.88.0" + uuid "^3.0.0" + +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +upath@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.2.tgz#3db658600edaeeccbe6db5e684d67ee8c2acd068" + integrity sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q== + +update-notifier@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-3.0.1.tgz#78ecb68b915e2fd1be9f767f6e298ce87b736250" + integrity sha512-grrmrB6Zb8DUiyDIaeRTBCkgISYUgETNe7NglEbVsrLWXeESnlCSP50WfRSj/GmzMPl6Uchj24S/p80nP/ZQrQ== + dependencies: + boxen "^3.0.0" + chalk "^2.0.1" + configstore "^4.0.0" + has-yarn "^2.1.0" + import-lazy "^2.1.0" + is-ci "^2.0.0" + is-installed-globally "^0.1.0" + is-npm "^3.0.0" + is-yarn-global "^0.3.0" + latest-version "^5.0.0" + semver-diff "^2.0.0" + xdg-basedir "^3.0.0" + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" + integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= + dependencies: + prepend-http "^2.0.0" + +url-parse@^1.4.3: + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +url@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1" + integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +user-home@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" + integrity sha1-K1viOjK2Onyd640PKNSFcko98ZA= + +useragent@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/useragent/-/useragent-2.3.0.tgz#217f943ad540cb2128658ab23fc960f6a88c9972" + integrity sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw== + dependencies: + lru-cache "4.1.x" + tmp "0.0.x" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util-promisify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/util-promisify/-/util-promisify-2.1.0.tgz#3c2236476c4d32c5ff3c47002add7c13b9a82a53" + integrity sha1-PCI2R2xNMsX/PEcAKt18E7moKlM= + dependencies: + object.getownpropertydescriptors "^2.0.3" + +util@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" + integrity sha1-evsa/lCAUkZInj23/g7TeTNqwPk= + dependencies: + inherits "2.0.1" + +util@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" + integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== + dependencies: + inherits "2.0.3" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +uuid@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" + integrity sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho= + +uuid@^3.0.0, uuid@^3.0.1, uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +v8-compile-cache@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz#00f7494d2ae2b688cfe2899df6ed2c54bef91dbe" + integrity sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w== + +v8flags@^2.0.2: + version "2.1.1" + resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" + integrity sha1-qrGh+jDUX4jdMhFIh1rALAtV5bQ= + dependencies: + user-home "^1.1.1" + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +validate-npm-package-name@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz#5fa912d81eb7d0c74afc140de7317f0ca7df437e" + integrity sha1-X6kS2B630MdK/BQN5zF/DKffQ34= + dependencies: + builtins "^1.0.3" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vinyl-fs@^0.3.0: + version "0.3.14" + resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-0.3.14.tgz#9a6851ce1cac1c1cea5fe86c0931d620c2cfa9e6" + integrity sha1-mmhRzhysHBzqX+hsCTHWIMLPqeY= + dependencies: + defaults "^1.0.0" + glob-stream "^3.1.5" + glob-watcher "^0.0.6" + graceful-fs "^3.0.0" + mkdirp "^0.5.0" + strip-bom "^1.0.0" + through2 "^0.6.1" + vinyl "^0.4.0" + +vinyl@^0.4.0: + version "0.4.6" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" + integrity sha1-LzVsh6VQolVGHza76ypbqL94SEc= + dependencies: + clone "^0.2.0" + clone-stats "^0.0.1" + +vinyl@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.5.3.tgz#b0455b38fc5e0cf30d4325132e461970c2091cde" + integrity sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4= + dependencies: + clone "^1.0.0" + clone-stats "^0.0.1" + replace-ext "0.0.1" + +vm-browserify@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019" + integrity sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw== + +void-elements@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec" + integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w= + +watchpack@^1.5.0, watchpack@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00" + integrity sha512-i6dHe3EyLjMmDlU1/bGQpEw25XSjkJULPuAVKCbNRefQVq48yXKUpwg538F7AZTf9kyr57zj++pQFltUa5H7yA== + dependencies: + chokidar "^2.0.2" + graceful-fs "^4.1.2" + neo-async "^2.5.0" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webdriver-js-extender@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz#57d7a93c00db4cc8d556e4d3db4b5db0a80c3bb7" + integrity sha512-lcUKrjbBfCK6MNsh7xaY2UAUmZwe+/ib03AjVOpFobX4O7+83BUveSrLfU0Qsyb1DaKJdQRbuU+kM9aZ6QUhiQ== + dependencies: + "@types/selenium-webdriver" "^3.0.0" + selenium-webdriver "^3.0.1" + +webdriver-manager@^12.0.6: + version "12.1.6" + resolved "https://registry.yarnpkg.com/webdriver-manager/-/webdriver-manager-12.1.6.tgz#9e5410c506d1a7e0a7aa6af91ba3d5bb37f362b6" + integrity sha512-B1mOycNCrbk7xODw7Jgq/mdD3qzPxMaTsnKIQDy2nXlQoyjTrJTTD0vRpEZI9b8RibPEyQvh9zIZ0M1mpOxS3w== + dependencies: + adm-zip "^0.4.9" + chalk "^1.1.1" + del "^2.2.0" + glob "^7.0.3" + ini "^1.3.4" + minimist "^1.2.0" + q "^1.4.1" + request "^2.87.0" + rimraf "^2.5.2" + semver "^5.3.0" + xml2js "^0.4.17" + +webpack-cli@^3.2.3: + version "3.3.6" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.6.tgz#2c8c399a2642133f8d736a359007a052e060032c" + integrity sha512-0vEa83M7kJtxK/jUhlpZ27WHIOndz5mghWL2O53kiDoA9DIxSKnfqB92LoqEn77cT4f3H2cZm1BMEat/6AZz3A== + dependencies: + chalk "2.4.2" + cross-spawn "6.0.5" + enhanced-resolve "4.1.0" + findup-sync "3.0.0" + global-modules "2.0.0" + import-local "2.0.0" + interpret "1.2.0" + loader-utils "1.2.3" + supports-color "6.1.0" + v8-compile-cache "2.0.3" + yargs "13.2.4" + +webpack-core@^0.6.8: + version "0.6.9" + resolved "https://registry.yarnpkg.com/webpack-core/-/webpack-core-0.6.9.tgz#fc571588c8558da77be9efb6debdc5a3b172bdc2" + integrity sha1-/FcViMhVjad76e+23r3Fo7FyvcI= + dependencies: + source-list-map "~0.1.7" + source-map "~0.4.1" + +webpack-dev-middleware@3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.6.2.tgz#f37a27ad7c09cd7dc67cd97655413abaa1f55942" + integrity sha512-A47I5SX60IkHrMmZUlB0ZKSWi29TZTcPz7cha1Z75yYOsgWh/1AcPmQEbC8ZIbU3A1ytSv1PMU0PyPz2Lmz2jg== + dependencies: + memory-fs "^0.4.1" + mime "^2.3.1" + range-parser "^1.0.3" + webpack-log "^2.0.0" + +webpack-dev-middleware@^3.6.2, webpack-dev-middleware@^3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.0.tgz#ef751d25f4e9a5c8a35da600c5fda3582b5c6cff" + integrity sha512-qvDesR1QZRIAZHOE3iQ4CXLZZSQ1lAUsSpnQmlB1PBfoN/xdRjmge3Dok0W4IdaVLJOGJy3sGI4sZHwjRU0PCA== + dependencies: + memory-fs "^0.4.1" + mime "^2.4.2" + range-parser "^1.2.1" + webpack-log "^2.0.0" + +webpack-dev-server@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.3.1.tgz#7046e49ded5c1255a82c5d942bcdda552b72a62d" + integrity sha512-jY09LikOyGZrxVTXK0mgIq9y2IhCoJ05848dKZqX1gAGLU1YDqgpOT71+W53JH/wI4v6ky4hm+KvSyW14JEs5A== + dependencies: + ansi-html "0.0.7" + bonjour "^3.5.0" + chokidar "^2.1.5" + compression "^1.7.4" + connect-history-api-fallback "^1.6.0" + debug "^4.1.1" + del "^4.1.0" + express "^4.16.4" + html-entities "^1.2.1" + http-proxy-middleware "^0.19.1" + import-local "^2.0.0" + internal-ip "^4.2.0" + ip "^1.1.5" + killable "^1.0.1" + loglevel "^1.6.1" + opn "^5.5.0" + portfinder "^1.0.20" + schema-utils "^1.0.0" + selfsigned "^1.10.4" + semver "^6.0.0" + serve-index "^1.9.1" + sockjs "0.3.19" + sockjs-client "1.3.0" + spdy "^4.0.0" + strip-ansi "^3.0.1" + supports-color "^6.1.0" + url "^0.11.0" + webpack-dev-middleware "^3.6.2" + webpack-log "^2.0.0" + yargs "12.0.5" + +webpack-dev-server@^3.1.14: + version "3.7.2" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.7.2.tgz#f79caa5974b7f8b63268ef5421222a8486d792f5" + integrity sha512-mjWtrKJW2T9SsjJ4/dxDC2fkFVUw8jlpemDERqV0ZJIkjjjamR2AbQlr3oz+j4JLhYCHImHnXZK5H06P2wvUew== + dependencies: + ansi-html "0.0.7" + bonjour "^3.5.0" + chokidar "^2.1.6" + compression "^1.7.4" + connect-history-api-fallback "^1.6.0" + debug "^4.1.1" + del "^4.1.1" + express "^4.17.1" + html-entities "^1.2.1" + http-proxy-middleware "^0.19.1" + import-local "^2.0.0" + internal-ip "^4.3.0" + ip "^1.1.5" + killable "^1.0.1" + loglevel "^1.6.3" + opn "^5.5.0" + p-retry "^3.0.1" + portfinder "^1.0.20" + schema-utils "^1.0.0" + selfsigned "^1.10.4" + semver "^6.1.1" + serve-index "^1.9.1" + sockjs "0.3.19" + sockjs-client "1.3.0" + spdy "^4.0.0" + strip-ansi "^3.0.1" + supports-color "^6.1.0" + url "^0.11.0" + webpack-dev-middleware "^3.7.0" + webpack-log "^2.0.0" + yargs "12.0.5" + +webpack-log@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-2.0.0.tgz#5b7928e0637593f119d32f6227c1e0ac31e1b47f" + integrity sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg== + dependencies: + ansi-colors "^3.0.0" + uuid "^3.3.2" + +webpack-merge@4.2.1, webpack-merge@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-4.2.1.tgz#5e923cf802ea2ace4fd5af1d3247368a633489b4" + integrity sha512-4p8WQyS98bUJcCvFMbdGZyZmsKuWjWVnVHnAS3FFg0HDaRVrPbkivx2RYCre8UiemD67RsiFFLfn4JhLAin8Vw== + dependencies: + lodash "^4.17.5" + +webpack-sources@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.3.0.tgz#2a28dcb9f1f45fe960d8f1493252b5ee6530fa85" + integrity sha512-OiVgSrbGu7NEnEvQJJgdSFPl2qWKkWq5lHMhgiToIiN9w34EBnjYzSYs+VbL5KoYiLNtFFa7BZIKxRED3I32pA== + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack-sources@^1.1.0, webpack-sources@^1.2.0, webpack-sources@^1.3.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1: + version "1.4.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack-subresource-integrity@1.1.0-rc.6: + version "1.1.0-rc.6" + resolved "https://registry.yarnpkg.com/webpack-subresource-integrity/-/webpack-subresource-integrity-1.1.0-rc.6.tgz#37f6f1264e1eb378e41465a98da80fad76ab8886" + integrity sha512-Az7y8xTniNhaA0620AV1KPwWOqawurVVDzQSpPAeR5RwNbL91GoBSJAAo9cfd+GiFHwsS5bbHepBw1e6Hzxy4w== + dependencies: + webpack-core "^0.6.8" + +webpack@4.30.0: + version "4.30.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.30.0.tgz#aca76ef75630a22c49fcc235b39b4c57591d33a9" + integrity sha512-4hgvO2YbAFUhyTdlR4FNyt2+YaYBYHavyzjCMbZzgglo02rlKi/pcsEzwCuCpsn1ryzIl1cq/u8ArIKu8JBYMg== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-module-context" "1.8.5" + "@webassemblyjs/wasm-edit" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" + acorn "^6.0.5" + acorn-dynamic-import "^4.0.0" + ajv "^6.1.0" + ajv-keywords "^3.1.0" + chrome-trace-event "^1.0.0" + enhanced-resolve "^4.1.0" + eslint-scope "^4.0.0" + json-parse-better-errors "^1.0.2" + loader-runner "^2.3.0" + loader-utils "^1.1.0" + memory-fs "~0.4.1" + micromatch "^3.1.8" + mkdirp "~0.5.0" + neo-async "^2.5.0" + node-libs-browser "^2.0.0" + schema-utils "^1.0.0" + tapable "^1.1.0" + terser-webpack-plugin "^1.1.0" + watchpack "^1.5.0" + webpack-sources "^1.3.0" + +webpack@^4.29.5: + version "4.39.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.39.1.tgz#60ed9fb2b72cd60f26ea526c404d2a4cc97a1bd8" + integrity sha512-/LAb2TJ2z+eVwisldp3dqTEoNhzp/TLCZlmZm3GGGAlnfIWDgOEE758j/9atklNLfRyhKbZTCOIoPqLJXeBLbQ== + dependencies: + "@webassemblyjs/ast" "1.8.5" + "@webassemblyjs/helper-module-context" "1.8.5" + "@webassemblyjs/wasm-edit" "1.8.5" + "@webassemblyjs/wasm-parser" "1.8.5" + acorn "^6.2.1" + ajv "^6.10.2" + ajv-keywords "^3.4.1" + chrome-trace-event "^1.0.2" + enhanced-resolve "^4.1.0" + eslint-scope "^4.0.3" + json-parse-better-errors "^1.0.2" + loader-runner "^2.4.0" + loader-utils "^1.2.3" + memory-fs "^0.4.1" + micromatch "^3.1.10" + mkdirp "^0.5.1" + neo-async "^2.6.1" + node-libs-browser "^2.2.1" + schema-utils "^1.0.0" + tapable "^1.1.3" + terser-webpack-plugin "^1.4.1" + watchpack "^1.6.0" + webpack-sources "^1.4.1" + +websocket-driver@>=0.5.1: + version "0.7.3" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" + integrity sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg== + dependencies: + http-parser-js ">=0.4.0 <0.4.11" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" + integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== + +when@~3.6.x: + version "3.6.4" + resolved "https://registry.yarnpkg.com/when/-/when-3.6.4.tgz#473b517ec159e2b85005497a13983f095412e34e" + integrity sha1-RztRfsFZ4rhQBUl6E5g/CVQS404= + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@^1.2.1, which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +widest-line@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc" + integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA== + dependencies: + string-width "^2.1.1" + +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= + +worker-farm@^1.5.2, worker-farm@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.7.0.tgz#26a94c5391bbca926152002f69b84a4bf772e5a8" + integrity sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw== + dependencies: + errno "~0.1.7" + +worker-plugin@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/worker-plugin/-/worker-plugin-3.1.0.tgz#6311778f3514a87c273510ee3f809cc3fe161e6f" + integrity sha512-iQ9KTTmmN5fhfc2KMR7CcDblvcrg1QQ4pXymqZ3cRZF8L0890YLBcEqlIsGPdxoFwghyN8RA1pCEhCKuTF4Lkw== + dependencies: + loader-utils "^1.1.0" + +wrap-ansi@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= + dependencies: + string-width "^1.0.1" + strip-ansi "^3.0.1" + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@^2.0.0, write-file-atomic@^2.4.2: + version "2.4.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.3.tgz#1fd2e9ae1df3e75b8d8c367443c692d4ca81f481" + integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +ws@~3.3.1: + version "3.3.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2" + integrity sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA== + dependencies: + async-limiter "~1.0.0" + safe-buffer "~5.1.0" + ultron "~1.1.0" + +xdg-basedir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" + integrity sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ= + +xhr2@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/xhr2/-/xhr2-0.1.4.tgz#7f87658847716db5026323812f818cadab387a5f" + integrity sha1-f4dliEdxbbUCYyOBL4GMras4el8= + +xml2js@^0.4.17, xml2js@^0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= + +xmlhttprequest-ssl@~1.5.4: + version "1.5.5" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" + integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= + +"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +xxhashjs@^0.2.1: + version "0.2.2" + resolved "https://registry.yarnpkg.com/xxhashjs/-/xxhashjs-0.2.2.tgz#8a6251567621a1c46a5ae204da0249c7f8caa9d8" + integrity sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw== + dependencies: + cuint "^0.2.2" + +"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + +yallist@^3.0.0, yallist@^3.0.2, yallist@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" + integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A== + +yargs-parser@^11.1.1: + version "11.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" + integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^13.0.0, yargs-parser@^13.1.0, yargs-parser@^13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" + integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@12.0.5, yargs@^12.0.1: + version "12.0.5" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" + integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== + dependencies: + cliui "^4.0.0" + decamelize "^1.2.0" + find-up "^3.0.0" + get-caller-file "^1.0.1" + os-locale "^3.0.0" + require-directory "^2.1.1" + require-main-filename "^1.0.1" + set-blocking "^2.0.0" + string-width "^2.0.0" + which-module "^2.0.0" + y18n "^3.2.1 || ^4.0.0" + yargs-parser "^11.1.1" + +yargs@13.1.0: + version "13.1.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.1.0.tgz#b2729ce4bfc0c584939719514099d8a916ad2301" + integrity sha512-1UhJbXfzHiPqkfXNHYhiz79qM/kZqjTE8yGlEjZa85Q+3+OwcV6NRkV7XOV1W2Eom2bzILeUn55pQYffjVOLAg== + dependencies: + cliui "^4.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + os-locale "^3.1.0" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.0.0" + +yargs@13.2.4: + version "13.2.4" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" + integrity sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + os-locale "^3.1.0" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.0" + +yargs@^13.2.2: + version "13.3.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" + integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.1" + +yeast@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" + integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= + +yn@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +zone.js@~0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.9.1.tgz#e37c6e5c54c13fae4de26b5ffe8d8e9212da6d9b" + integrity sha512-GkPiJL8jifSrKReKaTZ5jkhrMEgXbXYC+IPo1iquBjayRa0q86w3Dipjn8b415jpitMExe9lV8iTsv8tk3DGag==