Full Code of fritx/vue-at for AI

vue3 43680e2102ef cached
18 files
45.2 KB
13.0k tokens
8 symbols
1 requests
Download .txt
Repository: fritx/vue-at
Branch: vue3
Commit: 43680e2102ef
Files: 18
Total size: 45.2 KB

Directory structure:
gitextract_8hv1zyjq/

├── .eslintignore
├── .eslintrc.js
├── .github/
│   └── FUNDING.yml
├── .gitignore
├── .npmrc
├── LICENSE
├── README.md
├── index.html
├── package.json
├── src/
│   ├── App.vue
│   ├── At.scss
│   ├── At.vue
│   ├── AtTemplate.vue
│   ├── AtTextarea.vue
│   ├── main.js
│   └── util.js
├── test_umd.html
└── vue.config.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintignore
================================================
/dist/
/package/dist/


================================================
FILE: .eslintrc.js
================================================
module.exports = {
  root: true,
  extends: [
    'plugin:vue/vue3-recommended',
    '@vue/standard'
  ],
  rules: {
    'vue/multiline-html-element-content-newline': 'off',
    'vue/first-attribute-linebreak': 'off',
    'vue/max-attributes-per-line': 'off',
    'vue/order-in-components': 'off',
    'vue/attributes-order': 'off',
    'vue/html-indent': 'off',
    'no-irregular-whitespace': 'off',
    'no-mixed-operators': 'off',
    'no-unused-vars': 'off',
    'prefer-const': 'off',
    'comma-dangle': 'off',
    'max-len': 'off',
    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
  }
}


================================================
FILE: .github/FUNDING.yml
================================================
# These are supported funding model platforms

github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: vue-at  # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
custom: # Replace with a single custom sponsorship URL


================================================
FILE: .gitignore
================================================
.DS_Store
node_modules*/
dist/
dist_demo/
package/
*.tgz
npm-debug.log
yarn-error.log


================================================
FILE: .npmrc
================================================
package-lock=false


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2016-present, Fritz Lin

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: README.md
================================================
# vue-at

<a href="https://we-demo.github.io/vue-at-vite-app/"><img width="76" height="20" src="https://img.shields.io/website?url=https%3A%2F%2Fwe-demo.github.io%2Fvue-at-vite-app%2F"></a>&nbsp;&nbsp;<a href="https://www.npmjs.com/package/vue-at"><img height="20" src="https://img.shields.io/npm/dm/vue-at.svg"></a>&nbsp;&nbsp;<a href="https://github.com/fritx/vue-at"><img width="90" height="20" src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" /></a>&nbsp;&nbsp;<a href="https://github.com/fritx/vue-at"><img width="84" height="20" src="https://img.shields.io/badge/license-MIT-blue.svg"></a>&nbsp;&nbsp;<a href="https://gitter.im/fritx/vue-at?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img width="92" src="https://badges.gitter.im/fritx/vue-at.svg" alt="Join the chat at https://gitter.im/fritx/vue-at"></a>

<img width="262" height="218" src="https://raw.githubusercontent.com/fritx/vue-at/master/WechatIMG1.jpeg">&nbsp;&nbsp;<img width="262" height="218" src="https://raw.githubusercontent.com/fritx/vue-at/master/WechatIMG2.jpeg">

- [x] Chrome / Firefox / Edge / IE9~IE11
- [x] Plain-text based, no jQuery, no extra nodes
- [x] Content-Editable / Textarea
- [x] Avatars, custom templates
- [x] Vite / Vue3 / Vue2 / Vue1
- [x] Vuetify / Element UI / Element Plus
- [x] Vue-CLI build migration
- [ ] Vite build migration
- [x] CommonJS / UMD Support

