Skip to content

Commit

Permalink
Add CheckboxRadio component
Browse files Browse the repository at this point in the history
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
  • Loading branch information
skjnldsv committed Apr 23, 2021
1 parent 220c657 commit 2edf378
Show file tree
Hide file tree
Showing 3 changed files with 425 additions and 0 deletions.
398 changes: 398 additions & 0 deletions src/components/CheckboxRadio/CheckboxRadio.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,398 @@
<!--
- @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
-
- @author John Molakvoæ <skjnldsv@protonmail.com>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->

<docs>

### General description

This is a standard input checkbox/radio design

### Standard checkbox
```vue
<template>
<div>
<CheckboxRadio :checked.sync="sharingEnabled">Enable sharing</CheckboxRadio>
<CheckboxRadio :checked.sync="sharingEnabled" :disabled="true">Enable sharing</CheckboxRadio>
</div>
</template>
<script>
export default {
data() {
return {
sharingEnabled: false,
}
}
}
</script>
```

### Standard radio set
```vue
<template>
<div>
<CheckboxRadio :checked.sync="sharingPermission" value="r" name="sharing_permission_radio" type="radio">Default permission read</CheckboxRadio>
<CheckboxRadio :checked.sync="sharingPermission" value="rw" name="sharing_permission_radio" type="radio">Default permission read+write</CheckboxRadio>
<br>
<div>sharingPermission: {{sharingPermission}}</div>
</div>
</template>
<script>
export default {
data() {
return {
sharingPermission: 'r',
}
}
}
</script>
```

### Standard checkbox set
```vue
<template>
<div>
<CheckboxRadio :disabled="true" :checked.sync="sharingPermission" value="r" name="sharing_permission">Permission read</CheckboxRadio>
<CheckboxRadio :checked.sync="sharingPermission" value="w" name="sharing_permission">Permission write</CheckboxRadio>
<CheckboxRadio :checked.sync="sharingPermission" value="d" name="sharing_permission">Permission delete</CheckboxRadio>
<br>
<div>sharingPermission: {{sharingPermission}}</div>
</div>
</template>
<script>
export default {
data() {
return {
sharingPermission: ['r', 'd'],
}
}
}
</script>
```

</docs>

<template>
<element :is="wrapperElement"
:class="{
'checkbox-radio--checked': isChecked,
'checkbox-radio--disabled': disabled,
'checkbox-radio--indeterminate': indeterminate,
}"
class="checkbox-radio">
<input :id="id"
:checked="isChecked"
:disabled="disabled"
:indeterminate="indeterminate"
:name="name"
:type="type"
:value="value"
class="checkbox-radio__input"
@change="onToggle">

<label :for="id" class="checkbox-radio__label">
<icon :is="checkboxRadioIconElement"
:size="24"
class="checkbox-radio__icon"
title=""
decorative />

<!-- @slot The checkbox/radio label -->
<slot />
</label>
</element>
</template>

<script>
import CheckboxBlankOutline from 'vue-material-design-icons/CheckboxBlankOutline'
import CheckboxIntermediate from 'vue-material-design-icons/CheckboxIntermediate'
import CheckboxMarked from 'vue-material-design-icons/CheckboxMarked'
import CheckboxBlankCircleOutline from 'vue-material-design-icons/CheckboxBlankCircleOutline'
import CheckboxMarkedCircle from 'vue-material-design-icons/CheckboxMarkedCircle'

import { subscribe, unsubscribe, emit } from '@nextcloud/event-bus'

import GenRandomId from '../../utils/GenRandomId'
import l10n from '../../mixins/l10n'

export const TYPE_CHECKBOX = 'checkbox'
export const TYPE_RADIO = 'radio'

