quasar
Version:
Build high-performance VueJS user interfaces (SPA, PWA, SSR, Mobile and Desktop) in record time
523 lines (452 loc) • 12.9 kB
JavaScript
import { h, ref, watch, computed, getCurrentInstance } from 'vue'
import QBtn from '../btn/QBtn.js'
import QInput from '../input/QInput.js'
import useDark, {
useDarkProps
} from '../../composables/private.use-dark/use-dark.js'
import { btnDesignOptions, btnPadding, getBtnDesign } from '../btn/use-btn.js'
import { createComponent } from '../../utils/private.create/create.js'
import { between } from '../../utils/format/format.js'
import { isKeyCode } from '../../utils/private.keyboard/key-composition.js'
function getBool(val, otherwise) {
return [true, false].includes(val) ? val : otherwise
}
export default createComponent({
name: 'QPagination',
props: {
...useDarkProps,
modelValue: {
type: Number,
required: true
},
min: {
type: [Number, String],
default: 1
},
max: {
type: [Number, String],
required: true
},
maxPages: {
type: [Number, String],
default: 0,
validator: v => (typeof v === 'string' ? parseInt(v, 10) : v) >= 0
},
inputStyle: [Array, String, Object],
inputClass: [Array, String, Object],
size: String,
disable: Boolean,
input: Boolean,
iconPrev: String,
iconNext: String,
iconFirst: String,
iconLast: String,
toFn: Function,
boundaryLinks: {
type: Boolean,
default: null
},
boundaryNumbers: {
type: Boolean,
default: null
},
directionLinks: {
type: Boolean,
default: null
},
ellipses: {
type: Boolean,
default: null
},
ripple: {
type: [Boolean, Object],
default: null
},
round: Boolean,
rounded: Boolean,
flat: Boolean,
outline: Boolean,
unelevated: Boolean,
push: Boolean,
glossy: Boolean,
color: {
type: String,
default: 'primary'
},
textColor: String,
activeDesign: {
type: String,
default: '',
values: v => v === '' || btnDesignOptions.includes(v)
},
activeColor: String,
activeTextColor: String,
gutter: String,
padding: {
type: String,
default: '3px 2px'
}
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const { proxy } = getCurrentInstance()
const { $q } = proxy
const isDark = useDark(props, $q)
const minProp = computed(() => parseInt(props.min, 10))
const maxProp = computed(() => parseInt(props.max, 10))
const maxPagesProp = computed(() => parseInt(props.maxPages, 10))
const inputPlaceholder = computed(() => model.value + ' / ' + maxProp.value)
const boundaryLinksProp = computed(() =>
getBool(props.boundaryLinks, props.input)
)
const boundaryNumbersProp = computed(() =>
getBool(props.boundaryNumbers, !props.input)
)
const directionLinksProp = computed(() =>
getBool(props.directionLinks, props.input)
)
const ellipsesProp = computed(() => getBool(props.ellipses, !props.input))
const newPage = ref(null)
const model = computed({
get: () => props.modelValue,
set: val => {
val = parseInt(val, 10)
if (props.disable || isNaN(val)) return
const value = between(val, minProp.value, maxProp.value)
if (props.modelValue !== value) {
emit('update:modelValue', value)
}
}
})
watch(
() => `${minProp.value}|${maxProp.value}`,
() => {
model.value = props.modelValue
}
)
const classes = computed(
() =>
'q-pagination row no-wrap items-center' +
(props.disable === true ? ' disabled' : '')
)
const gutterProp = computed(() =>
props.gutter in btnPadding
? `${btnPadding[props.gutter]}px`
: props.gutter || null
)
const gutterStyle = computed(() =>
gutterProp.value !== null
? `--q-pagination-gutter-parent:-${gutterProp.value};--q-pagination-gutter-child:${gutterProp.value}`
: null
)
const icons = computed(() => {
const ico = [
props.iconFirst || $q.iconSet.pagination.first,
props.iconPrev || $q.iconSet.pagination.prev,
props.iconNext || $q.iconSet.pagination.next,
props.iconLast || $q.iconSet.pagination.last
]
return $q.lang.rtl === true ? ico.reverse() : ico
})
const attrs = computed(() => ({
'aria-disabled': props.disable === true ? 'true' : 'false',
role: 'navigation'
}))
const btnDesignProp = computed(() => getBtnDesign(props, 'flat'))
const btnProps = computed(() => ({
[btnDesignProp.value]: true,
round: props.round,
rounded: props.rounded,
padding: props.padding,
color: props.color,
textColor: props.textColor,
size: props.size,
ripple: props.ripple !== null ? props.ripple : true
}))
const btnActiveDesignProp = computed(() => {
// we also reset non-active design
const acc = { [btnDesignProp.value]: false }
if (props.activeDesign !== '') {
acc[props.activeDesign] = true
}
return acc
})
const activeBtnProps = computed(() => ({
...btnActiveDesignProp.value,
color: props.activeColor || props.color,
textColor: props.activeTextColor || props.textColor
}))
const btnConfig = computed(() => {
let maxPages = Math.max(
maxPagesProp.value,
1 + (ellipsesProp.value ? 2 : 0) + (boundaryNumbersProp.value ? 2 : 0)
)
const acc = {
pgFrom: minProp.value,
pgTo: maxProp.value,
ellipsesStart: false,
ellipsesEnd: false,
boundaryStart: false,
boundaryEnd: false,
marginalStyle: {
minWidth: `${Math.max(2, String(maxProp.value).length)}em`
}
}
if (maxPagesProp.value && maxPages < maxProp.value - minProp.value + 1) {
maxPages = 1 + Math.floor(maxPages / 2) * 2
acc.pgFrom = Math.max(
minProp.value,
Math.min(
maxProp.value - maxPages + 1,
props.modelValue - Math.floor(maxPages / 2)
)
)
acc.pgTo = Math.min(maxProp.value, acc.pgFrom + maxPages - 1)
if (boundaryNumbersProp.value) {
acc.boundaryStart = true
acc.pgFrom++
}
if (
ellipsesProp.value &&
acc.pgFrom > minProp.value + (boundaryNumbersProp.value ? 1 : 0)
) {
acc.ellipsesStart = true
acc.pgFrom++
}
if (boundaryNumbersProp.value) {
acc.boundaryEnd = true
acc.pgTo--
}
if (
ellipsesProp.value &&
acc.pgTo < maxProp.value - (boundaryNumbersProp.value ? 1 : 0)
) {
acc.ellipsesEnd = true
acc.pgTo--
}
}
return acc
})
function set(value) {
model.value = value
}
function setByOffset(offset) {
model.value = model.value + offset
}
function updateModel() {
model.value = newPage.value
newPage.value = null
if ($q.platform.is.mobile === true) document.activeElement.blur()
}
function onInputValue(val) {
newPage.value = val
}
function onKeyup(e) {
if (isKeyCode(e, 13) === true) updateModel()
}
function getBtn(cfg, page, active) {
const data = {
'aria-label': page,
'aria-current': 'false',
...btnProps.value,
...cfg
}
if (active === true) {
Object.assign(data, {
'aria-current': 'true',
...activeBtnProps.value
})
}
if (page !== void 0) {
if (props.toFn !== void 0) {
data.to = props.toFn(page)
} else {
data.onClick = () => {
set(page)
}
}
}
return h(QBtn, data)
}
// expose public methods
Object.assign(proxy, { set, setByOffset })
return () => {
const contentStart = []
const contentEnd = []
let contentMiddle
if (boundaryLinksProp.value === true) {
contentStart.push(
getBtn(
{
key: 'bls',
disable: props.disable || props.modelValue <= minProp.value,
icon: icons.value[0],
'aria-label': $q.lang.pagination.first
},
minProp.value
)
)
contentEnd.unshift(
getBtn(
{
key: 'ble',
disable: props.disable || props.modelValue >= maxProp.value,
icon: icons.value[3],
'aria-label': $q.lang.pagination.last
},
maxProp.value
)
)
}
if (directionLinksProp.value === true) {
contentStart.push(
getBtn(
{
key: 'bdp',
disable: props.disable || props.modelValue <= minProp.value,
icon: icons.value[1],
'aria-label': $q.lang.pagination.prev
},
props.modelValue - 1
)
)
contentEnd.unshift(
getBtn(
{
key: 'bdn',
disable: props.disable || props.modelValue >= maxProp.value,
icon: icons.value[2],
'aria-label': $q.lang.pagination.next
},
props.modelValue + 1
)
)
}
if (props.input !== true) {
// has buttons instead of inputbox
contentMiddle = []
const { pgFrom, pgTo, marginalStyle: style } = btnConfig.value
if (btnConfig.value.boundaryStart === true) {
const active = minProp.value === props.modelValue
contentStart.push(
getBtn(
{
key: 'bns',
style,
disable: props.disable,
label: minProp.value
},
minProp.value,
active
)
)
}
if (btnConfig.value.boundaryEnd === true) {
const active = maxProp.value === props.modelValue
contentEnd.unshift(
getBtn(
{
key: 'bne',
style,
disable: props.disable,
label: maxProp.value
},
maxProp.value,
active
)
)
}
if (btnConfig.value.ellipsesStart === true) {
contentStart.push(
getBtn(
{
key: 'bes',
style,
disable: props.disable,
label: '…',
ripple: false
},
pgFrom - 1
)
)
}
if (btnConfig.value.ellipsesEnd === true) {
contentEnd.unshift(
getBtn(
{
key: 'bee',
style,
disable: props.disable,
label: '…',
ripple: false
},
pgTo + 1
)
)
}
for (let i = pgFrom; i <= pgTo; i++) {
contentMiddle.push(
getBtn(
{
key: `bpg${i}`,
style,
disable: props.disable,
label: i
},
i,
i === props.modelValue
)
)
}
}
return h(
'div',
{
class: classes.value,
...attrs.value
},
[
h(
'div',
{
class: 'q-pagination__content row no-wrap items-center',
style: gutterStyle.value
},
[
...contentStart,
props.input === true
? h(QInput, {
class: 'inline',
style: {
width: `${inputPlaceholder.value.length / 1.5}em`
},
type: 'number',
dense: true,
value: newPage.value,
disable: props.disable,
dark: isDark.value,
borderless: true,
inputClass: props.inputClass,
inputStyle: props.inputStyle,
placeholder: inputPlaceholder.value,
min: minProp.value,
max: maxProp.value,
'onUpdate:modelValue': onInputValue,
onKeyup,
onBlur: updateModel
})
: h(
'div',
{
class: 'q-pagination__middle row justify-center'
},
contentMiddle
),
...contentEnd
]
)
]
)
}
}
})