Playground: https://we-demo.github.io/vue-at-vite-app/<br>
Vue2 Docs: https://github.com/fritx/vue-at/tree/vue2#readme<br>
Vue3 Docs: See below<br>
See also: [react-at](https://github.com/fritx/react-at)

**If you're using Vue2, read [branch vue2](https://github.com/fritx/vue-at/tree/vue2#readme) instead.**

```plain
npm i vue-at@next  # for Vue3 (branch vue3)
npm i vue-at@2.x  # for Vue2 (branch vue2)
npm i vue-at@1.x  # for Vue1 (branch vue1-legacy)
npm i vue1-at  # for Vue1 (branch vue1-new)
```

```vue
<template>
  <at :members="members">
    <div :contenteditable="true"></div>
  </at>
  <at-ta :members="members">
    <textarea></textarea>
  </at-ta>
</template>

<script>
import At from 'vue-at' // for content-editable
import AtTa from 'vue-at/dist/vue-at-textarea' // for textarea

export default {
  components: { At, AtTa },
  data () {
    return {
      members: ['Roxie Miles', 'grace.carroll', '小浩']
    }
  }
}
</script>

<style>
#app .atwho-view { /* more */ }
#app .atwho-ul { /* more */ }
</style>
```

## UMD Also Supported

```html
<!-- for Vue2 -->
<script src="//unpkg.com/vue@2"></script>
<script src="//unpkg.com/vue-at@2/dist/vue-at.umd.js"></script>
<script src="//unpkg.com/vue-at@2/dist/vue-at-textarea.umd.js"></script>
<!-- ...-->

<!-- for Vue3 -->
<script src="//unpkg.com/vue@3"></script>
<script src="//unpkg.com/vue-at@next/dist/vue-at.umd.js"></script>
<script src="//unpkg.com/vue-at@next/dist/vue-at-textarea.umd.js"></script>
<div id="app">
  <at v-model:value="html">
    <div contenteditable></div>
  </at>
  <at-textarea>
    <textarea v-model="text"></textarea>
  </at-textarea>
</div>
<script>
Vue.createApp({
  components: { At, AtTextarea },
  // ...
}).mount('#app')
</script>
```

## Using V-Model (Recommended)

With Content-Editable, use `<at v-model:value="v">`<br>
With Textarea, you can use either `<at-ta v-model:value="v">` or `<textarea v-model="v">`

```vue
<at v-model:value="html">
  <div :contenteditable="true"></div>
</at>
<at-ta v-model:value="text">
  <textarea></textarea>
</at-ta>
<at-ta>
  <textarea v-model="text"></textarea>
</at-ta>
```

## Custom Templates

### Custom List

```vue
<template>
  <at :members="members" name-key="name">
    <template slot="item" slot-scope="s">
      <img :src="s.item.avatar">
      <span v-text="s.item.name"></span>
    </template>
    <div :contenteditable="true"></div>
  </at>
</template>

<script>
// ...
members: [{
  avatar: 'https://randomuser.me/api/portraits/men/2.jpg',
  name: 'myrtie.green'
}, {
  avatar: 'https://randomuser.me/api/portraits/men/8.jpg',
  name: '椿木'
}]
</script>

<style>
#app .atwho-li { /* more */ }
#app .atwho-li img { /* more */ }
#app .atwho-li span { /* more */ }
</style>
```

#### Custom List with Vue 1.x

There is no "scoped slot" feature in Vue 1.<br>
Use a "normal slot" with `data-` attribute instead.

```vue
<!-- vue1-at for vue@1.x -->
<template slot="item">
  <img data-src="item.avatar">
  <span data-text="item.name"></span>
</template>
```

### Custom Tags

This gives you the option of changing the style of inserted tagged items. It is only supported for ContentEditable version, not Textarea.

```vue
<span slot="embeddedItem" slot-scope="s">
  <span class="tag"><img :src="s.current.avatar">{{ s.current.name }}</span>
</span>

<!-- with Vue 2.6+ 'v-slot' / '#slot' directive -->
<!-- note at least two '<span>' wrapper are required to work -->
<template #embeddedItem="s">
  <span><span class="tag"><img class="avatar" :src="s.current.avatar">{{ s.current.name }}</span></span>
</template>
```

## Used with 3rd-party libraries

### Vuetify v-textarea

```vue
<at-ta :members="members">
  <!-- slots -->
  <v-textarea v-model="text"></v-textarea>
</at-ta>
```

### Element UI / Element-Plus el-input

```vue
<at-ta :members="members">
  <!-- slots -->
  <el-input v-model="text" type="textarea"></el-input>
</at-ta>
```


================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width">
    <title>vue-at</title>
  </head>
  <body>
    <div id="app"></div>
    <script src="dist/demo.js"></script>
  </body>
</html>


================================================
FILE: package.json
================================================
{
  "name": "vue-at",
  "description": "At.js for Vue",
  "version": "3.0.0-alpha.3",
  "author": "Fritz Lin <uxfritz@163.com>",
  "repository": "https://github.com/fritx/vue-at",
  "scripts": {
    "lint:fix": "vue-cli-service lint",
    "lint": "vue-cli-service lint --no-fix",
    "dev:dist": "vue-cli-service serve --skip-plugins eslint",
    "dev": "vue-cli-service serve",
    "demo": "vue-cli-service build --dest dist_demo",
    "build:at": "vue-cli-service build --no-clean ./src/At.vue --target lib --name At --filename vue-at && shx mv dist/vue-at.common.js dist/vue-at.js",
    "build:at-ta": "vue-cli-service build --no-clean ./src/AtTextarea.vue --target lib --name AtTextarea --filename vue-at-textarea && shx mv dist/vue-at-textarea.common.js dist/vue-at-textarea.js",
    "build": "shx rm -rf dist && run-p build:at build:at-ta && shx rm dist/demo.html",
    "prepublish": "npm run build"
  },
  "main": "dist/vue-at.js",
  "files": [
    "dist"
  ],
  "engines": {
    "node": ">= 14.x"
  },
  "dependencies": {
    "textarea-caret": "^3.1.0"
  },
  "peerDependencies": {
    "vue": "3.x"
  },
  "devDependencies": {
    "@babel/core": "^7.18.9",
    "@vue/cli-plugin-eslint": "^5.0.8",
    "@vue/cli-service": "^5.0.8",
    "@vue/compat": "^3.1.0",
    "@vue/compiler-sfc": "^3.1.0",
    "@vue/eslint-config-standard": "^8.0.1",
    "element-plus": "^2.2.12",
    "eslint": "^8.21.0",
    "eslint-plugin-vue": "^9.3.0",
    "npm-run-all": "^4.1.5",
    "sass": "^1.53.0",
    "sass-loader": "^13.0.2",
    "shx": "^0.3.4",
    "vue": "^3.1.0",
    "vue-loader": "^16.0.0",
    "vuetify": "3.0.0",
    "webpack": "^5.73.0"
  }
}


================================================
FILE: src/App.vue
================================================
<template>
  <div id="app">

    <!--
      vue3 migration.2
      fix: [Vue warn]: (deprecation COMPONENT_V_MODEL) v-model usage on component has changed in Vue 3. Component that expects to work with v-model should now use the "modelValue" prop and emit the "update:modelValue" event. You can update the usage and then opt-in to Vue 3 behavior on a per-component basis with `compatConfig: { COMPONENT_V_MODEL: false }`.
      Details: https://v3-migration.vuejs.org/breaking-changes/v-model.html
    -->
    <at :members="members" name-key="name" v-model:value="html">
      <!-- custom: same as default slot -->
      <!-- <template #item="s">
        <span v-text="s.item"></span>
      </template> -->

      <!-- custom: with avatars -->
      <template #item="s">
        <img :src="s.item.avatar">
        <span v-text="s.item.name" />
      </template>

      <!--
        // vue3 migration.4
        fix: [Vue warn]: (deprecation ATTR_ENUMERATED_COERCION) Enumerated attribute "contenteditable" with v-bind value `` will render the value as-is instead of coercing the value to "true" in Vue 3. Always use explicit "true" or "false" values for enumerated attributes. If the usage is intended, you can disable the compat behavior and suppress this warning with:
          configureCompat({ ATTR_ENUMERATED_COERCION: false })
        Details: https://v3-migration.vuejs.org/breaking-changes/attribute-coercion.html
      -->
      <div class="editor" :contenteditable="true" />
    </at>

    <at :members="members" name-key="name" v-model:value="html2">
      <template #embeddedItem="s">
        <span><span class="tag"><img class="avatar" :src="s.current.avatar">{{ s.current.name }}</span></span>
      </template>

      <!-- custom: with avatars -->
      <template #item="s">
        <img :src="s.item.avatar">
        <span v-text="s.item.name" />
      </template>

      <div class="editor" :contenteditable="true" />
    </at>

    <br>

    <at-ta :members="members" name-key="name" v-model:value="text">
      <!-- custom: with avatars -->
      <template #item="s">
        <img :src="s.item.avatar">
        <span v-text="s.item.name" />
      </template>

      <textarea class="editor" />
    </at-ta>

    <at-ta :members="members" name-key="name">
      <!-- custom: with avatars -->
      <template #item="s">
        <img :src="s.item.avatar">
        <span v-text="s.item.name" />
      </template>

      <v-textarea class="vuetify-editor" v-model="text2" />
    </at-ta>

    <br>

    <at-ta :members="members" name-key="name">
      <!-- custom: with avatars -->
      <template #item="s">
        <img :src="s.item.avatar">
        <span v-text="s.item.name" />
      </template>

      <el-input type="textarea" v-model="text3" class="element-editor" />
    </at-ta>
  </div>
</template>

<script>
import At from './At.vue'
import AtTa from './AtTextarea.vue'

// testing dist
// import At from '../dist/vue-at'
// import AtTa from '../dist/vue-at-textarea'

// testing npm_pack
// import At from '../package/'
// import AtTa from '../package/dist/vue-at-textarea'

// testing node_modules
// import At from 'vue-at'
// import AtTa from 'vue-at/dist/vue-at-textarea'

let members = [
  /* eslint-disable */
  "Roxie Miles","grace.carroll",
  "小浩",
  "Helena Perez","melvin.miller",
  "椿木",
  "myrtie.green","elsie.graham","Elva Neal",
  "肖逵",
  "amy.sandoval","katie.leonard","lottie.hamilton",
  /* eslint-enable */
]
members = members.map((v, i) => {
  return {
    avatar: `https://randomuser.me/api/portraits/men/${i % 5}.jpg`,
    name: v
  }
})

export default {
  components: { At, AtTa },
  name: 'App',
  data () {
    const data = {
      members,
      text: `
<<< Textarea >>>
Awesome Electron
Useful resources for creating apps with Electron
Inspired by the awesome list thing. You might also like awesome-nodejs.
Example apps
Some good apps written with Electron.
Open Source
Atom - Code editor.
Nuclide - Unified IDE.
Playback - Video player.
Awesome Electron
Useful resources for creating apps with Electron
Inspired by the awesome list thing. You might also like awesome-nodejs.
Example apps
Some good apps written with Electron.
Open Source
Atom - Code editor.
Nuclide - Unified IDE.
Playback - Video player.
      `.trim(), // fix trailing abnormal nodes
      html: `
        <div>&lt;&lt;&lt; Content Editable Div &gt;&gt;&gt;</div><div>Awesome Electron&nbsp;
        <img src="awesome.svg"></div><div><img style="max-width: 50px;" src="electron.svg"></div><div>Useful resources for creating apps with&nbsp;Electron</div><div>Inspired by the&nbsp;awesome&nbsp;list thing. You might also like&nbsp;awesome-nodejs.</div><div>Example apps</div><div>Some good apps written with Electron.</div><div>Open Source</div><div>Atom&nbsp;- Code editor.</div><div>Nuclide&nbsp;- Unified IDE.</div><div>Playback&nbsp;- Video player.</div>
        <div>&lt;&lt;&lt; Content Editable Div &gt;&gt;&gt;</div><div>Awesome Electron&nbsp;<img style="max-width: 50px;" src="awesome.svg"></div><div><img style="max-width: 50px;" src="electron.svg"></div><div>Useful resources for creating apps with&nbsp;Electron</div><div>Inspired by the&nbsp;awesome&nbsp;list thing. You might also like&nbsp;awesome-nodejs.</div><div>Example apps</div><div>Some good apps written with Electron.</div><div>Open Source</div><div>Atom&nbsp;- Code editor.</div><div>Nuclide&nbsp;- Unified IDE.</div><div>Playback&nbsp;- Video player.</div>
      `.trim() // fix trailing abnormal nodes
    }
    data.text2 = data.text
    data.text3 = data.text
    data.html2 = data.html
    return data
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
  margin-top: 30px;
}

.tag {
  border-radius: 5px;
  background: beige;
  border: 1ps outset yellow;
}

.editor {
  width: 400px;
  height: 200px;
  overflow: auto;
  white-space: pre-wrap;
  border: solid 2px rgba(0,0,0,.5);
}
textarea {
  display: block;
}

.vuetify-editor {
  width: 400px;
}
.vuetify-editor textarea {
  height: 200px;
}
.v-text-field__details {
  display: none;
}

.element-editor {
  width: 400px;
}
.element-editor textarea {
  height: 200px;
}

.editor img {
  max-width: 10em;
  vertical-align: bottom;
}
.tag .avatar {
  max-width: 1em;
  vertical-align: middle;
}
textarea {
  padding: 0;
  font-size: inherit;
  resize: none;
}

/* override styles */
#app .atwho-li {
  padding: 0 4px;
}
#app .atwho-li img {
  height: 100%;
  width: auto;
  transform: scale(.8);
  -webkit-transform: scale(.8);
}
#app .atwho-li span {
  padding-left: 8px;
}
#app .atwho-wrap {
  display: inline-block;
  vertical-align: top;
  margin-left: 40px;
  margin-top: 30px;
}
</style>


