node_modules updated, buefy add to lib folder
This commit is contained in:
+707
@@ -0,0 +1,707 @@
|
||||
# Buefy Changelog
|
||||
|
||||
## 0.7.5
|
||||
|
||||
* **New component: Numberinput**, [see docs](https://buefy.org/documentation/numberinput)
|
||||
* **Loading improved**: can show a custom content, [see docs](https://buefy.org/documentation/loading)
|
||||
* Add ``destroy-on-hide`` prop to tabs
|
||||
* Add ``custom-detail-row`` prop to table
|
||||
* Add ``open-on-focus`` prop and improved accessibility for datepicker (thanks @imfaber)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix #1320 space keydown on Firefox to checkbox and switch
|
||||
* Fix #1325 input emit to upload
|
||||
* Enable native timepicker to clockpicker when ``mobile-native`` (default ``true``)
|
||||
* Fix clockpicker docs and input size
|
||||
* Fix upload opening in IE 11
|
||||
|
||||
## 0.7.4
|
||||
|
||||
### New features
|
||||
|
||||
* **New component: Button**, [see docs](https://buefy.org/documentation/button)
|
||||
* **New component: Clockpicker**, [see docs](https://buefy.org/documentation/clockpicker) (thanks @SharpSeeEr)
|
||||
* **Notification improved**: can show a notification programmatically, [see docs](https://buefy.org/documentation/notification)
|
||||
* Add ``custom-formatter`` prop to autocomplete
|
||||
* Add ``draggable`` prop to table (thanks @suneab)
|
||||
* Improve accessibility for notification, message and pagination (thanks @mateuswetah)
|
||||
|
||||
### Fixes
|
||||
|
||||
* #956 Fix hidden overflow on tabs (thanks @Baggs)
|
||||
* #1246 Fix Cleave.js example using Vue 2.6.x (thanks @yxngl)
|
||||
* #1251 Fix Datepicker closing when page back to ``min-date`` month
|
||||
* #1257 Fix Datepicker slow/freeze on big year input
|
||||
* #1282 Fix Datepicker event display wich have a time set (thanks @DonNicoJs)
|
||||
* #1290 Fix double click event for CheckBox(Button) and Radio(Button) (thanks @yxngl)
|
||||
* Fix tabindex on switch (thanks @yxngl), radio (thanks @Sacmanxman2) and checkbox
|
||||
|
||||
## 0.7.3
|
||||
|
||||
### New features
|
||||
|
||||
* **Update Bulma to 0.7.4**
|
||||
* Add ``required`` prop to radio-button and checkbox-button
|
||||
* Add ``defaultModalCanCancel`` constructor options (thanks @quimarche)
|
||||
* Add ``typing`` event to autocomplete (thanks @adrlen)
|
||||
* Add ``native-autocomplete`` prop to taginput
|
||||
* Add support to [Font Awesome SVG component](https://www.npmjs.com/package/@fortawesome/vue-fontawesome) (thanks @rhwilr)
|
||||
* Add ``custom-key-row`` prop to table
|
||||
* Add ``defaultToastPosition`` and ``defaultSnackbarPosition`` constructor options
|
||||
* Improve accessibility for collapse and dropdown (thanks @mateuswetah)
|
||||
* Revert tab as confirm key code to taginput
|
||||
* #1233 Change left margin to right margin when spacing nested fields (thanks @yxngl)
|
||||
|
||||
### Fixes
|
||||
|
||||
* #1177 Fix IE/Edge datepicker content size
|
||||
* #1216 Fix reset v-model when upload cancel
|
||||
* #1219 Fix modal ``width`` prop (thanks @yxngl)
|
||||
|
||||
## 0.7.2
|
||||
|
||||
### New features
|
||||
|
||||
* Add ``header`` autocomplete slot to taginput
|
||||
* Add ``required`` prop to switch
|
||||
* Add ``mobile-sort-placeholder`` prop to table
|
||||
* Add ``contextmenu`` event to table (thanks @gps-dev)
|
||||
* Add ``show-detail-icon`` prop to table and toggle row detail programmatically (thanks @omer2500)
|
||||
* Add ``use-html5-validation`` prop to form components and ``defaultUseHtml5Validation`` constructor options (thanks @lukio3)
|
||||
* #1164 Disable selecting row on checkbox click when table is ``checkable``
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix taginput when ``allow-new`` is enabled
|
||||
* Fix pagination order when simple
|
||||
* #1126 Fix init sort using columns table prop
|
||||
* #898, #1154 Fix browser autocomplete using multiple fields
|
||||
|
||||
## 0.7.1
|
||||
|
||||
### New features
|
||||
|
||||
* **Update Bulma to 0.7.2**
|
||||
* Add ``indeterminate`` prop to checkbox (thanks @guanzo)
|
||||
* #1029 Upgrade vue-test-utils to latest version (thanks @lordfuoco)
|
||||
* #1065 Add tab as confirm key code to taginput (thanks @williamabbott)
|
||||
* #1083 Add ``date-creator`` prop to datepicker (thanks @jonagoldman)
|
||||
|
||||
### Fixes
|
||||
|
||||
* #1024 Fix upload SSR support
|
||||
* #1027 Fix ``icon-pack`` prop when individual components
|
||||
* #1040 Fix prevent radio with multi-line text from shrinking (thanks @rhwilr)
|
||||
* #1067 Fix IE 11 UI switch (thanks @patrickabkarian)
|
||||
* #1072 Fix sorting boolean fields
|
||||
|
||||
## 0.7.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Change path to import components to use outside main vue instance, like:
|
||||
```javascript
|
||||
// see Toast, Snackbar, Modal and Dialog doc pages for more examples
|
||||
import { Toast } from 'buefy/dist/components/toast'
|
||||
Toast.open('Toasty!')
|
||||
```
|
||||
* Upload component accepts a single `File` and `Array<File>` when multiple as ``v-model``
|
||||
* Rename ``readonly`` to ``editable`` on datepicker and timepicker (now just use the prop without false value)
|
||||
* **Removed Panel component**, it was deprecated since 0.5.3
|
||||
|
||||
### New features
|
||||
|
||||
* Add ``closable`` prop to taginput (thanks @adrlen)
|
||||
* #970 Add Object type to ``type`` prop of field and icon
|
||||
|
||||
### Fixes
|
||||
|
||||
* Updated timepicker style (it's a little smaller now)
|
||||
* #992 Fix table header disappear with pagination
|
||||
* #999 Fix ``size`` prop to datepicker and timepicker
|
||||
|
||||
## 0.6.7
|
||||
|
||||
### New features
|
||||
|
||||
* **Table improved**: can change columns dynamically
|
||||
* Add ``auto-close`` and ``duration`` props to message and notification
|
||||
* Add ``allow-duplicates`` prop to taginput
|
||||
* Add ``required`` prop to radio and checkbox
|
||||
* Add ``defaultInputHasCounter`` constructor options
|
||||
|
||||
### Fixes and changes
|
||||
|
||||
* #839 Fix ``icon-pack`` from icon to be reactive
|
||||
* #840 Fix loading position to tagInput (thanks @adrlen)
|
||||
* #852 Fix label class to field when horizontal
|
||||
* #872 Fix datepicker default formatter
|
||||
* #895 Fix loading overlay when not in full page (thanks @adrlen)
|
||||
* #927 Fix autocomplete when option text is too long (thanks @8bu)
|
||||
* #948 Fix switch when multi line text (thanks @rhwilr)
|
||||
|
||||
## 0.6.6
|
||||
|
||||
### New features
|
||||
|
||||
* **Update Bulma to 0.7.1**
|
||||
* Add ``header`` slot to autocomplete
|
||||
* Add ``type`` prop to checkbox and radio
|
||||
* Add ``on-paste-separators`` and ``before-adding`` props to tagInput
|
||||
* #830 Add ``ellipsis`` prop to tagInput and tag components
|
||||
|
||||
### Fixes and changes
|
||||
|
||||
* #746 Fix default ``dateFormatter`` function to datepicker
|
||||
* #755 Fix arrow centered when table detail (thanks @wanxe)
|
||||
* #787 Fix type in horizontal field when message is empty
|
||||
* #814 Reset scroll to modal before destroy
|
||||
* #481 Fix tagInput height to match other inputs
|
||||
|
||||
## 0.6.5
|
||||
|
||||
* Revert some changes on Checkbox, CheckboxButton, Radio, RadioButton, Select and Switch, working as before now
|
||||
* #711 Fix click event to switch (thanks @adrlen)
|
||||
* #712 Fix SSR support (thanks @anteriovieira)
|
||||
* #719 Fix ``header`` slot to tabItem
|
||||
* #718 Fix ``changeMonth`` and ``changeYear`` events to datepicker in ``change-month`` and ``change-year``
|
||||
* #727 Fix upload check file
|
||||
* #730 Fix field with addons not updating on re-render (tanks @arlen)
|
||||
|
||||
## 0.6.4
|
||||
|
||||
### New features
|
||||
|
||||
* **Add nuxt-buefy on docs**, [see here](https://buefy.org/#/documentation/start) (thanks @anteriovieira)
|
||||
* Add ``indefinite`` prop to snackbar
|
||||
* Add ``header`` slot to datepicker
|
||||
* Add ``header`` slot to tabItem
|
||||
* #485 Add ``custom-class`` prop to field (thanks @docnoe)
|
||||
* #509 Add autocomplete slot support to taginput (thanks @gkinsman)
|
||||
* #581 Add ``is-row-checkable`` prop to table (thanks @jvmonjo)
|
||||
* #602 Add ``changeMonth`` and ``changeYear`` events to datepicker
|
||||
* #604 Add ``remove-on-keys`` prop to taginput, allowing to remove last tag (thanks @adrlen)
|
||||
* #609 Add ``is-full-page`` prop to loading, to limit it to its container (thanks @adrlen)
|
||||
* #610 Add ``visible`` prop to tabItem
|
||||
* #644 Add ``clear-on-select`` prop to autocomplete (thanks @raniesantos)
|
||||
* #645 Add ``selectable-dates`` prop to datepicker (thanks @pip63)
|
||||
* #682 Add ``has-detailed-visible`` prop to table (thanks @wanxe)
|
||||
* #687 Add ``focus-on`` prop to dialog
|
||||
|
||||
### Fixes and changes
|
||||
|
||||
* #576 Fix upload check file type
|
||||
* #586 Fix dropdown change event
|
||||
* #593 Fix timepicker update only time of Date object
|
||||
* #599 Fix ``on-cancel`` prop to dialog, get called when clicking confirm
|
||||
* #648 Fix dropdown emit change event (thanks @adrlen)
|
||||
* #670 Fix tabs emit change event (thanks @adrlen)
|
||||
* #686 Remove 'is-light' color to cancel button of dialog
|
||||
|
||||
## 0.6.3
|
||||
|
||||
### New features
|
||||
|
||||
* **New extension: Sortable.js**, [see here](https://buefy.org/#/extensions/sortablejs)
|
||||
* **Update Bulma to 0.6.2**
|
||||
* **Add Font Awesome 5 support** (http://fontawesome.com/)
|
||||
* Add ``columns`` prop to table, you can now pass an array instead of adding scoped slots (for simple tables)
|
||||
* Add ``rounded`` prop to Input, Select, Autocomplete, Datepicker and Timepicker, Pagination
|
||||
* Add "is-toggle-rounded" type to Tabs
|
||||
* Add ``open`` prop to collapse ``trigger`` slot
|
||||
* Add ``native`` prop to upload
|
||||
* #222 Add ``open-on-focus`` prop to autocomplete (good to simulate a select element)
|
||||
* #482 Add ``allow-new`` prop to taginput
|
||||
* #483 Add ``unselectable-days-of-week`` prop to datepicker
|
||||
* #487 Add ``icon-size`` prop to message
|
||||
* #492 Add isAsc parameter to ``custom-sort`` function of table (thanks @frantic1048)
|
||||
* #494 Add ``unselectable-times`` prop to timepicker
|
||||
* #525 Add ``pagination-size`` prop to table (thanks @nateleavitt)
|
||||
* #531 Add ``confirm-key-codes`` prop to taginput
|
||||
|
||||
### Fixes and changes
|
||||
|
||||
* Remove ``max-results`` prop from autocomplete, it now has a scrollbar with all results
|
||||
* #493 Fix ``keep-first`` autocomplete prop with async data
|
||||
* #498 Fix horizontal field on mobile
|
||||
* #500 Fix message position for horizontal field (thanks @electrolinux)
|
||||
* #543 Fix dropdown toggle programmatic
|
||||
|
||||
## 0.6.2
|
||||
|
||||
### New features
|
||||
|
||||
* **New component: Tag Input**, [see docs](https://buefy.org/#/documentation/taginput) (thanks @jgerbes)
|
||||
* #125 Add ``queue`` prop to toast/snackbar and ``defaultNoticeQueue`` constructor option
|
||||
* Note that the animations aren't perfect right now
|
||||
* #432 **Datepicker improved**: can add event indicators on it (thanks @chasegiunta)
|
||||
* #439 Add support to horizontal field, with the ``horizontal`` prop (thanks @electrolinux)
|
||||
* Add ``mobile-native`` prop to datepicker and timepicker
|
||||
* Add ``defaultDatepickerMobileNative`` and ``defaultTimepickerMobileNative`` constructor options
|
||||
* Add ``events`` prop to modal (to bind events programmatically)
|
||||
* Add ``position`` prop to datepicker and timepicker
|
||||
* Add ``size`` prop to dialog
|
||||
* Add ``tabstop`` and ``disabled`` props to tag
|
||||
* Add ``.sync`` option to table ``current-page``
|
||||
|
||||
### Fixes and changes
|
||||
|
||||
* Maxlength counter for input only shows on focus
|
||||
* Add padding to tab content
|
||||
* Fix autocomplete with ``keep-first`` selecting on blur when nothing is shown
|
||||
* Fix tabs buttons size sometimes flickering
|
||||
* #416 Fix pagination color
|
||||
* #419 Fix input icon size
|
||||
* #428 Fix ``current-page`` from table to be reactive
|
||||
* #440 Fix upload input file width (thanks @frantic1048)
|
||||
|
||||
## 0.6.1
|
||||
|
||||
* #409 Fix notification delete button
|
||||
* #410 Fix alert triggering on upload
|
||||
|
||||
## 0.6.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Vue 2.5+ is now the minimum required version
|
||||
* Bulma breaking changes, [see changelog](https://github.com/jgthms/bulma/blob/master/CHANGELOG.md#breaking-changes)
|
||||
* **Removed** the deprecated prompt dialog ``inputPlaceholder``, ``inputName`` and ``inputMaxlength`` props
|
||||
* Buefy now uses [MDI icons from community](https://materialdesignicons.com/) instead, you'll have to change icon names and if you use a CDN you should change:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="//cdn.materialdesignicons.com/2.0.46/css/materialdesignicons.min.css">
|
||||
```
|
||||
|
||||
### New features
|
||||
|
||||
* **New component: Timepicker**, [see docs](https://buefy.org/#/documentation/timepicker)
|
||||
* **New component: Checkbox Button**, [see docs](https://buefy.org/#/documentation/checkbox)
|
||||
* **New docs section: Extensions**, [see here](https://buefy.org/#/extensions/cleavejs)
|
||||
* Support for Bulma 0.6.1
|
||||
* Add ``disabled`` prop to tabItem
|
||||
* Add ``focusable`` prop to table
|
||||
* Add ``focus()`` method to table
|
||||
* Add ``hoverable`` prop to table
|
||||
* Add ``custom-size`` prop to icon
|
||||
* Add ``custom-class`` prop to icon
|
||||
* Add ``detail-key`` and ``opened-detailed`` props to table (thanks @wanxe)
|
||||
* Add ``scroll`` prop to modal/dialog (changes what to do with ``<body>`` scrollbar)
|
||||
* Add ``parent`` prop to modal, pass a Vue object to it to open component modal programmatically
|
||||
* #359 Add ``mobile-modal`` prop to dropdown to enable/disable modal on mobile
|
||||
* #365 Add ``src/scss/utils/_all.scss`` for easier modular importing (thanks @gwvt)
|
||||
* #377 Add support to MDI icons from community
|
||||
* #387 Add ``current-page`` prop to table (thanks @frantic1048)
|
||||
* #388 Add ``size`` prop to radio and checkbox
|
||||
* #398 Add ``unselectable-dates`` prop to datepicker to disable an array of dates
|
||||
* #399 Add configurable closing options for dialog (like modal)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Update tooltip font size and padding, it's a bit bigger now
|
||||
* Update table keyboard navigation, now doesn't require pressing enter and only works when ``focusable``
|
||||
* #288 Fix input message from parent field (thanks @yarbshk)
|
||||
* #358 Fix modal/dialog shifting content (``scroll`` prop)
|
||||
* #380 Fix clearing autocomplete input
|
||||
* #395 Fix ref dropdown availability (thanks @frantic1048)
|
||||
|
||||
## 0.5.4
|
||||
|
||||
* **Deprecated** prompt dialog ``inputPlaceholder``, ``inputName`` and ``inputMaxlength`` props, use new ``inputAttrs`` prop, [see docs](https://buefy.org/#/documentation/dialog)
|
||||
* Fix ``focus`` method on select
|
||||
* Table with ``selected`` prop can now be natigated with keyboard
|
||||
* #251, #345 Add ``bottom-left`` slot to table
|
||||
* #334 Add ``defaultDialogConfirmText`` and ``defaultDialogCancelText`` constructor options
|
||||
* #335 Add ``icon`` and ``iconPack`` prop to dialog
|
||||
* #350 Add Function type to ``component`` prop of modal (thanks @ivalkenburg)
|
||||
* #354 Add tab key press to autocomplete (thanks @tmepple)
|
||||
* #356 Fix table header alignment
|
||||
|
||||
## 0.5.3
|
||||
|
||||
* **New component: Collapse**, [see docs](https://buefy.org/#/documentation/collapse)
|
||||
* **Panel deprecated**, use new Collapse component, [old docs here](https://buefy.org/#/documentation/panel)
|
||||
* Update Bulma to v0.5.3
|
||||
* #223 Add ``footer`` slot to table
|
||||
* #246 Remove ``key`` on detailed rows of table component (thanks @Tirke)
|
||||
* #301 Fix Bulma style override for radio and checkbox
|
||||
* #303 Fix dropdown close on custom item when is replaced or changed
|
||||
* #305 Fix datepicker calendar display when disabled and inline
|
||||
* #309 Add ``labelFor`` prop to field component, to set native ``for`` attribute to the label tag (thanks @tmepple)
|
||||
* #314 Set pack prop to next/prev icon buttons for datepicker
|
||||
* #315 Fix escape regexp characters to autocomplete
|
||||
* #316 Fix checkbox shrinking when text is very long (thanks @Tirke)
|
||||
* #317 Add default datepicker constructor options
|
||||
* #324 Fix table broken sort state
|
||||
* #330 Fix table empty and detail disposition
|
||||
* #339 Fix upload warning when is required
|
||||
* #340 Add ``size`` prop to radio button
|
||||
|
||||
## 0.5.2
|
||||
|
||||
* #262 Add ``empty`` slot to autocomplete (empty message)
|
||||
* #264 Add ``has-counter`` prop to input, add it when you want show or hide character counter when maxlength prop is passed
|
||||
* #265 Fix disabled pagination (thanks @YassinOuider)
|
||||
* #272 Add ``readonly`` prop to allow typing a date on datepicker
|
||||
* #273 Fix autocomplete for SSR (thanks @LANSELOT)
|
||||
* #276 Add ``first-day-of-week`` prop to datepicker
|
||||
* #279 Fix table not default sorting async data
|
||||
* #289 Fix dropdown-trigger container html tag
|
||||
* #291 Add configurable closing options for Modal (thanks @jwahdatehagh)
|
||||
* #298 Fix mobile detection datepicker for SSR
|
||||
* #302 Fix datepicker calendar display when disabled
|
||||
|
||||
## 0.5.1
|
||||
|
||||
* **New component: Datepicker**, [see docs](https://buefy.org/#/documentation/datepicker) (thanks @bartboy011)
|
||||
* **New component: Tag**, [see docs](https://buefy.org/#/documentation/tag)
|
||||
* #195 Add ``custom-key`` prop to table column, add it when you want a column without label
|
||||
* #221 Add backend pagination support to table (thanks @cappuc)
|
||||
* #214 Add ``active`` prop to tooltip
|
||||
* #215 Add ``header`` slot to table (custom headers)
|
||||
* #228 Fix table checked rows not working on mobile
|
||||
* #235 Add ``custom-is-checked`` prop to table column, add it when you want a custom compare function when is checkable
|
||||
* #237 Fix emit blur event for input
|
||||
* #239 Fix table columns on v-for loop
|
||||
* #241 Fix remove tabItem when it's bounded to array
|
||||
* #242 Update required Vue.js version (2.4+) (thanks @iwan-wijaya)
|
||||
* #243 Add ``default-sort-direction`` prop to table, add it when you want change default sort direction on the first click
|
||||
* #260 Fix radio/radio button emitting ``input`` multiple times (thanks @frantic1048)
|
||||
* Add ``meta`` prop to table column, useful for custom headers
|
||||
* Add ``type`` prop to switch (thanks @Tirke)
|
||||
* Add ``multiple`` support to select
|
||||
* Add ``inline`` prop to dropdown, items are shown inline and trigger removed
|
||||
* Remove ``has-custom-template`` prop from autocomplete, just use scoped slot to have the same feature
|
||||
|
||||
## 0.5.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* **Update Bulma to v0.5.1**, changes on Sass variables naming, [see docs](https://github.com/jgthms/bulma/blob/master/CHANGELOG.md#050)
|
||||
* Buefy Sass variables are also following the new convention
|
||||
* **Removed Menu component**, it was experimental and wasn't good enough, needed too much to work and can easily be replaced with pure HTML
|
||||
* Constructor option ``defaultContentElement`` renamed to ``defaultContainerElement``
|
||||
* ``b-dropdown-option`` renamed to ``b-dropdown-item``
|
||||
* ``subheader`` prop renamed to ``custom``
|
||||
* Any attributes on Input, Select and Autocomplete will be added directly to ``<input>`` or ``<select>`` itself instead of the root element
|
||||
* ``change`` events for all form controls (Input, Select Autocomplete, Checkbox, RadioGroup, Switch) that returned pure value will now return ``$event`` (and require ``.native`` modifier), use ``input`` instead
|
||||
* Checkbox ``custom-value`` prop renamed to ``native-value``
|
||||
* ``checked`` prop removed
|
||||
* **Removed CheckboxGroup**, just add the same ``v-model`` to multiple Checkboxes and set a ``native-value``, [see docs](https://buefy.org/#/documentation/checkbox)
|
||||
* Radio and Radio Button ``value`` prop renamed to ``native-value``
|
||||
* Add ``v-model`` support
|
||||
* Radio Buttons have to be wrapped on a field
|
||||
* **Removed RadioGroup**, just add the same ``v-model`` to multiple Radios/RadioButtons and set a ``native-value`` [see docs](https://buefy.org/#/documentation/radio)
|
||||
* Remove switch ``checked`` prop
|
||||
|
||||
### New stuff
|
||||
|
||||
* **New component: Upload**, [see docs](https://buefy.org/#/documentation/upload) (thanks @jtommy)
|
||||
* Field ``position`` prop works for ``grouped`` fields
|
||||
* Add ``group-multiline`` prop to field
|
||||
* Add ``size`` prop to message
|
||||
* #191 Add detail option (collapse) to table row, [see docs](https://buefy.org/#/documentation/table) (thanks @wanxe)
|
||||
* #201 Add support to array on Field ``message`` prop (thanks @jtommy)
|
||||
* #207 Add ``centered`` prop to table column
|
||||
* Add ``native-value``, ``true-value`` and ``false-value`` props to switch
|
||||
|
||||
### Bug fixes
|
||||
|
||||
* #206 Fix radio and checkbox within ``v-for``
|
||||
|
||||
## 0.4.6
|
||||
|
||||
* **New component: Menu**, [see docs](https://buefy.org/#/documentation/menu) (thanks @tsctao)
|
||||
* **New component: Panel**, [see docs](https://buefy.org/#/documentation/panel) (thanks @jtommy)
|
||||
* **New component: Loading**, [see docs](https://buefy.org/#/documentation/loading) (thanks @jtommy)
|
||||
* **Dropdown improved**: can add any content on it, can be triggered by hover, [see docs](https://buefy.org/#/documentation/dropdown)
|
||||
* Remove ``narrowed`` prop from dropdown
|
||||
* Update Bulma to v0.4.4
|
||||
* Add CDN install method and JSFiddle examples
|
||||
* Add boolean ``addons`` prop to field
|
||||
* Fix message and notification inside ``.media`` element
|
||||
* Fix pagination current page to not be clickable (thanks @jtommy)
|
||||
* #118 Add Nuxt.js installation on docs
|
||||
* #171 Fix modal not taking full width on mobile
|
||||
* #182 Fix table not default sorting async data (thanks @jtommy)
|
||||
* #184 Add ``input`` event on docs (input/autocomplete)
|
||||
* #190 Add Vue.js required version on docs
|
||||
* #197 Fix autocomplete ``expanded`` prop not working correctly
|
||||
|
||||
## 0.4.5
|
||||
|
||||
* Update Bulma to v0.4.3
|
||||
* Page is now clipped when a modal/dialog is active (to avoid weird scroll behavior)
|
||||
* Update modal docs example to not use the ``component`` prop, works better for events and is more common — Old way is not deprecated
|
||||
* Remove programmatic component modal from docs since isn't a good practice and was causing too many issues
|
||||
* Fix autocomplete ``focus()`` method
|
||||
* #106 Fix prompt dialog vue-warning
|
||||
* #144 Add ``rows`` prop to input (textarea)
|
||||
* #145 Fix autocomplete not preventing form from submitting (thanks @jtommy)
|
||||
* #153 Add return for ``checkHtml5Validity()`` method
|
||||
* #155 Fix input/autocomplete ``change`` event (thanks @jtommy)
|
||||
* #157 Fix table redundant level element generating extra padding
|
||||
* #159 Fix modal with ``.modal-card`` responsiveness, added a ``has-modal-card`` prop
|
||||
* #165 Fix safari button alignment on dialog (thanks @jtommy)
|
||||
* #154 Add prop for when table data is empty (thanks @jtommy)
|
||||
* #170 Fix dialogs min-width mobile
|
||||
|
||||
## 0.4.4
|
||||
|
||||
* #139 Fix notices durations and constructor options
|
||||
|
||||
## 0.4.3
|
||||
|
||||
* Update switch animation (it was laggy) and causing weird behavior within ``<keep-alive>`` components, [see updated animation](https://buefy.org/#/documentation/switch)
|
||||
* Add tons of props to Autocomplete, [see here](https://buefy.org/#/documentation/autocomplete)
|
||||
* **Dropdown button as addon now needs a ``.control`` element wrapping**
|
||||
* Add some useful methods on docs for Input, Select, Autocomplete and Table
|
||||
* Fix autocomplete list width
|
||||
* #38 Add ``animation`` prop for dialog and modal, for custom animation (transition name)
|
||||
* #115 Add ``row-class`` property to table to style row status, e.g.:
|
||||
|
||||
```html
|
||||
<b-table :data="tableData" :row-class="row => row.amount < 10 && 'is-warning'">[...]</b-table>
|
||||
```
|
||||
|
||||
* #117 Fix table row loop missing ``key`` prop
|
||||
* #122 Fix async tab-items
|
||||
* #124 Toast, Dialog, Snackbar and Programmatic Modal exposed
|
||||
* #128 Add default notices duration constructor options (thanks @quimarche)
|
||||
* #130 Fix checkbox/switch not emitting change event when using keyboard (thanks @gongzza)
|
||||
* #132 Add dropdown ``active-change`` event to know when it's dropdown list is active/visible (thanks @bartboy011)
|
||||
* #134 Add ``true-value`` and ``false-value`` props to checkbox
|
||||
|
||||
## 0.4.2
|
||||
|
||||
* Fix Buefy installation
|
||||
|
||||
## 0.4.1
|
||||
|
||||
* Fix table sorting null values
|
||||
* Table null values always come last when sorting
|
||||
* Add icon support to select
|
||||
* Fix dropdown to not stay on top of trigger
|
||||
* Fix switch disable state
|
||||
* Add click events for dropdown option
|
||||
* Fix dropdown disable state
|
||||
* Fix table columns not being reactive
|
||||
* #90 Fix tabs with long elements (thanks @mirshko)
|
||||
* #91 Update Bulma to v0.4.2
|
||||
* #96 Add ``visible`` prop to table column
|
||||
* #102 Fix anchor tag / router-link on dropdown
|
||||
* #104 Fix numeric zero values on input's maxlength (thanks @iwan-wijaya)
|
||||
|
||||
## 0.4.0
|
||||
|
||||
### I've broke some stuff...
|
||||
|
||||
* Remove ``label``, ``icon`` and ``icon-pack`` properties from **DropdownOption** thanks to the new syntax, [see docs](https://buefy.org/#/documentation/dropdown)
|
||||
* Remove ``content`` property from **TableColumn**, use ``v-html`` on an element inside TableColumn instead
|
||||
* **Field**, **Snackbar** and **Toast** ``position`` property now needs the ``is-`` prefix (eg. ``is-top-left`` or ``is-centered``) — *Gotta fix these inconsistencies while it's still in beta*
|
||||
* Remove ``on-off`` prop from Switch
|
||||
|
||||
### Additions and fixes
|
||||
|
||||
* **New component: Autocomplete**, [see docs](https://buefy.org/#/documentation/autocomplete)
|
||||
* **Switch design overhaul**, [see new style](https://buefy.org/#/documentation/switch)
|
||||
* Fix Tabs dynamic height
|
||||
* Add one more Sass variable: ``$speed-slower: 250ms !default``
|
||||
* Add ``size`` prop to Switch
|
||||
* Add ``.sync`` modifier to Pagination ``current`` prop
|
||||
* Add ``active`` property and changed animations of Notification and Message
|
||||
* Add Dropdown within nav example
|
||||
* All selections control (Checkbox, Radio, RadioButton and Switch) can now be focused and changed by keyboard
|
||||
* #69 Add loading state to Table
|
||||
* #71 Add background for checked rows on Table (thanks @mikejavier)
|
||||
* #72 Fix Table overflow
|
||||
* #74 Fix DropdownOption slots, can customize with HTML now
|
||||
* #76 Modal close event accepts parameters (thanks @gongzza)
|
||||
* #77 Add index to Table scoped slot (thanks @UrVerySpecial)
|
||||
* #78 Fix Table sorting zero values (thanks @gwvt)
|
||||
* #82 Fix Select with numeric zero value
|
||||
|
||||
## 0.3.2
|
||||
|
||||
* **New component: Tabs**, [see docs](https://buefy.org/#/documentation/tabs)
|
||||
* Add ``content`` prop to TableColumn, use this instead of ``v-html`` or it might break on tables with ``mobile-cards`` (collapsed rows)
|
||||
* Alert Dialog accepts string as well
|
||||
* Dialog open with focus on confirm button (easier to just hit enter)
|
||||
* Modal can be used in template as well (non programmatic)
|
||||
* Table automatically scrolls if overflowed
|
||||
* #62 Add sort direction with Table ``mobile-cards``
|
||||
* #67 Fix Table sort null values
|
||||
|
||||
## 0.3.1
|
||||
|
||||
* Field ``grouped`` now also works on nested Fields
|
||||
* Add Modal props
|
||||
* Fix all animations
|
||||
* Fix row click error
|
||||
* Fix ``.sync`` modifier
|
||||
* #56 Fix selection controls initial state always false
|
||||
* #58 Fix Table ``checked-rows`` prop not updating on Chrome
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Modal
|
||||
|
||||
Show any content you want in a modal programatically, like an image:
|
||||
|
||||
```javascript
|
||||
this.$modal.open(
|
||||
`<p class="image is-4by3">
|
||||
<img src="./static/placeholder-1280x960.png">
|
||||
</p>`
|
||||
)
|
||||
```
|
||||
|
||||
or a component:
|
||||
|
||||
```javascript
|
||||
import CustomComponent from './components/CustomComponent'
|
||||
|
||||
this.$modal.open({
|
||||
component: CustomComponent
|
||||
})
|
||||
```
|
||||
|
||||
### Table
|
||||
|
||||
**Breakable changes on it**, now requires a scoped slot to wrap the columns, and a slot inside the column. You have now full control over the content of cells:
|
||||
|
||||
```html
|
||||
<b-table :data="data">
|
||||
<template scope="props">
|
||||
<b-table-column field="id" label="ID" sortable numeric>
|
||||
{{ props.row.id }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="user.first_name" label="First Name" sortable>
|
||||
{{ props.row.user.first_name }}
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="date" label="Date" sortable
|
||||
v-html="formatDate(props.row.date)">
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="gender" label="Gender">
|
||||
<b-icon
|
||||
pack="fa"
|
||||
:icon="props.row.gender === 'Male' ? 'mars' : 'venus'">
|
||||
</b-icon>
|
||||
{{ props.row.gender }}
|
||||
</b-table-column>
|
||||
</template>
|
||||
</b-table>
|
||||
```
|
||||
* Prop ``selectable`` renamed to ``selected`` and expects an Object, if you have Vuejs v2.3+ you can use the ``.sync`` modifier to make it two-way binding:
|
||||
|
||||
```html
|
||||
<b-table :data="data" :selected.sync="selectedRow">
|
||||
```
|
||||
|
||||
is the same as:
|
||||
|
||||
```html
|
||||
<b-table :data="data" :selected="selectedRow" @select="row => selectedRow = row">
|
||||
```
|
||||
|
||||
* Table new prop ``checked-rows`` that expects an Array, if you have Vuejs v2.3+ you can use the ``.sync`` modifier to make it two-way binding
|
||||
* Table ``render-html`` prop removed thanks to the new syntax
|
||||
* Column ``component`` prop removed thanks to the new syntax
|
||||
* Column ``field`` prop now accepts nested object keys
|
||||
|
||||
### Other
|
||||
|
||||
* **Breaking:** Select ``b-option`` is now only ``option``
|
||||
* **Breaking:** Dropdown ``b-option`` is now ``b-dropdown-option``
|
||||
* Fix Dropdown background overlay index
|
||||
* #43 Fix Dialog cancel button focus state
|
||||
* #50 Add ``page-change`` event on Table
|
||||
* #52 Fix Select option
|
||||
* #53 Add ``type=button`` to notifications
|
||||
|
||||
## 0.2.2
|
||||
|
||||
* Update Vue to 2.2.6
|
||||
* Update Bulma to 0.4.1
|
||||
* #35 Fix Dropdown TypeError
|
||||
* #29, #39 Fix SSR
|
||||
* #42 Fix Table ``default-sort`` prop
|
||||
* #40 Select now use native select, removed ``searchable`` property
|
||||
|
||||
## 0.2.1
|
||||
|
||||
* Add ``loading`` property to Select
|
||||
* Fix Checkbox disabled style
|
||||
* Fix Input not being validated when v-model is updated
|
||||
* Fix Input ``loading`` overlapping other icons
|
||||
* Fix Input ``password-reveal`` icon being overlapped by Field ``type`` icon
|
||||
* #23 Fix Select with addons overflowing
|
||||
* #30 Fix Table ``backend-sorting`` resetting sorted column
|
||||
* #33 Fix Input and Select on-blur listener removing user-defined Field ``type`` and ``message``
|
||||
|
||||
## 0.2.0
|
||||
|
||||
* **New component: Select**
|
||||
* **New component: Dropdown**
|
||||
* **Breaking:** Rename Table property ``html`` to ``render-html``
|
||||
* Fix Pagination buttons messing up at pages 3 and 4
|
||||
* Fix Dialog keeping keyup event
|
||||
* Fix prompt Dialog not validating before confirming
|
||||
* #6 Fix docs not working on IE. Thanks to [AngelMunoz](https://github.com/AngelMunoz)!
|
||||
* #9 Fix Table ``bordered`` sorted column
|
||||
* #10 Add Table ``backend-sorting`` property and ``sort`` event
|
||||
* #14 Add ``component`` property on Table. Thanks to [korpa](https://github.com/korpa)!
|
||||
|
||||
## 0.1.5
|
||||
|
||||
* Fix #12
|
||||
* Refactored Notification and Message
|
||||
* Add animation when closing Notification and Message
|
||||
|
||||
## 0.1.4
|
||||
|
||||
* Fix #5 — now serving compiled files
|
||||
* Fix #6 (partially) — docs now working on Edge
|
||||
* Add "UI Elements" on docs to specify that the user can use anything from Bulma
|
||||
* Redone the construtor options structure to work with the compiled build
|
||||
|
||||
## 0.1.2
|
||||
|
||||
* Fix Switch two-way data binding not working correctly
|
||||
* Fix Radio animation not working properly
|
||||
* Fix #2 — select arrow misplaced
|
||||
* Fix Input's help message overlapping maxlength's counter
|
||||
* Fix Table column sorting — should start with ASC
|
||||
* Fix Table mobile sorting not working
|
||||
|
||||
## 0.1.1
|
||||
|
||||
* Fix #1 — ``lib`` folder not published
|
||||
|
||||
## 0.1.0
|
||||
|
||||
* **New component: Input**
|
||||
* **New component: Field**
|
||||
* Documentation done
|
||||
|
||||
## 0.0.3
|
||||
|
||||
* **New component: Message**
|
||||
* **New component: Tooltip**
|
||||
* **New component: Dialog**
|
||||
* Add size option to Pagination
|
||||
* Notification slightly modified to keep original Bulma style
|
||||
* Toasts and Snackbars now queue between each other
|
||||
* Toast duration reduced to 2s
|
||||
|
||||
## 0.0.2
|
||||
|
||||
* **New component: Pagination**
|
||||
* **Component rework: Notify is now Snackbar**
|
||||
* Table now uses the Pagination component
|
||||
* Toasts are now queued
|
||||
* Option to load components individually
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2019 Rafael Beraldo
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
<p align="center">
|
||||
<a href="https://buefy.org">
|
||||
<img src="https://raw.githubusercontent.com/buefy/buefy/master/static/img/buefy-banner.png" />
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/buefy"><img src="https://img.shields.io/npm/v/buefy.svg" /></a>
|
||||
<a href="https://www.npmjs.com/package/buefy"><img src="https://img.shields.io/npm/dt/buefy.svg" /></a>
|
||||
<a href="https://circleci.com/gh/buefy/buefy"><img src="https://img.shields.io/circleci/project/github/buefy/buefy.svg?style=flat-square" /></a>
|
||||
<a href="https://codecov.io/gh/buefy/buefy"><img src="https://img.shields.io/codecov/c/github/buefy/buefy.svg?style=flat-square" /></a>
|
||||
<a href="https://www.npmjs.com/package/buefy"><img src="https://img.shields.io/npm/l/buefy.svg" /></a>
|
||||
<a href="https://discordapp.com/invite/ZkdFJMr"><img src="https://img.shields.io/badge/chat-on%20discord-7289DA.svg" /></a>
|
||||
<a href="https://buefy.org"><img src="https://img.shields.io/badge/code_style-buefy-7957d5.svg?style=flat-square" /></a>
|
||||
</p>
|
||||
|
||||
> Buefy is a lightweight library of responsive UI components for [Vue.js](https://vuejs.org/) based on [Bulma](http://bulma.io/) framework and design.
|
||||
|
||||
## Features
|
||||
|
||||
* Keep your current Bulma theme / variables easily
|
||||
* Supports both [Material Design Icons](https://materialdesignicons.com/) and [FontAwesome](http://fontawesome.io/)
|
||||
* Very lightweight with none internal dependencies aside from Vue & Bulma
|
||||
* About 60KB min+gzip (with Bulma included)
|
||||
* Semantic code output
|
||||
* Follows Bulma design and some of the [Material Design UX](https://material.io/)
|
||||
* Focus on usability and performance without *over-animating* stuff
|
||||
|
||||
## Documentation
|
||||
|
||||
The documentation is in the docs directory, it serves as the demo as well.
|
||||
|
||||
Browse [online documentation here](https://buefy.org/).
|
||||
|
||||
## Quick start
|
||||
|
||||
You need [Vue.js](https://vuejs.org/) **version 2.5+**.
|
||||
|
||||
### 1 Install via npm
|
||||
|
||||
```bash
|
||||
npm install buefy
|
||||
```
|
||||
|
||||
### 2 Import and use Buefy
|
||||
|
||||
Bundle
|
||||
```javascript
|
||||
import Vue from 'vue';
|
||||
import Buefy from 'buefy';
|
||||
import 'buefy/dist/buefy.css';
|
||||
|
||||
Vue.use(Buefy);
|
||||
|
||||
```
|
||||
or Individual Components
|
||||
```javascript
|
||||
|
||||
import Vue from 'vue'
|
||||
import { Field, Input } from 'buefy/dist/components'
|
||||
import 'buefy/dist/buefy.css'
|
||||
|
||||
Vue.use(Field)
|
||||
Vue.use(Input)
|
||||
|
||||
or
|
||||
|
||||
import Vue from 'vue'
|
||||
import Field from 'buefy/dist/components/field'
|
||||
import Input from 'buefy/dist/components/input'
|
||||
import 'buefy/dist/buefy.css'
|
||||
|
||||
Vue.use(Field)
|
||||
Vue.use(Input)
|
||||
```
|
||||
|
||||
### 3 Include Material Design Icons
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="//cdn.materialdesignicons.com/2.0.46/css/materialdesignicons.min.css">
|
||||
```
|
||||
|
||||
If you want to customize the icons or the theme, refer to the [customization section on the documentation](https://buefy.org/#/documentation/customization).
|
||||
|
||||
### Alternatively, you can use a CDN or even download
|
||||
|
||||
```html
|
||||
<!-- Buefy CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/buefy/dist/buefy.min.css">
|
||||
|
||||
<!-- Buefy JavaScript -->
|
||||
<script src="https://unpkg.com/buefy/dist/buefy.min.js"></script>
|
||||
```
|
||||
|
||||
## Browser support
|
||||
|
||||
Recent versions of Firefox, Chrome, Edge, Opera and Safari. IE10+ is only partially supported.
|
||||
|
||||
## Versioning
|
||||
|
||||
While it's still in beta, version will follow **v0.Y.Z**, where:
|
||||
|
||||
* **Y**: Major (breaking changes)
|
||||
* **Z**: Minor or patch
|
||||
|
||||
## Maintainers
|
||||
|
||||
* [Walter Tommasi](https://twitter.com/walter_tommasi)
|
||||
|
||||
## Supporting through Patreon
|
||||
|
||||
Buefy is an open source MIT project if you are interested in supporting this project, please consider becoming a patron.
|
||||
<p align="center">
|
||||
<a href="https://www.patreon.com/jtommy">
|
||||
<img src="https://c5.patreon.com/external/logo/become_a_patron_button.png" alt="Become a Patron" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## License
|
||||
|
||||
Code released under [MIT](https://github.com/buefy/buefy/blob/master/LICENSE) license.
|
||||
|
||||
Copyright (c), [Rafael Beraldo](https://twitter.com/rafaelpimpa).
|
||||
+9193
File diff suppressed because it is too large
Load Diff
+14166
File diff suppressed because it is too large
Load Diff
+4
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
@@ -0,0 +1,2 @@
|
||||
/*! Buefy v0.7.4 | MIT License | github.com/buefy/buefy */
|
||||
!(function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var n=t();for(var o in n)("object"==typeof exports?exports:e)[o]=n[o]}})("undefined"!=typeof self?self:this,(function(){return (function(e){function t(o){if(n[o])return n[o].exports;var r=n[o]={i:o,l:!1,exports:{}};return e[o].call(r.exports,r,r.exports,t),r.l=!0,r.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:o})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/",t(t.s=254)})({0:function(e,t){e.exports=function(e,t,n,o,r){var i,s=e=e||{},a=typeof e.default;"object"!==a&&"function"!==a||(i=e,s=e.default);var u="function"==typeof s?s.options:s;t&&(u.render=t.render,u.staticRenderFns=t.staticRenderFns),o&&(u._scopeId=o);var c;if(r?(c=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),n&&n.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(r)},u._ssrRegister=c):n&&(c=n),c){var f=u.functional,p=f?u.render:u.beforeCreate;f?u.render=function(e,t){return c.call(t),p(e,t)}:u.beforeCreate=p?[].concat(p,c):[c]}return{esModule:i,exports:s,options:u}}},12:function(e,t,n){"use strict";n.d(t,"c",(function(){return o})),n.d(t,"a",(function(){return r})),n.d(t,"b",(function(){return i}));var o=function(e){"undefined"!=typeof window&&window.Vue&&window.Vue.use(e)},r=function(e,t){e.component(t.name,t)},i=function(e,t,n){e.prototype[t]=n}},135:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=n(136),r=n.n(o),i=n(12);n.d(t,"Collapse",(function(){return r.a}));var s={install:function(e){Object(i.a)(e,r.a)}};Object(i.c)(s),t.default=s},136:function(e,t,n){var o=n(0)(n(137),n(138),null,null,null);e.exports=o.exports},137:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={name:"BCollapse",props:{open:{type:Boolean,default:!0},animation:{type:String,default:"fade"},ariaId:{type:String,default:""}},data:function(){return{isOpen:this.open}},watch:{open:function(e){this.isOpen=e}},methods:{toggle:function(){this.isOpen=!this.isOpen,this.$emit("update:open",this.isOpen),this.$emit(this.isOpen?"open":"close")}}}},138:function(e,t){e.exports={render:function(){var e=this,t=e.$createElement,n=e._self._c||t;return n("div",{staticClass:"collapse"},[n("div",{staticClass:"collapse-trigger",on:{click:e.toggle}},[e._t("trigger",null,{open:e.isOpen})],2),e._v(" "),n("transition",{attrs:{name:e.animation}},[n("div",{directives:[{name:"show",rawName:"v-show",value:e.isOpen,expression:"isOpen"}],staticClass:"collapse-content",attrs:{id:e.ariaId,"aria-expanded":e.isOpen}},[e._t("default")],2)])],1)},staticRenderFns:[]}},254:function(e,t,n){e.exports=n(135)}})}));
|
||||
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
@@ -0,0 +1,2 @@
|
||||
/*! Buefy v0.7.4 | MIT License | github.com/buefy/buefy */
|
||||
!(function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var n=t();for(var o in n)("object"==typeof exports?exports:e)[o]=n[o]}})("undefined"!=typeof self?self:this,(function(){return (function(e){function t(o){if(n[o])return n[o].exports;var s=n[o]={i:o,l:!1,exports:{}};return e[o].call(s.exports,s,s.exports,t),s.l=!0,s.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:o})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/",t(t.s=273)})({0:function(e,t){e.exports=function(e,t,n,o,s){var r,a=e=e||{},i=typeof e.default;"object"!==i&&"function"!==i||(r=e,a=e.default);var l="function"==typeof a?a.options:a;t&&(l.render=t.render,l.staticRenderFns=t.staticRenderFns),o&&(l._scopeId=o);var u;if(s?(u=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),n&&n.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(s)},l._ssrRegister=u):n&&(u=n),u){var c=l.functional,d=c?l.render:l.beforeCreate;c?l.render=function(e,t){return u.call(t),d(e,t)}:l.beforeCreate=d?[].concat(d,u):[u]}return{esModule:r,exports:a,options:l}}},112:function(e,t,n){var o=n(0)(n(124),n(125),null,null,null);e.exports=o.exports},12:function(e,t,n){"use strict";n.d(t,"c",(function(){return o})),n.d(t,"a",(function(){return s})),n.d(t,"b",(function(){return r}));var o=function(e){"undefined"!=typeof window&&window.Vue&&window.Vue.use(e)},s=function(e,t){e.component(t.name,t)},r=function(e,t,n){e.prototype[t]=n}},124:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={name:"BTag",props:{attached:Boolean,closable:Boolean,type:String,size:String,rounded:Boolean,disabled:Boolean,ellipsis:Boolean,tabstop:{type:Boolean,default:!0}},methods:{close:function(){this.disabled||this.$emit("close")}}}},125:function(e,t){e.exports={render:function(){var e=this,t=e.$createElement,n=e._self._c||t;return e.attached&&e.closable?n("div",{staticClass:"tags has-addons"},[n("span",{staticClass:"tag",class:[e.type,e.size,{"is-rounded":e.rounded}]},[n("span",{class:{"has-ellipsis":e.ellipsis}},[e._t("default")],2)]),e._v(" "),n("a",{staticClass:"tag is-delete",class:[e.size,{"is-rounded":e.rounded}],attrs:{role:"button",tabindex:!!e.tabstop&&0,disabled:e.disabled},on:{click:function(t){e.close()},keyup:function(t){if(!("button"in t)&&e._k(t.keyCode,"delete",[8,46],t.key))return null;t.preventDefault(),e.close()}}})]):n("span",{staticClass:"tag",class:[e.type,e.size,{"is-rounded":e.rounded}]},[n("span",{class:{"has-ellipsis":e.ellipsis}},[e._t("default")],2),e._v(" "),e.closable?n("a",{staticClass:"delete is-small",attrs:{role:"button",disabled:e.disabled,tabindex:!!e.tabstop&&0},on:{click:function(t){e.close()},keyup:function(t){if(!("button"in t)&&e._k(t.keyCode,"delete",[8,46],t.key))return null;t.preventDefault(),e.close()}}}):e._e()])},staticRenderFns:[]}},224:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=n(112),s=n.n(o),r=n(225),a=n.n(r),i=n(12);n.d(t,"Tag",(function(){return s.a})),n.d(t,"Taglist",(function(){return a.a}));var l={install:function(e){Object(i.a)(e,s.a),Object(i.a)(e,a.a)}};Object(i.c)(l),t.default=l},225:function(e,t,n){var o=n(0)(n(226),n(227),null,null,null);e.exports=o.exports},226:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default={name:"BTaglist",props:{attached:Boolean}}},227:function(e,t){e.exports={render:function(){var e=this,t=e.$createElement;return(e._self._c||t)("div",{staticClass:"tags",class:{"has-addons":e.attached}},[e._t("default")],2)},staticRenderFns:[]}},273:function(e,t,n){e.exports=n(224)}})}));
|
||||
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
+2
@@ -0,0 +1,2 @@
|
||||
/*! Buefy v0.7.4 | MIT License | github.com/buefy/buefy */
|
||||
!(function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{var n=t();for(var o in n)("object"==typeof exports?exports:e)[o]=n[o]}})("undefined"!=typeof self?self:this,(function(){return (function(e){function t(o){if(n[o])return n[o].exports;var a=n[o]={i:o,l:!1,exports:{}};return e[o].call(a.exports,a,a.exports,t),a.l=!0,a.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,o){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:o})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="/",t(t.s=277)})({0:function(e,t){e.exports=function(e,t,n,o,a){var i,l=e=e||{},u=typeof e.default;"object"!==u&&"function"!==u||(i=e,l=e.default);var r="function"==typeof l?l.options:l;t&&(r.render=t.render,r.staticRenderFns=t.staticRenderFns),o&&(r._scopeId=o);var f;if(a?(f=function(e){e=e||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,e||"undefined"==typeof __VUE_SSR_CONTEXT__||(e=__VUE_SSR_CONTEXT__),n&&n.call(this,e),e&&e._registeredComponents&&e._registeredComponents.add(a)},r._ssrRegister=f):n&&(f=n),f){var d=r.functional,s=d?r.render:r.beforeCreate;d?r.render=function(e,t){return f.call(t),s(e,t)}:r.beforeCreate=s?[].concat(s,f):[f]}return{esModule:i,exports:l,options:r}}},12:function(e,t,n){"use strict";n.d(t,"c",(function(){return o})),n.d(t,"a",(function(){return a})),n.d(t,"b",(function(){return i}));var o=function(e){"undefined"!=typeof window&&window.Vue&&window.Vue.use(e)},a=function(e,t){e.component(t.name,t)},i=function(e,t,n){e.prototype[t]=n}},13:function(e,t,n){"use strict";var o={defaultContainerElement:null,defaultIconPack:"mdi",defaultIconComponent:null,defaultDialogConfirmText:null,defaultDialogCancelText:null,defaultSnackbarDuration:3500,defaultSnackbarPosition:null,defaultToastDuration:2e3,defaultToastPosition:null,detaultNotificationDuration:2e3,defaultNotificationPosition:null,defaultTooltipType:"is-primary",defaultTooltipAnimated:!1,defaultInputAutocomplete:"on",defaultDateFormatter:null,defaultDateParser:null,defaultDateCreator:null,defaultDayNames:null,defaultMonthNames:null,defaultFirstDayOfWeek:null,defaultUnselectableDaysOfWeek:null,defaultTimeFormatter:null,defaultTimeParser:null,defaultModalCanCancel:null,defaultModalScroll:null,defaultDatepickerMobileNative:!0,defaultTimepickerMobileNative:!0,defaultNoticeQueue:!0,defaultInputHasCounter:!0,defaultUseHtml5Validation:!0};t.a=o},240:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=n(241),a=n.n(o),i=n(12);n.d(t,"Tooltip",(function(){return a.a}));var l={install:function(e){Object(i.a)(e,a.a)}};Object(i.c)(l),t.default=l},241:function(e,t,n){var o=n(0)(n(242),n(243),null,null,null);e.exports=o.exports},242:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var o=n(13);t.default={name:"BTooltip",props:{active:{type:Boolean,default:!0},type:String,label:String,position:{type:String,default:"is-top",validator:function(e){return["is-top","is-bottom","is-left","is-right"].indexOf(e)>-1}},always:Boolean,animated:Boolean,square:Boolean,dashed:Boolean,multilined:Boolean,size:{type:String,default:"is-medium"}},computed:{newType:function(){return this.type||o.a.defaultTooltipType},newAnimated:function(){return this.animated||o.a.defaultTooltipAnimated}}}},243:function(e,t){e.exports={render:function(){var e=this,t=e.$createElement;return(e._self._c||t)("span",{class:[e.newType,e.position,e.size,{"b-tooltip":e.active,"is-square":e.square,"is-animated":e.newAnimated,"is-always":e.always,"is-multiline":e.multilined,"is-dashed":e.dashed}],attrs:{"data-label":e.label}},[e._t("default")],2)},staticRenderFns:[]}},277:function(e,t,n){e.exports=n(240)}})}));
|
||||
+2
File diff suppressed because one or more lines are too long
+169
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"_from": "buefy",
|
||||
"_id": "buefy@0.7.5",
|
||||
"_inBundle": false,
|
||||
"_integrity": "sha512-p6yz4XKbfXZd4f9d5rLHXCZWl9Y+8bN7gTaFQ4VdLXh7bdo4WYkPYyFyUFE+fIXhx/VEzSh2TeCEYqn/e8xY8w==",
|
||||
"_location": "/buefy",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "tag",
|
||||
"registry": true,
|
||||
"raw": "buefy",
|
||||
"name": "buefy",
|
||||
"escapedName": "buefy",
|
||||
"rawSpec": "",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "latest"
|
||||
},
|
||||
"_requiredBy": [
|
||||
"#USER",
|
||||
"/"
|
||||
],
|
||||
"_resolved": "https://registry.npmjs.org/buefy/-/buefy-0.7.5.tgz",
|
||||
"_shasum": "d25fcbb69beeb898849b47b52be78dfd2849452f",
|
||||
"_spec": "buefy",
|
||||
"_where": "/home/erik/Documents/workspace_brackets/a1_BME_Project_Ohm/om",
|
||||
"author": {
|
||||
"name": "Rafael Beraldo",
|
||||
"email": "rafael.pimpa@gmail.com"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 8"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/buefy/buefy/issues"
|
||||
},
|
||||
"bundleDependencies": false,
|
||||
"dependencies": {
|
||||
"bulma": "0.7.4"
|
||||
},
|
||||
"deprecated": false,
|
||||
"description": "Lightweight UI components for Vue.js based on Bulma",
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-free": "5.3.1",
|
||||
"@mdi/font": "2.7.94",
|
||||
"@vue/test-utils": "^1.0.0-beta.25",
|
||||
"autoprefixer": "7.1.1",
|
||||
"axios": "0.16.2",
|
||||
"babel-core": "6.25.0",
|
||||
"babel-eslint": "7.1.1",
|
||||
"babel-jest": "22.1.0",
|
||||
"babel-loader": "7.1.2",
|
||||
"babel-plugin-transform-runtime": "6.22.0",
|
||||
"babel-preset-env": "1.3.2",
|
||||
"babel-preset-stage-2": "6.22.0",
|
||||
"babel-register": "6.22.0",
|
||||
"chalk": "1.1.3",
|
||||
"cleave.js": "1.0.1",
|
||||
"clipboard": "1.7.1",
|
||||
"codecov": "3.0.0",
|
||||
"connect-history-api-fallback": "1.3.0",
|
||||
"copy-webpack-plugin": "4.3.1",
|
||||
"cross-env": "4.0.0",
|
||||
"cross-spawn": "5.0.1",
|
||||
"css-loader": "0.28.0",
|
||||
"eslint": "3.19.0",
|
||||
"eslint-config-buefy": "0.0.1",
|
||||
"eventsource-polyfill": "0.9.6",
|
||||
"express": "4.14.1",
|
||||
"extract-text-webpack-plugin": "3.0.2",
|
||||
"file-loader": "0.11.1",
|
||||
"font-awesome": "4.7.0",
|
||||
"friendly-errors-webpack-plugin": "1.6.1",
|
||||
"highlight.js": "9.12.0",
|
||||
"html-webpack-plugin": "2.30.1",
|
||||
"http-proxy-middleware": "0.17.3",
|
||||
"jest": "23.5.0",
|
||||
"jest-serializer-vue": "0.3.0",
|
||||
"lint-staged": "6.1.1",
|
||||
"node-sass": "4.11.0",
|
||||
"opn": "4.0.2",
|
||||
"optimize-css-assets-webpack-plugin": "3.2.0",
|
||||
"optimize-js-plugin": "0.0.4",
|
||||
"ora": "1.2.0",
|
||||
"pre-commit": "1.2.2",
|
||||
"prerender-spa-plugin": "3.2.1",
|
||||
"raw-loader": "0.5.1",
|
||||
"rimraf": "2.6.0",
|
||||
"sass-loader": "6.0.6",
|
||||
"scrollreveal": "3.3.6",
|
||||
"semver": "5.3.0",
|
||||
"shelljs": "0.7.6",
|
||||
"sortablejs": "1.7.0",
|
||||
"uglifyjs-webpack-plugin": "1.2.5",
|
||||
"url-loader": "0.5.8",
|
||||
"vee-validate": "2.1.0-beta.9",
|
||||
"vue": "2.5.13",
|
||||
"vue-analytics": "4.1.3",
|
||||
"vue-jest": "2.5.0",
|
||||
"vue-loader": "12.1.0",
|
||||
"vue-progressbar": "0.7.2",
|
||||
"vue-router": "2.3.1",
|
||||
"vue-style-loader": "3.0.1",
|
||||
"vue-template-compiler": "2.5.13",
|
||||
"webpack": "3.10.0",
|
||||
"webpack-bundle-analyzer": "2.9.2",
|
||||
"webpack-dev-server": "2.11.1",
|
||||
"webpack-hot-middleware": "2.21.0",
|
||||
"webpack-merge": "4.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"types/*.d.ts"
|
||||
],
|
||||
"homepage": "https://buefy.org",
|
||||
"keywords": [
|
||||
"bulma",
|
||||
"vue",
|
||||
"vuejs",
|
||||
"vue-bulma",
|
||||
"components"
|
||||
],
|
||||
"license": "MIT",
|
||||
"lint-staged": {
|
||||
"*.{vue,js}": [
|
||||
"lint --fix",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"main": "dist/buefy.js",
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Walter Tommasi",
|
||||
"email": "tommsi20@gmail.com"
|
||||
}
|
||||
],
|
||||
"name": "buefy",
|
||||
"peerDependencies": {
|
||||
"vue": "^2.5.13"
|
||||
},
|
||||
"pre-commit": "lint:staged",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/buefy/buefy.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:lib && npm run build:docs",
|
||||
"build:docs": "node build/build.js",
|
||||
"build:lib": "node build/build-lib.js",
|
||||
"cov": "./node_modules/codcov/bin/codcov",
|
||||
"dev": "node build/dev-server.js",
|
||||
"lint": "eslint --ext .js,.vue src docs",
|
||||
"lint:fix": "eslint --ext .js,.vue src docs --fix",
|
||||
"lint:staged": "lint-staged",
|
||||
"start": "node build/dev-server.js",
|
||||
"test": "npm run lint && npm run unit",
|
||||
"test:watch": "npm run unit -- --watchAll",
|
||||
"unit": "jest"
|
||||
},
|
||||
"typings": "types/index.d.ts",
|
||||
"unpkg": "dist/buefy.min.js",
|
||||
"version": "0.7.5"
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import BAutocomplete from '@components/autocomplete/Autocomplete'
|
||||
|
||||
const findStringsStartingWith = (array, value) =>
|
||||
array.filter((x) => x.startsWith(value))
|
||||
|
||||
const DATA_LIST = [
|
||||
'Angular',
|
||||
'Angular 2',
|
||||
'Aurelia',
|
||||
'Backbone',
|
||||
'Ember',
|
||||
'jQuery',
|
||||
'Meteor',
|
||||
'Node.js',
|
||||
'Polymer',
|
||||
'React',
|
||||
'RxJS',
|
||||
'Vue.js'
|
||||
]
|
||||
const dropdownMenu = '.dropdown-menu'
|
||||
let wrapper, $input, $dropdown
|
||||
|
||||
describe('BAutocomplete', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount(BAutocomplete)
|
||||
|
||||
$input = wrapper.find('input')
|
||||
$dropdown = wrapper.find(dropdownMenu)
|
||||
})
|
||||
|
||||
it('is called', () => {
|
||||
expect(wrapper.name()).toBe('BAutocomplete')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has an input type', () => {
|
||||
expect(wrapper.contains('.control .input[type=text]')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has a dropdown menu hidden by default', () => {
|
||||
expect(wrapper.contains(dropdownMenu)).toBeTruthy()
|
||||
expect($dropdown.isVisible()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('can emit input, focus and blur events', async () => {
|
||||
const VALUE_TYPED = 'test'
|
||||
wrapper.setProps({ data: DATA_LIST })
|
||||
|
||||
$input.trigger('focus')
|
||||
expect(wrapper.emitted()['focus']).toBeTruthy()
|
||||
$input.setValue(VALUE_TYPED)
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const valueEmitted = wrapper.emitted()['input'][0]
|
||||
expect(valueEmitted).toContainEqual(VALUE_TYPED)
|
||||
|
||||
$input.trigger('blur')
|
||||
expect(wrapper.emitted()['blur']).toBeTruthy()
|
||||
})
|
||||
|
||||
it('can autocomplete with keydown', async () => {
|
||||
const VALUE_TYPED = 'Ang'
|
||||
wrapper.setProps({ data: DATA_LIST })
|
||||
|
||||
$input.trigger('focus')
|
||||
$input.setValue(VALUE_TYPED)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect($dropdown.isVisible()).toBeTruthy()
|
||||
|
||||
const itemsInDropdowm = findStringsStartingWith(DATA_LIST, VALUE_TYPED)
|
||||
|
||||
$input.trigger('keydown.down')
|
||||
$input.trigger('keydown.enter')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect($input.element.value).toBe(itemsInDropdowm[0])
|
||||
expect($dropdown.isVisible()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('check validity when value change', () => {
|
||||
wrapper.setProps({ data: DATA_LIST })
|
||||
wrapper.vm.$refs.input.checkHtml5Validity = jest.fn()
|
||||
|
||||
wrapper.vm.isValid = false
|
||||
wrapper.setProps({ value: 'test' })
|
||||
|
||||
expect(wrapper.vm.$refs.input.checkHtml5Validity).toBeCalled()
|
||||
})
|
||||
|
||||
it('close dropdown on esc', () => {
|
||||
wrapper.setProps({ data: DATA_LIST })
|
||||
|
||||
wrapper.vm.isActive = true
|
||||
expect($dropdown.isVisible()).toBeTruthy()
|
||||
|
||||
$input.trigger('keyup.esc')
|
||||
|
||||
expect($dropdown.isVisible()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('close dropdown on click outside', () => {
|
||||
wrapper.setProps({ data: DATA_LIST })
|
||||
|
||||
wrapper.vm.isActive = true
|
||||
expect($dropdown.isVisible()).toBeTruthy()
|
||||
|
||||
window.document.body.click()
|
||||
|
||||
expect($dropdown.isVisible()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('open dropdown on down key click', () => {
|
||||
wrapper.setProps({ data: DATA_LIST })
|
||||
|
||||
expect($dropdown.isVisible()).toBeFalsy()
|
||||
|
||||
$input.trigger('focus')
|
||||
$input.trigger('keydown.down')
|
||||
|
||||
expect($dropdown.isVisible()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('can openOnFocus and keepFirst', async () => {
|
||||
wrapper.setProps({
|
||||
data: DATA_LIST,
|
||||
openOnFocus: true,
|
||||
keepFirst: true
|
||||
})
|
||||
|
||||
expect($dropdown.isVisible()).toBeFalsy()
|
||||
|
||||
$input.trigger('focus')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect($dropdown.isVisible()).toBeTruthy()
|
||||
|
||||
expect(wrapper.vm.hovered).toBe(DATA_LIST[0])
|
||||
})
|
||||
|
||||
it('reset events before destroy', () => {
|
||||
document.removeEventListener = jest.fn()
|
||||
window.removeEventListener = jest.fn()
|
||||
|
||||
wrapper.destroy()
|
||||
|
||||
expect(document.removeEventListener).toBeCalledWith('click', expect.any(Function))
|
||||
expect(window.removeEventListener).toBeCalledWith('resize', expect.any(Function))
|
||||
})
|
||||
})
|
||||
+387
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
<div class="autocomplete control" :class="{'is-expanded': expanded}">
|
||||
<b-input
|
||||
v-model="newValue"
|
||||
ref="input"
|
||||
type="text"
|
||||
:size="size"
|
||||
:loading="loading"
|
||||
:rounded="rounded"
|
||||
:icon="icon"
|
||||
:icon-pack="iconPack"
|
||||
:maxlength="maxlength"
|
||||
:autocomplete="newAutocomplete"
|
||||
v-bind="$attrs"
|
||||
@input="onInput"
|
||||
@focus="focused"
|
||||
@blur="onBlur"
|
||||
@keyup.native.esc.prevent="isActive = false"
|
||||
@keydown.native.tab="tabPressed"
|
||||
@keydown.native.enter.prevent="enterPressed"
|
||||
@keydown.native.up.prevent="keyArrows('up')"
|
||||
@keydown.native.down.prevent="keyArrows('down')"
|
||||
/>
|
||||
|
||||
<transition name="fade">
|
||||
<div
|
||||
class="dropdown-menu"
|
||||
:class="{ 'is-opened-top': !isListInViewportVertically }"
|
||||
v-show="isActive && (data.length > 0 || hasEmptySlot || hasHeaderSlot)"
|
||||
ref="dropdown">
|
||||
<div class="dropdown-content" v-show="isActive">
|
||||
<div
|
||||
v-if="hasHeaderSlot"
|
||||
class="dropdown-item">
|
||||
<slot name="header"/>
|
||||
</div>
|
||||
<a
|
||||
v-for="(option, index) in data"
|
||||
:key="index"
|
||||
class="dropdown-item"
|
||||
:class="{ 'is-hovered': option === hovered }"
|
||||
@click="setSelected(option)">
|
||||
|
||||
<slot
|
||||
v-if="hasDefaultSlot"
|
||||
:option="option"
|
||||
:index="index"
|
||||
/>
|
||||
<span v-else>
|
||||
{{ getValue(option, true) }}
|
||||
</span>
|
||||
</a>
|
||||
<div
|
||||
v-if="data.length === 0 && hasEmptySlot"
|
||||
class="dropdown-item is-disabled">
|
||||
<slot name="empty"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getValueByPath } from '../../utils/helpers'
|
||||
import FormElementMixin from '../../utils/FormElementMixin'
|
||||
import Input from '../input/Input'
|
||||
|
||||
export default {
|
||||
name: 'BAutocomplete',
|
||||
components: {
|
||||
[Input.name]: Input
|
||||
},
|
||||
mixins: [FormElementMixin],
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
value: [Number, String],
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
field: {
|
||||
type: String,
|
||||
default: 'value'
|
||||
},
|
||||
keepFirst: Boolean,
|
||||
clearOnSelect: Boolean,
|
||||
openOnFocus: Boolean,
|
||||
customFormatter: Function
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: null,
|
||||
hovered: null,
|
||||
isActive: false,
|
||||
newValue: this.value,
|
||||
newAutocomplete: this.autocomplete || 'off',
|
||||
isListInViewportVertically: true,
|
||||
hasFocus: false,
|
||||
_isAutocomplete: true,
|
||||
_elementRef: 'input'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* White-listed items to not close when clicked.
|
||||
* Add input, dropdown and all children.
|
||||
*/
|
||||
whiteList() {
|
||||
const whiteList = []
|
||||
whiteList.push(this.$refs.input.$el.querySelector('input'))
|
||||
whiteList.push(this.$refs.dropdown)
|
||||
// Add all chidren from dropdown
|
||||
if (this.$refs.dropdown !== undefined) {
|
||||
const children = this.$refs.dropdown.querySelectorAll('*')
|
||||
for (const child of children) {
|
||||
whiteList.push(child)
|
||||
}
|
||||
}
|
||||
|
||||
return whiteList
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if exists default slot
|
||||
*/
|
||||
hasDefaultSlot() {
|
||||
return !!this.$scopedSlots.default
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if exists "empty" slot
|
||||
*/
|
||||
hasEmptySlot() {
|
||||
return !!this.$slots.empty
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if exists "header" slot
|
||||
*/
|
||||
hasHeaderSlot() {
|
||||
return !!this.$slots.header
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* When dropdown is toggled, check the visibility to know when
|
||||
* to open upwards.
|
||||
*/
|
||||
isActive(active) {
|
||||
if (active) {
|
||||
this.calcDropdownInViewportVertical()
|
||||
} else {
|
||||
this.$nextTick(() => this.setHovered(null))
|
||||
// Timeout to wait for the animation to finish before recalculating
|
||||
setTimeout(() => {
|
||||
this.calcDropdownInViewportVertical()
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* When updating input's value
|
||||
* 1. Emit changes
|
||||
* 2. If value isn't the same as selected, set null
|
||||
* 3. Close dropdown if value is clear or else open it
|
||||
*/
|
||||
newValue(value) {
|
||||
this.$emit('input', value)
|
||||
// Check if selected is invalid
|
||||
const currentValue = this.getValue(this.selected)
|
||||
if (currentValue && currentValue !== value) {
|
||||
this.setSelected(null, false)
|
||||
}
|
||||
// Close dropdown if input is clear or else open it
|
||||
if (this.hasFocus && (!this.openOnFocus || value)) {
|
||||
this.isActive = !!value
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* When v-model is changed:
|
||||
* 1. Update internal value.
|
||||
* 2. If it's invalid, validate again.
|
||||
*/
|
||||
value(value) {
|
||||
this.newValue = value
|
||||
!this.isValid && this.$refs.input.checkHtml5Validity()
|
||||
},
|
||||
|
||||
/**
|
||||
* Select first option if "keep-first
|
||||
*/
|
||||
data(value) {
|
||||
// Keep first option always pre-selected
|
||||
if (this.keepFirst) {
|
||||
this.selectFirstOption(value)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Set which option is currently hovered.
|
||||
*/
|
||||
setHovered(option) {
|
||||
if (option === undefined) return
|
||||
|
||||
this.hovered = option
|
||||
},
|
||||
|
||||
/**
|
||||
* Set which option is currently selected, update v-model,
|
||||
* update input value and close dropdown.
|
||||
*/
|
||||
setSelected(option, closeDropdown = true) {
|
||||
if (option === undefined) return
|
||||
|
||||
this.selected = option
|
||||
this.$emit('select', this.selected)
|
||||
if (this.selected !== null) {
|
||||
this.newValue = this.clearOnSelect ? '' : this.getValue(this.selected)
|
||||
}
|
||||
closeDropdown && this.$nextTick(() => { this.isActive = false })
|
||||
},
|
||||
|
||||
/**
|
||||
* Select first option
|
||||
*/
|
||||
selectFirstOption(options) {
|
||||
this.$nextTick(() => {
|
||||
if (options.length) {
|
||||
// If has visible data or open on focus, keep updating the hovered
|
||||
if (this.openOnFocus || (this.newValue !== '' && this.hovered !== options[0])) {
|
||||
this.setHovered(options[0])
|
||||
}
|
||||
} else {
|
||||
this.setHovered(null)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Enter key listener.
|
||||
* Select the hovered option.
|
||||
*/
|
||||
enterPressed() {
|
||||
if (this.hovered === null) return
|
||||
this.setSelected(this.hovered)
|
||||
},
|
||||
|
||||
/**
|
||||
* Tab key listener.
|
||||
* Select hovered option if it exists, close dropdown, then allow
|
||||
* native handling to move to next tabbable element.
|
||||
*/
|
||||
tabPressed() {
|
||||
if (this.hovered === null) {
|
||||
this.isActive = false
|
||||
return
|
||||
}
|
||||
this.setSelected(this.hovered)
|
||||
},
|
||||
|
||||
/**
|
||||
* Close dropdown if clicked outside.
|
||||
*/
|
||||
clickedOutside(event) {
|
||||
if (this.whiteList.indexOf(event.target) < 0) this.isActive = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Return display text for the input.
|
||||
* If object, get value from path, or else just the value.
|
||||
*/
|
||||
getValue(option) {
|
||||
if (!option) return
|
||||
|
||||
if (typeof this.customFormatter !== 'undefined') {
|
||||
return this.customFormatter(option)
|
||||
}
|
||||
return typeof option === 'object'
|
||||
? getValueByPath(option, this.field)
|
||||
: option
|
||||
},
|
||||
|
||||
/**
|
||||
* Calculate if the dropdown is vertically visible when activated,
|
||||
* otherwise it is openened upwards.
|
||||
*/
|
||||
calcDropdownInViewportVertical() {
|
||||
this.$nextTick(() => {
|
||||
/**
|
||||
* this.$refs.dropdown may be undefined
|
||||
* when Autocomplete is conditional rendered
|
||||
*/
|
||||
if (this.$refs.dropdown === undefined) return
|
||||
|
||||
const rect = this.$refs.dropdown.getBoundingClientRect()
|
||||
|
||||
this.isListInViewportVertically = (
|
||||
rect.top >= 0 &&
|
||||
rect.bottom <= (window.innerHeight ||
|
||||
document.documentElement.clientHeight)
|
||||
)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Arrows keys listener.
|
||||
* If dropdown is active, set hovered option, or else just open.
|
||||
*/
|
||||
keyArrows(direction) {
|
||||
const sum = direction === 'down' ? 1 : -1
|
||||
if (this.isActive) {
|
||||
let index = this.data.indexOf(this.hovered) + sum
|
||||
index = index > this.data.length - 1 ? this.data.length : index
|
||||
index = index < 0 ? 0 : index
|
||||
|
||||
this.setHovered(this.data[index])
|
||||
|
||||
const list = this.$refs.dropdown.querySelector('.dropdown-content')
|
||||
const element = list.querySelectorAll('a.dropdown-item:not(.is-disabled)')[index]
|
||||
|
||||
if (!element) return
|
||||
|
||||
const visMin = list.scrollTop
|
||||
const visMax = list.scrollTop + list.clientHeight - element.clientHeight
|
||||
|
||||
if (element.offsetTop < visMin) {
|
||||
list.scrollTop = element.offsetTop
|
||||
} else if (element.offsetTop >= visMax) {
|
||||
list.scrollTop = (
|
||||
element.offsetTop -
|
||||
list.clientHeight +
|
||||
element.clientHeight
|
||||
)
|
||||
}
|
||||
} else {
|
||||
this.isActive = true
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Focus listener.
|
||||
* If value is the same as selected, select all text.
|
||||
*/
|
||||
focused(event) {
|
||||
if (this.getValue(this.selected) === this.newValue) {
|
||||
this.$el.querySelector('input').select()
|
||||
}
|
||||
if (this.openOnFocus) {
|
||||
this.isActive = true
|
||||
if (this.keepFirst) {
|
||||
this.selectFirstOption(this.data)
|
||||
}
|
||||
}
|
||||
this.hasFocus = true
|
||||
this.$emit('focus', event)
|
||||
},
|
||||
|
||||
/**
|
||||
* Blur listener.
|
||||
*/
|
||||
onBlur(event) {
|
||||
this.hasFocus = false
|
||||
this.$emit('blur', event)
|
||||
},
|
||||
onInput(event) {
|
||||
const currentValue = this.getValue(this.selected)
|
||||
if (currentValue && currentValue === this.newValue) return
|
||||
this.$emit('typing', this.newValue)
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (typeof window !== 'undefined') {
|
||||
document.addEventListener('click', this.clickedOutside)
|
||||
window.addEventListener('resize', this.calcDropdownInViewportVertical)
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (typeof window !== 'undefined') {
|
||||
document.removeEventListener('click', this.clickedOutside)
|
||||
window.removeEventListener('resize', this.calcDropdownInViewportVertical)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import Autocomplete from './Autocomplete'
|
||||
|
||||
import { use, registerComponent } from '../../utils/plugins'
|
||||
|
||||
const Plugin = {
|
||||
install(Vue) {
|
||||
registerComponent(Vue, Autocomplete)
|
||||
}
|
||||
}
|
||||
|
||||
use(Plugin)
|
||||
|
||||
export default Plugin
|
||||
|
||||
export {
|
||||
Autocomplete
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { shallowMount, mount } from '@vue/test-utils'
|
||||
import BButton from '@components/button/Button'
|
||||
|
||||
describe('BButton', () => {
|
||||
it('is called', () => {
|
||||
const wrapper = shallowMount(BButton)
|
||||
expect(wrapper.name()).toBe('BButton')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('emit a click event', () => {
|
||||
const wrapper = shallowMount(BButton)
|
||||
const click = jest.fn()
|
||||
wrapper.vm.$on('click', click)
|
||||
wrapper.find('.button').trigger('click')
|
||||
expect(click).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should show icon', () => {
|
||||
const wrapper = mount(BButton, {
|
||||
propsData: {
|
||||
iconLeft: 'plus'
|
||||
}
|
||||
})
|
||||
expect(wrapper.contains('.icon')).toBe(true)
|
||||
})
|
||||
|
||||
it('should be medium', () => {
|
||||
const wrapper = shallowMount(BButton, {
|
||||
propsData: {
|
||||
size: 'is-medium'
|
||||
}
|
||||
})
|
||||
expect(wrapper.classes()).toContain('is-medium')
|
||||
})
|
||||
|
||||
it('should be small + icon', () => {
|
||||
const wrapper = mount(BButton, {
|
||||
propsData: {
|
||||
size: 'is-small',
|
||||
iconLeft: 'plus'
|
||||
}
|
||||
})
|
||||
expect(wrapper.classes()).toContain('is-small')
|
||||
expect(wrapper.contains('.icon')).toBe(true)
|
||||
})
|
||||
|
||||
it('should be large + icon', () => {
|
||||
const wrapper = mount(BButton, {
|
||||
propsData: {
|
||||
size: 'is-large',
|
||||
iconLeft: 'plus'
|
||||
}
|
||||
})
|
||||
expect(wrapper.classes()).toContain('is-large')
|
||||
expect(wrapper.contains('.icon')).toBe(true)
|
||||
})
|
||||
})
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<component
|
||||
:is="tag"
|
||||
class="button"
|
||||
:type="nativeType"
|
||||
:class="[size, type, {
|
||||
'is-rounded': rounded,
|
||||
'is-loading': loading,
|
||||
'is-outlined': outlined,
|
||||
'is-inverted': inverted,
|
||||
'is-focused': focused,
|
||||
'is-active': active,
|
||||
'is-hovered': hovered,
|
||||
'is-selected': selected
|
||||
}]"
|
||||
@click="$emit('click', $event)"
|
||||
>
|
||||
<b-icon
|
||||
v-if="iconLeft"
|
||||
:pack="iconPack"
|
||||
:icon="iconLeft"
|
||||
:size="iconSize"
|
||||
/>
|
||||
<span v-if="label">{{ label }}</span>
|
||||
<span v-else-if="$slots.default">
|
||||
<slot />
|
||||
</span>
|
||||
<b-icon
|
||||
v-if="iconRight"
|
||||
:pack="iconPack"
|
||||
:icon="iconRight"
|
||||
:size="iconSize"
|
||||
/>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Icon from '../icon/Icon'
|
||||
|
||||
export default {
|
||||
name: 'BButton',
|
||||
components: {
|
||||
[Icon.name]: Icon
|
||||
},
|
||||
props: {
|
||||
type: [String, Object],
|
||||
size: String,
|
||||
label: String,
|
||||
iconPack: String,
|
||||
iconLeft: String,
|
||||
iconRight: String,
|
||||
rounded: Boolean,
|
||||
loading: Boolean,
|
||||
outlined: Boolean,
|
||||
inverted: Boolean,
|
||||
focused: Boolean,
|
||||
active: Boolean,
|
||||
hovered: Boolean,
|
||||
selected: Boolean,
|
||||
nativeType: {
|
||||
type: String,
|
||||
default: 'button',
|
||||
validator: (value) => {
|
||||
return [
|
||||
'button',
|
||||
'submit',
|
||||
'reset'
|
||||
].indexOf(value) >= 0
|
||||
}
|
||||
},
|
||||
tag: {
|
||||
type: String,
|
||||
default: 'button',
|
||||
validator: (value) => {
|
||||
return [
|
||||
'button',
|
||||
'a',
|
||||
'input'
|
||||
].indexOf(value) >= 0
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
iconSize() {
|
||||
if (!this.size || this.size === 'is-medium') {
|
||||
return 'is-small'
|
||||
} else if (this.size === 'is-large') {
|
||||
return 'is-medium'
|
||||
}
|
||||
return this.size
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import Button from './Button'
|
||||
|
||||
import { use, registerComponent } from '../../utils/plugins'
|
||||
|
||||
const Plugin = {
|
||||
install(Vue) {
|
||||
registerComponent(Vue, Button)
|
||||
}
|
||||
}
|
||||
|
||||
use(Plugin)
|
||||
|
||||
export default Plugin
|
||||
|
||||
export {
|
||||
Button
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import BCheckbox from '@components/checkbox/Checkbox'
|
||||
|
||||
let wrapper
|
||||
|
||||
describe('BCheckbox', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(BCheckbox)
|
||||
})
|
||||
|
||||
it('is called', () => {
|
||||
expect(wrapper.name()).toBe('BCheckbox')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has an input checkbox', () => {
|
||||
expect(wrapper.contains('label input[type=checkbox]')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('emit input event with value when value change', () => {
|
||||
wrapper.setProps({ value: true })
|
||||
expect(wrapper.vm.computedValue).toBeTruthy()
|
||||
wrapper.vm.computedValue = false
|
||||
const valueEmitted = wrapper.emitted()['input'][0]
|
||||
expect(valueEmitted).toContainEqual(false)
|
||||
})
|
||||
})
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<label
|
||||
class="b-checkbox checkbox"
|
||||
:class="[size, { 'is-disabled': disabled }]"
|
||||
ref="label"
|
||||
:disabled="disabled"
|
||||
@keydown.prevent.enter="$refs.label.click()">
|
||||
<input
|
||||
v-model="computedValue"
|
||||
:indeterminate.prop="indeterminate"
|
||||
type="checkbox"
|
||||
@click.stop
|
||||
:disabled="disabled"
|
||||
:required="required"
|
||||
:name="name"
|
||||
:value="nativeValue"
|
||||
:true-value="trueValue"
|
||||
:false-value="falseValue">
|
||||
<span class="check" :class="type" />
|
||||
<span class="control-label"><slot/></span>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BCheckbox',
|
||||
props: {
|
||||
value: [String, Number, Boolean, Function, Object, Array, Symbol],
|
||||
nativeValue: [String, Number, Boolean, Function, Object, Array, Symbol],
|
||||
indeterminate: Boolean,
|
||||
type: String,
|
||||
disabled: Boolean,
|
||||
required: Boolean,
|
||||
name: String,
|
||||
size: String,
|
||||
trueValue: {
|
||||
type: [String, Number, Boolean, Function, Object, Array, Symbol],
|
||||
default: true
|
||||
},
|
||||
falseValue: {
|
||||
type: [String, Number, Boolean, Function, Object, Array, Symbol],
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newValue: this.value
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedValue: {
|
||||
get() {
|
||||
return this.newValue
|
||||
},
|
||||
set(value) {
|
||||
this.newValue = value
|
||||
this.$emit('input', value)
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* When v-model change, set internal value.
|
||||
*/
|
||||
value(value) {
|
||||
this.newValue = value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import BCheckboxButton from '@components/checkbox/CheckboxButton'
|
||||
|
||||
let wrapper
|
||||
|
||||
describe('BCheckboxButton', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(BCheckboxButton)
|
||||
})
|
||||
|
||||
it('is called', () => {
|
||||
expect(wrapper.name()).toBe('BCheckboxButton')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has an input checkbox', () => {
|
||||
expect(wrapper.contains('label input[type=checkbox]')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('emit input event with value when value change', () => {
|
||||
wrapper.setProps({ value: true })
|
||||
expect(wrapper.vm.computedValue).toBeTruthy()
|
||||
wrapper.vm.computedValue = false
|
||||
const valueEmitted = wrapper.emitted()['input'][0]
|
||||
expect(valueEmitted).toContainEqual(false)
|
||||
})
|
||||
})
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="control">
|
||||
<label
|
||||
class="b-checkbox checkbox button"
|
||||
ref="label"
|
||||
:class="[checked ? type : null, size, {
|
||||
'is-disabled': disabled,
|
||||
'is-focused': isFocused
|
||||
}]"
|
||||
:disabled="disabled"
|
||||
@keydown.prevent.enter="$refs.label.click()">
|
||||
<slot/>
|
||||
<input
|
||||
v-model="computedValue"
|
||||
type="checkbox"
|
||||
@click.stop
|
||||
:disabled="disabled"
|
||||
:required="required"
|
||||
:name="name"
|
||||
:value="nativeValue"
|
||||
@focus="isFocused = true"
|
||||
@blur="isFocused = false">
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BCheckboxButton',
|
||||
props: {
|
||||
value: [String, Number, Boolean, Function, Object, Array, Symbol],
|
||||
nativeValue: [String, Number, Boolean, Function, Object, Array, Symbol],
|
||||
disabled: Boolean,
|
||||
required: Boolean,
|
||||
name: String,
|
||||
size: String,
|
||||
type: {
|
||||
type: String,
|
||||
default: 'is-primary'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newValue: this.value,
|
||||
isFocused: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedValue: {
|
||||
get() {
|
||||
return this.newValue
|
||||
},
|
||||
set(value) {
|
||||
this.newValue = value
|
||||
this.$emit('input', value)
|
||||
}
|
||||
},
|
||||
checked() {
|
||||
if (Array.isArray(this.newValue)) {
|
||||
return this.newValue.indexOf(this.nativeValue) >= 0
|
||||
}
|
||||
return this.newValue === this.nativeValue
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* When v-model change, set internal value.
|
||||
*/
|
||||
value(value) {
|
||||
this.newValue = value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import Checkbox from './Checkbox'
|
||||
import CheckboxButton from './CheckboxButton'
|
||||
|
||||
import { use, registerComponent } from '../../utils/plugins'
|
||||
|
||||
const Plugin = {
|
||||
install(Vue) {
|
||||
registerComponent(Vue, Checkbox)
|
||||
registerComponent(Vue, CheckboxButton)
|
||||
}
|
||||
}
|
||||
|
||||
use(Plugin)
|
||||
|
||||
export default Plugin
|
||||
|
||||
export {
|
||||
Checkbox,
|
||||
CheckboxButton
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import BClockpicker from '@components/clockpicker/Clockpicker'
|
||||
|
||||
describe('BClockpicker', () => {
|
||||
it('is called', () => {
|
||||
const wrapper = shallowMount(BClockpicker)
|
||||
expect(wrapper.name()).toBe('BClockpicker')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<div class="b-clockpicker control" :class="[size, type, {'is-expanded': expanded}]">
|
||||
<b-dropdown
|
||||
v-if="!isMobile || inline"
|
||||
ref="dropdown"
|
||||
:position="position"
|
||||
:disabled="disabled"
|
||||
:inline="inline">
|
||||
<b-input
|
||||
v-if="!inline"
|
||||
ref="input"
|
||||
slot="trigger"
|
||||
autocomplete="off"
|
||||
:value="formatValue(dateSelected)"
|
||||
:placeholder="placeholder"
|
||||
:size="size"
|
||||
:icon="icon"
|
||||
:icon-pack="iconPack"
|
||||
:loading="loading"
|
||||
:disabled="disabled"
|
||||
:readonly="!editable"
|
||||
:rounded="rounded"
|
||||
v-bind="$attrs"
|
||||
@change.native="onChange($event.target.value)"
|
||||
@focus="$emit('focus', $event)"
|
||||
@blur="$emit('blur', $event) && checkHtml5Validity()"/>
|
||||
|
||||
<div
|
||||
class="card"
|
||||
:disabled="disabled"
|
||||
custom>
|
||||
<header v-if="inline" class="card-header">
|
||||
<div class="b-clockpicker-header card-header-title">
|
||||
<div class="b-clockpicker-time">
|
||||
<span
|
||||
class="b-clockpicker-btn"
|
||||
:class="{ active: isSelectingHour }"
|
||||
@click="isSelectingHour = true">{{ hoursDisplay }}</span>
|
||||
<span>:</span>
|
||||
<span
|
||||
class="b-clockpicker-btn"
|
||||
:class="{ active: !isSelectingHour }"
|
||||
@click="isSelectingHour = false">{{ minutesDisplay }}</span>
|
||||
</div>
|
||||
<div v-if="!isHourFormat24" class="b-clockpicker-period">
|
||||
<div
|
||||
class="b-clockpicker-btn"
|
||||
:class="{ active: meridienSelected == AM }"
|
||||
@click="onMeridienClick(AM)">am</div>
|
||||
<div
|
||||
class="b-clockpicker-btn"
|
||||
:class="{ active: meridienSelected == PM }"
|
||||
@click="onMeridienClick(PM)">pm</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div
|
||||
class="b-clockpicker-body"
|
||||
:style="{ width: faceSize + 'px', height: faceSize + 'px' }">
|
||||
<div v-if="!inline" class="b-clockpicker-time">
|
||||
<div
|
||||
class="b-clockpicker-btn"
|
||||
:class="{ active: isSelectingHour }"
|
||||
@click="isSelectingHour = true">Hours</div>
|
||||
<span
|
||||
class="b-clockpicker-btn"
|
||||
:class="{ active: !isSelectingHour }"
|
||||
@click="isSelectingHour = false">Min</span>
|
||||
</div>
|
||||
<div v-if="!isHourFormat24 && !inline" class="b-clockpicker-period">
|
||||
<div
|
||||
class="b-clockpicker-btn"
|
||||
:class="{ active: meridienSelected == AM }"
|
||||
@click="onMeridienClick(AM)">{{ AM }}</div>
|
||||
<div
|
||||
class="b-clockpicker-btn"
|
||||
:class="{ active: meridienSelected == PM }"
|
||||
@click="onMeridienClick(PM)">{{ PM }}</div>
|
||||
</div>
|
||||
<b-clockpicker-face
|
||||
:picker-size="faceSize"
|
||||
:min="minFaceValue"
|
||||
:max="maxFaceValue"
|
||||
:face-numbers="isSelectingHour ? hours : minutes"
|
||||
:disabled-values="faceDisabledValues()"
|
||||
:double="isSelectingHour && isHourFormat24"
|
||||
:value="isSelectingHour ? hoursSelected : minutesSelected"
|
||||
@input="onClockInput"
|
||||
@change="onClockChange" />
|
||||
</div>
|
||||
</div>
|
||||
<footer
|
||||
v-if="$slots.default !== undefined && $slots.default.length"
|
||||
class="b-clockpicker-footer card-footer">
|
||||
<slot/>
|
||||
</footer>
|
||||
</div>
|
||||
</b-dropdown>
|
||||
<b-input
|
||||
v-else
|
||||
ref="input"
|
||||
type="time"
|
||||
autocomplete="off"
|
||||
:value="formatHHMMSS(value)"
|
||||
:placeholder="placeholder"
|
||||
:size="size"
|
||||
:icon="icon"
|
||||
:icon-pack="iconPack"
|
||||
:loading="loading"
|
||||
:max="formatHHMMSS(maxTime)"
|
||||
:min="formatHHMMSS(minTime)"
|
||||
:disabled="disabled"
|
||||
:readonly="false"
|
||||
v-bind="$attrs"
|
||||
@change.native="onChangeNativePicker"
|
||||
@focus="$emit('focus', $event)"
|
||||
@blur="$emit('blur', $event) && checkHtml5Validity()"/>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TimepickerMixin from '../../utils/TimepickerMixin'
|
||||
|
||||
import Dropdown from '../dropdown/Dropdown'
|
||||
import DropdownItem from '../dropdown/DropdownItem'
|
||||
import Input from '../input/Input'
|
||||
import Field from '../field/Field'
|
||||
import Icon from '../icon/Icon'
|
||||
import ClockpickerFace from './ClockpickerFace'
|
||||
|
||||
const outerPadding = 12
|
||||
|
||||
export default {
|
||||
name: 'BClockpicker',
|
||||
components: {
|
||||
[ClockpickerFace.name]: ClockpickerFace,
|
||||
[Input.name]: Input,
|
||||
[Field.name]: Field,
|
||||
[Icon.name]: Icon,
|
||||
[Dropdown.name]: Dropdown,
|
||||
[DropdownItem.name]: DropdownItem
|
||||
},
|
||||
mixins: [TimepickerMixin],
|
||||
props: {
|
||||
pickerSize: {
|
||||
type: Number,
|
||||
default: 290
|
||||
},
|
||||
hourFormat: {
|
||||
type: String,
|
||||
default: '12',
|
||||
validator: (value) => {
|
||||
return value === '24' || value === '12'
|
||||
}
|
||||
},
|
||||
incrementMinutes: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
autoSwitch: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'is-primary'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isSelectingHour: true,
|
||||
isDragging: false,
|
||||
_isClockpicker: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hoursDisplay() {
|
||||
if (this.hoursSelected == null) return '--'
|
||||
if (this.isHourFormat24) return this.pad(this.hoursSelected)
|
||||
|
||||
let display = this.hoursSelected
|
||||
if (this.meridienSelected === this.PM) display -= 12
|
||||
if (display === 0) display = 12
|
||||
return display
|
||||
},
|
||||
minutesDisplay() {
|
||||
return this.minutesSelected == null ? '--' : this.pad(this.minutesSelected)
|
||||
},
|
||||
minFaceValue() {
|
||||
return this.isSelectingHour &&
|
||||
!this.isHourFormat24 &&
|
||||
this.meridienSelected === this.PM ? 12 : 0
|
||||
},
|
||||
maxFaceValue() {
|
||||
return this.isSelectingHour
|
||||
? (!this.isHourFormat24 && this.meridienSelected === this.AM ? 11 : 23)
|
||||
: 59
|
||||
},
|
||||
faceFormatter() {
|
||||
return this.isSelectingHour && !this.isHourFormat24
|
||||
? (val) => val
|
||||
: this.formatNumber
|
||||
},
|
||||
faceSize() {
|
||||
return this.pickerSize - (outerPadding * 2)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClockInput(value) {
|
||||
if (this.isSelectingHour) {
|
||||
this.hoursSelected = value
|
||||
this.onHoursChange(value)
|
||||
} else {
|
||||
this.minutesSelected = value
|
||||
this.onMinutesChange(value)
|
||||
}
|
||||
},
|
||||
onClockChange(value) {
|
||||
if (this.autoSwitch && this.isSelectingHour) {
|
||||
this.isSelectingHour = !this.isSelectingHour
|
||||
}
|
||||
},
|
||||
onMeridienClick(value) {
|
||||
if (this.meridienSelected !== value) {
|
||||
this.meridienSelected = value
|
||||
this.onMeridienChange(value)
|
||||
}
|
||||
},
|
||||
faceDisabledValues() {
|
||||
return this.isSelectingHour ? this.isHourDisabled : this.isMinuteDisabled
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.incrementMinutes = 5
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import BClockpickerFace from '@components/clockpicker/ClockpickerFace'
|
||||
|
||||
describe('BClockpickerFace', () => {
|
||||
it('is called', () => {
|
||||
const wrapper = shallowMount(BClockpickerFace)
|
||||
|
||||
expect(wrapper.name()).toBe('BClockpickerFace')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<div
|
||||
class="b-clockpicker-face"
|
||||
@mousedown="onMouseDown"
|
||||
@mouseup="onMouseUp"
|
||||
@mousemove="onDragMove"
|
||||
@touchstart="onMouseDown"
|
||||
@touchend="onMouseUp"
|
||||
@touchmove="onDragMove">
|
||||
<div
|
||||
class="b-clockpicker-face-outer-ring"
|
||||
ref="clock">
|
||||
<div
|
||||
class="b-clockpicker-face-hand"
|
||||
:style="handStyle" />
|
||||
<span
|
||||
v-for="(num, index) of faceNumbers"
|
||||
:key="index"
|
||||
class="b-clockpicker-face-number"
|
||||
:class="getFaceNumberClasses(num)"
|
||||
:style="{ transform: getNumberTranslate(num.value) }">
|
||||
<span>{{ num.label }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// These should match the variables in clockpicker.scss
|
||||
const indicatorSize = 40
|
||||
const paddingInner = 5
|
||||
|
||||
export default {
|
||||
name: 'BClockpickerFace',
|
||||
props: {
|
||||
pickerSize: Number,
|
||||
min: Number,
|
||||
max: Number,
|
||||
double: Boolean,
|
||||
value: Number,
|
||||
faceNumbers: Array,
|
||||
disabledValues: Function
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isDragging: false,
|
||||
inputValue: this.value,
|
||||
prevAngle: 720
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* How many number indicators are shown on the face
|
||||
*/
|
||||
count() {
|
||||
return this.max - this.min + 1
|
||||
},
|
||||
/**
|
||||
* How many number indicators are shown per ring on the face
|
||||
*/
|
||||
countPerRing() {
|
||||
return this.double ? (this.count / 2) : this.count
|
||||
},
|
||||
/**
|
||||
* Radius of the clock face
|
||||
*/
|
||||
radius() {
|
||||
return this.pickerSize / 2
|
||||
},
|
||||
/**
|
||||
* Radius of the outer ring of number indicators
|
||||
*/
|
||||
outerRadius() {
|
||||
return this.radius -
|
||||
paddingInner -
|
||||
indicatorSize / 2
|
||||
},
|
||||
/**
|
||||
* Radius of the inner ring of number indicators
|
||||
*/
|
||||
innerRadius() {
|
||||
return Math.max(this.outerRadius * 0.6,
|
||||
this.outerRadius - paddingInner - indicatorSize)
|
||||
// 48px gives enough room for the outer ring of numbers
|
||||
},
|
||||
/**
|
||||
* The angle for each selectable value
|
||||
* For hours this ends up being 30 degrees, for minutes 6 degrees
|
||||
*/
|
||||
degreesPerUnit() {
|
||||
return 360 / this.countPerRing
|
||||
},
|
||||
/**
|
||||
* Used for calculating x/y grid location based on degrees
|
||||
*/
|
||||
degrees() {
|
||||
return this.degreesPerUnit * Math.PI / 180
|
||||
},
|
||||
/**
|
||||
* Calculates the angle the clock hand should be rotated for the
|
||||
* selected value
|
||||
*/
|
||||
handRotateAngle() {
|
||||
let currentAngle = this.prevAngle
|
||||
while (currentAngle < 0) currentAngle += 360
|
||||
let targetAngle = this.calcHandAngle(this.displayedValue)
|
||||
let degreesDiff = this.shortestDistanceDegrees(currentAngle, targetAngle)
|
||||
let angle = this.prevAngle + degreesDiff
|
||||
return angle
|
||||
},
|
||||
/**
|
||||
* Determines how long the selector hand is based on if the
|
||||
* selected value is located along the outer or inner ring
|
||||
*/
|
||||
handScale() {
|
||||
return this.calcHandScale(this.displayedValue)
|
||||
},
|
||||
handStyle() {
|
||||
return {
|
||||
transform: `rotate(${this.handRotateAngle}deg) scaleY(${this.handScale})`,
|
||||
transition: '.3s cubic-bezier(.25,.8,.50,1)'
|
||||
}
|
||||
},
|
||||
/**
|
||||
* The value the hand should be pointing at
|
||||
*/
|
||||
displayedValue() {
|
||||
return this.inputValue == null ? this.min : this.inputValue
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value(value) {
|
||||
if (value !== this.inputValue) {
|
||||
this.prevAngle = this.handRotateAngle
|
||||
}
|
||||
this.inputValue = value
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isDisabled(value) {
|
||||
return this.disabledValues && this.disabledValues(value)
|
||||
},
|
||||
/**
|
||||
* Calculates the distance between two points
|
||||
*/
|
||||
euclidean(p0, p1) {
|
||||
const dx = p1.x - p0.x
|
||||
const dy = p1.y - p0.y
|
||||
|
||||
return Math.sqrt(dx * dx + dy * dy)
|
||||
},
|
||||
shortestDistanceDegrees(start, stop) {
|
||||
const modDiff = (stop - start) % 360
|
||||
let shortestDistance = 180 - Math.abs(Math.abs(modDiff) - 180)
|
||||
return (modDiff + 360) % 360 < 180 ? shortestDistance * 1 : shortestDistance * -1
|
||||
},
|
||||
/**
|
||||
* Calculates the angle of the line from the center point
|
||||
* to the given point.
|
||||
*/
|
||||
coordToAngle(center, p1) {
|
||||
const value = 2 *
|
||||
Math.atan2(p1.y - center.y - this.euclidean(center, p1), p1.x - center.x)
|
||||
return Math.abs(value * 180 / Math.PI)
|
||||
},
|
||||
/**
|
||||
* Generates the inline style translate() property for a
|
||||
* number indicator, which determines it's location on the
|
||||
* clock face
|
||||
*/
|
||||
getNumberTranslate(value) {
|
||||
const { x, y } = this.getNumberCoords(value)
|
||||
return `translate(${x}px, ${y}px)`
|
||||
},
|
||||
/***
|
||||
* Calculates the coordinates on the clock face for a number
|
||||
* indicator value
|
||||
*/
|
||||
getNumberCoords(value) {
|
||||
const radius = this.isInnerRing(value) ? this.innerRadius : this.outerRadius
|
||||
return {
|
||||
x: Math.round(radius * Math.sin((value - this.min) * this.degrees)),
|
||||
y: Math.round(-radius * Math.cos((value - this.min) * this.degrees))
|
||||
}
|
||||
},
|
||||
getFaceNumberClasses(num) {
|
||||
return {
|
||||
'active': num.value === this.displayedValue,
|
||||
'disabled': this.isDisabled(num.value)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Determines if a value resides on the inner ring
|
||||
*/
|
||||
isInnerRing(value) {
|
||||
return this.double && (value - this.min >= this.countPerRing)
|
||||
},
|
||||
calcHandAngle(value) {
|
||||
let angle = this.degreesPerUnit * (value - this.min)
|
||||
if (this.isInnerRing(value)) angle -= 360
|
||||
return angle
|
||||
},
|
||||
calcHandScale(value) {
|
||||
return this.isInnerRing(value)
|
||||
? ((this.innerRadius) / this.outerRadius)
|
||||
: 1
|
||||
},
|
||||
onMouseDown(e) {
|
||||
e.preventDefault()
|
||||
this.isDragging = true
|
||||
this.onDragMove(e)
|
||||
},
|
||||
onMouseUp() {
|
||||
this.isDragging = false
|
||||
if (!this.isDisabled(this.inputValue)) {
|
||||
this.$emit('change', this.inputValue)
|
||||
}
|
||||
},
|
||||
onDragMove(e) {
|
||||
e.preventDefault()
|
||||
if (!this.isDragging && e.type !== 'click') return
|
||||
|
||||
const { width, top, left } = this.$refs.clock.getBoundingClientRect()
|
||||
const { clientX, clientY } = 'touches' in e ? e.touches[0] : e
|
||||
const center = { x: width / 2, y: -width / 2 }
|
||||
const coords = { x: clientX - left, y: top - clientY }
|
||||
const handAngle = Math.round(this.coordToAngle(center, coords) + 360) % 360
|
||||
const insideClick = this.double && this.euclidean(center, coords) <
|
||||
(this.outerRadius + this.innerRadius) / 2 - 16
|
||||
|
||||
let value = Math.round(handAngle / this.degreesPerUnit) +
|
||||
this.min +
|
||||
(insideClick ? this.countPerRing : 0)
|
||||
|
||||
// Necessary to fix edge case when selecting left part of max value
|
||||
if (handAngle >= (360 - this.degreesPerUnit / 2)) {
|
||||
value = insideClick ? this.max : this.min
|
||||
}
|
||||
this.update(value)
|
||||
},
|
||||
update(value) {
|
||||
if (this.inputValue !== value && !this.isDisabled(value)) {
|
||||
this.prevAngle = this.handRotateAngle
|
||||
this.inputValue = value
|
||||
this.$emit('input', value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import Clockpicker from './Clockpicker'
|
||||
|
||||
import { use, registerComponent } from '../../utils/plugins'
|
||||
|
||||
const Plugin = {
|
||||
install(Vue) {
|
||||
registerComponent(Vue, Clockpicker)
|
||||
}
|
||||
}
|
||||
|
||||
use(Plugin)
|
||||
|
||||
export default Plugin
|
||||
|
||||
export {
|
||||
Clockpicker
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import BCollapse from '@components/collapse/Collapse'
|
||||
|
||||
describe('BCollapse', () => {
|
||||
it('is called', () => {
|
||||
const wrapper = shallowMount(BCollapse)
|
||||
expect(wrapper.name()).toBe('BCollapse')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('default props and vm', () => {
|
||||
const wrapper = shallowMount(BCollapse)
|
||||
expect(wrapper.props().open).toBe(true)
|
||||
expect(wrapper.props().animation).toBe('fade')
|
||||
expect(wrapper.vm.isOpen).toBe(true)
|
||||
})
|
||||
|
||||
it('set default open prop', () => {
|
||||
const wrapper = shallowMount(BCollapse, {
|
||||
propsData: {
|
||||
open: false
|
||||
}
|
||||
})
|
||||
expect(wrapper.props().open).toBe(false)
|
||||
expect(wrapper.vm.isOpen).toBe(false)
|
||||
})
|
||||
|
||||
it('call toggle method', async () => {
|
||||
const wrapper = shallowMount(BCollapse, {
|
||||
propsData: {
|
||||
open: false
|
||||
}
|
||||
})
|
||||
expect(wrapper.find('.collapse-content').isVisible()).toBe(false)
|
||||
wrapper.vm.toggle()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.vm.isOpen).toBe(true)
|
||||
expect(wrapper.find('.collapse-content').isVisible()).toBe(true)
|
||||
})
|
||||
|
||||
it('emit a click event', () => {
|
||||
const wrapper = shallowMount(BCollapse, {
|
||||
propsData: {
|
||||
open: false
|
||||
}
|
||||
})
|
||||
expect(wrapper.find('.collapse-content').isVisible()).toBe(false)
|
||||
const updateOpen = jest.fn()
|
||||
wrapper.vm.$on('update:open', updateOpen)
|
||||
wrapper.find('.collapse-trigger').trigger('click')
|
||||
expect(updateOpen).toHaveBeenCalledTimes(1)
|
||||
expect(updateOpen).toHaveBeenCalledWith(true)
|
||||
expect(wrapper.vm.isOpen).toBe(true)
|
||||
expect(wrapper.find('.collapse-content').isVisible()).toBe(true)
|
||||
})
|
||||
|
||||
it('update open prop', () => {
|
||||
const wrapper = shallowMount(BCollapse, {
|
||||
propsData: {
|
||||
open: false
|
||||
}
|
||||
})
|
||||
expect(wrapper.find('.collapse-content').isVisible()).toBe(false)
|
||||
wrapper.setProps({ open: true })
|
||||
expect(wrapper.vm.isOpen).toBe(true)
|
||||
expect(wrapper.find('.collapse-content').isVisible()).toBe(true)
|
||||
})
|
||||
|
||||
it('should have trigger slot', () => {
|
||||
const triggerSlot = '<strong> Header </strong>'
|
||||
const wrapper = shallowMount(BCollapse, {
|
||||
slots: {
|
||||
trigger: triggerSlot
|
||||
}
|
||||
})
|
||||
expect(wrapper.find('.collapse-trigger :first-child').html()).toBe(triggerSlot)
|
||||
})
|
||||
|
||||
it('should have default slot', () => {
|
||||
const slotDefault = '<div> Content </div>'
|
||||
const wrapper = shallowMount(BCollapse, {
|
||||
slots: {
|
||||
default: slotDefault
|
||||
}
|
||||
})
|
||||
expect(wrapper.find('.collapse-content :first-child').html()).toBe(slotDefault)
|
||||
})
|
||||
|
||||
it('should emit open event', () => {
|
||||
const wrapper = shallowMount(BCollapse, {
|
||||
propsData: {
|
||||
open: false
|
||||
}
|
||||
})
|
||||
const open = jest.fn()
|
||||
wrapper.vm.$on('open', open)
|
||||
wrapper.find('.collapse-trigger').trigger('click')
|
||||
expect(open).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should emit close event', () => {
|
||||
const wrapper = shallowMount(BCollapse, {
|
||||
propsData: {
|
||||
open: true
|
||||
}
|
||||
})
|
||||
const close = jest.fn()
|
||||
wrapper.vm.$on('close', close)
|
||||
wrapper.find('.collapse-trigger').trigger('click')
|
||||
expect(close).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="collapse">
|
||||
<div class="collapse-trigger" @click="toggle">
|
||||
<slot name="trigger" :open="isOpen" />
|
||||
</div>
|
||||
<transition :name="animation">
|
||||
<div
|
||||
:id="ariaId"
|
||||
:aria-expanded="isOpen"
|
||||
class="collapse-content"
|
||||
v-show="isOpen">
|
||||
<slot/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BCollapse',
|
||||
props: {
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
animation: {
|
||||
type: String,
|
||||
default: 'fade'
|
||||
},
|
||||
ariaId: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOpen: this.open
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
open(value) {
|
||||
this.isOpen = value
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Toggle and emit events
|
||||
*/
|
||||
toggle() {
|
||||
this.isOpen = !this.isOpen
|
||||
this.$emit('update:open', this.isOpen)
|
||||
this.$emit(this.isOpen ? 'open' : 'close')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import Collapse from './Collapse'
|
||||
|
||||
import { use, registerComponent } from '../../utils/plugins'
|
||||
|
||||
const Plugin = {
|
||||
install(Vue) {
|
||||
registerComponent(Vue, Collapse)
|
||||
}
|
||||
}
|
||||
|
||||
use(Plugin)
|
||||
|
||||
export default Plugin
|
||||
|
||||
export {
|
||||
Collapse
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import {
|
||||
mount
|
||||
} from '@vue/test-utils'
|
||||
import BDatepicker from '@components/datepicker/Datepicker'
|
||||
|
||||
const dropdownMenu = '.dropdown-menu'
|
||||
let wrapper, $dropdown, $input
|
||||
|
||||
describe('BDatepicker', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mount(BDatepicker, {
|
||||
stubs: {
|
||||
transition: false
|
||||
}
|
||||
})
|
||||
$dropdown = wrapper.find(dropdownMenu)
|
||||
$input = wrapper.find('input')
|
||||
})
|
||||
|
||||
it('is called', () => {
|
||||
expect(wrapper.name()).toBe('BDatepicker')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has an input type', () => {
|
||||
expect(wrapper.contains('.control .input[type=text]')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('has a dropdown menu hidden by default', () => {
|
||||
expect(wrapper.contains(dropdownMenu)).toBeTruthy()
|
||||
expect($dropdown.isVisible()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('has no input type when inline', () => {
|
||||
wrapper.setProps({
|
||||
inline: 'true'
|
||||
})
|
||||
expect(wrapper.contains('.control .input[type=text]')).toBeFalsy()
|
||||
})
|
||||
|
||||
it('has a dropdown menu visible when inline', () => {
|
||||
wrapper.setProps({
|
||||
inline: 'true'
|
||||
})
|
||||
expect(wrapper.contains(dropdownMenu)).toBeTruthy()
|
||||
expect($dropdown.isVisible()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('call toggle method', () => {
|
||||
wrapper.vm.$refs.dropdown.isActive = false
|
||||
wrapper.vm.toggle()
|
||||
expect(wrapper.vm.$refs.dropdown.isActive).toBeTruthy()
|
||||
expect($dropdown.isVisible()).toBeTruthy()
|
||||
|
||||
wrapper.vm.toggle(false)
|
||||
expect(wrapper.vm.$refs.dropdown.isActive).toBeFalsy()
|
||||
})
|
||||
|
||||
it('can emit input, focus and blur events', () => {
|
||||
$input.trigger('focus')
|
||||
expect(wrapper.emitted()['focus']).toBeTruthy()
|
||||
$input.trigger('blur')
|
||||
expect(wrapper.emitted()['blur']).toBeTruthy()
|
||||
})
|
||||
|
||||
it('can open datepicker on openOnfocus', () => {
|
||||
wrapper.setProps({
|
||||
openOnFocus: true
|
||||
})
|
||||
$input.trigger('focus')
|
||||
expect(wrapper.vm.$refs.dropdown.isActive).toBeTruthy()
|
||||
expect($dropdown.isVisible()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('can open datepicker on enter', () => {
|
||||
wrapper.setProps({
|
||||
openOnFocus: false
|
||||
})
|
||||
$input.trigger('focus')
|
||||
$input.trigger('keyup.enter')
|
||||
expect(wrapper.vm.$refs.dropdown.isActive).toBeTruthy()
|
||||
expect($dropdown.isVisible()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('can close datepicker on esc', () => {
|
||||
wrapper.vm.$refs.dropdown.isActive = true
|
||||
const keyupEvent = new Event('keyup')
|
||||
keyupEvent.keyCode = 27
|
||||
window.document.dispatchEvent(keyupEvent)
|
||||
wrapper.vm.$nextTick(() => expect($dropdown.isVisible()).toBeFalsy())
|
||||
})
|
||||
})
|
||||
+520
@@ -0,0 +1,520 @@
|
||||
<template>
|
||||
<div class="datepicker control" :class="[size, {'is-expanded': expanded}]">
|
||||
<b-dropdown
|
||||
v-if="!isMobile || inline"
|
||||
ref="dropdown"
|
||||
:position="position"
|
||||
:disabled="disabled"
|
||||
:inline="inline">
|
||||
<b-input
|
||||
v-if="!inline"
|
||||
ref="input"
|
||||
slot="trigger"
|
||||
autocomplete="off"
|
||||
:value="formatValue(dateSelected)"
|
||||
:placeholder="placeholder"
|
||||
:size="size"
|
||||
:icon="icon"
|
||||
:icon-pack="iconPack"
|
||||
:rounded="rounded"
|
||||
:loading="loading"
|
||||
:disabled="disabled"
|
||||
:readonly="!editable"
|
||||
v-bind="$attrs"
|
||||
@click.native.stop="toggle(true)"
|
||||
@keyup.native.enter="toggle(true)"
|
||||
@change.native="onChange($event.target.value)"
|
||||
@focus="handleOnFocus"
|
||||
@blur="onBlur" />
|
||||
|
||||
<b-dropdown-item :disabled="disabled" custom>
|
||||
<header class="datepicker-header">
|
||||
<template v-if="$slots.header !== undefined && $slots.header.length">
|
||||
<slot name="header" />
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="pagination field is-centered"
|
||||
:class="size">
|
||||
<a
|
||||
v-show="!isFirstMonth && !disabled"
|
||||
class="pagination-previous"
|
||||
role="button"
|
||||
href="#"
|
||||
:disabled="disabled"
|
||||
@click.prevent="decrementMonth"
|
||||
@keydown.enter.prevent="decrementMonth"
|
||||
@keydown.space.prevent="decrementMonth">
|
||||
|
||||
<b-icon
|
||||
icon="chevron-left"
|
||||
:pack="iconPack"
|
||||
both
|
||||
type="is-primary is-clickable"/>
|
||||
</a>
|
||||
<a
|
||||
v-show="!isLastMonth && !disabled"
|
||||
class="pagination-next"
|
||||
role="button"
|
||||
href="#"
|
||||
:disabled="disabled"
|
||||
@click.prevent="incrementMonth"
|
||||
@keydown.enter.prevent="incrementMonth"
|
||||
@keydown.space.prevent="incrementMonth">
|
||||
|
||||
<b-icon
|
||||
icon="chevron-right"
|
||||
:pack="iconPack"
|
||||
both
|
||||
type="is-primary is-clickable"/>
|
||||
</a>
|
||||
<div class="pagination-list">
|
||||
<b-field>
|
||||
<b-select
|
||||
v-model="focusedDateData.month"
|
||||
:disabled="disabled"
|
||||
:size="size">
|
||||
<option
|
||||
v-for="(month, index) in monthNames"
|
||||
:value="index"
|
||||
:key="month">
|
||||
{{ month }}
|
||||
</option>
|
||||
</b-select>
|
||||
<b-select
|
||||
v-model="focusedDateData.year"
|
||||
:disabled="disabled"
|
||||
:size="size">
|
||||
<option
|
||||
v-for="year in listOfYears"
|
||||
:value="year"
|
||||
:key="year">
|
||||
{{ year }}
|
||||
</option>
|
||||
</b-select>
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="datepicker-content">
|
||||
<b-datepicker-table
|
||||
v-model="dateSelected"
|
||||
:day-names="dayNames"
|
||||
:month-names="monthNames"
|
||||
:first-day-of-week="firstDayOfWeek"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
:focused="focusedDateData"
|
||||
:disabled="disabled"
|
||||
:unselectable-dates="unselectableDates"
|
||||
:unselectable-days-of-week="unselectableDaysOfWeek"
|
||||
:selectable-dates="selectableDates"
|
||||
:events="events"
|
||||
:indicators="indicators"
|
||||
:date-creator="dateCreator"
|
||||
@close="toggle(false)"/>
|
||||
</div>
|
||||
|
||||
<footer
|
||||
v-if="$slots.default !== undefined && $slots.default.length"
|
||||
class="datepicker-footer">
|
||||
<slot/>
|
||||
</footer>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
|
||||
<b-input
|
||||
v-else
|
||||
ref="input"
|
||||
type="date"
|
||||
autocomplete="off"
|
||||
:value="formatYYYYMMDD(value)"
|
||||
:placeholder="placeholder"
|
||||
:size="size"
|
||||
:icon="icon"
|
||||
:icon-pack="iconPack"
|
||||
:loading="loading"
|
||||
:max="formatYYYYMMDD(maxDate)"
|
||||
:min="formatYYYYMMDD(minDate)"
|
||||
:disabled="disabled"
|
||||
:readonly="false"
|
||||
v-bind="$attrs"
|
||||
@change.native="onChangeNativePicker"
|
||||
@click.native.stop="toggle(true)"
|
||||
@keyup.native.enter="toggle(true)"
|
||||
@focus="handleOnFocus"
|
||||
@blur="onBlur"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormElementMixin from '../../utils/FormElementMixin'
|
||||
import { isMobile } from '../../utils/helpers'
|
||||
import config from '../../utils/config'
|
||||
|
||||
import Dropdown from '../dropdown/Dropdown'
|
||||
import DropdownItem from '../dropdown/DropdownItem'
|
||||
import Input from '../input/Input'
|
||||
import Field from '../field/Field'
|
||||
import Select from '../select/Select'
|
||||
import Icon from '../icon/Icon'
|
||||
import DatepickerTable from './DatepickerTable'
|
||||
|
||||
export default {
|
||||
name: 'BDatepicker',
|
||||
components: {
|
||||
[DatepickerTable.name]: DatepickerTable,
|
||||
[Input.name]: Input,
|
||||
[Field.name]: Field,
|
||||
[Select.name]: Select,
|
||||
[Icon.name]: Icon,
|
||||
[Dropdown.name]: Dropdown,
|
||||
[DropdownItem.name]: DropdownItem
|
||||
},
|
||||
mixins: [FormElementMixin],
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
value: Date,
|
||||
dayNames: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
if (Array.isArray(config.defaultDayNames)) {
|
||||
return config.defaultDayNames
|
||||
} else {
|
||||
return [
|
||||
'Su',
|
||||
'M',
|
||||
'Tu',
|
||||
'W',
|
||||
'Th',
|
||||
'F',
|
||||
'S'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
monthNames: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
if (Array.isArray(config.defaultMonthNames)) {
|
||||
return config.defaultMonthNames
|
||||
} else {
|
||||
return [
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
firstDayOfWeek: {
|
||||
type: Number,
|
||||
default: () => {
|
||||
if (typeof config.defaultFirstDayOfWeek === 'number') {
|
||||
return config.defaultFirstDayOfWeek
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
},
|
||||
inline: Boolean,
|
||||
minDate: Date,
|
||||
maxDate: Date,
|
||||
focusedDate: Date,
|
||||
placeholder: String,
|
||||
editable: Boolean,
|
||||
disabled: Boolean,
|
||||
unselectableDates: Array,
|
||||
unselectableDaysOfWeek: {
|
||||
type: Array,
|
||||
default: () => { return config.defaultUnselectableDaysOfWeek }
|
||||
},
|
||||
selectableDates: Array,
|
||||
dateFormatter: {
|
||||
type: Function,
|
||||
default: (date) => {
|
||||
if (typeof config.defaultDateFormatter === 'function') {
|
||||
return config.defaultDateFormatter(date)
|
||||
} else {
|
||||
const yyyyMMdd = date.getFullYear() +
|
||||
'/' + (date.getMonth() + 1) +
|
||||
'/' + date.getDate()
|
||||
const d = new Date(yyyyMMdd)
|
||||
return d.toLocaleDateString()
|
||||
}
|
||||
}
|
||||
},
|
||||
dateParser: {
|
||||
type: Function,
|
||||
default: (date) => {
|
||||
if (typeof config.defaultDateParser === 'function') {
|
||||
return config.defaultDateParser(date)
|
||||
} else {
|
||||
return new Date(Date.parse(date))
|
||||
}
|
||||
}
|
||||
},
|
||||
dateCreator: {
|
||||
type: Function,
|
||||
default: () => {
|
||||
if (typeof config.defaultDateCreator === 'function') {
|
||||
return config.defaultDateCreator()
|
||||
} else {
|
||||
return new Date()
|
||||
}
|
||||
}
|
||||
},
|
||||
mobileNative: {
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return config.defaultDatepickerMobileNative
|
||||
}
|
||||
},
|
||||
position: String,
|
||||
events: Array,
|
||||
indicators: {
|
||||
type: String,
|
||||
default: 'dots'
|
||||
},
|
||||
openOnFocus: Boolean
|
||||
},
|
||||
data() {
|
||||
const focusedDate = this.value || this.focusedDate || this.dateCreator()
|
||||
|
||||
return {
|
||||
dateSelected: this.value,
|
||||
focusedDateData: {
|
||||
month: focusedDate.getMonth(),
|
||||
year: focusedDate.getFullYear()
|
||||
},
|
||||
_elementRef: 'input',
|
||||
_isDatepicker: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/*
|
||||
* Returns an array of years for the year dropdown. If earliest/latest
|
||||
* dates are set by props, range of years will fall within those dates.
|
||||
*/
|
||||
listOfYears() {
|
||||
let latestYear = this.focusedDateData.year + 3
|
||||
if (this.maxDate && this.maxDate.getFullYear() < latestYear) {
|
||||
latestYear = this.maxDate.getFullYear()
|
||||
}
|
||||
|
||||
let earliestYear = (latestYear - 100) + 3
|
||||
if (this.minDate && this.minDate.getFullYear() > earliestYear) {
|
||||
earliestYear = this.minDate.getFullYear()
|
||||
}
|
||||
|
||||
const arrayOfYears = []
|
||||
for (let i = earliestYear; i <= latestYear; i++) {
|
||||
arrayOfYears.push(i)
|
||||
}
|
||||
|
||||
return arrayOfYears.reverse()
|
||||
},
|
||||
|
||||
isFirstMonth() {
|
||||
if (!this.minDate) return false
|
||||
const dateToCheck = new Date(this.focusedDateData.year, this.focusedDateData.month)
|
||||
const date = new Date(this.minDate.getFullYear(), this.minDate.getMonth())
|
||||
return (dateToCheck <= date)
|
||||
},
|
||||
|
||||
isLastMonth() {
|
||||
if (!this.maxDate) return false
|
||||
const dateToCheck = new Date(this.focusedDateData.year, this.focusedDateData.month)
|
||||
const date = new Date(this.maxDate.getFullYear(), this.maxDate.getMonth())
|
||||
return (dateToCheck >= date)
|
||||
},
|
||||
|
||||
isMobile() {
|
||||
return this.mobileNative && isMobile.any()
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
/*
|
||||
* Emit input event with selected date as payload, set isActive to false.
|
||||
* Update internal focusedDateData
|
||||
*/
|
||||
dateSelected(value) {
|
||||
const currentDate = !value ? this.dateCreator() : value
|
||||
this.focusedDateData = {
|
||||
month: currentDate.getMonth(),
|
||||
year: currentDate.getFullYear()
|
||||
}
|
||||
this.$emit('input', value)
|
||||
this.toggle()
|
||||
},
|
||||
|
||||
/**
|
||||
* When v-model is changed:
|
||||
* 1. Update internal value.
|
||||
* 2. If it's invalid, validate again.
|
||||
*/
|
||||
value(value) {
|
||||
this.dateSelected = value
|
||||
|
||||
!this.isValid && this.$refs.input.checkHtml5Validity()
|
||||
},
|
||||
|
||||
focusedDate(value) {
|
||||
if (value) {
|
||||
this.focusedDateData = {
|
||||
month: value.getMonth(),
|
||||
year: value.getFullYear()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Emit input event on month and/or year change
|
||||
*/
|
||||
'focusedDateData.month'(value) {
|
||||
this.$emit('change-month', value)
|
||||
},
|
||||
'focusedDateData.year'(value) {
|
||||
this.$emit('change-year', value)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/*
|
||||
* Emit input event with selected date as payload for v-model in parent
|
||||
*/
|
||||
updateSelectedDate(date) {
|
||||
this.dateSelected = date
|
||||
},
|
||||
|
||||
/*
|
||||
* Parse string into date
|
||||
*/
|
||||
onChange(value) {
|
||||
const date = this.dateParser(value)
|
||||
if (date && !isNaN(date)) {
|
||||
this.dateSelected = date
|
||||
} else {
|
||||
// Force refresh input value when not valid date
|
||||
this.dateSelected = null
|
||||
this.$refs.input.newValue = this.dateSelected
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Format date into string
|
||||
*/
|
||||
formatValue(value) {
|
||||
if (value && !isNaN(value)) {
|
||||
return this.dateFormatter(value)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Either decrement month by 1 if not January or decrement year by 1
|
||||
* and set month to 11 (December)
|
||||
*/
|
||||
decrementMonth() {
|
||||
if (this.disabled) return
|
||||
|
||||
if (this.focusedDateData.month > 0) {
|
||||
this.focusedDateData.month -= 1
|
||||
} else {
|
||||
this.focusedDateData.month = 11
|
||||
this.focusedDateData.year -= 1
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Either increment month by 1 if not December or increment year by 1
|
||||
* and set month to 0 (January)
|
||||
*/
|
||||
incrementMonth() {
|
||||
if (this.disabled) return
|
||||
|
||||
if (this.focusedDateData.month < 11) {
|
||||
this.focusedDateData.month += 1
|
||||
} else {
|
||||
this.focusedDateData.month = 0
|
||||
this.focusedDateData.year += 1
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Format date into string 'YYYY-MM-DD'
|
||||
*/
|
||||
formatYYYYMMDD(value) {
|
||||
const date = new Date(value)
|
||||
if (value && !isNaN(date)) {
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
return year + '-' +
|
||||
((month < 10 ? '0' : '') + month) + '-' +
|
||||
((day < 10 ? '0' : '') + day)
|
||||
}
|
||||
return ''
|
||||
},
|
||||
|
||||
/*
|
||||
* Parse date from string
|
||||
*/
|
||||
onChangeNativePicker(event) {
|
||||
const date = event.target.value
|
||||
this.dateSelected = date ? new Date(date.replace(/-/g, '/')) : null
|
||||
},
|
||||
|
||||
/*
|
||||
* Toggle datepicker
|
||||
*/
|
||||
toggle(active) {
|
||||
if (this.$refs.dropdown) {
|
||||
this.$refs.dropdown.isActive = typeof active === 'boolean'
|
||||
? active
|
||||
: !this.$refs.dropdown.isActive
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* Call default onFocus method and show datepicker
|
||||
*/
|
||||
handleOnFocus() {
|
||||
this.onFocus()
|
||||
if (this.openOnFocus) {
|
||||
this.toggle(true)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Keypress event that is bound to the document.
|
||||
*/
|
||||
keyPress(event) {
|
||||
// Esc key
|
||||
if (this.$refs.dropdown.isActive && event.keyCode === 27) {
|
||||
this.toggle(false)
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (typeof window !== 'undefined') {
|
||||
document.addEventListener('keyup', this.keyPress)
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (typeof window !== 'undefined') {
|
||||
document.removeEventListener('keyup', this.keyPress)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import BDatepickerTable from '@components/datepicker/DatepickerTable'
|
||||
|
||||
import config, {setOptions} from '@utils/config'
|
||||
|
||||
describe('BDatepickerTable', () => {
|
||||
beforeEach(() => {
|
||||
setOptions(Object.assign(config, {
|
||||
defaultMonthNames: [
|
||||
'January', 'February', 'March', 'April', 'May', 'June', 'July',
|
||||
'August', 'September', 'October', 'November', 'December'
|
||||
],
|
||||
defaultDayNames: ['Su', 'M', 'Tu', 'W', 'Th', 'F', 'S'],
|
||||
focusedDate: new Date('2018-07')
|
||||
}))
|
||||
})
|
||||
|
||||
it('is called', () => {
|
||||
const wrapper = shallowMount(BDatepickerTable, {
|
||||
propsData: {
|
||||
dayNames: config.defaultDayNames,
|
||||
monthNames: config.defaultMonthNames,
|
||||
focused: {
|
||||
month: config.focusedDate.getMonth(),
|
||||
year: config.focusedDate.getFullYear()
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(wrapper.name()).toBe('BDatepickerTable')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('assign events to weeks even if the event has a time', () => {
|
||||
const wrapper = shallowMount(BDatepickerTable, {
|
||||
propsData: {
|
||||
dayNames: config.defaultDayNames,
|
||||
monthNames: config.defaultMonthNames,
|
||||
events: [new Date('July 22, 2018 07:22:13'), new Date('July 23, 2018 00:00:00')],
|
||||
focused: {
|
||||
month: config.focusedDate.getMonth(),
|
||||
year: config.focusedDate.getFullYear()
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(wrapper.vm.eventsInThisWeek(wrapper.vm.weeksInThisMonth[3]).length).toEqual(2)
|
||||
})
|
||||
})
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
<template>
|
||||
<section class="datepicker-table">
|
||||
<header class="datepicker-header">
|
||||
<div
|
||||
v-for="(day, index) in visibleDayNames"
|
||||
:key="index"
|
||||
class="datepicker-cell">
|
||||
{{ day }}
|
||||
</div>
|
||||
</header>
|
||||
<div class="datepicker-body" :class="{'has-events':hasEvents}">
|
||||
<b-datepicker-table-row
|
||||
v-for="(week, index) in weeksInThisMonth"
|
||||
:key="index"
|
||||
:selected-date="value"
|
||||
:week="week"
|
||||
:month="focused.month"
|
||||
:min-date="minDate"
|
||||
:max-date="maxDate"
|
||||
:disabled="disabled"
|
||||
:unselectable-dates="unselectableDates"
|
||||
:unselectable-days-of-week="unselectableDaysOfWeek"
|
||||
:selectable-dates="selectableDates"
|
||||
:events="eventsInThisWeek(week)"
|
||||
:indicators="indicators"
|
||||
:date-creator="dateCreator"
|
||||
@select="updateSelectedDate"/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DatepickerTableRow from './DatepickerTableRow'
|
||||
|
||||
export default {
|
||||
name: 'BDatepickerTable',
|
||||
components: {
|
||||
[DatepickerTableRow.name]: DatepickerTableRow
|
||||
},
|
||||
props: {
|
||||
value: Date,
|
||||
dayNames: Array,
|
||||
monthNames: Array,
|
||||
firstDayOfWeek: Number,
|
||||
events: Array,
|
||||
indicators: String,
|
||||
minDate: Date,
|
||||
maxDate: Date,
|
||||
focused: Object,
|
||||
disabled: Boolean,
|
||||
dateCreator: Function,
|
||||
unselectableDates: Array,
|
||||
unselectableDaysOfWeek: Array,
|
||||
selectableDates: Array
|
||||
},
|
||||
computed: {
|
||||
visibleDayNames() {
|
||||
const visibleDayNames = []
|
||||
let index = this.firstDayOfWeek
|
||||
while (visibleDayNames.length < this.dayNames.length) {
|
||||
const currentDayName = this.dayNames[(index % this.dayNames.length)]
|
||||
visibleDayNames.push(currentDayName)
|
||||
index++
|
||||
}
|
||||
return visibleDayNames
|
||||
},
|
||||
|
||||
hasEvents() {
|
||||
return this.events && this.events.length
|
||||
},
|
||||
|
||||
/*
|
||||
* Return array of all events in the specified month
|
||||
*/
|
||||
eventsInThisMonth() {
|
||||
if (!this.events) return []
|
||||
|
||||
const monthEvents = []
|
||||
|
||||
for (let i = 0; i < this.events.length; i++) {
|
||||
let event = this.events[i]
|
||||
|
||||
if (!event.hasOwnProperty('date')) {
|
||||
event = { date: event }
|
||||
}
|
||||
if (!event.hasOwnProperty('type')) {
|
||||
event.type = 'is-primary'
|
||||
}
|
||||
if (
|
||||
event.date.getMonth() === this.focused.month &&
|
||||
event.date.getFullYear() === this.focused.year
|
||||
) {
|
||||
monthEvents.push(event)
|
||||
}
|
||||
}
|
||||
|
||||
return monthEvents
|
||||
},
|
||||
/*
|
||||
* Return array of all weeks in the specified month
|
||||
*/
|
||||
weeksInThisMonth() {
|
||||
const month = this.focused.month
|
||||
const year = this.focused.year
|
||||
const weeksInThisMonth = []
|
||||
const daysInThisMonth = new Date(year, month + 1, 0).getDate()
|
||||
|
||||
let startingDay = 1
|
||||
|
||||
while (startingDay <= daysInThisMonth + 6) {
|
||||
const newWeek = this.weekBuilder(startingDay, month, year)
|
||||
let weekValid = false
|
||||
|
||||
newWeek.forEach((day) => {
|
||||
if (day.getMonth() === month) {
|
||||
weekValid = true
|
||||
}
|
||||
})
|
||||
|
||||
if (weekValid) {
|
||||
weeksInThisMonth.push(newWeek)
|
||||
}
|
||||
|
||||
startingDay += 7
|
||||
}
|
||||
|
||||
return weeksInThisMonth
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/*
|
||||
* Emit input event with selected date as payload for v-model in parent
|
||||
*/
|
||||
updateSelectedDate(date) {
|
||||
this.$emit('input', date)
|
||||
},
|
||||
|
||||
/*
|
||||
* Return array of all days in the week that the startingDate is within
|
||||
*/
|
||||
weekBuilder(startingDate, month, year) {
|
||||
const thisMonth = new Date(year, month)
|
||||
|
||||
const thisWeek = []
|
||||
|
||||
const dayOfWeek = new Date(year, month, startingDate).getDay()
|
||||
|
||||
const end = dayOfWeek >= this.firstDayOfWeek
|
||||
? (dayOfWeek - this.firstDayOfWeek)
|
||||
: ((7 - this.firstDayOfWeek) + dayOfWeek)
|
||||
|
||||
let daysAgo = 1
|
||||
for (let i = 0; i < end; i++) {
|
||||
thisWeek.unshift(new Date(
|
||||
thisMonth.getFullYear(),
|
||||
thisMonth.getMonth(),
|
||||
startingDate - daysAgo)
|
||||
)
|
||||
daysAgo++
|
||||
}
|
||||
|
||||
thisWeek.push(new Date(year, month, startingDate))
|
||||
|
||||
let daysForward = 1
|
||||
while (thisWeek.length < 7) {
|
||||
thisWeek.push(new Date(year, month, startingDate + daysForward))
|
||||
daysForward++
|
||||
}
|
||||
|
||||
return thisWeek
|
||||
},
|
||||
|
||||
eventsInThisWeek(week) {
|
||||
return this.eventsInThisMonth.filter((event) => {
|
||||
const stripped = new Date(Date.parse(event.date))
|
||||
stripped.setHours(0)
|
||||
stripped.setMinutes(0)
|
||||
stripped.setSeconds(0)
|
||||
stripped.setMilliseconds(0)
|
||||
const timed = stripped.getTime()
|
||||
|
||||
return week.some((weekDate) => weekDate.getTime() === timed)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import BDatepickerTableRow from '@components/datepicker/DatepickerTableRow'
|
||||
|
||||
describe('BDatepickerTableRow', () => {
|
||||
it('is called', () => {
|
||||
const wrapper = shallowMount(BDatepickerTableRow, {
|
||||
propsData: {
|
||||
week: [
|
||||
new Date('Sun Dec 31 2017 00:00:00 GMT-0200 (-02)'),
|
||||
new Date('Mon Jan 01 2018 00:00:00 GMT-0200 (-02)'),
|
||||
new Date('Tue Jan 02 2018 00:00:00 GMT-0200 (-02)'),
|
||||
new Date('Wed Jan 03 2018 00:00:00 GMT-0200 (-02)'),
|
||||
new Date('Thu Jan 04 2018 00:00:00 GMT-0200 (-02)'),
|
||||
new Date('Fri Jan 05 2018 00:00:00 GMT-0200 (-02)'),
|
||||
new Date('Sat Jan 06 2018 00:00:00 GMT-0200 (-02)')
|
||||
],
|
||||
month: 12,
|
||||
dateCreator: function () {
|
||||
return new Date()
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(wrapper.name()).toBe('BDatepickerTableRow')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="datepicker-row">
|
||||
<template v-for="(day, index) in week">
|
||||
<a
|
||||
v-if="selectableDate(day) && !disabled"
|
||||
:key="index"
|
||||
:class="[classObject(day), {'has-event':eventsDateMatch(day)}, indicators]"
|
||||
class="datepicker-cell"
|
||||
role="button"
|
||||
href="#"
|
||||
:disabled="disabled"
|
||||
@click.prevent="emitChosenDate(day)"
|
||||
@keydown.enter.prevent="emitChosenDate(day)"
|
||||
@keydown.space.prevent="emitChosenDate(day)">
|
||||
{{ day.getDate() }}
|
||||
|
||||
<div class="events" v-if="eventsDateMatch(day)">
|
||||
<div
|
||||
class="event"
|
||||
:class="event.type"
|
||||
v-for="(event, index) in eventsDateMatch(day)"
|
||||
:key="index"/>
|
||||
</div>
|
||||
|
||||
</a>
|
||||
<div
|
||||
v-else
|
||||
:key="index"
|
||||
:class="classObject(day)"
|
||||
class="datepicker-cell">
|
||||
{{ day.getDate() }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BDatepickerTableRow',
|
||||
props: {
|
||||
selectedDate: Date,
|
||||
week: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
month: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
minDate: Date,
|
||||
maxDate: Date,
|
||||
disabled: Boolean,
|
||||
unselectableDates: Array,
|
||||
unselectableDaysOfWeek: Array,
|
||||
selectableDates: Array,
|
||||
events: Array,
|
||||
indicators: String,
|
||||
dateCreator: Function
|
||||
},
|
||||
methods: {
|
||||
/*
|
||||
* Check that selected day is within earliest/latest params and
|
||||
* is within this month
|
||||
*/
|
||||
selectableDate(day) {
|
||||
const validity = []
|
||||
|
||||
if (this.minDate) {
|
||||
validity.push(day >= this.minDate)
|
||||
}
|
||||
|
||||
if (this.maxDate) {
|
||||
validity.push(day <= this.maxDate)
|
||||
}
|
||||
|
||||
validity.push(day.getMonth() === this.month)
|
||||
|
||||
if (this.selectableDates) {
|
||||
for (let i = 0; i < this.selectableDates.length; i++) {
|
||||
const enabledDate = this.selectableDates[i]
|
||||
if (day.getDate() === enabledDate.getDate() &&
|
||||
day.getFullYear() === enabledDate.getFullYear() &&
|
||||
day.getMonth() === enabledDate.getMonth()) {
|
||||
return true
|
||||
} else {
|
||||
validity.push(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.unselectableDates) {
|
||||
for (let i = 0; i < this.unselectableDates.length; i++) {
|
||||
const disabledDate = this.unselectableDates[i]
|
||||
validity.push(
|
||||
day.getDate() !== disabledDate.getDate() ||
|
||||
day.getFullYear() !== disabledDate.getFullYear() ||
|
||||
day.getMonth() !== disabledDate.getMonth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.unselectableDaysOfWeek) {
|
||||
for (let i = 0; i < this.unselectableDaysOfWeek.length; i++) {
|
||||
const dayOfWeek = this.unselectableDaysOfWeek[i]
|
||||
validity.push(day.getDay() !== dayOfWeek)
|
||||
}
|
||||
}
|
||||
|
||||
return validity.indexOf(false) < 0
|
||||
},
|
||||
|
||||
/*
|
||||
* Emit select event with chosen date as payload
|
||||
*/
|
||||
emitChosenDate(day) {
|
||||
if (this.disabled) return
|
||||
|
||||
if (this.selectableDate(day)) {
|
||||
this.$emit('select', day)
|
||||
}
|
||||
},
|
||||
|
||||
eventsDateMatch(day) {
|
||||
if (!this.events.length) return false
|
||||
|
||||
const dayEvents = []
|
||||
|
||||
for (let i = 0; i < this.events.length; i++) {
|
||||
if (this.events[i].date.getDay() === day.getDay()) {
|
||||
dayEvents.push(this.events[i])
|
||||
}
|
||||
}
|
||||
|
||||
if (!dayEvents.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
return dayEvents
|
||||
},
|
||||
|
||||
/*
|
||||
* Build classObject for cell using validations
|
||||
*/
|
||||
classObject(day) {
|
||||
function dateMatch(dateOne, dateTwo) {
|
||||
// if either date is null or undefined, return false
|
||||
if (!dateOne || !dateTwo) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (dateOne.getDate() === dateTwo.getDate() &&
|
||||
dateOne.getFullYear() === dateTwo.getFullYear() &&
|
||||
dateOne.getMonth() === dateTwo.getMonth())
|
||||
}
|
||||
|
||||
return {
|
||||
'is-selected': dateMatch(day, this.selectedDate),
|
||||
'is-today': dateMatch(day, this.dateCreator()),
|
||||
'is-selectable': this.selectableDate(day) && !this.disabled,
|
||||
'is-unselectable': !this.selectableDate(day) || this.disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import Datepicker from './Datepicker'
|
||||
|
||||
import { use, registerComponent } from '../../utils/plugins'
|
||||
|
||||
const Plugin = {
|
||||
install(Vue) {
|
||||
registerComponent(Vue, Datepicker)
|
||||
}
|
||||
}
|
||||
|
||||
use(Plugin)
|
||||
|
||||
export default Plugin
|
||||
|
||||
export {
|
||||
Datepicker
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import BDialog from '@components/dialog/Dialog'
|
||||
|
||||
describe('BDialog', () => {
|
||||
it('is called', () => {
|
||||
const wrapper = shallowMount(BDialog, {
|
||||
attachToDocument: true
|
||||
})
|
||||
expect(wrapper.name()).toBe('BDialog')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<transition :name="animation">
|
||||
<div
|
||||
v-if="isActive"
|
||||
class="dialog modal is-active"
|
||||
:class="size">
|
||||
<div class="modal-background" @click="cancel('outside')"/>
|
||||
<div class="modal-card animation-content">
|
||||
<header class="modal-card-head" v-if="title">
|
||||
<p class="modal-card-title">{{ title }}</p>
|
||||
</header>
|
||||
|
||||
<section
|
||||
class="modal-card-body"
|
||||
:class="{ 'is-titleless': !title, 'is-flex': hasIcon }">
|
||||
<div class="media">
|
||||
<div class="media-left" v-if="hasIcon">
|
||||
<b-icon
|
||||
:icon="icon ? icon : iconByType"
|
||||
:pack="iconPack"
|
||||
:type="type"
|
||||
:both="!icon"
|
||||
size="is-large"/>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p v-html="message"/>
|
||||
|
||||
<div v-if="hasInput" class="field">
|
||||
<div class="control">
|
||||
<input
|
||||
v-model="prompt"
|
||||
class="input"
|
||||
ref="input"
|
||||
:class="{ 'is-danger': validationMessage }"
|
||||
v-bind="inputAttrs"
|
||||
@keyup.enter="confirm">
|
||||
</div>
|
||||
<p class="help is-danger">{{ validationMessage }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="modal-card-foot">
|
||||
<button
|
||||
v-if="showCancel"
|
||||
class="button"
|
||||
ref="cancelButton"
|
||||
@click="cancel('button')">
|
||||
{{ cancelText }}
|
||||
</button>
|
||||
<button
|
||||
class="button"
|
||||
:class="type"
|
||||
ref="confirmButton"
|
||||
@click="confirm">
|
||||
{{ confirmText }}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Icon from '../icon/Icon'
|
||||
import Modal from '../modal/Modal'
|
||||
import config from '../../utils/config'
|
||||
import { removeElement } from '../../utils/helpers'
|
||||
|
||||
export default {
|
||||
name: 'BDialog',
|
||||
components: {
|
||||
[Icon.name]: Icon
|
||||
},
|
||||
extends: Modal,
|
||||
props: {
|
||||
title: String,
|
||||
message: String,
|
||||
icon: String,
|
||||
iconPack: String,
|
||||
hasIcon: Boolean,
|
||||
type: {
|
||||
type: String,
|
||||
default: 'is-primary'
|
||||
},
|
||||
size: String,
|
||||
confirmText: {
|
||||
type: String,
|
||||
default: () => {
|
||||
return config.defaultDialogConfirmText
|
||||
? config.defaultDialogConfirmText
|
||||
: 'OK'
|
||||
}
|
||||
},
|
||||
cancelText: {
|
||||
type: String,
|
||||
default: () => {
|
||||
return config.defaultDialogCancelText
|
||||
? config.defaultDialogCancelText
|
||||
: 'Cancel'
|
||||
}
|
||||
},
|
||||
hasInput: Boolean, // Used internally to know if it's prompt
|
||||
inputAttrs: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
onConfirm: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
focusOn: {
|
||||
type: String,
|
||||
default: 'confirm'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
const prompt = this.hasInput
|
||||
? this.inputAttrs.value || ''
|
||||
: ''
|
||||
|
||||
return {
|
||||
prompt,
|
||||
isActive: false,
|
||||
validationMessage: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* Icon name (MDI) based on the type.
|
||||
*/
|
||||
iconByType() {
|
||||
switch (this.type) {
|
||||
case 'is-info':
|
||||
return 'information'
|
||||
case 'is-success':
|
||||
return 'check-circle'
|
||||
case 'is-warning':
|
||||
return 'alert'
|
||||
case 'is-danger':
|
||||
return 'alert-circle'
|
||||
default:
|
||||
return null
|
||||
}
|
||||
},
|
||||
showCancel() {
|
||||
return this.cancelOptions.indexOf('button') >= 0
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* If it's a prompt Dialog, validate the input.
|
||||
* Call the onConfirm prop (function) and close the Dialog.
|
||||
*/
|
||||
confirm() {
|
||||
if (this.$refs.input !== undefined) {
|
||||
if (!this.$refs.input.checkValidity()) {
|
||||
this.validationMessage = this.$refs.input.validationMessage
|
||||
this.$nextTick(() => this.$refs.input.select())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.onConfirm(this.prompt)
|
||||
this.close()
|
||||
},
|
||||
|
||||
/**
|
||||
* Close the Dialog.
|
||||
*/
|
||||
close() {
|
||||
this.isActive = false
|
||||
// Timeout for the animation complete before destroying
|
||||
setTimeout(() => {
|
||||
this.$destroy()
|
||||
removeElement(this.$el)
|
||||
}, 150)
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
// Insert the Dialog component in body tag
|
||||
this.$nextTick(() => {
|
||||
document.body.appendChild(this.$el)
|
||||
})
|
||||
},
|
||||
mounted() {
|
||||
this.isActive = true
|
||||
|
||||
if (typeof this.inputAttrs.required === 'undefined') {
|
||||
this.$set(this.inputAttrs, 'required', true)
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
// Handle which element receives focus
|
||||
if (this.hasInput) {
|
||||
this.$refs.input.focus()
|
||||
} else if (this.focusOn === 'cancel' && this.showCancel) {
|
||||
this.$refs.cancelButton.focus()
|
||||
} else {
|
||||
this.$refs.confirmButton.focus()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
import Vue from 'vue'
|
||||
import Dialog from './Dialog'
|
||||
|
||||
import { use, registerComponent, registerComponentProgrammatic } from '../../utils/plugins'
|
||||
|
||||
function open(propsData) {
|
||||
const vm = typeof window !== 'undefined' && window.Vue ? window.Vue : Vue
|
||||
const DialogComponent = vm.extend(Dialog)
|
||||
return new DialogComponent({
|
||||
el: document.createElement('div'),
|
||||
propsData
|
||||
})
|
||||
}
|
||||
|
||||
const DialogProgrammatic = {
|
||||
alert(params) {
|
||||
let message
|
||||
if (typeof params === 'string') message = params
|
||||
const defaultParam = {
|
||||
canCancel: false,
|
||||
message
|
||||
}
|
||||
const propsData = Object.assign(defaultParam, params)
|
||||
return open(propsData)
|
||||
},
|
||||
confirm(params) {
|
||||
const defaultParam = {}
|
||||
const propsData = Object.assign(defaultParam, params)
|
||||
return open(propsData)
|
||||
},
|
||||
prompt(params) {
|
||||
const defaultParam = {
|
||||
hasInput: true,
|
||||
confirmText: 'Done'
|
||||
}
|
||||
const propsData = Object.assign(defaultParam, params)
|
||||
return open(propsData)
|
||||
}
|
||||
}
|
||||
|
||||
const Plugin = {
|
||||
install(Vue) {
|
||||
registerComponent(Vue, Dialog)
|
||||
registerComponentProgrammatic(Vue, '$dialog', DialogProgrammatic)
|
||||
}
|
||||
}
|
||||
|
||||
use(Plugin)
|
||||
|
||||
export default Plugin
|
||||
|
||||
export {
|
||||
DialogProgrammatic as Dialog
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import BDropdown from '@components/dropdown/Dropdown'
|
||||
|
||||
describe('BDropdown', () => {
|
||||
it('is called', () => {
|
||||
const wrapper = shallowMount(BDropdown)
|
||||
expect(wrapper.name()).toBe('BDropdown')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
+189
@@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<div class="dropdown" :class="rootClasses">
|
||||
<div
|
||||
v-if="!inline"
|
||||
role="button"
|
||||
ref="trigger"
|
||||
class="dropdown-trigger"
|
||||
@click="toggle"
|
||||
aria-haspopup="true">
|
||||
<slot name="trigger"/>
|
||||
</div>
|
||||
|
||||
<transition :name="animation">
|
||||
<div
|
||||
v-if="isMobileModal"
|
||||
v-show="isActive"
|
||||
class="background"
|
||||
:aria-hidden="!isActive"
|
||||
/>
|
||||
</transition>
|
||||
<transition :name="animation">
|
||||
<div
|
||||
v-show="(!disabled && (isActive || hoverable)) || inline"
|
||||
ref="dropdownMenu"
|
||||
class="dropdown-menu"
|
||||
:aria-hidden="!isActive">
|
||||
<div
|
||||
class="dropdown-content"
|
||||
:role="ariaRoleMenu">
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BDropdown',
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Boolean, Object, Array, Symbol, Function],
|
||||
default: null
|
||||
},
|
||||
disabled: Boolean,
|
||||
hoverable: Boolean,
|
||||
inline: Boolean,
|
||||
position: {
|
||||
type: String,
|
||||
validator(value) {
|
||||
return [
|
||||
'is-top-right',
|
||||
'is-top-left',
|
||||
'is-bottom-left'
|
||||
].indexOf(value) > -1
|
||||
}
|
||||
},
|
||||
mobileModal: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
ariaRole: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
animation: {
|
||||
type: String,
|
||||
default: 'fade'
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selected: this.value,
|
||||
isActive: false,
|
||||
_isDropdown: true // Used internally by DropdownItem
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rootClasses() {
|
||||
return [this.position, {
|
||||
'is-disabled': this.disabled,
|
||||
'is-hoverable': this.hoverable,
|
||||
'is-inline': this.inline,
|
||||
'is-active': this.isActive || this.inline,
|
||||
'is-mobile-modal': this.isMobileModal
|
||||
}]
|
||||
},
|
||||
isMobileModal() {
|
||||
return this.mobileModal && !this.inline && !this.hoverable
|
||||
},
|
||||
ariaRoleMenu() {
|
||||
return this.ariaRole === 'menu' || this.ariaRole === 'list' ? this.ariaRole : null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* When v-model is changed set the new selected item.
|
||||
*/
|
||||
value(value) {
|
||||
this.selected = value
|
||||
},
|
||||
|
||||
/**
|
||||
* Emit event when isActive value is changed.
|
||||
*/
|
||||
isActive(value) {
|
||||
this.$emit('active-change', value)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Click listener from DropdownItem.
|
||||
* 1. Set new selected item.
|
||||
* 2. Emit input event to update the user v-model.
|
||||
* 3. Close the dropdown.
|
||||
*/
|
||||
selectItem(value) {
|
||||
if (this.selected !== value) {
|
||||
this.$emit('change', value)
|
||||
this.selected = value
|
||||
}
|
||||
this.$emit('input', value)
|
||||
this.isActive = false
|
||||
},
|
||||
|
||||
/**
|
||||
* White-listed items to not close when clicked.
|
||||
*/
|
||||
isInWhiteList(el) {
|
||||
if (el === this.$refs.dropdownMenu) return true
|
||||
if (el === this.$refs.trigger) return true
|
||||
// All chidren from dropdown
|
||||
if (this.$refs.dropdownMenu !== undefined) {
|
||||
const children = this.$refs.dropdownMenu.querySelectorAll('*')
|
||||
for (const child of children) {
|
||||
if (el === child) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
// All children from trigger
|
||||
if (this.$refs.trigger !== undefined) {
|
||||
const children = this.$refs.trigger.querySelectorAll('*')
|
||||
for (const child of children) {
|
||||
if (el === child) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
/**
|
||||
* Close dropdown if clicked outside.
|
||||
*/
|
||||
clickedOutside(event) {
|
||||
if (this.inline) return
|
||||
|
||||
if (!this.isInWhiteList(event.target)) this.isActive = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle dropdown if it's not disabled.
|
||||
*/
|
||||
toggle() {
|
||||
if (this.disabled || this.hoverable) return
|
||||
|
||||
if (!this.isActive) {
|
||||
// if not active, toggle after clickOutside event
|
||||
// this fixes toggling programmatic
|
||||
this.$nextTick(() => { this.isActive = !this.isActive })
|
||||
} else {
|
||||
this.isActive = !this.isActive
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (typeof window !== 'undefined') {
|
||||
document.addEventListener('click', this.clickedOutside)
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (typeof window !== 'undefined') {
|
||||
document.removeEventListener('click', this.clickedOutside)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<hr v-if="separator" class="dropdown-divider">
|
||||
<a
|
||||
v-else-if="!custom && !hasLink"
|
||||
class="dropdown-item"
|
||||
:class="anchorClasses"
|
||||
@click="selectItem"
|
||||
:role="ariaRoleItem"
|
||||
tabindex="0">
|
||||
<slot/>
|
||||
</a>
|
||||
<div
|
||||
v-else
|
||||
:class="itemClasses"
|
||||
@click="selectItem"
|
||||
:role="ariaRoleItem"
|
||||
tabindex="0">
|
||||
<slot/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'BDropdownItem',
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Boolean, Object, Array, Symbol, Function],
|
||||
default: null
|
||||
},
|
||||
separator: Boolean,
|
||||
disabled: Boolean,
|
||||
custom: Boolean,
|
||||
paddingless: Boolean,
|
||||
hasLink: Boolean,
|
||||
ariaRole: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
anchorClasses() {
|
||||
return {
|
||||
'is-disabled': this.$parent.disabled || this.disabled,
|
||||
'is-paddingless': this.paddingless,
|
||||
'is-active': this.value !== null && this.value === this.$parent.selected
|
||||
}
|
||||
},
|
||||
itemClasses() {
|
||||
return {
|
||||
'dropdown-item': !this.hasLink,
|
||||
'is-disabled': this.disabled,
|
||||
'is-paddingless': this.paddingless,
|
||||
'is-active': this.value !== null && this.value === this.$parent.selected,
|
||||
'has-link': this.hasLink
|
||||
}
|
||||
},
|
||||
ariaRoleItem() {
|
||||
return this.ariaRole === 'menuitem' || this.ariaRole === 'listitem' ? this.ariaRole : null
|
||||
},
|
||||
/**
|
||||
* Check if item can be clickable.
|
||||
*/
|
||||
isClickable() {
|
||||
return !this.$parent.disabled && !this.separator && !this.disabled && !this.custom
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Click listener, select the item.
|
||||
*/
|
||||
selectItem() {
|
||||
if (!this.isClickable) return
|
||||
|
||||
this.$parent.selectItem(this.value)
|
||||
this.$emit('click')
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (!this.$parent.$data._isDropdown) {
|
||||
this.$destroy()
|
||||
throw new Error('You should wrap bDropdownItem on a bDropdown')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import Dropdown from './Dropdown'
|
||||
import DropdownItem from './DropdownItem'
|
||||
|
||||
import { use, registerComponent } from '../../utils/plugins'
|
||||
|
||||
const Plugin = {
|
||||
install(Vue) {
|
||||
registerComponent(Vue, Dropdown)
|
||||
registerComponent(Vue, DropdownItem)
|
||||
}
|
||||
}
|
||||
|
||||
use(Plugin)
|
||||
|
||||
export default Plugin
|
||||
|
||||
export {
|
||||
Dropdown,
|
||||
DropdownItem
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
import { shallowMount, mount, createLocalVue } from '@vue/test-utils'
|
||||
import BField from '@components/field/Field'
|
||||
import BFieldBody from '@components/field/FieldBody'
|
||||
import BInput from '@components/input/Input'
|
||||
|
||||
const localVue = createLocalVue()
|
||||
localVue.component('b-field', BField)
|
||||
localVue.component('b-field-body', BFieldBody)
|
||||
localVue.component('b-input', BInput)
|
||||
|
||||
describe('BField', () => {
|
||||
it('is called', () => {
|
||||
const wrapper = shallowMount(BField)
|
||||
expect(wrapper.name()).toBe('BField')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('sets fieldLabelSize to "is-normal" when horizontal==true and input elements are inside', () => {
|
||||
const wrapper = mount(BField, {
|
||||
localVue,
|
||||
propsData: { horizontal: true },
|
||||
slots: { default: [BInput] }
|
||||
})
|
||||
|
||||
expect(wrapper.vm._data).toEqual(expect.objectContaining({fieldLabelSize: 'is-normal'}))
|
||||
})
|
||||
|
||||
describe('class names for the root div.field', () => {
|
||||
it('contains "is-expanded" when prop "expanded" is set', () => {
|
||||
const wrapper = shallowMount(BField, { propsData: { expanded: true } })
|
||||
expect(wrapper.find('.field').classes()).toContain('is-expanded')
|
||||
})
|
||||
|
||||
it('contains "is-grouped-multiline" when prop "groupMultiline" is set', () => {
|
||||
const wrapper = shallowMount(BField, { propsData: { groupMultiline: true } })
|
||||
expect(wrapper.find('.field').classes()).toContain('is-grouped-multiline')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Passing a message prop', () => {
|
||||
const generateMountOptions = ({message}) => {
|
||||
return {
|
||||
propsData: {message},
|
||||
localVue,
|
||||
slots: {
|
||||
default: [BInput, '<button class="button">Button</button>']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('adds a help <p> element in the root div.field when "message" prop is passed', () => {
|
||||
const message = 'Some string message'
|
||||
const mountOptions = generateMountOptions({message})
|
||||
const wrapper = shallowMount(BField, mountOptions)
|
||||
expect(wrapper.find('.field').find('p.help').text()).toEqual(message)
|
||||
})
|
||||
|
||||
it('given an array of string as message, it joins the messages with line breaks', () => {
|
||||
const message = [
|
||||
'Some string message 1',
|
||||
'Some string message 2',
|
||||
'Some string message 3',
|
||||
'Some string message 4',
|
||||
'Some string message 5'
|
||||
]
|
||||
const mountOptions = generateMountOptions({message})
|
||||
const wrapper = shallowMount(BField, mountOptions)
|
||||
expect(wrapper.find('p.help').html()).toContain(message.join(' <br> '))
|
||||
})
|
||||
|
||||
it('given an object as message, it grabs the keys and joins them with line breaks', () => {
|
||||
const message = {
|
||||
message1: 'Some string message 1',
|
||||
message2: 'Some string message 2',
|
||||
message3: 'Some string message 3',
|
||||
message4: 'Some string message 4',
|
||||
message5: 'Some string message 5'
|
||||
}
|
||||
const mountOptions = generateMountOptions({message})
|
||||
const wrapper = shallowMount(BField, mountOptions)
|
||||
expect(wrapper.find('p.help').html()).toContain(Object.keys(message).join(' <br> '))
|
||||
})
|
||||
|
||||
it(`given an array of string with an object as one of the elements as message, it grabs the
|
||||
keys of the object and joins them with the messages with line breaks`, () => {
|
||||
const message = [
|
||||
'Some string message 1',
|
||||
{
|
||||
message2: 'Some string message 2',
|
||||
message3: 'Some string message 3'
|
||||
},
|
||||
'Some string message 4',
|
||||
'Some string message 5'
|
||||
]
|
||||
const mountOptions = generateMountOptions({message})
|
||||
const wrapper = shallowMount(BField, mountOptions)
|
||||
expect(wrapper.find('p.help').html()).toContain([
|
||||
message[0],
|
||||
'message2',
|
||||
'message3',
|
||||
message[2],
|
||||
message[3]
|
||||
].join(' <br> '))
|
||||
})
|
||||
})
|
||||
|
||||
describe('Passing true for grouped prop', () => {
|
||||
const mountOptions = {
|
||||
propsData: {
|
||||
grouped: true
|
||||
},
|
||||
localVue,
|
||||
slots: {
|
||||
default: [BInput, '<button class="button">Button</button>']
|
||||
}
|
||||
}
|
||||
|
||||
it('groups the children together', () => {
|
||||
const wrapper = shallowMount(BField, mountOptions)
|
||||
expect(wrapper.find('.field').classes()).toContain('is-grouped')
|
||||
})
|
||||
|
||||
it('appends the classname with value of position when "position" prop is passed', () => {
|
||||
const {propsData} = mountOptions
|
||||
const wrapper = shallowMount(BField, {
|
||||
...mountOptions,
|
||||
propsData: {
|
||||
...propsData,
|
||||
position: 'is-centered'
|
||||
}
|
||||
})
|
||||
expect(wrapper.find('.field').classes()).toContain('is-grouped')
|
||||
expect(wrapper.find('.field').classes()).toContain('is-grouped-centered')
|
||||
})
|
||||
|
||||
it('adds a label element under the root div.field when "label" prop is passed', () => {
|
||||
const {propsData} = mountOptions
|
||||
const wrapper = shallowMount(BField, {
|
||||
...mountOptions,
|
||||
propsData: {
|
||||
...propsData,
|
||||
label: 'Some label'
|
||||
}
|
||||
})
|
||||
expect(wrapper.find('.field').find('label').isVisible()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Passing true for horizontal prop', () => {
|
||||
const generateMountOptions = (props) => {
|
||||
return {
|
||||
propsData: {
|
||||
horizontal: true,
|
||||
...props
|
||||
},
|
||||
localVue,
|
||||
slots: {
|
||||
default: BField
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('adds "is-horizontal" to root div.field', () => {
|
||||
const mountOptions = generateMountOptions()
|
||||
const wrapper = shallowMount(BField, mountOptions)
|
||||
expect(wrapper.find('.field').classes()).toContain('is-horizontal')
|
||||
})
|
||||
|
||||
it('produces a div.field-label + .field-body', () => {
|
||||
const mountOptions = generateMountOptions()
|
||||
const wrapper = mount(BField, mountOptions)
|
||||
expect(wrapper.find('.field').find('.field-label').isVisible()).toBe(true)
|
||||
expect(wrapper.find('.field').find('.field-label + .field-body').isVisible()).toBe(true)
|
||||
})
|
||||
|
||||
it('adds a label element under .field-label when "label" prop is passed', () => {
|
||||
const mountOptions = generateMountOptions({label: 'Some label'})
|
||||
const wrapper = mount(BField, mountOptions)
|
||||
expect(wrapper.find('.field-label').find('label.label').isVisible()).toBe(true)
|
||||
})
|
||||
|
||||
it('adds a help <p> element in the div.field-body when "message" prop is passed', () => {
|
||||
const message = 'Some message'
|
||||
const mountOptions = generateMountOptions({message})
|
||||
const wrapper = mount(BField, mountOptions)
|
||||
const helpWrapper = wrapper.find('.field').find('.field-body').find('.help')
|
||||
expect(helpWrapper.isVisible()).toBe(true)
|
||||
expect(helpWrapper.text()).toEqual(message)
|
||||
})
|
||||
})
|
||||
})
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<div class="field" :class="[rootClasses, fieldType()]">
|
||||
<div
|
||||
v-if="horizontal"
|
||||
class="field-label"
|
||||
:class="[customClass, fieldLabelSize]">
|
||||
<label
|
||||
v-if="label"
|
||||
:for="labelFor"
|
||||
:class="customClass"
|
||||
class="label" >
|
||||
{{ label }}
|
||||
</label>
|
||||
</div>
|
||||
<template v-else>
|
||||
<label
|
||||
v-if="label"
|
||||
:for="labelFor"
|
||||
:class="customClass"
|
||||
class="label">
|
||||
{{ label }}
|
||||
</label>
|
||||
</template>
|
||||
<b-field-body
|
||||
v-if="horizontal"
|
||||
:message="newMessage ? formattedMessage : ''"
|
||||
:type="newType">
|
||||
<slot/>
|
||||
</b-field-body>
|
||||
<template v-else>
|
||||
<slot/>
|
||||
</template>
|
||||
<p
|
||||
v-if="newMessage && !horizontal"
|
||||
v-html="formattedMessage"
|
||||
class="help"
|
||||
:class="newType"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FieldBody from './FieldBody'
|
||||
|
||||
export default {
|
||||
name: 'BField',
|
||||
components: {
|
||||
[FieldBody.name]: FieldBody
|
||||
},
|
||||
props: {
|
||||
type: [String, Object],
|
||||
label: String,
|
||||
labelFor: String,
|
||||
message: [String, Array, Object],
|
||||
grouped: Boolean,
|
||||
groupMultiline: Boolean,
|
||||
position: String,
|
||||
expanded: Boolean,
|
||||
horizontal: Boolean,
|
||||
addons: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
customClass: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newType: this.type,
|
||||
newMessage: this.message,
|
||||
fieldLabelSize: null,
|
||||
_isField: true // Used internally by Input and Select
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
rootClasses() {
|
||||
return [this.newPosition, {
|
||||
'is-expanded': this.expanded,
|
||||
'is-grouped-multiline': this.groupMultiline,
|
||||
'is-horizontal': this.horizontal
|
||||
}]
|
||||
},
|
||||
/**
|
||||
* Correct Bulma class for the side of the addon or group.
|
||||
*
|
||||
* This is not kept like the others (is-small, etc.),
|
||||
* because since 'has-addons' is set automatically it
|
||||
* doesn't make sense to teach users what addons are exactly.
|
||||
*/
|
||||
newPosition() {
|
||||
if (this.position === undefined) return
|
||||
|
||||
const position = this.position.split('-')
|
||||
if (position.length < 1) return
|
||||
|
||||
const prefix = this.grouped
|
||||
? 'is-grouped-'
|
||||
: 'has-addons-'
|
||||
|
||||
if (this.position) return prefix + position[1]
|
||||
},
|
||||
/**
|
||||
* Formatted message in case it's an array
|
||||
* (each element is separated by <br> tag)
|
||||
*/
|
||||
formattedMessage() {
|
||||
if (typeof this.newMessage === 'string') {
|
||||
return this.newMessage
|
||||
} else {
|
||||
let messages = []
|
||||
if (Array.isArray(this.newMessage)) {
|
||||
this.newMessage.forEach((message) => {
|
||||
if (typeof message === 'string') {
|
||||
messages.push(message)
|
||||
} else {
|
||||
for (let key in message) {
|
||||
if (message[key]) {
|
||||
messages.push(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
for (let key in this.newMessage) {
|
||||
if (this.newMessage[key]) {
|
||||
messages.push(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
return messages.filter((m) => { if (m) return m }).join(' <br> ')
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* Set internal type when prop change.
|
||||
*/
|
||||
type(value) {
|
||||
this.newType = value
|
||||
},
|
||||
|
||||
/**
|
||||
* Set internal message when prop change.
|
||||
*/
|
||||
message(value) {
|
||||
this.newMessage = value
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Field has addons if there are more than one slot
|
||||
* (element / component) in the Field.
|
||||
* Or is grouped when prop is set.
|
||||
* Is a method to be called when component re-render.
|
||||
*/
|
||||
fieldType() {
|
||||
if (this.grouped) return 'is-grouped'
|
||||
|
||||
let renderedNode = 0
|
||||
if (this.$slots.default) {
|
||||
renderedNode = this.$slots.default.reduce((i, node) => node.tag ? i + 1 : i, 0)
|
||||
}
|
||||
if (
|
||||
renderedNode > 1 &&
|
||||
this.addons &&
|
||||
!this.horizontal
|
||||
) {
|
||||
return 'has-addons'
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.horizontal) {
|
||||
// Bulma docs: .is-normal for any .input or .button
|
||||
const elements = this.$el.querySelectorAll('.input, .select, .button, .textarea')
|
||||
if (elements.length > 0) {
|
||||
this.fieldLabelSize = 'is-normal'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { shallowMount, mount, createLocalVue } from '@vue/test-utils'
|
||||
import BFieldBody from '@components/field/FieldBody'
|
||||
import BField from '@components/field/Field'
|
||||
import BInput from '@components/input/Input'
|
||||
|
||||
const localVue = createLocalVue()
|
||||
localVue.component('b-field', BField)
|
||||
|
||||
describe('BFieldBody', () => {
|
||||
it('is called', () => {
|
||||
const wrapper = shallowMount(BFieldBody, {
|
||||
slots: {
|
||||
default: 'content'
|
||||
}
|
||||
})
|
||||
expect(wrapper.name()).toBe('BFieldBody')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should render the text when the child is a text', () => {
|
||||
const text = 'text'
|
||||
const wrapper = shallowMount(BFieldBody, {
|
||||
slots: {
|
||||
default: text
|
||||
}
|
||||
})
|
||||
expect(wrapper.text()).toBe(text)
|
||||
})
|
||||
|
||||
describe('when child is an element', () => {
|
||||
it('should wrap the element in a b-field', () => {
|
||||
const wrapper = mount(BFieldBody, {
|
||||
slots: {
|
||||
default: BInput
|
||||
},
|
||||
localVue
|
||||
})
|
||||
expect(wrapper.exists(BField)).toBe(true)
|
||||
})
|
||||
|
||||
it('should pass type prop to the b-field element', () => {
|
||||
const type = 'is-success'
|
||||
const wrapper = mount(BFieldBody, {
|
||||
slots: {
|
||||
default: BInput
|
||||
},
|
||||
propsData: {
|
||||
type
|
||||
},
|
||||
localVue
|
||||
})
|
||||
expect(wrapper.find(BField).find('input').classes()).toContain(type)
|
||||
})
|
||||
|
||||
it('should render the message when message prop is passed', () => {
|
||||
const message = 'some message yo'
|
||||
const type = 'is-danger'
|
||||
const wrapper = mount(BFieldBody, {
|
||||
slots: {
|
||||
default: BInput
|
||||
},
|
||||
propsData: {
|
||||
type,
|
||||
message
|
||||
},
|
||||
localVue
|
||||
})
|
||||
expect(wrapper.find('p.help').classes()).toContain(type)
|
||||
expect(wrapper.find('p.help').text()).toBe(message)
|
||||
})
|
||||
})
|
||||
})
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'BFieldBody',
|
||||
props: {
|
||||
message: {
|
||||
type: String
|
||||
},
|
||||
type: {
|
||||
type: [String, Object]
|
||||
}
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('div', { attrs: { 'class': 'field-body' } }, this.$slots.default.map((element) => {
|
||||
// skip returns and comments
|
||||
if (!element.tag) {
|
||||
return element
|
||||
}
|
||||
if (this.message) {
|
||||
return createElement('b-field', { attrs: { message: this.message, 'type': this.type } }, [element])
|
||||
}
|
||||
return createElement('b-field', { attrs: { 'type': this.type } }, [element])
|
||||
}))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import Field from './Field'
|
||||
|
||||
import { use, registerComponent } from '../../utils/plugins'
|
||||
|
||||
const Plugin = {
|
||||
install(Vue) {
|
||||
registerComponent(Vue, Field)
|
||||
}
|
||||
}
|
||||
|
||||
use(Plugin)
|
||||
|
||||
export default Plugin
|
||||
|
||||
export {
|
||||
Field
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import BIcon from '@components/icon/Icon'
|
||||
|
||||
describe('BIcon', () => {
|
||||
it('render correctly', () => {
|
||||
const wrapper = shallowMount(BIcon)
|
||||
|
||||
expect(wrapper.html()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('is vue instance', () => {
|
||||
const wrapper = shallowMount(BIcon)
|
||||
|
||||
expect(wrapper.name()).toBe('BIcon')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('render icon when icon property is passed', () => {
|
||||
const wrapper = shallowMount(BIcon, {
|
||||
propsData: { icon: 'eye' }
|
||||
})
|
||||
|
||||
expect(wrapper.classes()).toContain('icon')
|
||||
expect(wrapper.find('i').classes()).toContain('mdi', 'mdi-eye', 'mdi-24px')
|
||||
})
|
||||
|
||||
it('render a colored icon when type is passed', () => {
|
||||
const wrapper = shallowMount(BIcon, {
|
||||
propsData: {
|
||||
icon: 'eye',
|
||||
type: 'is-primary'
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.classes()).toContain('has-text-primary')
|
||||
})
|
||||
|
||||
it('render icon package correctly when the pack property is is passed.', () => {
|
||||
const wrapper = shallowMount(BIcon, {
|
||||
propsData: {
|
||||
icon: 'eye',
|
||||
pack: 'fa'
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('i').classes()).toContain('fa-eye')
|
||||
})
|
||||
|
||||
it('use both packages when the both property is is passed', () => {
|
||||
const wrapper = shallowMount(BIcon, {
|
||||
propsData: {
|
||||
icon: 'eye',
|
||||
both: true
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('i').classes()).toContain('mdi-eye')
|
||||
wrapper.setProps({ pack: 'fa' })
|
||||
|
||||
const equivalentIcons = {
|
||||
'check': 'check',
|
||||
'information': 'info-circle',
|
||||
'check-circle': 'check-circle',
|
||||
'alert': 'exclamation-triangle',
|
||||
'alert-circle': 'exclamation-circle',
|
||||
'arrow-up': 'arrow-up',
|
||||
'chevron-right': 'angle-right',
|
||||
'chevron-left': 'angle-left',
|
||||
'chevron-down': 'angle-down',
|
||||
'eye': 'eye',
|
||||
'eye-off': 'eye-slash',
|
||||
'menu-down': 'caret-down',
|
||||
'menu-up': 'caret-up',
|
||||
'other': 'other'
|
||||
}
|
||||
|
||||
const expectEquivalentIcon = (icon, expected) => {
|
||||
wrapper.setProps({ icon })
|
||||
expect(wrapper.find('i').classes()).toContain(`fa-${expected}`)
|
||||
}
|
||||
|
||||
Object.keys(equivalentIcons).forEach((icon) => {
|
||||
expectEquivalentIcon(icon, equivalentIcons[icon])
|
||||
})
|
||||
})
|
||||
|
||||
it('display size when size propery is passed', () => {
|
||||
const wrapper = shallowMount(BIcon, {
|
||||
propsData: {
|
||||
icon: 'eye'
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('i').classes()).toContain('mdi-24px')
|
||||
wrapper.setProps({ size: 'is-small' })
|
||||
expect(wrapper.find('i').classes()).toContainEqual('mdi', 'mdi-eye')
|
||||
wrapper.setProps({ size: 'is-medium' })
|
||||
expect(wrapper.find('i').classes()).toContain('mdi-36px')
|
||||
wrapper.setProps({ size: 'is-large' })
|
||||
expect(wrapper.find('i').classes()).toContain('mdi-48px')
|
||||
})
|
||||
|
||||
it('overrides icon font size when customSize property is passed', () => {
|
||||
const wrapper = shallowMount(BIcon, {
|
||||
propsData: {
|
||||
icon: 'eye',
|
||||
pack: 'fa',
|
||||
customSize: 'fa-2x'
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('i').classes()).toContainEqual('fa', 'fa-2x')
|
||||
})
|
||||
|
||||
it('render custom classes when customClass property is passed', () => {
|
||||
const wrapper = shallowMount(BIcon, {
|
||||
propsData: {
|
||||
icon: 'eye',
|
||||
customClass: 'foo-bar'
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('i').classes()).toContain('foo-bar')
|
||||
})
|
||||
})
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<span class="icon" :class="[newType, size]">
|
||||
<i
|
||||
v-if="!useIconComponent"
|
||||
:class="[newPack, newIcon, newCustomSize, customClass]"/>
|
||||
|
||||
<component
|
||||
v-else
|
||||
:is="useIconComponent"
|
||||
:icon="[newPack, newIcon]"
|
||||
:size="newCustomSize"
|
||||
:class="[customClass]"/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import config from '../../utils/config'
|
||||
|
||||
export default {
|
||||
name: 'BIcon',
|
||||
props: {
|
||||
type: [String, Object],
|
||||
pack: String,
|
||||
icon: String,
|
||||
size: String,
|
||||
customSize: String,
|
||||
customClass: String,
|
||||
both: Boolean // This is used internally to show both MDI and FA icon
|
||||
},
|
||||
computed: {
|
||||
/**
|
||||
* Internal icon name based on the pack.
|
||||
* If pack is 'fa', gets the equivalent FA icon name of the MDI,
|
||||
* internal icons are always MDI.
|
||||
*/
|
||||
newIcon() {
|
||||
return this.newPack === 'mdi'
|
||||
? `${this.newPack}-${this.icon}`
|
||||
: this.addFAPrefix(this.getEquivalentIconOf(this.icon))
|
||||
},
|
||||
newPack() {
|
||||
return this.pack || config.defaultIconPack
|
||||
},
|
||||
newType() {
|
||||
if (!this.type) return
|
||||
|
||||
let splitType = []
|
||||
if (typeof this.type === 'string') {
|
||||
splitType = this.type.split('-')
|
||||
} else {
|
||||
for (let key in this.type) {
|
||||
if (this.type[key]) {
|
||||
splitType = key.split('-')
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if (splitType.length <= 1) return
|
||||
|
||||
return `has-text-${splitType[1]}`
|
||||
},
|
||||
newCustomSize() {
|
||||
return this.customSize || this.customSizeByPack
|
||||
},
|
||||
customSizeByPack() {
|
||||
const defaultSize = this.newPack === 'mdi'
|
||||
? 'mdi-24px'
|
||||
: this.addFAPrefix('lg')
|
||||
const mediumSize = this.newPack === 'mdi'
|
||||
? 'mdi-36px'
|
||||
: this.addFAPrefix('2x')
|
||||
const largeSize = this.newPack === 'mdi'
|
||||
? 'mdi-48px'
|
||||
: this.addFAPrefix('3x')
|
||||
switch (this.size) {
|
||||
case 'is-small': return
|
||||
case 'is-medium': return mediumSize
|
||||
case 'is-large': return largeSize
|
||||
default: return defaultSize
|
||||
}
|
||||
},
|
||||
useIconComponent() {
|
||||
return config.defaultIconComponent
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addFAPrefix(value) {
|
||||
if (this.useIconComponent) {
|
||||
return value
|
||||
}
|
||||
return `fa-${value}`
|
||||
},
|
||||
|
||||
/**
|
||||
* Equivalent FA icon name of the MDI.
|
||||
*/
|
||||
getEquivalentIconOf(value) {
|
||||
// Only transform the class if the both prop is set to true
|
||||
if (!this.both) {
|
||||
return value
|
||||
}
|
||||
|
||||
switch (value) {
|
||||
case 'check': return 'check'
|
||||
case 'information': return 'info-circle'
|
||||
case 'check-circle': return 'check-circle'
|
||||
case 'alert': return 'exclamation-triangle'
|
||||
case 'alert-circle': return 'exclamation-circle'
|
||||
case 'arrow-up': return 'arrow-up'
|
||||
case 'chevron-right': return 'angle-right'
|
||||
case 'chevron-left': return 'angle-left'
|
||||
case 'chevron-down': return 'angle-down'
|
||||
case 'eye': return 'eye'
|
||||
case 'eye-off': return 'eye-slash'
|
||||
case 'menu-down': return 'caret-down'
|
||||
case 'menu-up': return 'caret-up'
|
||||
default: return value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BIcon render correctly 1`] = `<span class="icon"><i class="mdi mdi-undefined mdi-24px"></i></span>`;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import Icon from './Icon'
|
||||
|
||||
import { use, registerComponent } from '../../utils/plugins'
|
||||
|
||||
const Plugin = {
|
||||
install(Vue) {
|
||||
registerComponent(Vue, Icon)
|
||||
}
|
||||
}
|
||||
|
||||
use(Plugin)
|
||||
|
||||
export default Plugin
|
||||
|
||||
export {
|
||||
Icon
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import Autocomplete from './autocomplete'
|
||||
import Button from './button'
|
||||
import Checkbox from './checkbox'
|
||||
import Collapse from './collapse'
|
||||
import Clockpicker from './clockpicker'
|
||||
import Datepicker from './datepicker'
|
||||
import Dialog from './dialog'
|
||||
import Dropdown from './dropdown'
|
||||
import Field from './field'
|
||||
import Icon from './icon'
|
||||
import Input from './input'
|
||||
import Loading from './loading'
|
||||
import Message from './message'
|
||||
import Modal from './modal'
|
||||
import Notification from './notification'
|
||||
import Numberinput from './numberinput'
|
||||
import Pagination from './pagination'
|
||||
import Radio from './radio'
|
||||
import Select from './select'
|
||||
import Snackbar from './snackbar'
|
||||
import Switch from './switch'
|
||||
import Table from './table'
|
||||
import Tabs from './tabs'
|
||||
import Tag from './tag'
|
||||
import Taginput from './taginput'
|
||||
import Timepicker from './timepicker'
|
||||
import Toast from './toast'
|
||||
import Tooltip from './tooltip'
|
||||
import Upload from './upload'
|
||||
|
||||
export {
|
||||
Autocomplete,
|
||||
Button,
|
||||
Checkbox,
|
||||
Clockpicker,
|
||||
Collapse,
|
||||
Datepicker,
|
||||
Dialog,
|
||||
Dropdown,
|
||||
Field,
|
||||
Icon,
|
||||
Input,
|
||||
Loading,
|
||||
Message,
|
||||
Modal,
|
||||
Notification,
|
||||
Numberinput,
|
||||
Pagination,
|
||||
Radio,
|
||||
Select,
|
||||
Snackbar,
|
||||
Switch,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
Taginput,
|
||||
Timepicker,
|
||||
Toast,
|
||||
Tooltip,
|
||||
Upload
|
||||
}
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
import { shallowMount, mount } from '@vue/test-utils'
|
||||
import BInput from '@components/input/Input'
|
||||
import BIcon from '@components/icon/Icon'
|
||||
|
||||
describe('BInput', () => {
|
||||
it('render correctly', () => {
|
||||
const wrapper = shallowMount(BInput)
|
||||
|
||||
expect(wrapper.html()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('is vue instance', () => {
|
||||
const wrapper = shallowMount(BInput)
|
||||
|
||||
expect(wrapper.name()).toBe('BInput')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders input element by default', () => {
|
||||
const wrapper = shallowMount(BInput)
|
||||
|
||||
expect(wrapper.contains('input')).toBeTruthy()
|
||||
expect(wrapper.classes()).toContain('control')
|
||||
})
|
||||
|
||||
it('render textarea element when type is textarea', () => {
|
||||
const wrapper = shallowMount(BInput, {
|
||||
propsData: { type: 'textarea' }
|
||||
})
|
||||
const target = wrapper.find('textarea')
|
||||
|
||||
expect(target.exists()).toBeTruthy()
|
||||
expect(target.classes()).toContain('textarea')
|
||||
})
|
||||
|
||||
it('displays the icon when the icon property is true', () => {
|
||||
const wrapper = shallowMount(BInput, {
|
||||
propsData: { icon: 'magnify' }
|
||||
})
|
||||
const target = wrapper.find(BIcon)
|
||||
|
||||
expect(target.exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('display counter when the maxlength property is passed', () => {
|
||||
const wrapper = shallowMount(BInput, {
|
||||
propsData: { value: 'foo', maxlength: 100 }
|
||||
})
|
||||
const counter = wrapper.find('small.counter')
|
||||
|
||||
expect(counter.exists()).toBeTruthy()
|
||||
expect(counter.text()).toBe('3 / 100')
|
||||
})
|
||||
|
||||
it('no display counter when hasCounter property set for false', () => {
|
||||
const wrapper = shallowMount(BInput, {
|
||||
propsData: { maxlength: 100 }
|
||||
})
|
||||
expect(wrapper.find('small.counter').exists()).toBeTruthy()
|
||||
|
||||
wrapper.setProps({ hasCounter: false })
|
||||
expect(wrapper.find('small.counter').exists()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('render field password when the type property is password', () => {
|
||||
const wrapper = shallowMount(BInput, {
|
||||
propsData: {
|
||||
type: 'password',
|
||||
passwordReveal: true
|
||||
}
|
||||
})
|
||||
|
||||
const target = wrapper.find('input')
|
||||
expect(target.exists()).toBeTruthy()
|
||||
expect(target.attributes().type).toBe('password')
|
||||
})
|
||||
|
||||
it('toggles the visibility of the password to true when the togglePasswordVisibility method is called', (done) => {
|
||||
const wrapper = mount(BInput, {
|
||||
propsData: {
|
||||
value: 'foo',
|
||||
type: 'password',
|
||||
passwordReveal: true
|
||||
}
|
||||
})
|
||||
|
||||
wrapper.setProps({ value: 'bar' })
|
||||
|
||||
expect(wrapper.find('input').exists()).toBeTruthy()
|
||||
expect(wrapper.find('input').attributes().type).toBe('password')
|
||||
|
||||
const visibilityIcon = wrapper.find('.icon.is-clickable')
|
||||
expect(visibilityIcon.exists()).toBeTruthy()
|
||||
visibilityIcon.trigger('click')
|
||||
wrapper.setProps({ passwordReveal: false })
|
||||
expect(wrapper.find('input').attributes().type).toBe('text')
|
||||
|
||||
wrapper.vm.$nextTick(done)
|
||||
})
|
||||
|
||||
it('render the placeholder and readonly attribute when passed', () => {
|
||||
const wrapper = shallowMount(BInput, {
|
||||
attrs: { placeholder: 'Awesome!', readonly: true }
|
||||
})
|
||||
const target = wrapper.find('input')
|
||||
|
||||
expect(target.element.getAttribute('placeholder')).toBe('Awesome!')
|
||||
expect(target.element.getAttribute('readonly')).toBe('readonly')
|
||||
})
|
||||
|
||||
it('expands input when expanded property is passed', () => {
|
||||
const wrapper = shallowMount(BInput, {
|
||||
propsData: { expanded: true }
|
||||
})
|
||||
|
||||
expect(wrapper.classes()).toContain('is-expanded')
|
||||
})
|
||||
|
||||
it('display loading icon when loading property passed', () => {
|
||||
const wrapper = shallowMount(BInput, {
|
||||
propsData: { loading: true, icon: 'magnify' }
|
||||
})
|
||||
|
||||
expect(wrapper.classes()).toContain('is-loading')
|
||||
})
|
||||
|
||||
it('keep its value on blur', async () => {
|
||||
const wrapper = mount(BInput, {
|
||||
propsData: {
|
||||
value: 'foo'
|
||||
},
|
||||
methods: {
|
||||
checkHtml5Validity: () => true
|
||||
}
|
||||
})
|
||||
|
||||
const input = wrapper.find('input')
|
||||
|
||||
input.element.value = 'bar'
|
||||
input.trigger('input')
|
||||
input.trigger('blur')
|
||||
|
||||
expect(input.element.value).toBe('bar')
|
||||
})
|
||||
|
||||
it('change status icon when statusType updated', () => {
|
||||
const parent = {
|
||||
data: () => ({
|
||||
newType: 'is-success',
|
||||
_isField: true
|
||||
}),
|
||||
components: {BInput},
|
||||
template: `<b-input />`
|
||||
}
|
||||
const wrapper = mount(parent)
|
||||
|
||||
const input = wrapper.find(BInput)
|
||||
expect(input.vm.statusTypeIcon).toBe('check')
|
||||
wrapper.setData({ newType: 'is-danger' })
|
||||
expect(input.vm.statusTypeIcon).toBe('alert-circle')
|
||||
wrapper.setData({ newType: 'is-info' })
|
||||
expect(input.vm.statusTypeIcon).toBe('information')
|
||||
wrapper.setData({ newType: 'is-warning' })
|
||||
expect(input.vm.statusTypeIcon).toBe('alert')
|
||||
})
|
||||
})
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div class="control" :class="rootClasses">
|
||||
<input
|
||||
v-if="type !== 'textarea'"
|
||||
ref="input"
|
||||
class="input"
|
||||
:class="[inputClasses, customClass]"
|
||||
:type="newType"
|
||||
:autocomplete="newAutocomplete"
|
||||
:maxlength="maxlength"
|
||||
:value="computedValue"
|
||||
v-bind="$attrs"
|
||||
@input="onInput"
|
||||
@blur="onBlur"
|
||||
@focus="onFocus">
|
||||
|
||||
<textarea
|
||||
v-else
|
||||
ref="textarea"
|
||||
class="textarea"
|
||||
:class="[inputClasses, customClass]"
|
||||
:maxlength="maxlength"
|
||||
:value="computedValue"
|
||||
v-bind="$attrs"
|
||||
@input="onInput"
|
||||
@blur="onBlur"
|
||||
@focus="onFocus"/>
|
||||
|
||||
<b-icon
|
||||
v-if="icon"
|
||||
class="is-left"
|
||||
:icon="icon"
|
||||
:pack="iconPack"
|
||||
:size="iconSize"/>
|
||||
|
||||
<b-icon
|
||||
v-if="!loading && (passwordReveal || statusType)"
|
||||
class="is-right"
|
||||
:class="{ 'is-clickable': passwordReveal }"
|
||||
:icon="passwordReveal ? passwordVisibleIcon : statusTypeIcon"
|
||||
:pack="iconPack"
|
||||
:size="iconSize"
|
||||
:type="!passwordReveal ? statusType : 'is-primary'"
|
||||
both
|
||||
@click.native="togglePasswordVisibility"/>
|
||||
|
||||
<small
|
||||
v-if="maxlength && hasCounter && type !== 'number'"
|
||||
class="help counter"
|
||||
:class="{ 'is-invisible': !isFocused }">
|
||||
{{ valueLength }} / {{ maxlength }}
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Icon from '../icon/Icon'
|
||||
import config from '../../utils/config'
|
||||
import FormElementMixin from '../../utils/FormElementMixin'
|
||||
|
||||
export default {
|
||||
name: 'BInput',
|
||||
components: {
|
||||
[Icon.name]: Icon
|
||||
},
|
||||
mixins: [FormElementMixin],
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
value: [Number, String],
|
||||
type: {
|
||||
type: String,
|
||||
default: 'text'
|
||||
},
|
||||
passwordReveal: Boolean,
|
||||
hasCounter: {
|
||||
type: Boolean,
|
||||
default: () => config.defaultInputHasCounter
|
||||
},
|
||||
customClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newValue: this.value,
|
||||
newType: this.type,
|
||||
newAutocomplete: this.autocomplete || config.defaultInputAutocomplete,
|
||||
isPasswordVisible: false,
|
||||
_elementRef: this.type === 'textarea'
|
||||
? 'textarea'
|
||||
: 'input'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedValue: {
|
||||
get() {
|
||||
return this.newValue
|
||||
},
|
||||
set(value) {
|
||||
this.newValue = value
|
||||
this.$emit('input', value)
|
||||
!this.isValid && this.checkHtml5Validity()
|
||||
}
|
||||
},
|
||||
rootClasses() {
|
||||
return [
|
||||
this.iconPosition,
|
||||
this.size,
|
||||
{
|
||||
'is-expanded': this.expanded,
|
||||
'is-loading': this.loading,
|
||||
'is-clearfix': !this.hasMessage
|
||||
}
|
||||
]
|
||||
},
|
||||
inputClasses() {
|
||||
return [
|
||||
this.statusType,
|
||||
this.size,
|
||||
{ 'is-rounded': this.rounded }
|
||||
]
|
||||
},
|
||||
hasIconRight() {
|
||||
return this.passwordReveal || this.loading || this.statusType
|
||||
},
|
||||
|
||||
/**
|
||||
* Position of the icon or if it's both sides.
|
||||
*/
|
||||
iconPosition() {
|
||||
if (this.icon && this.hasIconRight) {
|
||||
return 'has-icons-left has-icons-right'
|
||||
} else if (!this.icon && this.hasIconRight) {
|
||||
return 'has-icons-right'
|
||||
} else if (this.icon) {
|
||||
return 'has-icons-left'
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Icon name (MDI) based on the type.
|
||||
*/
|
||||
statusTypeIcon() {
|
||||
switch (this.statusType) {
|
||||
case 'is-success': return 'check'
|
||||
case 'is-danger': return 'alert-circle'
|
||||
case 'is-info': return 'information'
|
||||
case 'is-warning': return 'alert'
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if have any message prop from parent if it's a Field.
|
||||
*/
|
||||
hasMessage() {
|
||||
return !!this.statusMessage
|
||||
},
|
||||
|
||||
/**
|
||||
* Current password-reveal icon name.
|
||||
*/
|
||||
passwordVisibleIcon() {
|
||||
return !this.isPasswordVisible ? 'eye' : 'eye-off'
|
||||
},
|
||||
/**
|
||||
* Get value length
|
||||
*/
|
||||
valueLength() {
|
||||
if (typeof this.computedValue === 'string') {
|
||||
return this.computedValue.length
|
||||
} else if (typeof this.computedValue === 'number') {
|
||||
return this.computedValue.toString().length
|
||||
}
|
||||
return 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* When v-model is changed:
|
||||
* 1. Set internal value.
|
||||
*/
|
||||
value(value) {
|
||||
this.newValue = value
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Toggle the visibility of a password-reveal input
|
||||
* by changing the type and focus the input right away.
|
||||
*/
|
||||
togglePasswordVisibility() {
|
||||
this.isPasswordVisible = !this.isPasswordVisible
|
||||
this.newType = this.isPasswordVisible ? 'text' : 'password'
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.input.focus()
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Input's 'input' event listener, 'nextTick' is used to prevent event firing
|
||||
* before ui update, helps when using masks (Cleavejs and potentially others).
|
||||
*/
|
||||
onInput(event) {
|
||||
this.$nextTick(() => {
|
||||
if (event.target) {
|
||||
this.computedValue = event.target.value
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BInput render correctly 1`] = `
|
||||
<div class="control is-clearfix"><input type="text" autocomplete="on" class="input">
|
||||
<!---->
|
||||
<!---->
|
||||
<!---->
|
||||
</div>
|
||||
`;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import Input from './Input'
|
||||
|
||||
import { use, registerComponent } from '../../utils/plugins'
|
||||
|
||||
const Plugin = {
|
||||
install(Vue) {
|
||||
registerComponent(Vue, Input)
|
||||
}
|
||||
}
|
||||
|
||||
use(Plugin)
|
||||
|
||||
export default Plugin
|
||||
|
||||
export {
|
||||
Input
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import BLoading from '@components/loading/Loading'
|
||||
|
||||
describe('BLoading', () => {
|
||||
it('Is called', () => {
|
||||
const wrapper = shallowMount(BLoading)
|
||||
expect(wrapper.name()).toBe('BLoading')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<transition :name="animation">
|
||||
<div
|
||||
class="loading-overlay is-active"
|
||||
:class="{ 'is-full-page': isFullPage }"
|
||||
v-if="isActive">
|
||||
<div class="loading-background" @click="cancel"/>
|
||||
<slot>
|
||||
<div class="loading-icon" />
|
||||
</slot>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { removeElement } from '../../utils/helpers'
|
||||
import { HTMLElement } from '../../utils/ssr'
|
||||
|
||||
export default {
|
||||
name: 'BLoading',
|
||||
props: {
|
||||
active: Boolean,
|
||||
programmatic: Boolean,
|
||||
container: [Object, Function, HTMLElement],
|
||||
isFullPage: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
animation: {
|
||||
type: String,
|
||||
default: 'fade'
|
||||
},
|
||||
canCancel: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
onCancel: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isActive: this.active || false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
active(value) {
|
||||
this.isActive = value
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Close the Modal if canCancel.
|
||||
*/
|
||||
cancel() {
|
||||
if (!this.canCancel || !this.isActive) return
|
||||
|
||||
this.close()
|
||||
},
|
||||
/**
|
||||
* Emit events, and destroy modal if it's programmatic.
|
||||
*/
|
||||
close() {
|
||||
this.onCancel.apply(null, arguments)
|
||||
this.$emit('close')
|
||||
this.$emit('update:active', false)
|
||||
|
||||
// Timeout for the animation complete before destroying
|
||||
if (this.programmatic) {
|
||||
this.isActive = false
|
||||
setTimeout(() => {
|
||||
this.$destroy()
|
||||
removeElement(this.$el)
|
||||
}, 150)
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Keypress event that is bound to the document.
|
||||
*/
|
||||
keyPress(event) {
|
||||
// Esc key
|
||||
if (event.keyCode === 27) this.cancel()
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (typeof window !== 'undefined') {
|
||||
document.addEventListener('keyup', this.keyPress)
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
// Insert the Loading component in body tag
|
||||
// only if it's programmatic
|
||||
if (this.programmatic) {
|
||||
if (!this.container) {
|
||||
document.body.appendChild(this.$el)
|
||||
} else {
|
||||
this.isFullPage = false
|
||||
this.container.appendChild(this.$el)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.programmatic) this.isActive = true
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (typeof window !== 'undefined') {
|
||||
document.removeEventListener('keyup', this.keyPress)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import Vue from 'vue'
|
||||
import Loading from './Loading'
|
||||
|
||||
import { use, registerComponent, registerComponentProgrammatic } from '../../utils/plugins'
|
||||
|
||||
const LoadingProgrammatic = {
|
||||
open(params) {
|
||||
const defaultParam = {
|
||||
programmatic: true
|
||||
}
|
||||
const propsData = Object.assign(defaultParam, params)
|
||||
|
||||
const vm = typeof window !== 'undefined' && window.Vue ? window.Vue : Vue
|
||||
const LoadingComponent = vm.extend(Loading)
|
||||
return new LoadingComponent({
|
||||
el: document.createElement('div'),
|
||||
propsData
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const Plugin = {
|
||||
install(Vue) {
|
||||
registerComponent(Vue, Loading)
|
||||
registerComponentProgrammatic(Vue, '$loading', LoadingProgrammatic)
|
||||
}
|
||||
}
|
||||
|
||||
use(Plugin)
|
||||
|
||||
export default Plugin
|
||||
|
||||
export {
|
||||
Loading,
|
||||
LoadingProgrammatic
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import BMessage from '@components/message/Message'
|
||||
|
||||
describe('BMessage', () => {
|
||||
it('is called', () => {
|
||||
const wrapper = shallowMount(BMessage)
|
||||
expect(wrapper.name()).toBe('BMessage')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<article
|
||||
v-if="isActive"
|
||||
class="message"
|
||||
:class="[type, size]">
|
||||
<header v-if="title" class="message-header">
|
||||
<p>{{ title }}</p>
|
||||
<button
|
||||
v-if="closable"
|
||||
type="button"
|
||||
class="delete"
|
||||
@click="close"
|
||||
:aria-label="ariaCloseLabel"
|
||||
/>
|
||||
</header>
|
||||
<section class="message-body">
|
||||
<div class="media">
|
||||
<div v-if="icon && hasIcon" class="media-left">
|
||||
<b-icon
|
||||
:icon="icon"
|
||||
:pack="iconPack"
|
||||
:class="type"
|
||||
both
|
||||
:size="newIconSize"/>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<slot/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</article>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MessageMixin from '../../utils/MessageMixin.js'
|
||||
|
||||
export default {
|
||||
name: 'BMessage',
|
||||
mixins: [MessageMixin],
|
||||
props: {
|
||||
ariaCloseLabel: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newIconSize: this.iconSize || this.size || 'is-large'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import Message from './Message'
|
||||
|
||||
import { use, registerComponent } from '../../utils/plugins'
|
||||
|
||||
const Plugin = {
|
||||
install(Vue) {
|
||||
registerComponent(Vue, Message)
|
||||
}
|
||||
}
|
||||
|
||||
use(Plugin)
|
||||
|
||||
export default Plugin
|
||||
|
||||
export {
|
||||
Message
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import BModal from '@components/modal/Modal'
|
||||
|
||||
describe('BModal', () => {
|
||||
it('is called', () => {
|
||||
const wrapper = shallowMount(BModal)
|
||||
expect(wrapper.name()).toBe('BModal')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<transition :name="animation">
|
||||
<div v-if="isActive" class="modal is-active">
|
||||
<div class="modal-background" @click="cancel('outside')"/>
|
||||
<div
|
||||
class="animation-content"
|
||||
:class="{ 'modal-content': !hasModalCard }"
|
||||
:style="{ maxWidth: newWidth }">
|
||||
<component
|
||||
v-if="component"
|
||||
v-bind="props"
|
||||
v-on="events"
|
||||
:is="component"
|
||||
@close="close"/>
|
||||
<div
|
||||
v-else-if="content"
|
||||
v-html="content"/>
|
||||
<slot v-else/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
v-if="showX"
|
||||
class="modal-close is-large"
|
||||
@click="cancel('x')"/>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { removeElement } from '../../utils/helpers'
|
||||
import config from '../../utils/config'
|
||||
|
||||
export default {
|
||||
name: 'BModal',
|
||||
props: {
|
||||
active: Boolean,
|
||||
component: [Object, Function],
|
||||
content: String,
|
||||
programmatic: Boolean,
|
||||
props: Object,
|
||||
events: Object,
|
||||
width: {
|
||||
type: [String, Number],
|
||||
default: 960
|
||||
},
|
||||
hasModalCard: Boolean,
|
||||
animation: {
|
||||
type: String,
|
||||
default: 'zoom-out'
|
||||
},
|
||||
canCancel: {
|
||||
type: [Array, Boolean],
|
||||
default: () => {
|
||||
return config.defaultModalCanCancel
|
||||
? config.defaultModalCanCancel
|
||||
: ['escape', 'x', 'outside', 'button']
|
||||
}
|
||||
},
|
||||
onCancel: {
|
||||
type: Function,
|
||||
default: () => {}
|
||||
},
|
||||
scroll: {
|
||||
type: String,
|
||||
default: () => {
|
||||
return config.defaultModalScroll
|
||||
? config.defaultModalScroll
|
||||
: 'clip'
|
||||
},
|
||||
validator: (value) => {
|
||||
return [
|
||||
'clip',
|
||||
'keep'
|
||||
].indexOf(value) >= 0
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isActive: this.active || false,
|
||||
savedScrollTop: null,
|
||||
newWidth: typeof this.width === 'number'
|
||||
? this.width + 'px'
|
||||
: this.width
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
cancelOptions() {
|
||||
return typeof this.canCancel === 'boolean'
|
||||
? this.canCancel
|
||||
? ['escape', 'x', 'outside', 'button']
|
||||
: []
|
||||
: this.canCancel
|
||||
},
|
||||
showX() {
|
||||
return this.cancelOptions.indexOf('x') >= 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
active(value) {
|
||||
this.isActive = value
|
||||
},
|
||||
isActive() {
|
||||
this.handleScroll()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleScroll() {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
if (this.scroll === 'clip') {
|
||||
if (this.isActive) {
|
||||
document.documentElement.classList.add('is-clipped')
|
||||
} else {
|
||||
document.documentElement.classList.remove('is-clipped')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
this.savedScrollTop = !this.savedScrollTop
|
||||
? document.documentElement.scrollTop
|
||||
: this.savedScrollTop
|
||||
|
||||
if (this.isActive) {
|
||||
document.body.classList.add('is-noscroll')
|
||||
} else {
|
||||
document.body.classList.remove('is-noscroll')
|
||||
}
|
||||
|
||||
if (this.isActive) {
|
||||
document.body.style.top = `-${this.savedScrollTop}px`
|
||||
return
|
||||
}
|
||||
|
||||
document.documentElement.scrollTop = this.savedScrollTop
|
||||
document.body.style.top = null
|
||||
this.savedScrollTop = null
|
||||
},
|
||||
|
||||
/**
|
||||
* Close the Modal if canCancel and call the onCancel prop (function).
|
||||
*/
|
||||
cancel(method) {
|
||||
if (this.cancelOptions.indexOf(method) < 0) return
|
||||
|
||||
this.onCancel.apply(null, arguments)
|
||||
this.close()
|
||||
},
|
||||
|
||||
/**
|
||||
* Call the onCancel prop (function).
|
||||
* Emit events, and destroy modal if it's programmatic.
|
||||
*/
|
||||
close() {
|
||||
this.$emit('close')
|
||||
this.$emit('update:active', false)
|
||||
|
||||
// Timeout for the animation complete before destroying
|
||||
if (this.programmatic) {
|
||||
this.isActive = false
|
||||
setTimeout(() => {
|
||||
this.$destroy()
|
||||
removeElement(this.$el)
|
||||
}, 150)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Keypress event that is bound to the document.
|
||||
*/
|
||||
keyPress(event) {
|
||||
// Esc key
|
||||
if (this.isActive && event.keyCode === 27) this.cancel('escape')
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (typeof window !== 'undefined') {
|
||||
document.addEventListener('keyup', this.keyPress)
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
// Insert the Modal component in body tag
|
||||
// only if it's programmatic
|
||||
this.programmatic && document.body.appendChild(this.$el)
|
||||
},
|
||||
mounted() {
|
||||
if (this.programmatic) this.isActive = true
|
||||
else if (this.isActive) this.handleScroll()
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (typeof window !== 'undefined') {
|
||||
document.removeEventListener('keyup', this.keyPress)
|
||||
// reset scroll
|
||||
document.documentElement.classList.remove('is-clipped')
|
||||
const savedScrollTop = !this.savedScrollTop
|
||||
? document.documentElement.scrollTop
|
||||
: this.savedScrollTop
|
||||
document.body.classList.remove('is-noscroll')
|
||||
document.documentElement.scrollTop = savedScrollTop
|
||||
document.body.style.top = null
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
import Vue from 'vue'
|
||||
import Modal from './Modal'
|
||||
|
||||
import { use, registerComponent, registerComponentProgrammatic } from '../../utils/plugins'
|
||||
|
||||
const ModalProgrammatic = {
|
||||
open(params) {
|
||||
let content
|
||||
let parent
|
||||
if (typeof params === 'string') content = params
|
||||
|
||||
const defaultParam = {
|
||||
programmatic: true,
|
||||
content
|
||||
}
|
||||
if (params.parent) {
|
||||
parent = params.parent
|
||||
delete params.parent
|
||||
}
|
||||
const propsData = Object.assign(defaultParam, params)
|
||||
|
||||
const vm = typeof window !== 'undefined' && window.Vue ? window.Vue : Vue
|
||||
const ModalComponent = vm.extend(Modal)
|
||||
return new ModalComponent({
|
||||
parent,
|
||||
el: document.createElement('div'),
|
||||
propsData
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const Plugin = {
|
||||
install(Vue) {
|
||||
registerComponent(Vue, Modal)
|
||||
registerComponentProgrammatic(Vue, '$modal', ModalProgrammatic)
|
||||
}
|
||||
}
|
||||
|
||||
use(Plugin)
|
||||
|
||||
export default Plugin
|
||||
|
||||
export {
|
||||
Modal,
|
||||
ModalProgrammatic
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import BNotification from '@components/notification/Notification'
|
||||
|
||||
describe('BNotification', () => {
|
||||
it('is called', () => {
|
||||
const wrapper = shallowMount(BNotification)
|
||||
expect(wrapper.name()).toBe('BNotification')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<article
|
||||
v-show="isActive"
|
||||
class="notification"
|
||||
:class="[type, position]">
|
||||
<button
|
||||
v-if="closable"
|
||||
class="delete"
|
||||
type="button"
|
||||
@click="close"
|
||||
:aria-label="ariaCloseLabel"
|
||||
/>
|
||||
<div class="media">
|
||||
<div v-if="icon && hasIcon" class="media-left">
|
||||
<b-icon
|
||||
:icon="icon"
|
||||
:pack="iconPack"
|
||||
both
|
||||
size="is-large"
|
||||
aria-hidden/>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="text" v-if="message">{{ message }}</p>
|
||||
<slot v-else/>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MessageMixin from '../../utils/MessageMixin.js'
|
||||
|
||||
export default {
|
||||
name: 'BNotification',
|
||||
mixins: [MessageMixin],
|
||||
props: {
|
||||
position: String,
|
||||
ariaCloseLabel: String
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<b-notification
|
||||
v-bind="$options.propsData"
|
||||
@close="close" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import config from '../../utils/config'
|
||||
import NoticeMixin from '../../utils/NoticeMixin.js'
|
||||
|
||||
export default {
|
||||
name: 'BNotificationNotice',
|
||||
mixins: [NoticeMixin],
|
||||
props: {
|
||||
indefinite: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newDuration: this.duration || config.detaultNotificationDuration
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import Vue from 'vue'
|
||||
import Notification from './Notification'
|
||||
import NotificationNotice from './NotificationNotice'
|
||||
|
||||
import config from '../../utils/config'
|
||||
import { use, registerComponent, registerComponentProgrammatic } from '../../utils/plugins'
|
||||
|
||||
const NotificationProgrammatic = {
|
||||
open(params) {
|
||||
let message
|
||||
let parent
|
||||
if (typeof params === 'string') message = params
|
||||
|
||||
const defaultParam = {
|
||||
message,
|
||||
position: config.defaultNotificationPosition || 'is-top-right'
|
||||
}
|
||||
if (params.parent) {
|
||||
parent = params.parent
|
||||
delete params.parent
|
||||
}
|
||||
const propsData = Object.assign(defaultParam, typeof params === 'string' ? {} : params)
|
||||
|
||||
const vm = typeof window !== 'undefined' && window.Vue ? window.Vue : Vue
|
||||
const NotificationNoticeComponent = vm.extend(NotificationNotice)
|
||||
return new NotificationNoticeComponent({
|
||||
parent,
|
||||
el: document.createElement('div'),
|
||||
propsData
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const Plugin = {
|
||||
install(Vue) {
|
||||
registerComponent(Vue, Notification)
|
||||
registerComponentProgrammatic(Vue, '$notification', NotificationProgrammatic)
|
||||
}
|
||||
}
|
||||
|
||||
use(Plugin)
|
||||
|
||||
export default Plugin
|
||||
|
||||
export {
|
||||
Notification,
|
||||
NotificationProgrammatic
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { shallowMount, mount } from '@vue/test-utils'
|
||||
import BNumberinput from '@components/numberinput/Numberinput'
|
||||
|
||||
describe('BNumberinput', () => {
|
||||
it('is called', () => {
|
||||
const wrapper = shallowMount(BNumberinput)
|
||||
expect(wrapper.name()).toBe('BNumberinput')
|
||||
expect(wrapper.isVueInstance()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders by default', () => {
|
||||
const wrapper = mount(BNumberinput)
|
||||
|
||||
expect(wrapper.contains('input')).toBeTruthy()
|
||||
expect(wrapper.contains('button')).toBeTruthy()
|
||||
expect(wrapper.classes()).toContain('is-grouped')
|
||||
})
|
||||
|
||||
it('set controls position', () => {
|
||||
const wrapper = mount(BNumberinput, {
|
||||
propsData: {
|
||||
controlsPosition: 'compact'
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.contains('input')).toBeTruthy()
|
||||
expect(wrapper.contains('button')).toBeTruthy()
|
||||
expect(wrapper.classes()).toContain('has-addons')
|
||||
})
|
||||
|
||||
it('default value is 0', () => {
|
||||
const wrapper = mount(BNumberinput)
|
||||
|
||||
const input = wrapper.find('input')
|
||||
expect(input.element.value).toBe('0')
|
||||
})
|
||||
})
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<div class="b-numberinput field" :class="fieldClasses">
|
||||
<p class="control">
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
:class="buttonClasses"
|
||||
:disabled="disabled || disabledMin"
|
||||
@mousedown="onStartLongPress($event, false)"
|
||||
@mouseup="onStopLongPress(false)"
|
||||
@mouseleave="onStopLongPress(false)"
|
||||
@touchstart.prevent="onStartLongPress($event, false)"
|
||||
@touchend="onStopLongPress(false)"
|
||||
@touchcancel="onStopLongPress(false)"
|
||||
@click="onControlClick($event, false)">
|
||||
<b-icon
|
||||
icon="minus"
|
||||
:pack="iconPack"
|
||||
:size="iconSize"/>
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<b-input
|
||||
type="number"
|
||||
v-model.number="computedValue"
|
||||
v-bind="$attrs"
|
||||
:step="newStep"
|
||||
:max="max"
|
||||
:min="min"
|
||||
:size="size"
|
||||
:disabled="disabled"
|
||||
:readonly="!editable"
|
||||
:loading="loading"
|
||||
:rounded="rounded"
|
||||
:icon="icon"
|
||||
:icon-pack="iconPack"
|
||||
:autocomplete="autocomplete"
|
||||
:expanded="expanded"
|
||||
@focus="$emit('focus', $event)"
|
||||
@blur="$emit('blur', $event)" />
|
||||
|
||||
<p class="control">
|
||||
<button
|
||||
type="button"
|
||||
class="button"
|
||||
:class="buttonClasses"
|
||||
:disabled="disabled || disabledMax"
|
||||
@mousedown="onStartLongPress($event, true)"
|
||||
@mouseup="onStopLongPress(true)"
|
||||
@mouseleave="onStopLongPress(true)"
|
||||
@touchstart.prevent="onStartLongPress($event, true)"
|
||||
@touchend="onStopLongPress(true)"
|
||||
@touchcancel="onStopLongPress(true)"
|
||||
@click="onControlClick($event, true)">
|
||||
<b-icon
|
||||
icon="plus"
|
||||
:pack="iconPack"
|
||||
:size="iconSize"/>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Icon from '../icon/Icon'
|
||||
import Input from '../input/Input'
|
||||
import FormElementMixin from '../../utils/FormElementMixin'
|
||||
|
||||
export default {
|
||||
name: 'BNumberinput',
|
||||
components: {
|
||||
[Icon.name]: Icon,
|
||||
[Input.name]: Input
|
||||
},
|
||||
mixins: [FormElementMixin],
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
value: Number,
|
||||
min: [Number, String],
|
||||
max: [Number, String],
|
||||
step: [Number, String],
|
||||
disabled: Boolean,
|
||||
type: {
|
||||
type: String,
|
||||
default: 'is-primary'
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
controlsRounded: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
controlsPosition: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newValue: this.value || parseFloat(this.min) || 0,
|
||||
newStep: this.step || 1,
|
||||
_elementRef: 'input'
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
computedValue: {
|
||||
get() {
|
||||
return this.newValue
|
||||
},
|
||||
set(value) {
|
||||
this.newValue = value
|
||||
this.$emit('input', value)
|
||||
!this.isValid && this.checkHtml5Validity()
|
||||
}
|
||||
},
|
||||
fieldClasses() {
|
||||
return [
|
||||
{ 'has-addons': this.controlsPosition === 'compact' },
|
||||
{ 'is-grouped': this.controlsPosition !== 'compact' }
|
||||
]
|
||||
},
|
||||
buttonClasses() {
|
||||
return [
|
||||
this.type,
|
||||
this.size,
|
||||
{ 'is-rounded': this.controlsRounded }
|
||||
]
|
||||
},
|
||||
minNumber() {
|
||||
return typeof this.min === 'string'
|
||||
? parseFloat(this.min) : this.min
|
||||
},
|
||||
maxNumber() {
|
||||
return typeof this.max === 'string'
|
||||
? parseFloat(this.max) : this.max
|
||||
},
|
||||
stepNumber() {
|
||||
return typeof this.newStep === 'string'
|
||||
? parseFloat(this.newStep) : this.newStep
|
||||
},
|
||||
disabledMin() {
|
||||
return (this.computedValue - this.stepNumber) < this.minNumber
|
||||
},
|
||||
disabledMax() {
|
||||
return (this.computedValue + this.stepNumber) > this.maxNumber
|
||||
},
|
||||
stepDecimals() {
|
||||
const step = this.stepNumber.toString()
|
||||
const index = step.indexOf('.')
|
||||
if (index >= 0) {
|
||||
return step.substring(index + 1).length
|
||||
}
|
||||
return 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* When v-model is changed:
|
||||
* 1. Set internal value.
|
||||
*/
|
||||
value(value) {
|
||||
this.newValue = value
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
decrement() {
|
||||
if (typeof this.minNumber === 'undefined' ||
|
||||
(this.computedValue - this.stepNumber) >= this.minNumber) {
|
||||
const value = this.computedValue - this.stepNumber
|
||||
this.computedValue = parseFloat(value.toFixed(this.stepDecimals))
|
||||
}
|
||||
},
|
||||
increment() {
|
||||
if (typeof this.maxNumber === 'undefined' ||
|
||||
(this.computedValue + this.stepNumber) <= this.maxNumber) {
|
||||
const value = this.computedValue + this.stepNumber
|
||||
this.computedValue = parseFloat(value.toFixed(this.stepDecimals))
|
||||
}
|
||||
},
|
||||
onControlClick(event, inc) {
|
||||
if (event.detail !== 0) return
|
||||
if (inc) this.increment()
|
||||
else this.decrement()
|
||||
},
|
||||
onStartLongPress(event, inc) {
|
||||
if (event.button !== 0 && event.type !== 'touchstart') return
|
||||
this._$intervalTime = new Date()
|
||||
clearInterval(this._$intervalRef)
|
||||
this._$intervalRef = this._$intervalRef = setInterval(() => {
|
||||
if (inc) this.increment()
|
||||
else this.decrement()
|
||||
}, 100)
|
||||
},
|
||||
onStopLongPress(inc) {
|
||||
const d = new Date()
|
||||
if (d - this._$intervalTime < 100) {
|
||||
if (inc) this.increment()
|
||||
else this.decrement()
|
||||
}
|
||||
clearInterval(this._$intervalRef)
|
||||
this._$intervalRef = null
|
||||
},
|
||||
onChange(event) {
|
||||
const value = event.target.value
|
||||
if (value) {
|
||||
const number = parseFloat(value)
|
||||
if (number < this.minNumber) {
|
||||
this.computedValue = this.minNumber
|
||||
} else if (number > this.maxNumber) {
|
||||
this.computedValue = this.maxNumber
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user