mirror of
https://github.com/tenrok/vue-tribute.git
synced 2026-06-16 14:40:35 +03:00
Initial v2 commit
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
name: Tests
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [14, 16]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Run tests
|
||||
run: |
|
||||
yarn
|
||||
yarn test
|
||||
env:
|
||||
CI: true
|
||||
@@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-demo
|
||||
dist-ssr
|
||||
*.local
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 120,
|
||||
"vueIndentScriptAndStyle": false
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["johnsoncodehk.volar"]
|
||||
}
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "pwa-node",
|
||||
"request": "launch",
|
||||
"name": "Debug Current Test File",
|
||||
"autoAttachChildProcesses": true,
|
||||
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
|
||||
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
|
||||
"args": ["run", "${relativeFile}"],
|
||||
"smartStep": true,
|
||||
"console": "integratedTerminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
# Vue 3 + Typescript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and Typescript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar)
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's `.vue` type support plugin by running `Volar: Switch TS Plugin on/off` from VSCode command palette.
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="px-8 mt-16 mb-6 sm:mt-32 sm:mb-6 md:px-0">
|
||||
<div class="w-full mx-auto mt-8 max-w-prose">
|
||||
<BasicExample />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import BasicExample from './BasicExample.vue'
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.tribute-container {
|
||||
@apply absolute top-0 left-0 h-auto overflow-y-auto block z-10 rounded shadow;
|
||||
max-height: 300px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.tribute-container ul {
|
||||
@apply mt-5 p-0 list-none bg-white rounded overflow-hidden;
|
||||
}
|
||||
|
||||
.tribute-container li {
|
||||
@apply text-blue-600 pl-2 pr-6 py-2 cursor-pointer text-sm;
|
||||
}
|
||||
.tribute-container li.highlight,
|
||||
.tribute-container li:hover {
|
||||
@apply bg-blue-600 text-white;
|
||||
}
|
||||
.tribute-container li span {
|
||||
@apply font-bold;
|
||||
}
|
||||
.tribute-container li.no-match {
|
||||
@apply cursor-default;
|
||||
}
|
||||
.tribute-container .menu-highlighted {
|
||||
@apply font-bold;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div id="container">
|
||||
<vue-tribute :options="options">
|
||||
<input type="text" class="" placeholder="@..." />
|
||||
</vue-tribute>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import VueTribute from '../lib'
|
||||
|
||||
const options = {
|
||||
trigger: '@',
|
||||
values: [
|
||||
{ key: 'Collin Henderson', value: 'syropian' },
|
||||
{ key: 'Sarah Drasner', value: 'sarah_edo' },
|
||||
{ key: 'Evan You', value: 'youyuxi' },
|
||||
{ key: 'Adam Wathan', value: 'adamwathan' },
|
||||
],
|
||||
positionMenu: true,
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
@@ -0,0 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
@apply dark:bg-gray-900;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import 'highlight.js/lib/common'
|
||||
import hljsVuePlugin from '@highlightjs/vue-plugin'
|
||||
import 'highlight.js/styles/atom-one-dark.css'
|
||||
import './app.css'
|
||||
|
||||
createApp(App).use(hljsVuePlugin).mount('#app')
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>vue-input-autowidth</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/app.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import { DefineComponent } from 'vue'
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { App, Plugin } from 'vue'
|
||||
import { VueTribute } from './vue-tribute'
|
||||
|
||||
const install = (app: App) => {
|
||||
app.component(VueTribute.name, VueTribute)
|
||||
}
|
||||
|
||||
VueTribute.install = install
|
||||
|
||||
export { VueTribute }
|
||||
export default VueTribute as unknown as Plugin
|
||||
@@ -0,0 +1,58 @@
|
||||
import { defineComponent, h, onRenderTracked } from 'vue'
|
||||
import { render, screen } from '@testing-library/vue'
|
||||
import { VueTribute } from '../'
|
||||
import type Tribute from 'tributejs'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
interface TributeElement extends HTMLElement {
|
||||
tributeInstance?: Tribute<any>
|
||||
}
|
||||
|
||||
describe('VueTribute', () => {
|
||||
const options = {
|
||||
trigger: '@',
|
||||
values: [
|
||||
{ key: 'Collin Henderson', value: 'syropian' },
|
||||
{ key: 'Sarah Drasner', value: 'sarah_edo' },
|
||||
{ key: 'Evan You', value: 'youyuxi' },
|
||||
{ key: 'Adam Wathan', value: 'adamwathan' },
|
||||
],
|
||||
positionMenu: true,
|
||||
}
|
||||
|
||||
test('attaches Tribute instance to the slot DOM node', async () => {
|
||||
const containerStub = defineComponent({
|
||||
setup() {
|
||||
return () => h('div', { id: 'container' }, [h(VueTribute, { options }, () => h('input', { type: 'text' }))])
|
||||
},
|
||||
})
|
||||
render(containerStub)
|
||||
const input = screen.getByRole('textbox')
|
||||
|
||||
expect((input as TributeElement).tributeInstance).toBeTruthy()
|
||||
})
|
||||
|
||||
test('The slot DOM node passes through custom Tribute-related events', async () => {
|
||||
const activeSpy = vi.fn()
|
||||
const notActiveSpy = vi.fn()
|
||||
const user = userEvent.setup()
|
||||
|
||||
const containerStub = defineComponent({
|
||||
setup() {
|
||||
return () =>
|
||||
h('div', { id: 'container' }, [
|
||||
h(VueTribute, { options }, () =>
|
||||
h('input', { type: 'text', onTributeActiveTrue: activeSpy, onTributeActiveFalse: notActiveSpy })
|
||||
),
|
||||
])
|
||||
},
|
||||
})
|
||||
render(containerStub)
|
||||
const input = screen.getByRole('textbox')
|
||||
await user.type(input, '@')
|
||||
await user.type(input, '{Backspace}')
|
||||
|
||||
expect(activeSpy).toHaveBeenCalledOnce()
|
||||
expect(notActiveSpy).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
import { defineComponent, watch, h, onMounted, PropType, onBeforeUnmount, nextTick, Ref, ref, unref } from 'vue'
|
||||
import Tribute, { TributeOptions } from 'tributejs'
|
||||
|
||||
type Maybe<T> = T | undefined
|
||||
type MaybeRef<T> = T | Ref<T>
|
||||
|
||||
interface TributeElement extends HTMLElement {
|
||||
tributeInstance?: Tribute<any>
|
||||
}
|
||||
|
||||
export const VueTribute = defineComponent({
|
||||
name: 'vue-tribute',
|
||||
props: {
|
||||
options: {
|
||||
type: Object as PropType<MaybeRef<TributeOptions<any>>>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup(props, context) {
|
||||
if (typeof Tribute === 'undefined') {
|
||||
throw new Error('[vue-tribute] cannot locate tributejs.')
|
||||
}
|
||||
|
||||
const root = ref<HTMLElement>()
|
||||
const el = ref<TributeElement>()
|
||||
|
||||
const attachTribute = (el: Ref<Maybe<TributeElement>>, options: MaybeRef<TributeOptions<any>> = props.options) => {
|
||||
if (!el.value) return
|
||||
|
||||
let tribute = new Tribute(unref(options))
|
||||
tribute.attach(el.value)
|
||||
el.value.tributeInstance = tribute
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
el.value = root.value?.childNodes[0] as TributeElement
|
||||
|
||||
if (!el) {
|
||||
throw new Error('[vue-tribute] cannot find a suitable element to attach to.')
|
||||
}
|
||||
|
||||
attachTribute(el)
|
||||
|
||||
el.value.addEventListener('tribute-replaced', e => {
|
||||
e.target?.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
})
|
||||
})
|
||||
|
||||
const detachTribute = (el: Ref<Maybe<TributeElement>>) => {
|
||||
if (!el.value?.tributeInstance) return
|
||||
|
||||
el.value.tributeInstance.detach(el.value)
|
||||
el.value.tributeInstance = undefined
|
||||
delete el.value.dataset.tribute
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
detachTribute(el)
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.options,
|
||||
async newOptions => {
|
||||
if (el.value?.tributeInstance) {
|
||||
await nextTick()
|
||||
detachTribute(el)
|
||||
await nextTick()
|
||||
attachTribute(el, { ...newOptions })
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
return () =>
|
||||
h(
|
||||
'div',
|
||||
{ class: 'v-tribute', ref: root },
|
||||
[context.slots.default ? context.slots.default()[0] : null].filter(Boolean)
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "vue-tribute",
|
||||
"version": "2.0.0",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "./dist/vue-tribute.umd.js",
|
||||
"module": "./dist/vue-tribute.es.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/vue-tribute.es.js",
|
||||
"require": "./dist/vue-tribute.umd.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"start": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"build:demo": "vite build --mode demo",
|
||||
"preview": "vite preview",
|
||||
"serve": "vite preview",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@highlightjs/vue-plugin": "^2.1.0",
|
||||
"@tailwindcss/forms": "^0.4.0",
|
||||
"@tailwindcss/typography": "^0.5.0",
|
||||
"@testing-library/user-event": "^14.0.0-beta",
|
||||
"@testing-library/vue": "^6.4.2",
|
||||
"@types/node": "^17.0.10",
|
||||
"@vitejs/plugin-vue": "^2.0.0",
|
||||
"@vue/test-utils": "^2.0.0-rc.18",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"highlight.js": "^11.4.0",
|
||||
"jsdom": "^19.0.0",
|
||||
"postcss": "^8.4.5",
|
||||
"tailwindcss": "^3.0.15",
|
||||
"tributejs": "^5.1.3",
|
||||
"typescript": "^4.4.4",
|
||||
"vite": "^2.7.2",
|
||||
"vitest": "^0.1.27",
|
||||
"vue": "^3.2.25",
|
||||
"vue-tsc": "^0.29.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"tributejs": ">= 2.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,24 @@
|
||||
module.exports = {
|
||||
mode: 'jit',
|
||||
content: ['./demo/**/*.{ts,vue,html}'],
|
||||
theme: {
|
||||
extend: {
|
||||
typography: theme => ({
|
||||
dark: {
|
||||
css: {
|
||||
h2: {
|
||||
color: theme('colors.gray.200'),
|
||||
},
|
||||
h3: {
|
||||
color: theme('colors.gray.300'),
|
||||
},
|
||||
p: {
|
||||
color: theme('colors.gray.400'),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
plugins: [require('@tailwindcss/typography'), require('@tailwindcss/forms')],
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"baseUrl": "./",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["esnext", "dom"],
|
||||
"types": ["node", "vitest/globals"]
|
||||
},
|
||||
"include": ["vite.config.ts", "env.d.ts", "lib/**/*.ts", "demo/**/*.ts", "demo/**/*.vue"]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
const resolvePath = (str: string) => resolve(__dirname, str)
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
|
||||
const devConfig = defineConfig({
|
||||
root: './demo',
|
||||
plugins: [vue()],
|
||||
build: {
|
||||
outDir: '../dist-demo',
|
||||
emptyOutDir: true,
|
||||
rollupOptions: {
|
||||
input: {
|
||||
main: resolve(__dirname, 'demo/index.html'),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const prodConfig = defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolvePath('lib/index.ts'),
|
||||
name: 'vue-tribute',
|
||||
fileName: format => `vue-tribute.${format}.js`,
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ['vue', 'tributejs'],
|
||||
|
||||
output: {
|
||||
exports: 'named',
|
||||
globals: {
|
||||
vue: 'Vue',
|
||||
tributejs: 'Tribute',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default isProd ? prodConfig : devConfig
|
||||
@@ -0,0 +1,12 @@
|
||||
/// <reference types="vitest" />
|
||||
import { defineConfig } from 'vite'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [Vue()],
|
||||
test: {
|
||||
include: ['./lib/test/**/*.test.ts'],
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user