================================================
FILE: src/At.scss
================================================
// atwho.css https://github.com/ichord/At.js
.atwho-view {
    // position:absolute;
    // top: 0;
    // left: 0;
    // display: none;
    // margin-top: 18px;
    // background: white;
    color: black;
    // border: 1px solid #DDD;
    border-radius: 3px;
    box-shadow: 0 0 5px rgba(0,0,0,0.1);
    min-width: 120px;
    z-index: 11110 !important;
}
.atwho-ul {
    /* width: 100px; */
    list-style:none;
    // padding:0;
    // margin:auto;
    // max-height: 200px;
    // overflow-y: auto;
}
.atwho-li {
    display: block;
    // padding: 5px 10px;
    // border-bottom: 1px solid #DDD;
    // cursor: pointer;
    /* border-top: 1px solid #C8C8C8; */
}

////// added 1
.atwho-view {
  // font-size: 14px;
  // min-width: 140px;
  // max-width: 180px;
  border-radius: 6px;
  // overflow: hidden;
  box-shadow: 0 0 10px 0 rgba(101, 111, 122, .5);
}
.atwho-ul {
  max-height: 135px;
  padding: 0;
  margin: 0;
}
.atwho-li {
  box-sizing: border-box;
  height: 27px;
  padding: 0 12px;
  white-space: nowrap;
  display: flex;
  align-items: center;
  span {
    overflow: hidden;
    text-overflow: ellipsis;
  }
}
.atwho-cur {
  // background: #44a8f2;
  background: #5BB8FF;
  color: white;
}

////// added 2
.atwho-wrap {
  position: relative;
}
.atwho-panel {
  position: absolute;
}
.atwho-inner {
  position: relative;
}
.atwho-view {
  position: absolute;
  bottom: 0;
  left: -0.8em; // 抵消左边距
  cursor: default;
  background-color: rgba(255,255,255,.94);
  min-width: 140px;
  max-width: 180px;
  max-height: 200px;
  overflow-y: auto;
  &::-webkit-scrollbar {
    width: 11px;
    height: 11px;
  }
  &::-webkit-scrollbar-track {
    // background-color: rgba(127, 127, 127, .1);
    background-color: #F5F5F5;
  }
  &::-webkit-scrollbar-thumb {
    min-height: 36px;
    border: 2px solid transparent;
    border-top: 3px solid transparent;
    border-bottom: 3px solid transparent;
    background-clip: padding-box;
    border-radius: 7px;
    // background-color: rgba(0, 0, 0, 0.2);
    background-color: #C4C4C4;
  }
}


