跳到內容

每條游來游去的小魚,都是正在瀏覽這一頁的訪客,有一條是你自己 ( ´ ▽ ` )ノ 覺得干擾閱讀的話,可以把小魚關掉。

游標小跟班

cursor

跟著游標跑的小跟班。(´ ・ω・`)ノ╰(・ิω・ิ )

名稱描述
Floating UI用於定位浮動元素,例如:工具提示、彈出視窗、下拉選單等等
Anime.js輕量級 JavaScript 動畫函式庫
Pointer 事件偵測滑鼠或觸控點移動、點擊、懸停等等事件,取得座標、目標等等資訊
物理模擬模擬真實世界物理現象,如重力、碰撞、速度等物理效果
CSS 動畫基於 CSS transition 和 animation 實現
JS 動畫基於 JavaScript 實現的動畫,達成更複雜、精準的動畫控制,常見套件有 GSAP、anime.js 等
SVG Morph讓 SVG 在不同形狀間平滑過渡的技術
Document activeElement取得目前擁有焦點的元素
Document elementFromPoint()取得指定座標位置最上層的元素
Document getSelection()取得使用者目前選取的文字內容或游標位置

平時跟著游標跑,碰到特定元素會有特殊互動

我是標題
我是一段文字(嘗試反白文字看看)

可編輯的 div


鱈魚的魚缸貪吃的鱈魚
<template>
<div class="w-full flex flex-col gap-4 example-wrap">
<cursor-sidekick v-if="enable" />
<base-checkbox
v-model="enable"
:label="t('enableSidekick')"
class="example-ctrl"
/>
<div class="flex flex-col gap-2">
<div class="text-2xl font-bold">
{{ t('title') }}
</div>
<div>
{{ t('selectText') }}
</div>
<hr>
<base-input
v-model="text"
:label="t('textInput')"
/>
<div class="h-[20vh] w-1/2 border rounded">
<textarea
v-model="text"
:placeholder="t('multilineText')"
class="h-full w-full p-2"
/>
</div>
<div
contenteditable
class="w-2/3 border rounded p-2"
>
{{ t('editableDiv') }}
</div>
<hr>
<base-btn :label="t('button')" />
<base-btn
disabled
:label="t('naughtyBtn')"
/>
<hr>
<a
href="https://codlin.me/"
target="_blank"
>
{{ t('codlinBlog') }}
</a>
<img
src="/low/painting-codfish-bakery.webp"
:alt="t('greedyCodfish')"
>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseBtn from '../../base-btn.vue'
import BaseCheckbox from '../../base-checkbox.vue'
import BaseInput from '../../base-input.vue'
import CursorSidekick from '../cursor-sidekick.vue'
const enable = ref(false)
const text = ref('')
const { t } = useI18n()
</script>

自行設計 Provider,產生各種奇奇怪怪的互動吧!ლ(´∀`ლ)

嘗試反白這段文字中有鱈魚的部分和沒有鱈魚的部分,看看有甚麼差別。


稻穗攝影作品
<template>
<div class="w-full flex flex-col gap-4 example-wrap">
<cursor-sidekick
v-if="enable"
color="#35abf0"
:hover-providers="hoverProviderList"
:select-providers="selectProviderList"
/>
<base-checkbox
v-model="enable"
:label="t('enableSidekick')"
class="example-ctrl"
/>
<div class="flex flex-col gap-2">
<div>
{{ t('selectHint') }}
</div>
<hr>
<base-btn
disabled
:label="t('naughtyBtn')"
/>
<hr>
<img
src="/low/photography-ears-of-rice.webp"
url="https://www.flickr.com/photos/coodfish/albums/"
alt="稻穗攝影作品"
>
</div>
</div>
</template>
<script setup lang="ts">
import type { ContentProvider } from '../use-content-provider'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseBtn from '../../base-btn.vue'
import BaseCheckbox from '../../base-checkbox.vue'
import CursorSidekick from '../cursor-sidekick.vue'
const enable = ref(false)
const { t } = useI18n()
const hoverProviderList = computed<ContentProvider[]>(() => [
{
match(data) {
if ('rect' in data)
return false
if (
!(data instanceof HTMLButtonElement)
&& data?.getAttribute('role') !== 'button'
) {
return false
}
return data.innerHTML.includes('色色') || data.innerHTML.includes('naughty')
},
getContent: () => ({
btnList: [
{
label: t('naughtyPortal'),
onClick() {
window.open(
'https://raw.githubusercontent.com/tpai/dogedeck/main/cards/%E6%8A%97%E8%89%B2%E8%89%B2%E8%97%A5.png',
'_blank',
)
},
},
],
}),
},
{
match(data) {
if ('rect' in data)
return false
if (data instanceof HTMLImageElement) {
return true
}
return false
},
getContent(param) {
const { element } = param
const target = element?.value
if (!(target instanceof HTMLImageElement))
return
const url = target.getAttribute('url')
if (!url)
return
return {
btnList: [
{
label: t('viewMorePhotos'),
onClick() {
window.open(url ?? '', '_blank')
},
},
],
}
},
},
])
const selectProviderList = computed<ContentProvider[]>(() => [
{
match(data) {
if (!('rect' in data))
return false
return data.text.includes('鱈魚') || data.text.includes('Codfish')
},
getContent: () => ({
text: `${t('foundYouLine1')}<br>${t('foundYouLine2')}`,
class: ' text-nowrap ',
btnList: [
{
label: '🎬 Youtube',
onClick() {
window.open('https://www.youtube.com/@codfish2140', '_blank')
},
},
{
label: '💡 CodePen',
onClick() {
window.open('https://codepen.io/Codfish2140', '_blank')
},
},
{
label: t('codlinBlog'),
onClick() {
window.open('https://codlin.me/', '_blank')
},
},
],
}),
},
])
</script>

這個小廢物元件應該是使用最多種瀏覽器 API 的元件了,以下是 API 與其應用範圍:

小跟班的變形動畫是靠老朋友 anime.js 實現。

tooltip 定位使用 Floating UI 實現,功能超強大,極度推薦。

interface Props {
/** 單位 px */
size?: number;
/** \# 前綴之 HEX 格式
* @default '#515151'
*/
color?: string;
/** 最大速度。越慢小跟班越悠哉。單位 px/ms
* @default 1
*/
maxVelocity?: number;
/** @default 100 */
zIndex?: number;
/** 匹配 active element 的 provider。
*
* 通常用於可點擊或 focus 的元素。
*/
activeProviders?: ContentProvider[];
/** 匹配 hover element 的 provider
*
* 只要 hover 到符合條件的元素,即會觸發。
*/
hoverProviders?: ContentProvider[];
/** 匹配選取文字的 provider */
selectProviders?: ContentProvider[];
}

ContentProvider 定義如下:

interface BtnOption {
label: string;
onClick: (event?: Event) => void;
}
interface Content {
/** 用於調整內容樣式 */
class?: string;
text?: string;
/** 按鈕清單 */
btnList?: BtnOption[];
/** 預覽連結內容 */
preview?: {
src: string;
class: string;
};
}
export interface ContentProvider {
/** 判斷目前元素或文字是否符合 */
match: (
data: HTMLElement | SelectionState
) => boolean;
/** 取得小跟班顯示用內容 */
getContent: (
param: TargetParam
) => Content | undefined;
}