node_modules updated, buefy add to lib folder

This commit is contained in:
2019-06-04 14:29:48 +02:00
parent 0a3f6ff240
commit 5d28bc36e1
3957 changed files with 306575 additions and 21738 deletions
+707
View File
@@ -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
Generated Vendored
+21
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+14166
View File
File diff suppressed because it is too large Load Diff
+4
View File
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -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)}})}));
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -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)}})}));
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
View File
@@ -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)}})}));
File diff suppressed because one or more lines are too long
+169
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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