================================================
FILE: src/At.vue
================================================
<script>
import {
  closest, getOffset, getPrecedingRange,
  getRange, applyRange,
  scrollIntoView, getAtAndIndex
} from './util'
import AtTemplate from './AtTemplate.vue'

export default {
  name: 'VueAt',
  mixins: [AtTemplate],
  emits: ['update:value', 'at', 'insert'],
  props: {
    value: {
      type: String, // value not required
      default: null
    },
    at: {
      type: String,
      default: null
    },
    ats: {
      type: Array,
      default: () => ['@']
    },
    suffix: {
      type: String,
      default: ' '
    },
    loop: {
      type: Boolean,
      default: true
    },
    allowSpaces: {
      type: Boolean,
      default: true
    },
    tabSelect: {
      type: Boolean,
      default: false
    },
    avoidEmail: {
      type: Boolean,
      default: true
    },
    showUnique: {
      type: Boolean,
      default: true
    },
    hoverSelect: {
      type: Boolean,
      default: true
    },
    members: {
      type: Array,
      default: () => []
    },
    nameKey: {
      type: String,
      default: ''
    },
    filterMatch: {
      type: Function,
      default: (name, chunk, at) => {
        // match at lower-case
        return name.toLowerCase()
          .indexOf(chunk.toLowerCase()) > -1
      }
    },
    deleteMatch: {
      type: Function,
      default: (name, chunk, suffix) => {
        return chunk === name + suffix
      }
    },
    scrollRef: {
      type: String,
      default: ''
    }
  },

  data () {
    return {
      // at[v-model] mode should be on only when
      // initial :value/v-model is present (not nil)
      bindsValue: this.value != null,
      customsEmbedded: false,
      hasComposition: false,
      atwho: null
    }
  },
  computed: {
    atItems () {
      return this.at ? [this.at] : this.ats
    },

    currentItem () {
      if (this.atwho) {
        return this.atwho.list[this.atwho.cur]
      }
      return ''
    },

    style () {
      if (this.atwho) {
        const { list, cur, x, y } = this.atwho
        const { wrap } = this.$refs
        if (wrap) {
          const offset = getOffset(wrap)
          const scrollLeft = this.scrollRef ? document.querySelector(this.scrollRef).scrollLeft : 0
          const scrollTop = this.scrollRef ? document.querySelector(this.scrollRef).scrollTop : 0
          const left = x + scrollLeft + window.pageXOffset - offset.left + 'px'
          const top = y + scrollTop + window.pageYOffset - offset.top + 'px'
          return { left, top }
        }
      }
      return null
    }
  },
  watch: {
    'atwho.cur' (index) {
      if (index != null) { // cur index exists
        this.$nextTick(() => {
          this.scrollToCur()
        })
      }
    },
    members () {
      this.handleInput(true)
    },
    value (value, oldValue) {
      if (this.bindsValue) {
        this.handleValueUpdate(value)
      }
    }
  },
  mounted () {
    // vue3 migration.5
    // [Vue warn]: (deprecation INSTANCE_SCOPED_SLOTS) vm.$scopedSlots has been removed. Use vm.$slots instead.
    // Details: https://v3-migration.vuejs.org/breaking-changes/slots-unification.html
    if (this.$slots.embeddedItem) {
      this.customsEmbedded = true
    }
    if (this.bindsValue) {
      this.handleValueUpdate(this.value)
    }
  },

  methods: {
    itemName (v) {
      const { nameKey } = this
      return nameKey ? v[nameKey] : v
    },
    isCur (index) {
      return index === this.atwho.cur
    },
    handleValueUpdate (value) {
      const el = this.$el.querySelector('[contenteditable]')
      if (value !== el.innerHTML) { // avoid range reset
        el.innerHTML = value
        this.dispatchInput()
      }
    },
    dispatchInput () {
      let el = this.$el.querySelector('[contenteditable]')
      let ev = new Event('input', { bubbles: true })
      el.dispatchEvent(ev)
    },

    handleItemHover (e) {
      if (this.hoverSelect) {
        this.selectByMouse(e)
      }
    },
    handleItemClick (e) {
      this.selectByMouse(e)
      this.insertItem()
    },
    handleDelete (e) {
      const range = getPrecedingRange()
      if (range) {
        // fixme: Very bad code from me
        if (this.customsEmbedded && range.endOffset >= 1) {
          let a = range.endContainer.childNodes[range.endOffset] ||
            range.endContainer.childNodes[range.endOffset - 1]
          if (!a || a.nodeType === Node.TEXT_NODE && !/^\s?$/.test(a.data)) {
            return
          } else if (a.nodeType === Node.TEXT_NODE) {
            if (a.previousSibling) a = a.previousSibling
          } else {
            if (a.previousElementSibling) a = a.previousElementSibling
          }
          let ch = [].slice.call(a.childNodes)
          ch = [].reverse.call(ch)
          ch.unshift(a)
          let last
          ;[].some.call(ch, c => {
            if (c.getAttribute && c.getAttribute('data-at-embedded') != null) {
              last = c
              return true
            }
          })
          if (last) {
            e.preventDefault()
            e.stopPropagation()
            const r = getRange()
            if (r) {
              r.setStartBefore(last)
              r.deleteContents()
              applyRange(r)
              this.handleInput()
            }
          }
          return
        }

        const { atItems, members, suffix, deleteMatch, itemName } = this
        const text = range.toString()
        const { at, index } = getAtAndIndex(text, atItems)

        if (index > -1) {
          const chunk = text.slice(index + at.length)
          const has = members.some(v => {
            const name = itemName(v)
            return deleteMatch(name, chunk, suffix)
          })
          if (has) {
            e.preventDefault()
            e.stopPropagation()
            const r = getRange()
            if (r) {
              r.setStart(r.endContainer, index)
              r.deleteContents()
              applyRange(r)
              this.handleInput()
            }
          }
        }
      }
    },
    handleKeyDown (e) {
      const { atwho } = this
      if (atwho) {
        if (e.keyCode === 38 || e.keyCode === 40) { // ↑/↓
          if (!(e.metaKey || e.ctrlKey)) {
            e.preventDefault()
            e.stopPropagation()
            this.selectByKeyboard(e)
          }
          return
        }
        if (e.keyCode === 13 || (this.tabSelect && e.keyCode === 9)) { // enter or tab
          e.preventDefault()
          e.stopPropagation()
          this.insertItem()
          return
        }
        if (e.keyCode === 27) { // esc
          this.closePanel()
          return
        }
      }

      // 为了兼容ie ie9~11 editable无input事件 只能靠keydown触发 textarea正常
      // 另 ie9 textarea的delete不触发input
      const isValid = e.keyCode >= 48 && e.keyCode <= 90 || e.keyCode === 8
      if (isValid) {
        setTimeout(() => {
          this.handleInput()
        }, 50)
      }

      if (e.keyCode === 8) {
        this.handleDelete(e)
      }
    },

    // compositionStart -> input -> compositionEnd
    handleCompositionStart () {
      this.hasComposition = true
    },
    handleCompositionEnd () {
      this.hasComposition = false
      this.handleInput()
    },
    handleInput (keep) {
      if (this.hasComposition) return
      const el = this.$el.querySelector('[contenteditable]')

      // vue3 migration.2.1
      // https://vuejs.org/guide/components/events.html#usage-with-v-model
      // https://laracasts.com/discuss/channels/vue/how-do-emit-to-v-model-in-vue-3
      // this.$emit('input', el.innerHTML)
      this.$emit('update:value', el.innerHTML)

      const range = getPrecedingRange()

      if (range) {
        if (keep) {
          // exit the function if the range is not inside this.$el
          let container = range.commonAncestorContainer;
          while (container) {
            if (container === this.$el) break;
            container = container.parentElement;
          }
          if (!container) return;
        }

        const { atItems, avoidEmail, allowSpaces, showUnique } = this

        let show = true
        const text = range.toString()

        const { at, index } = getAtAndIndex(text, atItems)

        if (index < 0) show = false
        const prev = text[index - 1]

        const chunk = text.slice(index + at.length, text.length)

        if (avoidEmail) {
          // 上一个字符不能为字母数字 避免与邮箱冲突
          // 微信则是避免 所有字母数字及半角符号
          if (/^[a-z0-9]$/i.test(prev)) show = false
        }

        if (!allowSpaces && /\s/.test(chunk)) {
          show = false
        }

        // chunk以空白字符开头不匹配 避免`@ `也匹配
        if (/^\s/.test(chunk)) show = false

        if (!show) {
          this.closePanel()
        } else {
          const { members, filterMatch, itemName } = this
          if (!keep && chunk) { // fixme: should be consistent with AtTextarea.vue
            this.$emit('at', chunk)
          }
          const matched = members.filter(v => {
            const name = itemName(v)
            return filterMatch(name, chunk, at, v)
          })

          show = false
          if (matched.length) {
            show = true
            if (!showUnique) {
              let item = matched[0]
              if (chunk === itemName(item)) {
                show = false
              }
            }
          }

          if (show) {
            this.openPanel(matched, range, index, at)
          } else {
            this.closePanel()
          }
        }
      }
    },

    closePanel () {
      if (this.atwho) {
        this.atwho = null
      }
    },
    openPanel (list, range, offset, at) {
      const fn = () => {
        const r = range.cloneRange()
        r.setStart(r.endContainer, offset + at.length) // 从@后第一位开始
        // todo: 根据窗口空间 判断向上或是向下展开
        const rect = r.getClientRects()[0]
        this.atwho = {
          range,
          offset,
          list,
          x: rect.left,
          y: rect.top - 4,
          cur: 0 // todo: 尽可能记录
        }
      }
      if (this.atwho) {
        fn()
      } else { // 焦点超出了显示区域 需要提供延时以移动指针 再计算位置
        setTimeout(fn, 10)
      }
    },

    scrollToCur () {
      // vue3 migration.6
      // fix: [Vue warn]: Missing ref owner context. ref cannot be used on hoisted vnodes.
      // A vnode with ref must be created inside the render function.
      // at selectByMouse
      // at handleItemHover
      // const curEl = this.$refs.cur[0]
      let { wrap } = this.$refs
      let { cur } = this.atwho
      const curEl = wrap.querySelector(`.atwho-li[data-index="${cur}"]`)

      const scrollParent = curEl.parentElement.parentElement // .atwho-view
      scrollIntoView(curEl, scrollParent)
    },
    selectByMouse (e) {
      const el = closest(e.target, d => {
        return d.getAttribute('data-index')
      })
      const cur = +el.getAttribute('data-index')
      this.atwho = {
        ...this.atwho,
        cur
      }
    },
    selectByKeyboard (e) {
      const offset = e.keyCode === 38 ? -1 : 1
      const { cur, list } = this.atwho
      const nextCur = this.loop
        ? (cur + offset + list.length) % list.length
        : Math.max(0, Math.min(cur + offset, list.length - 1))
      this.atwho = {
        ...this.atwho,
        cur: nextCur
      }
    },

    // todo: 抽离成库并测试
    insertText (text, r) {
      r.deleteContents()
      const node = r.endContainer
      if (node.nodeType === Node.TEXT_NODE) {
        const cut = r.endOffset
        node.data = node.data.slice(0, cut) +
          text + node.data.slice(cut)
        r.setEnd(node, cut + text.length)
      } else {
        const t = document.createTextNode(text)
        r.insertNode(t)
        r.setEndAfter(t)
      }
      r.collapse(false) // 参数在IE下必传
      applyRange(r)
      this.dispatchInput()
    },

    insertHtml (html, r) {
      r.deleteContents()
      const node = r.endContainer
      const newElement = document.createElement('span')

      // Seems `contentediable=false` should includes spaces,
      // otherwise, caret can't be placed well across them
      newElement.appendChild(document.createTextNode(' '))
      newElement.appendChild(this.htmlToElement(html))
      newElement.appendChild(document.createTextNode(' '))
      newElement.setAttribute('data-at-embedded', '')
      newElement.setAttribute('contenteditable', false)

      if (node.nodeType === Node.TEXT_NODE) {
        const cut = r.endOffset
        let secondPart = node.splitText(cut)
        node.parentNode.insertBefore(newElement, secondPart)
        r.setEndBefore(secondPart)
      } else {
        const t = document.createTextNode(this.suffix)
        r.insertNode(newElement)
        r.setEndAfter(newElement)
        r.insertNode(t)
        r.setEndAfter(t)
      }
      r.collapse(false) // 参数在IE下必传
      applyRange(r)
    },

    insertItem () {
      const { range, offset, list, cur } = this.atwho
      const { suffix, atItems, itemName, customsEmbedded } = this
      const r = range.cloneRange()
      const text = range.toString()
      const { at, index } = getAtAndIndex(text, atItems)

      // Leading `@` is automatically dropped as `customsEmbedded=true`
      // You can fully custom the output inside the embedded slot
      const start = customsEmbedded ? index : index + at.length
      r.setStart(r.endContainer, start)

      // hack: 连续两次 可以确保click后 focus回来 range真正生效
      applyRange(r)
      applyRange(r)
      const curItem = list[cur]

      if (customsEmbedded) {
        // `suffix` is ignored as `customsEmbedded=true` has to be
        // wrapped around by spaces

        // vue3 migration.7
        // fix: Uncaught TypeError: Cannot read properties of undefined (reading 'innerHTML')
        // at Proxy.insertItem (At.vue?075e:490:1)
        // at Proxy.handleItemClick (At.vue?075e:184:1)
        // const html = this.$refs.embeddedItem.firstChild.innerHTML
        const html = this.$refs.embeddedItem.firstElementChild.innerHTML

        this.insertHtml(html, r)
      } else {
        const t = itemName(curItem) + suffix
        this.insertText(t, r)
      }
      scrollIntoView(window.getSelection())

      this.$emit('insert', curItem)
      this.handleInput()

      // fix safari: use `r` instead of `window.getSelection()`
      // scrollIntoView(window.getSelection())
      scrollIntoView(r)
    },
    htmlToElement (html) {
      const template = document.createElement('template')
      html = html.trim() // Never return a text node of whitespace as the result
      template.innerHTML = html
      return template.content.firstChild
    }
  }
}
</script>