export default {
name: 'CheckboxRadio',

components: {
CheckboxBlankOutline,
CheckboxIntermediate,
CheckboxMarked,
},

mixins: [l10n],

props: {

/**
* Unique id attribute of the input
*/
id: {
type: String,
default: () => 'checkbox-radio-' + GenRandomId(),
validator: id => id.trim() !== '',
},

/**
* Input name. Especially required for radios set
*/
name: {
type: String,
default: null,
},

/**
* Type of the input. Checkbox or radio
*/
type: {
type: String,
default: 'checkbox',
validator: type => type === TYPE_CHECKBOX || type === TYPE_RADIO,
},

/**
* Checked state. To be used with `:value.sync`
*/
checked: {
type: [Boolean, Array, String],
default: false,
},

/**
* Value to be synced on check
*/
value: {
type: String,
default: null,
},

/**
* Disabled state
*/
disabled: {
type: Boolean,
default: false,
},

/**
* Wrapping element tag
*/
wrapperElement: {
type: String,
default: 'span',
},
},

data() {
return {
indeterminate: false,
}
},

computed: {
/**
* Check if that entry is checked
* If value is defined, we use that as the checked value
* If not, we expect true/false in this.checked
* @returns {boolean}
*/
isChecked() {
if (this.value !== null) {
if (Array.isArray(this.checked)) {
return [...this.checked].indexOf(this.value) > -1
}
return this.checked === this.value
}
return this.checked === true
},

/**
* Returns the proper Material icon depending on the select case
* @returns {Component}
*/
checkboxRadioIconElement() {
if (this.type === 'radio') {
if (this.isChecked) {
return CheckboxMarkedCircle
}
return CheckboxBlankCircleOutline
}

// Checkbox
if (this.indeterminate) {
return CheckboxIntermediate
}
if (this.isChecked) {
return CheckboxMarked
}
return CheckboxBlankOutline
},
},

beforeMount() {
if (this.name && this.type === TYPE_CHECKBOX) {
subscribe('components:checkboxradio.updated', this.onExternalChange)
}
},

mounted() {
if (this.name && this.type === TYPE_CHECKBOX) {
if (!Array.isArray(this.checked)) {
throw new Error('When using groups of checkboxes, the updated value will be an array')
}
this.updateIndeterminate()
}
},

beforeUnmount() {
if (this.name && this.type === TYPE_CHECKBOX) {
unsubscribe('components:checkboxradio.updated', this.onExternalChange)
}
},

methods: {
onToggle() {
if (this.disabled) {
return
}

// If this is a radio, there can only be one value
if (this.type === TYPE_RADIO) {
this.$emit('update:checked', this.value)
return
}

// If the initial value was a boolean, let's keep it that way
if (typeof this.checked === 'boolean') {
this.$emit('update:checked', !this.isChecked)
return
}

// Dispatch the checked values as an array if multiple, or single value otherwise
const values = this.getInputsSet()
.filter(input => input.checked)
.map(input => input.value)
this.$emit('update:checked', values)

// This is a checkbox and it's part of a group
// Emitted AFTER the checked update
if (this.type === TYPE_CHECKBOX && this.name) {
this.$nextTick(() => emit('components:checkboxradio.updated', this.name))
}
},

/**
* On external input change,
* @param {string} name the input set name
*/
onExternalChange(name) {
if (name === this.name) {
this.updateIndeterminate()
}
},

/**
* We verify if the set is all checked,
* partially checked or completely unchecked
* and update the indeterminate prop accordingly
*/
updateIndeterminate() {
const inputs = this.getInputsSet()
const checked = inputs.filter(input => input.checked)

if (this.isChecked
&& inputs.length !== checked.length
&& checked.length !== 0) {
this.indeterminate = true
return
}
this.indeterminate = false
},

/**
* Get the input set based on this name
* @returns {Node[]}
*/
getInputsSet() {
return [...document.getElementsByName(this.name)]
},
},
}
</script>

<style lang="scss" scoped>
$spacing: 4px;

.checkbox-radio {
display: flex;

&__input {
position: fixed;
z-index: -1;
top: -5000px;
left: -5000px;
opacity: 0;
}

&__label {
display: flex;
align-items: center;
user-select: none;
padding-right: $spacing;

&, * {
cursor: pointer;
}
}

&__icon {
margin-right: $spacing;
opacity: $opacity_normal;
color: var(--color-primary-element);
}

&--disabled &__label {
opacity: $opacity_disabled;
.checkbox-radio__icon {
color: var(--color-text-light)
}
}

&:not(&--disabled) {
.checkbox-radio__input:focus + .checkbox-radio__label {
outline: 1px solid;
}

.checkbox-radio__input:focus + .checkbox-radio__icon,
&:hover .checkbox-radio__icon {
opacity: $opacity_full;
}
}
}

</style>
Loading

0 comments on commit 2edf378

Please sign in to comment.