Jigsaw Puzzle Button button
A button that can only be clicked after you finish the jigsaw puzzle ◝( ゚∀゚ )◟
Usage Examples
Basic Usage
Drag the pieces and complete the puzzle!
View example source code
vue
<template>
<div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6">
<div class="flex justify-center">
<btn-jigsaw-puzzle
:label="t('clickMe')"
@click="handleClick"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import BtnJigsawPuzzle from '../btn-jigsaw-puzzle.vue'
const { t } = useI18n()
function handleClick() {
// eslint-disable-next-line no-alert
alert(t('congratulations'))
}
</script>Form Example
Block bots and annoying complaints! (・∀・)9 (eh?)
Complaint Feedback Form
If you have any concerns, please fill in your feedback here, we will handle your problem as soon as possible.
To prevent abuse, the jigsaw puzzle must be completed before submitting (*´∀`)~♥
View example source code
vue
<template>
<div class="relative w-full flex flex-col gap-10 border border-gray-200 rounded-xl p-6">
<div class="flex flex-col gap-3">
<div class="text-3xl font-bold">
{{ t('complaintFeedbackForm') }}
</div>
<div class="text-sm text-gray-500">
{{ t('complaintFeedbackFormTip') }}
</div>
<div class="flex justify-center border border-gray-200 rounded-lg p-3">
<textarea
v-model="text"
:placeholder="t('inputPlaceholder')"
class="min-h-[30vh] w-full"
/>
</div>
<div class="w-full flex justify-center">
<btn-jigsaw-puzzle
ref="jigsawPuzzleRef"
:row-count="3"
:col-count="4"
@click="handleSubmit"
>
<button class="btn w-full select-none rounded p-3 px-20 text-3xl">
{{ t('submitBtn') }}
</button>
</btn-jigsaw-puzzle>
</div>
<div class="mt-2 text-center text-xs text-gray-500">
{{ t('jigsawPuzzleTip') }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import BtnJigsawPuzzle from '../btn-jigsaw-puzzle.vue'
const { t } = useI18n()
const jigsawPuzzleRef = useTemplateRef('jigsawPuzzleRef')
const text = ref('')
function handleSubmit() {
// eslint-disable-next-line no-alert
alert(t('thanks'))
text.value = ''
jigsawPuzzleRef.value?.scatter()
}
</script>
<style scoped lang="sass">
.btn
background-color: light-dark(#444, #EEE)
color: light-dark(#EEE, #444)
transition-duration: 0.2s
&:active
transition-duration: 0.1s
transform: scale(0.98)
</style>Anti-Scalping
Block those pesky scalper bots! (⌐■_■)✧
Select "Codfish Asia Concert" tickets:
Time: 2030/02/30 03:00
Location: Taiwan Strait - 24°19'22.3"N 119°58'43.3"E
Location: Taiwan Strait - 24°19'22.3"N 119°58'43.3"E
To prevent abuse, complete the puzzle within 4 seconds before submitting (*´∀`)~♥
Passerby: "This even blocks real people! ლ(´口`ლ)"
View example source code
vue
<template>
<div class="relative w-full flex flex-col gap-10 border border-gray-200 rounded-xl p-6">
<div class="flex flex-col gap-3">
<div class="max-w-xl w-full flex flex-col gap-3">
<div class="mb-1 text-lg font-semibold">
{{ t('selectTicket') }}
</div>
<div class="text-sm text-gray-500">
{{ t('eventTime') }}<br>
{{ t('eventLocation') }}
</div>
<div class="grid grid-cols-1 gap-3">
<label
v-for="ticket in ticketOptionList"
:key="ticket.id"
class="cursor-pointer"
>
<input
v-model="selectedTicketId"
type="radio"
class="peer hidden"
:value="ticket.id"
>
<div
class="w-full flex flex-col gap-1 border rounded-lg px-4 py-3 text-sm transition-all hover:border-blue-400 peer-checked:border-blue-500 peer-checked:bg-blue-50"
>
<div class="flex items-center justify-between">
<div class="text-lg text-gray-500 font-black">
{{ ticket.area }}
</div>
<div class="text-base font-bold">
NT$ {{ ticket.price.toLocaleString() }}
</div>
</div>
<div
v-if="ticket.note"
class="text-xs text-amber-600"
>
{{ ticket.note }}
</div>
</div>
</label>
</div>
</div>
<div class="py-2 text-center text-gray-500">
{{ t('timerTip', { seconds: MAX_TIMER_COUNT }) }}
</div>
<div
class="w-full flex justify-center duration-300"
:class="{ 'opacity-50 pointer-events-none': !selectedTicketId }"
>
<btn-jigsaw-puzzle
ref="jigsawPuzzleRef"
@drag-start="handleDragStart"
@click="handleSubmit"
>
<button class="btn w-full select-none rounded p-3 px-6 text-3xl">
{{ t('buyButton') }}
</button>
</btn-jigsaw-puzzle>
</div>
<div class="text-center text-xs text-red-500">
{{ isActive ? t('remainingTime', { seconds: timerCount }) : ' ' }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useIntervalFn } from '@vueuse/core'
import { computed, ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import BtnJigsawPuzzle from '../btn-jigsaw-puzzle.vue'
const { t } = useI18n()
const MAX_TIMER_COUNT = 4
const jigsawPuzzleRef = useTemplateRef('jigsawPuzzleRef')
const timerCount = ref(MAX_TIMER_COUNT)
const { isActive, pause, resume } = useIntervalFn(() => {
timerCount.value--
if (timerCount.value <= 0) {
reset()
}
}, 1000, {
immediate: false,
})
const ticketOptionList = computed(() => [
{
id: 't1',
area: t('rockZone'),
price: 5200,
note: t('rockZoneNote'),
},
{
id: 't2',
area: t('vipZone'),
price: 4200,
note: t('vipZoneNote'),
},
{
id: 't3',
area: t('weirdZone'),
price: 3200,
note: t('weirdZoneNote'),
},
])
const selectedTicketId = ref('')
function reset() {
selectedTicketId.value = ''
jigsawPuzzleRef.value?.scatter()
pause()
timerCount.value = MAX_TIMER_COUNT
}
function handleDragStart() {
if (isActive.value) {
return
}
resume()
}
function handleSubmit() {
// eslint-disable-next-line no-alert
alert(t('soldOut'))
reset()
}
</script>
<style scoped lang="sass">
.btn
background-color: light-dark(#444, #EEE)
color: light-dark(#EEE, #444)
transition-duration: 0.2s
&:active
transition-duration: 0.1s
transform: scale(0.98)
</style>How It Works
Uses an SVG mask to implement the jigsaw splitting, and Pointer events to implement the drag-and-drop behavior.
Source Code
API
Props
interface Props {
/** 按鈕內文字 */
label?: string;
/** 是否停用 */
disabled?: boolean;
/** 同 CSS z-index */
zIndex?: number | string;
/** 同 html tabindex */
tabindex?: number | string;
/** 拼圖行數 */
rowCount?: number;
/** 拼圖列數 */
colCount?: number;
}Emits
const emit = defineEmits<{
/** 開始拖動 */
dragStart: [piece: Piece, evt: PointerEvent];
dragging: [piece: Piece, evt: PointerEvent];
dragStop: [piece: Piece, evt: PointerEvent];
completed: [];
click: [];
}>()Methods
defineExpose({
/** 打散拼圖 */
scatter,
/** 自動完成 */
autoComplete,
})Slots
defineSlots<{
/** 按鈕 */
default?: (params: { isAllCompleted: boolean }) => unknown;
}>()