================================================
FILE: src/AtTemplate.vue
================================================
<template>
  <div
    ref="wrap"
    class="atwho-wrap"
    @compositionstart="handleCompositionStart"
    @compositionend="handleCompositionEnd"
    @input="handleInput()"
    @keydown.capture="handleKeyDown"
  >
    <div
      v-if="atwho"
      class="atwho-panel"
      :style="style"
    >
      <div class="atwho-inner">
        <div class="atwho-view">
          <ul class="atwho-ul">
            <li
              v-for="(item, index) in atwho.list"
              class="atwho-li"
              :key="index"
              :class="isCur(index) && 'atwho-cur'"
              :data-index="index"
              @mouseenter="handleItemHover"
              @click="handleItemClick"
            >
              <slot
                name="item"
                :item="item"
              >
                <span v-text="itemName(item)" />
              </slot>
            </li>
          </ul>
        </div>
      </div>
    </div>
    <span
      v-show="false"
      ref="embeddedItem"
    >
      <slot
        name="embeddedItem"
        :current="currentItem"
      />
    </span>
    <slot />
  </div>
</template>

<style lang="scss" src="./At.scss"></style>


================================================
FILE: src/AtTextarea.vue
================================================
<script>
import At from './At.vue'
import getCaretCoordinates from 'textarea-caret'
import { getAtAndIndex, getPrecedingRange } from './util'

export default {
  extends: At,
  name: 'AtTextarea',
  emits: ['update:value', 'at', 'insert'],
  computed: {
    style () {
      if (this.atwho) {
        const { list, cur, x, y } = this.atwho
        const { wrap } = this.$refs
        const el = this.$el.querySelector('textarea')
        if (wrap) {
          const left = x + el.offsetLeft - el.scrollLeft + 'px'
          const top = y + el.offsetTop - el.scrollTop + 'px'
          return { left, top }
        }
      }
      return null
    }
  },
  methods: {
    handleValueUpdate (value) {
      const el = this.$el.querySelector('textarea')
      if (value !== el.value) { // avoid range reset
        el.value = value
        this.dispatchInput()
      }
    },
    dispatchInput () {
      let el = this.$el.querySelector('textarea')
      let ev = new Event('input', { bubbles: true })
      el.dispatchEvent(ev)
    },

    handleDelete (e) {
      const el = this.$el.querySelector('textarea')
      // fix https://github.com/fritx/vue-at/issues/139
      const hasSelection = el.selectionEnd - el.selectionStart > 0
      if (hasSelection) return
      const text = el.value.slice(0, el.selectionEnd)
      if (text) {
        const { atItems, members, suffix, deleteMatch, itemName } = this
        const { at, index } = getAtAndIndex(text, atItems)
        if (index > -1) {
          const chunk = text.slice(index + at.length)
          const has = members.some(v => {
            const name = itemName(v)
            return deleteMatch(name, chunk, suffix)
          })
          if (has) {
            el.value = el.value.slice(0, index) +
              el.value.slice(el.selectionEnd - 1)
            el.selectionStart = index + 1
            el.selectionEnd = index + 1
            this.handleInput()
          }
        }
      }
    },

    handleInput (keep) {
      if (this.hasComposition) return
      const el = this.$el.querySelector('textarea')

      // vue3 migration.2.1
      // https://vuejs.org/guide/components/events.html#usage-with-v-model
      // https://laracasts.com/discuss/channels/vue/how-do-emit-to-v-model-in-vue-3
      // this.$emit('input', el.value)
      this.$emit('update:value', el.value)

      if (keep) {
        // exit the function if the range is not inside this.$el
        const range = getPrecedingRange()
        let container = range && range.commonAncestorContainer;
        while (container) {
          if (container === this.$el) break;
          container = container.parentElement;
        }
        if (!container) return;
      }

      const text = el.value.slice(0, el.selectionEnd)
      if (text) {
        const { atItems, avoidEmail, allowSpaces } = this
        let show = true
        const { at, index } = getAtAndIndex(text, atItems)
        if (index < 0) show = false
        const prev = text[index - 1]
        const chunk = text.slice(index + at.length, text.length)
        if (avoidEmail) {
          // 上一个字符不能为字母数字 避免与邮箱冲突
          // 微信则是避免 所有字母数字及半角符号
          if (/^[a-z0-9]$/i.test(prev)) show = false
        }
        if (!allowSpaces && /\s/.test(chunk)) {
          show = false
        }

        // chunk以空白字符开头不匹配 避免`@ `也匹配
        if (/^\s/.test(chunk)) show = false
        if (!show) {
          this.closePanel()
        } else {
          const { members, filterMatch, itemName } = this
          if (!keep) { // fixme: should be consistent with At.vue
            this.$emit('at', chunk)
          }
          const matched = members.filter(v => {
            const name = itemName(v)
            return filterMatch(name, chunk, at, v)
          })
          if (matched.length) {
            this.openPanel(matched, chunk, index, at, keep)
          } else {
            this.closePanel()
          }
        }
      } else {
        this.closePanel()
      }
    },

    openPanel (list, chunk, offset, at) {
      const fn = () => {
        const el = this.$el.querySelector('textarea')
        const atEnd = offset + at.length // 从@后第一位开始
        const rect = getCaretCoordinates(el, atEnd)
        this.atwho = {
          chunk,
          offset,
          list,
          atEnd,
          x: rect.left,
          y: rect.top - 4,
          cur: 0, // todo: 尽可能记录
        }
      }
      if (this.atwho) {
        fn()
      } else { // 焦点超出了显示区域 需要提供延时以移动指针 再计算位置
        setTimeout(fn, 10)
      }
    },

    // todo: 抽离成库并测试
    insertText (text, ta) {
      const start = ta.selectionStart
      const end = ta.selectionEnd
      ta.value = ta.value.slice(0, start) +
        text + ta.value.slice(end)
      const newEnd = start + text.length
      ta.selectionStart = newEnd
      ta.selectionEnd = newEnd
      this.dispatchInput()
    },
    insertItem () {
      const { chunk, offset, list, cur, atEnd } = this.atwho
      const { suffix, atItems, itemName } = this
      const el = this.$el.querySelector('textarea')
      const text = el.value.slice(0, atEnd)
      const { at, index } = getAtAndIndex(text, atItems)
      const start = index + at.length // 从@后第一位开始
      el.selectionStart = start
      el.focus() // textarea必须focus回来
      const curItem = list[cur]
      const t = itemName(curItem) + suffix
      this.insertText(t, el)
      this.$emit('insert', curItem)
      this.handleInput()
    }
  }
}
</script>


================================================
FILE: src/main.js
================================================
// Element-Plus x Vue3
// import 'element-ui/lib/theme-chalk/index.css'
import 'element-plus/dist/index.css'
import ElementPlus from 'element-plus'

// Vuetify x Vue3
// https://next.vuetifyjs.com/en/getting-started/installation/
import 'vuetify/styles'
import { VTextarea } from 'vuetify/components'
import { createVuetify } from 'vuetify'

// import { createApp, configureCompat } from 'vue'
import { createApp } from 'vue'
import App from './App.vue'

let configureCompat = () => {}

configureCompat({
  // vue3 migration.3
  // fix: [Vue warn]: (deprecation WATCH_ARRAY) "watch" option or vm.$watch on an array value will no longer trigger on array mutation unless the "deep" option is specified. If current usage is intended, you can disable the compat behavior and suppress this warning with:
  //   configureCompat({ WATCH_ARRAY: false })
  // Details: https://v3-migration.vuejs.org/breaking-changes/watch.html
  WATCH_ARRAY: false,

  // vue3 migration.4
  // fix: [Vue warn]: (deprecation ATTR_ENUMERATED_COERCION) Enumerated attribute "contenteditable" with v-bind value `` will render the value as-is instead of coercing the value to "true" in Vue 3. Always use explicit "true" or "false" values for enumerated attributes. If the usage is intended, you can disable the compat behavior and suppress this warning with:
  //   configureCompat({ ATTR_ENUMERATED_COERCION: false })
  // Details: https://v3-migration.vuejs.org/breaking-changes/attribute-coercion.html
  ATTR_ENUMERATED_COERCION: false,
})

// vue3 migration.1
// fix: [Vue warn]: (deprecation GLOBAL_MOUNT) The global app bootstrapping API has changed: vm.$mount() and the "el" option have been removed. Use createApp(RootComponent).mount() instead.
// Details: https://v3-migration.vuejs.org/breaking-changes/global-api.html#mounting-app-instance
let app = createApp(App)

let vuetify = createVuetify({
  components: { VTextarea }
})

app.use(vuetify)
app.use(ElementPlus)

app.mount('#app')


================================================
FILE: src/util.js
================================================
// Scrolling The Selection Into View
// http://roysharon.com/blog/37
export function scrollIntoView (t, scrollParent) {
  if (typeof (t) !== 'object') return

  if (t.getRangeAt) {
    // we have a Selection object
    if (t.rangeCount === 0) return
    t = t.getRangeAt(0)
  }

  if (t.cloneRange) {
    // we have a Range object
    let r = t.cloneRange() // do not modify the source range
    r.collapse(true) // collapse to start
    t = r.startContainer
    // if start is an element, then startOffset is the child number
    // in which the range starts
    if (t.nodeType === 1) t = t.childNodes[r.startOffset]
  }
  if (!t) return

  // if t is not an element node, then we need to skip back until we find the
  // previous element with which we can call scrollIntoView()
  let o = t
  while (o && o.nodeType !== 1) o = o.previousSibling
  t = o || t.parentNode
  if (t) scrollIntoViewElement(t, scrollParent)
}

// bug report: https://github.com/vuejs/awesome-vue/pull/1028
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoViewIfNeeded
export function scrollIntoViewElement (el, scrollParent) {
  if (el.scrollIntoViewIfNeeded) {
    el.scrollIntoViewIfNeeded(false) // alignToCenter=false
  } else {
    // should not use `el.scrollIntoView(false)` // alignToTop=false
    // bug report: https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move
    scrollParent = scrollParent || el.parentElement
    const diff = el.offsetTop - scrollParent.scrollTop
    if (diff < 0 || diff > scrollParent.offsetHeight - el.offsetHeight) {
      scrollParent.scrollTop = el.offsetTop
    }
  }
}

export function applyRange (range) {
  const selection = window.getSelection()
  if (selection) { // 容错
    selection.removeAllRanges()
    selection.addRange(range)
  }
}
export function getRange () {
  const selection = window.getSelection()
  if (selection && selection.rangeCount > 0) {
    return selection.getRangeAt(0)
  }
}

export function getAtAndIndex (text, ats) {
  return ats.map((at) => {
    return { at, index: text.lastIndexOf(at) }
  }).reduce((a, b) => {
    return a.index > b.index ? a : b
  })
}

/* eslint-disable */
// http://stackoverflow.com/questions/26747240/plain-javascript-replication-to-offset-and-position
export function getOffset(element, target) {
    // var element = document.getElementById(element),
    //     target  = target ? document.getElementById(target) : window;
    target = target || window
    var offset = {top: element.offsetTop, left: element.offsetLeft},
        parent = element.offsetParent;
    while (parent != null && parent != target) {
       offset.left += parent.offsetLeft;
       offset.top  += parent.offsetTop;
       parent = parent.offsetParent;
    }
    return offset;
}
// http://stackoverflow.com/questions/3972014/get-caret-position-in-contenteditable-div
export function closest (el, predicate) {
  /* eslint-disable */
  do if (predicate(el)) return el;
  while (el = el && el.parentNode);
}
// http://stackoverflow.com/questions/15157435/get-last-character-before-caret-position-in-javascript
// 修复 "空格+表情+空格+@" range报错 应设(endContainer, 0)
// stackoverflow上的这段代码有bug
export function getPrecedingRange() {
  const r = getRange()
  if (r) {
    const range = r.cloneRange()
    range.collapse(true)
    // var el = closest(range.endContainer, d => d.contentEditable)
    // range.setStart(el, 0)
    range.setStart(range.endContainer, 0)
    return range
  }
}
/* eslint-enable */


================================================
FILE: test_umd.html
================================================
<meta charset="utf8">

<!-- for Vue2 -->
<!-- <script src="//unpkg.com/vue@2"></script> -->
<!-- for Vue3 -->
<script src="//unpkg.com/vue@3"></script>

<script src="dist/vue-at.umd.js"></script>
<script src="dist/vue-at-textarea.umd.js"></script>

<div id="app">
  <h2>UMD: Vue3 x Vue-At</h2>
  <at v-model:value="html">
    <div contenteditable></div>
  </at>
  <at-ta v-model:value="text">
    <textarea></textarea>
  </at-ta>
  <at-ta>
    <textarea v-model="text1"></textarea>
  </at-ta>
  <br>
  html: {{html}}
  <br>
  text: {{text}}
  <br>
  text1: {{text1}}
</div>

<script>
Vue.createApp({
  components: { At, AtTa: AtTextarea },
  data() {
    return {
      html: 'content-editable',
      text: 'textarea',
      text1: 'textarea'
    }
  }
}).mount('#app')
</script>


================================================
FILE: vue.config.js
================================================

module.exports = {
  publicPath: './',
  css: {
    extract: false, // inline css into js
    loaderOptions: {
      sass: {},
      scss: {}
    }
  },
  chainWebpack: config => {
    // config.resolve.alias.set('vue', '@vue/compat')

    config.module
      .rule('vue')
      .use('vue-loader')
      .tap(options => {
        return {
          ...options,
          compilerOptions: {
            compatConfig: {
              MODE: 2
            }
          }
        }
      })
  }
}
Download .txt
gitextract_8hv1zyjq/

├── .eslintignore
├── .eslintrc.js
├── .github/
│   └── FUNDING.yml
├── .gitignore
├── .npmrc
├── LICENSE
├── README.md
├── index.html
├── package.json
├── src/
│   ├── App.vue
│   ├── At.scss
│   ├── At.vue
│   ├── AtTemplate.vue
│   ├── AtTextarea.vue
│   ├── main.js
│   └── util.js
├── test_umd.html
└── vue.config.js
Download .txt
SYMBOL INDEX (8 symbols across 1 files)

FILE: src/util.js
  function scrollIntoView (line 3) | function scrollIntoView (t, scrollParent) {
  function scrollIntoViewElement (line 33) | function scrollIntoViewElement (el, scrollParent) {
  function applyRange (line 47) | function applyRange (range) {
  function getRange (line 54) | function getRange () {
  function getAtAndIndex (line 61) | function getAtAndIndex (text, ats) {
  function getOffset (line 71) | function getOffset(element, target) {
  function closest (line 85) | function closest (el, predicate) {
  function getPrecedingRange (line 93) | function getPrecedingRange() {
Condensed preview — 18 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (50K chars).
[
  {
    "path": ".eslintignore",
    "chars": 22,
    "preview": "/dist/\n/package/dist/\n"
  },
  {
    "path": ".eslintrc.js",
    "chars": 693,
    "preview": "module.exports = {\n  root: true,\n  extends: [\n    'plugin:vue/vue3-recommended',\n    '@vue/standard'\n  ],\n  rules: {\n   "
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 444,
    "preview": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [u"
  },
  {
    "path": ".gitignore",
    "chars": 86,
    "preview": ".DS_Store\nnode_modules*/\ndist/\ndist_demo/\npackage/\n*.tgz\nnpm-debug.log\nyarn-error.log\n"
  },
  {
    "path": ".npmrc",
    "chars": 19,
    "preview": "package-lock=false\n"
  },
  {
    "path": "LICENSE",
    "chars": 1085,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016-present, Fritz Lin\n\nPermission is hereby granted, free of charge, to any perso"
  },
  {
    "path": "README.md",
    "chars": 5293,
    "preview": "# vue-at\n\n<a href=\"https://we-demo.github.io/vue-at-vite-app/\"><img width=\"76\" height=\"20\" src=\"https://img.shields.io/w"
  },
  {
    "path": "index.html",
    "chars": 254,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-wid"
  },
  {
    "path": "package.json",
    "chars": 1647,
    "preview": "{\n  \"name\": \"vue-at\",\n  \"description\": \"At.js for Vue\",\n  \"version\": \"3.0.0-alpha.3\",\n  \"author\": \"Fritz Lin <uxfritz@16"
  },
  {
    "path": "src/App.vue",
    "chars": 6777,
    "preview": "<template>\n  <div id=\"app\">\n\n    <!--\n      vue3 migration.2\n      fix: [Vue warn]: (deprecation COMPONENT_V_MODEL) v-mo"
  },
  {
    "path": "src/At.scss",
    "chars": 2046,
    "preview": "// atwho.css https://github.com/ichord/At.js\n.atwho-view {\n    // position:absolute;\n    // top: 0;\n    // left: 0;\n    "
  },
  {
    "path": "src/At.vue",
    "chars": 14598,
    "preview": "<script>\nimport {\n  closest, getOffset, getPrecedingRange,\n  getRange, applyRange,\n  scrollIntoView, getAtAndIndex\n} fro"
  },
  {
    "path": "src/AtTemplate.vue",
    "chars": 1168,
    "preview": "<template>\n  <div\n    ref=\"wrap\"\n    class=\"atwho-wrap\"\n    @compositionstart=\"handleCompositionStart\"\n    @compositione"
  },
  {
    "path": "src/AtTextarea.vue",
    "chars": 5458,
    "preview": "<script>\nimport At from './At.vue'\nimport getCaretCoordinates from 'textarea-caret'\nimport { getAtAndIndex, getPreceding"
  },
  {
    "path": "src/main.js",
    "chars": 1967,
    "preview": "// Element-Plus x Vue3\n// import 'element-ui/lib/theme-chalk/index.css'\nimport 'element-plus/dist/index.css'\nimport Elem"
  },
  {
    "path": "src/util.js",
    "chars": 3506,
    "preview": "// Scrolling The Selection Into View\n// http://roysharon.com/blog/37\nexport function scrollIntoView (t, scrollParent) {\n"
  },
  {
    "path": "test_umd.html",
    "chars": 781,
    "preview": "<meta charset=\"utf8\">\n\n<!-- for Vue2 -->\n<!-- <script src=\"//unpkg.com/vue@2\"></script> -->\n<!-- for Vue3 -->\n<script sr"
  },
  {
    "path": "vue.config.js",
    "chars": 492,
    "preview": "\nmodule.exports = {\n  publicPath: './',\n  css: {\n    extract: false, // inline css into js\n    loaderOptions: {\n      sa"
  }
]

About this extraction

This page contains the full source code of the fritx/vue-at GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 18 files (45.2 KB), approximately 13.0k tokens, and a symbol index with 8 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!