[
  {
    "path": ".eslintignore",
    "content": "/dist/\n/package/dist/\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  root: true,\n  extends: [\n    'plugin:vue/vue3-recommended',\n    '@vue/standard'\n  ],\n  rules: {\n    'vue/multiline-html-element-content-newline': 'off',\n    'vue/first-attribute-linebreak': 'off',\n    'vue/max-attributes-per-line': 'off',\n    'vue/order-in-components': 'off',\n    'vue/attributes-order': 'off',\n    'vue/html-indent': 'off',\n    'no-irregular-whitespace': 'off',\n    'no-mixed-operators': 'off',\n    'no-unused-vars': 'off',\n    'prefer-const': 'off',\n    'comma-dangle': 'off',\n    'max-len': 'off',\n    'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',\n    'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'\n  }\n}\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]\npatreon: # Replace with a single Patreon username\nopen_collective: vue-at  # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncustom: # Replace with a single custom sponsorship URL\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\nnode_modules*/\ndist/\ndist_demo/\npackage/\n*.tgz\nnpm-debug.log\nyarn-error.log\n"
  },
  {
    "path": ".npmrc",
    "content": "package-lock=false\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016-present, Fritz Lin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# 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/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>\n\n<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\">\n\n- [x] Chrome / Firefox / Edge / IE9~IE11\n- [x] Plain-text based, no jQuery, no extra nodes\n- [x] Content-Editable / Textarea\n- [x] Avatars, custom templates\n- [x] Vite / Vue3 / Vue2 / Vue1\n- [x] Vuetify / Element UI / Element Plus\n- [x] Vue-CLI build migration\n- [ ] Vite build migration\n- [x] CommonJS / UMD Support\n\nPlayground: https://we-demo.github.io/vue-at-vite-app/<br>\nVue2 Docs: https://github.com/fritx/vue-at/tree/vue2#readme<br>\nVue3 Docs: See below<br>\nSee also: [react-at](https://github.com/fritx/react-at)\n\n**If you're using Vue2, read [branch vue2](https://github.com/fritx/vue-at/tree/vue2#readme) instead.**\n\n```plain\nnpm i vue-at@next  # for Vue3 (branch vue3)\nnpm i vue-at@2.x  # for Vue2 (branch vue2)\nnpm i vue-at@1.x  # for Vue1 (branch vue1-legacy)\nnpm i vue1-at  # for Vue1 (branch vue1-new)\n```\n\n```vue\n<template>\n  <at :members=\"members\">\n    <div :contenteditable=\"true\"></div>\n  </at>\n  <at-ta :members=\"members\">\n    <textarea></textarea>\n  </at-ta>\n</template>\n\n<script>\nimport At from 'vue-at' // for content-editable\nimport AtTa from 'vue-at/dist/vue-at-textarea' // for textarea\n\nexport default {\n  components: { At, AtTa },\n  data () {\n    return {\n      members: ['Roxie Miles', 'grace.carroll', '小浩']\n    }\n  }\n}\n</script>\n\n<style>\n#app .atwho-view { /* more */ }\n#app .atwho-ul { /* more */ }\n</style>\n```\n\n## UMD Also Supported\n\n```html\n<!-- for Vue2 -->\n<script src=\"//unpkg.com/vue@2\"></script>\n<script src=\"//unpkg.com/vue-at@2/dist/vue-at.umd.js\"></script>\n<script src=\"//unpkg.com/vue-at@2/dist/vue-at-textarea.umd.js\"></script>\n<!-- ...-->\n\n<!-- for Vue3 -->\n<script src=\"//unpkg.com/vue@3\"></script>\n<script src=\"//unpkg.com/vue-at@next/dist/vue-at.umd.js\"></script>\n<script src=\"//unpkg.com/vue-at@next/dist/vue-at-textarea.umd.js\"></script>\n<div id=\"app\">\n  <at v-model:value=\"html\">\n    <div contenteditable></div>\n  </at>\n  <at-textarea>\n    <textarea v-model=\"text\"></textarea>\n  </at-textarea>\n</div>\n<script>\nVue.createApp({\n  components: { At, AtTextarea },\n  // ...\n}).mount('#app')\n</script>\n```\n\n## Using V-Model (Recommended)\n\nWith Content-Editable, use `<at v-model:value=\"v\">`<br>\nWith Textarea, you can use either `<at-ta v-model:value=\"v\">` or `<textarea v-model=\"v\">`\n\n```vue\n<at v-model:value=\"html\">\n  <div :contenteditable=\"true\"></div>\n</at>\n<at-ta v-model:value=\"text\">\n  <textarea></textarea>\n</at-ta>\n<at-ta>\n  <textarea v-model=\"text\"></textarea>\n</at-ta>\n```\n\n## Custom Templates\n\n### Custom List\n\n```vue\n<template>\n  <at :members=\"members\" name-key=\"name\">\n    <template slot=\"item\" slot-scope=\"s\">\n      <img :src=\"s.item.avatar\">\n      <span v-text=\"s.item.name\"></span>\n    </template>\n    <div :contenteditable=\"true\"></div>\n  </at>\n</template>\n\n<script>\n// ...\nmembers: [{\n  avatar: 'https://randomuser.me/api/portraits/men/2.jpg',\n  name: 'myrtie.green'\n}, {\n  avatar: 'https://randomuser.me/api/portraits/men/8.jpg',\n  name: '椿木'\n}]\n</script>\n\n<style>\n#app .atwho-li { /* more */ }\n#app .atwho-li img { /* more */ }\n#app .atwho-li span { /* more */ }\n</style>\n```\n\n#### Custom List with Vue 1.x\n\nThere is no \"scoped slot\" feature in Vue 1.<br>\nUse a \"normal slot\" with `data-` attribute instead.\n\n```vue\n<!-- vue1-at for vue@1.x -->\n<template slot=\"item\">\n  <img data-src=\"item.avatar\">\n  <span data-text=\"item.name\"></span>\n</template>\n```\n\n### Custom Tags\n\nThis gives you the option of changing the style of inserted tagged items. It is only supported for ContentEditable version, not Textarea.\n\n```vue\n<span slot=\"embeddedItem\" slot-scope=\"s\">\n  <span class=\"tag\"><img :src=\"s.current.avatar\">{{ s.current.name }}</span>\n</span>\n\n<!-- with Vue 2.6+ 'v-slot' / '#slot' directive -->\n<!-- note at least two '<span>' wrapper are required to work -->\n<template #embeddedItem=\"s\">\n  <span><span class=\"tag\"><img class=\"avatar\" :src=\"s.current.avatar\">{{ s.current.name }}</span></span>\n</template>\n```\n\n## Used with 3rd-party libraries\n\n### Vuetify v-textarea\n\n```vue\n<at-ta :members=\"members\">\n  <!-- slots -->\n  <v-textarea v-model=\"text\"></v-textarea>\n</at-ta>\n```\n\n### Element UI / Element-Plus el-input\n\n```vue\n<at-ta :members=\"members\">\n  <!-- slots -->\n  <el-input v-model=\"text\" type=\"textarea\"></el-input>\n</at-ta>\n```\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width\">\n    <title>vue-at</title>\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script src=\"dist/demo.js\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"vue-at\",\n  \"description\": \"At.js for Vue\",\n  \"version\": \"3.0.0-alpha.3\",\n  \"author\": \"Fritz Lin <uxfritz@163.com>\",\n  \"repository\": \"https://github.com/fritx/vue-at\",\n  \"scripts\": {\n    \"lint:fix\": \"vue-cli-service lint\",\n    \"lint\": \"vue-cli-service lint --no-fix\",\n    \"dev:dist\": \"vue-cli-service serve --skip-plugins eslint\",\n    \"dev\": \"vue-cli-service serve\",\n    \"demo\": \"vue-cli-service build --dest dist_demo\",\n    \"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\",\n    \"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\",\n    \"build\": \"shx rm -rf dist && run-p build:at build:at-ta && shx rm dist/demo.html\",\n    \"prepublish\": \"npm run build\"\n  },\n  \"main\": \"dist/vue-at.js\",\n  \"files\": [\n    \"dist\"\n  ],\n  \"engines\": {\n    \"node\": \">= 14.x\"\n  },\n  \"dependencies\": {\n    \"textarea-caret\": \"^3.1.0\"\n  },\n  \"peerDependencies\": {\n    \"vue\": \"3.x\"\n  },\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.18.9\",\n    \"@vue/cli-plugin-eslint\": \"^5.0.8\",\n    \"@vue/cli-service\": \"^5.0.8\",\n    \"@vue/compat\": \"^3.1.0\",\n    \"@vue/compiler-sfc\": \"^3.1.0\",\n    \"@vue/eslint-config-standard\": \"^8.0.1\",\n    \"element-plus\": \"^2.2.12\",\n    \"eslint\": \"^8.21.0\",\n    \"eslint-plugin-vue\": \"^9.3.0\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"sass\": \"^1.53.0\",\n    \"sass-loader\": \"^13.0.2\",\n    \"shx\": \"^0.3.4\",\n    \"vue\": \"^3.1.0\",\n    \"vue-loader\": \"^16.0.0\",\n    \"vuetify\": \"3.0.0\",\n    \"webpack\": \"^5.73.0\"\n  }\n}\n"
  },
  {
    "path": "src/App.vue",
    "content": "<template>\n  <div id=\"app\">\n\n    <!--\n      vue3 migration.2\n      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 }`.\n      Details: https://v3-migration.vuejs.org/breaking-changes/v-model.html\n    -->\n    <at :members=\"members\" name-key=\"name\" v-model:value=\"html\">\n      <!-- custom: same as default slot -->\n      <!-- <template #item=\"s\">\n        <span v-text=\"s.item\"></span>\n      </template> -->\n\n      <!-- custom: with avatars -->\n      <template #item=\"s\">\n        <img :src=\"s.item.avatar\">\n        <span v-text=\"s.item.name\" />\n      </template>\n\n      <!--\n        // vue3 migration.4\n        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:\n          configureCompat({ ATTR_ENUMERATED_COERCION: false })\n        Details: https://v3-migration.vuejs.org/breaking-changes/attribute-coercion.html\n      -->\n      <div class=\"editor\" :contenteditable=\"true\" />\n    </at>\n\n    <at :members=\"members\" name-key=\"name\" v-model:value=\"html2\">\n      <template #embeddedItem=\"s\">\n        <span><span class=\"tag\"><img class=\"avatar\" :src=\"s.current.avatar\">{{ s.current.name }}</span></span>\n      </template>\n\n      <!-- custom: with avatars -->\n      <template #item=\"s\">\n        <img :src=\"s.item.avatar\">\n        <span v-text=\"s.item.name\" />\n      </template>\n\n      <div class=\"editor\" :contenteditable=\"true\" />\n    </at>\n\n    <br>\n\n    <at-ta :members=\"members\" name-key=\"name\" v-model:value=\"text\">\n      <!-- custom: with avatars -->\n      <template #item=\"s\">\n        <img :src=\"s.item.avatar\">\n        <span v-text=\"s.item.name\" />\n      </template>\n\n      <textarea class=\"editor\" />\n    </at-ta>\n\n    <at-ta :members=\"members\" name-key=\"name\">\n      <!-- custom: with avatars -->\n      <template #item=\"s\">\n        <img :src=\"s.item.avatar\">\n        <span v-text=\"s.item.name\" />\n      </template>\n\n      <v-textarea class=\"vuetify-editor\" v-model=\"text2\" />\n    </at-ta>\n\n    <br>\n\n    <at-ta :members=\"members\" name-key=\"name\">\n      <!-- custom: with avatars -->\n      <template #item=\"s\">\n        <img :src=\"s.item.avatar\">\n        <span v-text=\"s.item.name\" />\n      </template>\n\n      <el-input type=\"textarea\" v-model=\"text3\" class=\"element-editor\" />\n    </at-ta>\n  </div>\n</template>\n\n<script>\nimport At from './At.vue'\nimport AtTa from './AtTextarea.vue'\n\n// testing dist\n// import At from '../dist/vue-at'\n// import AtTa from '../dist/vue-at-textarea'\n\n// testing npm_pack\n// import At from '../package/'\n// import AtTa from '../package/dist/vue-at-textarea'\n\n// testing node_modules\n// import At from 'vue-at'\n// import AtTa from 'vue-at/dist/vue-at-textarea'\n\nlet members = [\n  /* eslint-disable */\n  \"Roxie Miles\",\"grace.carroll\",\n  \"小浩\",\n  \"Helena Perez\",\"melvin.miller\",\n  \"椿木\",\n  \"myrtie.green\",\"elsie.graham\",\"Elva Neal\",\n  \"肖逵\",\n  \"amy.sandoval\",\"katie.leonard\",\"lottie.hamilton\",\n  /* eslint-enable */\n]\nmembers = members.map((v, i) => {\n  return {\n    avatar: `https://randomuser.me/api/portraits/men/${i % 5}.jpg`,\n    name: v\n  }\n})\n\nexport default {\n  components: { At, AtTa },\n  name: 'App',\n  data () {\n    const data = {\n      members,\n      text: `\n<<< Textarea >>>\nAwesome Electron\nUseful resources for creating apps with Electron\nInspired by the awesome list thing. You might also like awesome-nodejs.\nExample apps\nSome good apps written with Electron.\nOpen Source\nAtom - Code editor.\nNuclide - Unified IDE.\nPlayback - Video player.\nAwesome Electron\nUseful resources for creating apps with Electron\nInspired by the awesome list thing. You might also like awesome-nodejs.\nExample apps\nSome good apps written with Electron.\nOpen Source\nAtom - Code editor.\nNuclide - Unified IDE.\nPlayback - Video player.\n      `.trim(), // fix trailing abnormal nodes\n      html: `\n        <div>&lt;&lt;&lt; Content Editable Div &gt;&gt;&gt;</div><div>Awesome Electron&nbsp;\n        <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>\n        <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>\n      `.trim() // fix trailing abnormal nodes\n    }\n    data.text2 = data.text\n    data.text3 = data.text\n    data.html2 = data.html\n    return data\n  }\n}\n</script>\n\n<style>\n#app {\n  font-family: 'Avenir', Helvetica, Arial, sans-serif;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n  color: #2c3e50;\n  margin-top: 30px;\n}\n\n.tag {\n  border-radius: 5px;\n  background: beige;\n  border: 1ps outset yellow;\n}\n\n.editor {\n  width: 400px;\n  height: 200px;\n  overflow: auto;\n  white-space: pre-wrap;\n  border: solid 2px rgba(0,0,0,.5);\n}\ntextarea {\n  display: block;\n}\n\n.vuetify-editor {\n  width: 400px;\n}\n.vuetify-editor textarea {\n  height: 200px;\n}\n.v-text-field__details {\n  display: none;\n}\n\n.element-editor {\n  width: 400px;\n}\n.element-editor textarea {\n  height: 200px;\n}\n\n.editor img {\n  max-width: 10em;\n  vertical-align: bottom;\n}\n.tag .avatar {\n  max-width: 1em;\n  vertical-align: middle;\n}\ntextarea {\n  padding: 0;\n  font-size: inherit;\n  resize: none;\n}\n\n/* override styles */\n#app .atwho-li {\n  padding: 0 4px;\n}\n#app .atwho-li img {\n  height: 100%;\n  width: auto;\n  transform: scale(.8);\n  -webkit-transform: scale(.8);\n}\n#app .atwho-li span {\n  padding-left: 8px;\n}\n#app .atwho-wrap {\n  display: inline-block;\n  vertical-align: top;\n  margin-left: 40px;\n  margin-top: 30px;\n}\n</style>\n"
  },
  {
    "path": "src/At.scss",
    "content": "// atwho.css https://github.com/ichord/At.js\n.atwho-view {\n    // position:absolute;\n    // top: 0;\n    // left: 0;\n    // display: none;\n    // margin-top: 18px;\n    // background: white;\n    color: black;\n    // border: 1px solid #DDD;\n    border-radius: 3px;\n    box-shadow: 0 0 5px rgba(0,0,0,0.1);\n    min-width: 120px;\n    z-index: 11110 !important;\n}\n.atwho-ul {\n    /* width: 100px; */\n    list-style:none;\n    // padding:0;\n    // margin:auto;\n    // max-height: 200px;\n    // overflow-y: auto;\n}\n.atwho-li {\n    display: block;\n    // padding: 5px 10px;\n    // border-bottom: 1px solid #DDD;\n    // cursor: pointer;\n    /* border-top: 1px solid #C8C8C8; */\n}\n\n////// added 1\n.atwho-view {\n  // font-size: 14px;\n  // min-width: 140px;\n  // max-width: 180px;\n  border-radius: 6px;\n  // overflow: hidden;\n  box-shadow: 0 0 10px 0 rgba(101, 111, 122, .5);\n}\n.atwho-ul {\n  max-height: 135px;\n  padding: 0;\n  margin: 0;\n}\n.atwho-li {\n  box-sizing: border-box;\n  height: 27px;\n  padding: 0 12px;\n  white-space: nowrap;\n  display: flex;\n  align-items: center;\n  span {\n    overflow: hidden;\n    text-overflow: ellipsis;\n  }\n}\n.atwho-cur {\n  // background: #44a8f2;\n  background: #5BB8FF;\n  color: white;\n}\n\n////// added 2\n.atwho-wrap {\n  position: relative;\n}\n.atwho-panel {\n  position: absolute;\n}\n.atwho-inner {\n  position: relative;\n}\n.atwho-view {\n  position: absolute;\n  bottom: 0;\n  left: -0.8em; // 抵消左边距\n  cursor: default;\n  background-color: rgba(255,255,255,.94);\n  min-width: 140px;\n  max-width: 180px;\n  max-height: 200px;\n  overflow-y: auto;\n  &::-webkit-scrollbar {\n    width: 11px;\n    height: 11px;\n  }\n  &::-webkit-scrollbar-track {\n    // background-color: rgba(127, 127, 127, .1);\n    background-color: #F5F5F5;\n  }\n  &::-webkit-scrollbar-thumb {\n    min-height: 36px;\n    border: 2px solid transparent;\n    border-top: 3px solid transparent;\n    border-bottom: 3px solid transparent;\n    background-clip: padding-box;\n    border-radius: 7px;\n    // background-color: rgba(0, 0, 0, 0.2);\n    background-color: #C4C4C4;\n  }\n}\n"
  },
  {
    "path": "src/At.vue",
    "content": "<script>\nimport {\n  closest, getOffset, getPrecedingRange,\n  getRange, applyRange,\n  scrollIntoView, getAtAndIndex\n} from './util'\nimport AtTemplate from './AtTemplate.vue'\n\nexport default {\n  name: 'VueAt',\n  mixins: [AtTemplate],\n  emits: ['update:value', 'at', 'insert'],\n  props: {\n    value: {\n      type: String, // value not required\n      default: null\n    },\n    at: {\n      type: String,\n      default: null\n    },\n    ats: {\n      type: Array,\n      default: () => ['@']\n    },\n    suffix: {\n      type: String,\n      default: ' '\n    },\n    loop: {\n      type: Boolean,\n      default: true\n    },\n    allowSpaces: {\n      type: Boolean,\n      default: true\n    },\n    tabSelect: {\n      type: Boolean,\n      default: false\n    },\n    avoidEmail: {\n      type: Boolean,\n      default: true\n    },\n    showUnique: {\n      type: Boolean,\n      default: true\n    },\n    hoverSelect: {\n      type: Boolean,\n      default: true\n    },\n    members: {\n      type: Array,\n      default: () => []\n    },\n    nameKey: {\n      type: String,\n      default: ''\n    },\n    filterMatch: {\n      type: Function,\n      default: (name, chunk, at) => {\n        // match at lower-case\n        return name.toLowerCase()\n          .indexOf(chunk.toLowerCase()) > -1\n      }\n    },\n    deleteMatch: {\n      type: Function,\n      default: (name, chunk, suffix) => {\n        return chunk === name + suffix\n      }\n    },\n    scrollRef: {\n      type: String,\n      default: ''\n    }\n  },\n\n  data () {\n    return {\n      // at[v-model] mode should be on only when\n      // initial :value/v-model is present (not nil)\n      bindsValue: this.value != null,\n      customsEmbedded: false,\n      hasComposition: false,\n      atwho: null\n    }\n  },\n  computed: {\n    atItems () {\n      return this.at ? [this.at] : this.ats\n    },\n\n    currentItem () {\n      if (this.atwho) {\n        return this.atwho.list[this.atwho.cur]\n      }\n      return ''\n    },\n\n    style () {\n      if (this.atwho) {\n        const { list, cur, x, y } = this.atwho\n        const { wrap } = this.$refs\n        if (wrap) {\n          const offset = getOffset(wrap)\n          const scrollLeft = this.scrollRef ? document.querySelector(this.scrollRef).scrollLeft : 0\n          const scrollTop = this.scrollRef ? document.querySelector(this.scrollRef).scrollTop : 0\n          const left = x + scrollLeft + window.pageXOffset - offset.left + 'px'\n          const top = y + scrollTop + window.pageYOffset - offset.top + 'px'\n          return { left, top }\n        }\n      }\n      return null\n    }\n  },\n  watch: {\n    'atwho.cur' (index) {\n      if (index != null) { // cur index exists\n        this.$nextTick(() => {\n          this.scrollToCur()\n        })\n      }\n    },\n    members () {\n      this.handleInput(true)\n    },\n    value (value, oldValue) {\n      if (this.bindsValue) {\n        this.handleValueUpdate(value)\n      }\n    }\n  },\n  mounted () {\n    // vue3 migration.5\n    // [Vue warn]: (deprecation INSTANCE_SCOPED_SLOTS) vm.$scopedSlots has been removed. Use vm.$slots instead.\n    // Details: https://v3-migration.vuejs.org/breaking-changes/slots-unification.html\n    if (this.$slots.embeddedItem) {\n      this.customsEmbedded = true\n    }\n    if (this.bindsValue) {\n      this.handleValueUpdate(this.value)\n    }\n  },\n\n  methods: {\n    itemName (v) {\n      const { nameKey } = this\n      return nameKey ? v[nameKey] : v\n    },\n    isCur (index) {\n      return index === this.atwho.cur\n    },\n    handleValueUpdate (value) {\n      const el = this.$el.querySelector('[contenteditable]')\n      if (value !== el.innerHTML) { // avoid range reset\n        el.innerHTML = value\n        this.dispatchInput()\n      }\n    },\n    dispatchInput () {\n      let el = this.$el.querySelector('[contenteditable]')\n      let ev = new Event('input', { bubbles: true })\n      el.dispatchEvent(ev)\n    },\n\n    handleItemHover (e) {\n      if (this.hoverSelect) {\n        this.selectByMouse(e)\n      }\n    },\n    handleItemClick (e) {\n      this.selectByMouse(e)\n      this.insertItem()\n    },\n    handleDelete (e) {\n      const range = getPrecedingRange()\n      if (range) {\n        // fixme: Very bad code from me\n        if (this.customsEmbedded && range.endOffset >= 1) {\n          let a = range.endContainer.childNodes[range.endOffset] ||\n            range.endContainer.childNodes[range.endOffset - 1]\n          if (!a || a.nodeType === Node.TEXT_NODE && !/^\\s?$/.test(a.data)) {\n            return\n          } else if (a.nodeType === Node.TEXT_NODE) {\n            if (a.previousSibling) a = a.previousSibling\n          } else {\n            if (a.previousElementSibling) a = a.previousElementSibling\n          }\n          let ch = [].slice.call(a.childNodes)\n          ch = [].reverse.call(ch)\n          ch.unshift(a)\n          let last\n          ;[].some.call(ch, c => {\n            if (c.getAttribute && c.getAttribute('data-at-embedded') != null) {\n              last = c\n              return true\n            }\n          })\n          if (last) {\n            e.preventDefault()\n            e.stopPropagation()\n            const r = getRange()\n            if (r) {\n              r.setStartBefore(last)\n              r.deleteContents()\n              applyRange(r)\n              this.handleInput()\n            }\n          }\n          return\n        }\n\n        const { atItems, members, suffix, deleteMatch, itemName } = this\n        const text = range.toString()\n        const { at, index } = getAtAndIndex(text, atItems)\n\n        if (index > -1) {\n          const chunk = text.slice(index + at.length)\n          const has = members.some(v => {\n            const name = itemName(v)\n            return deleteMatch(name, chunk, suffix)\n          })\n          if (has) {\n            e.preventDefault()\n            e.stopPropagation()\n            const r = getRange()\n            if (r) {\n              r.setStart(r.endContainer, index)\n              r.deleteContents()\n              applyRange(r)\n              this.handleInput()\n            }\n          }\n        }\n      }\n    },\n    handleKeyDown (e) {\n      const { atwho } = this\n      if (atwho) {\n        if (e.keyCode === 38 || e.keyCode === 40) { // ↑/↓\n          if (!(e.metaKey || e.ctrlKey)) {\n            e.preventDefault()\n            e.stopPropagation()\n            this.selectByKeyboard(e)\n          }\n          return\n        }\n        if (e.keyCode === 13 || (this.tabSelect && e.keyCode === 9)) { // enter or tab\n          e.preventDefault()\n          e.stopPropagation()\n          this.insertItem()\n          return\n        }\n        if (e.keyCode === 27) { // esc\n          this.closePanel()\n          return\n        }\n      }\n\n      // 为了兼容ie ie9~11 editable无input事件 只能靠keydown触发 textarea正常\n      // 另 ie9 textarea的delete不触发input\n      const isValid = e.keyCode >= 48 && e.keyCode <= 90 || e.keyCode === 8\n      if (isValid) {\n        setTimeout(() => {\n          this.handleInput()\n        }, 50)\n      }\n\n      if (e.keyCode === 8) {\n        this.handleDelete(e)\n      }\n    },\n\n    // compositionStart -> input -> compositionEnd\n    handleCompositionStart () {\n      this.hasComposition = true\n    },\n    handleCompositionEnd () {\n      this.hasComposition = false\n      this.handleInput()\n    },\n    handleInput (keep) {\n      if (this.hasComposition) return\n      const el = this.$el.querySelector('[contenteditable]')\n\n      // vue3 migration.2.1\n      // https://vuejs.org/guide/components/events.html#usage-with-v-model\n      // https://laracasts.com/discuss/channels/vue/how-do-emit-to-v-model-in-vue-3\n      // this.$emit('input', el.innerHTML)\n      this.$emit('update:value', el.innerHTML)\n\n      const range = getPrecedingRange()\n\n      if (range) {\n        if (keep) {\n          // exit the function if the range is not inside this.$el\n          let container = range.commonAncestorContainer;\n          while (container) {\n            if (container === this.$el) break;\n            container = container.parentElement;\n          }\n          if (!container) return;\n        }\n\n        const { atItems, avoidEmail, allowSpaces, showUnique } = this\n\n        let show = true\n        const text = range.toString()\n\n        const { at, index } = getAtAndIndex(text, atItems)\n\n        if (index < 0) show = false\n        const prev = text[index - 1]\n\n        const chunk = text.slice(index + at.length, text.length)\n\n        if (avoidEmail) {\n          // 上一个字符不能为字母数字 避免与邮箱冲突\n          // 微信则是避免 所有字母数字及半角符号\n          if (/^[a-z0-9]$/i.test(prev)) show = false\n        }\n\n        if (!allowSpaces && /\\s/.test(chunk)) {\n          show = false\n        }\n\n        // chunk以空白字符开头不匹配 避免`@ `也匹配\n        if (/^\\s/.test(chunk)) show = false\n\n        if (!show) {\n          this.closePanel()\n        } else {\n          const { members, filterMatch, itemName } = this\n          if (!keep && chunk) { // fixme: should be consistent with AtTextarea.vue\n            this.$emit('at', chunk)\n          }\n          const matched = members.filter(v => {\n            const name = itemName(v)\n            return filterMatch(name, chunk, at, v)\n          })\n\n          show = false\n          if (matched.length) {\n            show = true\n            if (!showUnique) {\n              let item = matched[0]\n              if (chunk === itemName(item)) {\n                show = false\n              }\n            }\n          }\n\n          if (show) {\n            this.openPanel(matched, range, index, at)\n          } else {\n            this.closePanel()\n          }\n        }\n      }\n    },\n\n    closePanel () {\n      if (this.atwho) {\n        this.atwho = null\n      }\n    },\n    openPanel (list, range, offset, at) {\n      const fn = () => {\n        const r = range.cloneRange()\n        r.setStart(r.endContainer, offset + at.length) // 从@后第一位开始\n        // todo: 根据窗口空间 判断向上或是向下展开\n        const rect = r.getClientRects()[0]\n        this.atwho = {\n          range,\n          offset,\n          list,\n          x: rect.left,\n          y: rect.top - 4,\n          cur: 0 // todo: 尽可能记录\n        }\n      }\n      if (this.atwho) {\n        fn()\n      } else { // 焦点超出了显示区域 需要提供延时以移动指针 再计算位置\n        setTimeout(fn, 10)\n      }\n    },\n\n    scrollToCur () {\n      // vue3 migration.6\n      // fix: [Vue warn]: Missing ref owner context. ref cannot be used on hoisted vnodes.\n      // A vnode with ref must be created inside the render function.\n      // at selectByMouse\n      // at handleItemHover\n      // const curEl = this.$refs.cur[0]\n      let { wrap } = this.$refs\n      let { cur } = this.atwho\n      const curEl = wrap.querySelector(`.atwho-li[data-index=\"${cur}\"]`)\n\n      const scrollParent = curEl.parentElement.parentElement // .atwho-view\n      scrollIntoView(curEl, scrollParent)\n    },\n    selectByMouse (e) {\n      const el = closest(e.target, d => {\n        return d.getAttribute('data-index')\n      })\n      const cur = +el.getAttribute('data-index')\n      this.atwho = {\n        ...this.atwho,\n        cur\n      }\n    },\n    selectByKeyboard (e) {\n      const offset = e.keyCode === 38 ? -1 : 1\n      const { cur, list } = this.atwho\n      const nextCur = this.loop\n        ? (cur + offset + list.length) % list.length\n        : Math.max(0, Math.min(cur + offset, list.length - 1))\n      this.atwho = {\n        ...this.atwho,\n        cur: nextCur\n      }\n    },\n\n    // todo: 抽离成库并测试\n    insertText (text, r) {\n      r.deleteContents()\n      const node = r.endContainer\n      if (node.nodeType === Node.TEXT_NODE) {\n        const cut = r.endOffset\n        node.data = node.data.slice(0, cut) +\n          text + node.data.slice(cut)\n        r.setEnd(node, cut + text.length)\n      } else {\n        const t = document.createTextNode(text)\n        r.insertNode(t)\n        r.setEndAfter(t)\n      }\n      r.collapse(false) // 参数在IE下必传\n      applyRange(r)\n      this.dispatchInput()\n    },\n\n    insertHtml (html, r) {\n      r.deleteContents()\n      const node = r.endContainer\n      const newElement = document.createElement('span')\n\n      // Seems `contentediable=false` should includes spaces,\n      // otherwise, caret can't be placed well across them\n      newElement.appendChild(document.createTextNode(' '))\n      newElement.appendChild(this.htmlToElement(html))\n      newElement.appendChild(document.createTextNode(' '))\n      newElement.setAttribute('data-at-embedded', '')\n      newElement.setAttribute('contenteditable', false)\n\n      if (node.nodeType === Node.TEXT_NODE) {\n        const cut = r.endOffset\n        let secondPart = node.splitText(cut)\n        node.parentNode.insertBefore(newElement, secondPart)\n        r.setEndBefore(secondPart)\n      } else {\n        const t = document.createTextNode(this.suffix)\n        r.insertNode(newElement)\n        r.setEndAfter(newElement)\n        r.insertNode(t)\n        r.setEndAfter(t)\n      }\n      r.collapse(false) // 参数在IE下必传\n      applyRange(r)\n    },\n\n    insertItem () {\n      const { range, offset, list, cur } = this.atwho\n      const { suffix, atItems, itemName, customsEmbedded } = this\n      const r = range.cloneRange()\n      const text = range.toString()\n      const { at, index } = getAtAndIndex(text, atItems)\n\n      // Leading `@` is automatically dropped as `customsEmbedded=true`\n      // You can fully custom the output inside the embedded slot\n      const start = customsEmbedded ? index : index + at.length\n      r.setStart(r.endContainer, start)\n\n      // hack: 连续两次 可以确保click后 focus回来 range真正生效\n      applyRange(r)\n      applyRange(r)\n      const curItem = list[cur]\n\n      if (customsEmbedded) {\n        // `suffix` is ignored as `customsEmbedded=true` has to be\n        // wrapped around by spaces\n\n        // vue3 migration.7\n        // fix: Uncaught TypeError: Cannot read properties of undefined (reading 'innerHTML')\n        // at Proxy.insertItem (At.vue?075e:490:1)\n        // at Proxy.handleItemClick (At.vue?075e:184:1)\n        // const html = this.$refs.embeddedItem.firstChild.innerHTML\n        const html = this.$refs.embeddedItem.firstElementChild.innerHTML\n\n        this.insertHtml(html, r)\n      } else {\n        const t = itemName(curItem) + suffix\n        this.insertText(t, r)\n      }\n      scrollIntoView(window.getSelection())\n\n      this.$emit('insert', curItem)\n      this.handleInput()\n\n      // fix safari: use `r` instead of `window.getSelection()`\n      // scrollIntoView(window.getSelection())\n      scrollIntoView(r)\n    },\n    htmlToElement (html) {\n      const template = document.createElement('template')\n      html = html.trim() // Never return a text node of whitespace as the result\n      template.innerHTML = html\n      return template.content.firstChild\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "src/AtTemplate.vue",
    "content": "<template>\n  <div\n    ref=\"wrap\"\n    class=\"atwho-wrap\"\n    @compositionstart=\"handleCompositionStart\"\n    @compositionend=\"handleCompositionEnd\"\n    @input=\"handleInput()\"\n    @keydown.capture=\"handleKeyDown\"\n  >\n    <div\n      v-if=\"atwho\"\n      class=\"atwho-panel\"\n      :style=\"style\"\n    >\n      <div class=\"atwho-inner\">\n        <div class=\"atwho-view\">\n          <ul class=\"atwho-ul\">\n            <li\n              v-for=\"(item, index) in atwho.list\"\n              class=\"atwho-li\"\n              :key=\"index\"\n              :class=\"isCur(index) && 'atwho-cur'\"\n              :data-index=\"index\"\n              @mouseenter=\"handleItemHover\"\n              @click=\"handleItemClick\"\n            >\n              <slot\n                name=\"item\"\n                :item=\"item\"\n              >\n                <span v-text=\"itemName(item)\" />\n              </slot>\n            </li>\n          </ul>\n        </div>\n      </div>\n    </div>\n    <span\n      v-show=\"false\"\n      ref=\"embeddedItem\"\n    >\n      <slot\n        name=\"embeddedItem\"\n        :current=\"currentItem\"\n      />\n    </span>\n    <slot />\n  </div>\n</template>\n\n<style lang=\"scss\" src=\"./At.scss\"></style>\n"
  },
  {
    "path": "src/AtTextarea.vue",
    "content": "<script>\nimport At from './At.vue'\nimport getCaretCoordinates from 'textarea-caret'\nimport { getAtAndIndex, getPrecedingRange } from './util'\n\nexport default {\n  extends: At,\n  name: 'AtTextarea',\n  emits: ['update:value', 'at', 'insert'],\n  computed: {\n    style () {\n      if (this.atwho) {\n        const { list, cur, x, y } = this.atwho\n        const { wrap } = this.$refs\n        const el = this.$el.querySelector('textarea')\n        if (wrap) {\n          const left = x + el.offsetLeft - el.scrollLeft + 'px'\n          const top = y + el.offsetTop - el.scrollTop + 'px'\n          return { left, top }\n        }\n      }\n      return null\n    }\n  },\n  methods: {\n    handleValueUpdate (value) {\n      const el = this.$el.querySelector('textarea')\n      if (value !== el.value) { // avoid range reset\n        el.value = value\n        this.dispatchInput()\n      }\n    },\n    dispatchInput () {\n      let el = this.$el.querySelector('textarea')\n      let ev = new Event('input', { bubbles: true })\n      el.dispatchEvent(ev)\n    },\n\n    handleDelete (e) {\n      const el = this.$el.querySelector('textarea')\n      // fix https://github.com/fritx/vue-at/issues/139\n      const hasSelection = el.selectionEnd - el.selectionStart > 0\n      if (hasSelection) return\n      const text = el.value.slice(0, el.selectionEnd)\n      if (text) {\n        const { atItems, members, suffix, deleteMatch, itemName } = this\n        const { at, index } = getAtAndIndex(text, atItems)\n        if (index > -1) {\n          const chunk = text.slice(index + at.length)\n          const has = members.some(v => {\n            const name = itemName(v)\n            return deleteMatch(name, chunk, suffix)\n          })\n          if (has) {\n            el.value = el.value.slice(0, index) +\n              el.value.slice(el.selectionEnd - 1)\n            el.selectionStart = index + 1\n            el.selectionEnd = index + 1\n            this.handleInput()\n          }\n        }\n      }\n    },\n\n    handleInput (keep) {\n      if (this.hasComposition) return\n      const el = this.$el.querySelector('textarea')\n\n      // vue3 migration.2.1\n      // https://vuejs.org/guide/components/events.html#usage-with-v-model\n      // https://laracasts.com/discuss/channels/vue/how-do-emit-to-v-model-in-vue-3\n      // this.$emit('input', el.value)\n      this.$emit('update:value', el.value)\n\n      if (keep) {\n        // exit the function if the range is not inside this.$el\n        const range = getPrecedingRange()\n        let container = range && range.commonAncestorContainer;\n        while (container) {\n          if (container === this.$el) break;\n          container = container.parentElement;\n        }\n        if (!container) return;\n      }\n\n      const text = el.value.slice(0, el.selectionEnd)\n      if (text) {\n        const { atItems, avoidEmail, allowSpaces } = this\n        let show = true\n        const { at, index } = getAtAndIndex(text, atItems)\n        if (index < 0) show = false\n        const prev = text[index - 1]\n        const chunk = text.slice(index + at.length, text.length)\n        if (avoidEmail) {\n          // 上一个字符不能为字母数字 避免与邮箱冲突\n          // 微信则是避免 所有字母数字及半角符号\n          if (/^[a-z0-9]$/i.test(prev)) show = false\n        }\n        if (!allowSpaces && /\\s/.test(chunk)) {\n          show = false\n        }\n\n        // chunk以空白字符开头不匹配 避免`@ `也匹配\n        if (/^\\s/.test(chunk)) show = false\n        if (!show) {\n          this.closePanel()\n        } else {\n          const { members, filterMatch, itemName } = this\n          if (!keep) { // fixme: should be consistent with At.vue\n            this.$emit('at', chunk)\n          }\n          const matched = members.filter(v => {\n            const name = itemName(v)\n            return filterMatch(name, chunk, at, v)\n          })\n          if (matched.length) {\n            this.openPanel(matched, chunk, index, at, keep)\n          } else {\n            this.closePanel()\n          }\n        }\n      } else {\n        this.closePanel()\n      }\n    },\n\n    openPanel (list, chunk, offset, at) {\n      const fn = () => {\n        const el = this.$el.querySelector('textarea')\n        const atEnd = offset + at.length // 从@后第一位开始\n        const rect = getCaretCoordinates(el, atEnd)\n        this.atwho = {\n          chunk,\n          offset,\n          list,\n          atEnd,\n          x: rect.left,\n          y: rect.top - 4,\n          cur: 0, // todo: 尽可能记录\n        }\n      }\n      if (this.atwho) {\n        fn()\n      } else { // 焦点超出了显示区域 需要提供延时以移动指针 再计算位置\n        setTimeout(fn, 10)\n      }\n    },\n\n    // todo: 抽离成库并测试\n    insertText (text, ta) {\n      const start = ta.selectionStart\n      const end = ta.selectionEnd\n      ta.value = ta.value.slice(0, start) +\n        text + ta.value.slice(end)\n      const newEnd = start + text.length\n      ta.selectionStart = newEnd\n      ta.selectionEnd = newEnd\n      this.dispatchInput()\n    },\n    insertItem () {\n      const { chunk, offset, list, cur, atEnd } = this.atwho\n      const { suffix, atItems, itemName } = this\n      const el = this.$el.querySelector('textarea')\n      const text = el.value.slice(0, atEnd)\n      const { at, index } = getAtAndIndex(text, atItems)\n      const start = index + at.length // 从@后第一位开始\n      el.selectionStart = start\n      el.focus() // textarea必须focus回来\n      const curItem = list[cur]\n      const t = itemName(curItem) + suffix\n      this.insertText(t, el)\n      this.$emit('insert', curItem)\n      this.handleInput()\n    }\n  }\n}\n</script>\n"
  },
  {
    "path": "src/main.js",
    "content": "// Element-Plus x Vue3\n// import 'element-ui/lib/theme-chalk/index.css'\nimport 'element-plus/dist/index.css'\nimport ElementPlus from 'element-plus'\n\n// Vuetify x Vue3\n// https://next.vuetifyjs.com/en/getting-started/installation/\nimport 'vuetify/styles'\nimport { VTextarea } from 'vuetify/components'\nimport { createVuetify } from 'vuetify'\n\n// import { createApp, configureCompat } from 'vue'\nimport { createApp } from 'vue'\nimport App from './App.vue'\n\nlet configureCompat = () => {}\n\nconfigureCompat({\n  // vue3 migration.3\n  // 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:\n  //   configureCompat({ WATCH_ARRAY: false })\n  // Details: https://v3-migration.vuejs.org/breaking-changes/watch.html\n  WATCH_ARRAY: false,\n\n  // vue3 migration.4\n  // 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:\n  //   configureCompat({ ATTR_ENUMERATED_COERCION: false })\n  // Details: https://v3-migration.vuejs.org/breaking-changes/attribute-coercion.html\n  ATTR_ENUMERATED_COERCION: false,\n})\n\n// vue3 migration.1\n// 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.\n// Details: https://v3-migration.vuejs.org/breaking-changes/global-api.html#mounting-app-instance\nlet app = createApp(App)\n\nlet vuetify = createVuetify({\n  components: { VTextarea }\n})\n\napp.use(vuetify)\napp.use(ElementPlus)\n\napp.mount('#app')\n"
  },
  {
    "path": "src/util.js",
    "content": "// Scrolling The Selection Into View\n// http://roysharon.com/blog/37\nexport function scrollIntoView (t, scrollParent) {\n  if (typeof (t) !== 'object') return\n\n  if (t.getRangeAt) {\n    // we have a Selection object\n    if (t.rangeCount === 0) return\n    t = t.getRangeAt(0)\n  }\n\n  if (t.cloneRange) {\n    // we have a Range object\n    let r = t.cloneRange() // do not modify the source range\n    r.collapse(true) // collapse to start\n    t = r.startContainer\n    // if start is an element, then startOffset is the child number\n    // in which the range starts\n    if (t.nodeType === 1) t = t.childNodes[r.startOffset]\n  }\n  if (!t) return\n\n  // if t is not an element node, then we need to skip back until we find the\n  // previous element with which we can call scrollIntoView()\n  let o = t\n  while (o && o.nodeType !== 1) o = o.previousSibling\n  t = o || t.parentNode\n  if (t) scrollIntoViewElement(t, scrollParent)\n}\n\n// bug report: https://github.com/vuejs/awesome-vue/pull/1028\n// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoViewIfNeeded\nexport function scrollIntoViewElement (el, scrollParent) {\n  if (el.scrollIntoViewIfNeeded) {\n    el.scrollIntoViewIfNeeded(false) // alignToCenter=false\n  } else {\n    // should not use `el.scrollIntoView(false)` // alignToTop=false\n    // bug report: https://stackoverflow.com/questions/11039885/scrollintoview-causing-the-whole-page-to-move\n    scrollParent = scrollParent || el.parentElement\n    const diff = el.offsetTop - scrollParent.scrollTop\n    if (diff < 0 || diff > scrollParent.offsetHeight - el.offsetHeight) {\n      scrollParent.scrollTop = el.offsetTop\n    }\n  }\n}\n\nexport function applyRange (range) {\n  const selection = window.getSelection()\n  if (selection) { // 容错\n    selection.removeAllRanges()\n    selection.addRange(range)\n  }\n}\nexport function getRange () {\n  const selection = window.getSelection()\n  if (selection && selection.rangeCount > 0) {\n    return selection.getRangeAt(0)\n  }\n}\n\nexport function getAtAndIndex (text, ats) {\n  return ats.map((at) => {\n    return { at, index: text.lastIndexOf(at) }\n  }).reduce((a, b) => {\n    return a.index > b.index ? a : b\n  })\n}\n\n/* eslint-disable */\n// http://stackoverflow.com/questions/26747240/plain-javascript-replication-to-offset-and-position\nexport function getOffset(element, target) {\n    // var element = document.getElementById(element),\n    //     target  = target ? document.getElementById(target) : window;\n    target = target || window\n    var offset = {top: element.offsetTop, left: element.offsetLeft},\n        parent = element.offsetParent;\n    while (parent != null && parent != target) {\n       offset.left += parent.offsetLeft;\n       offset.top  += parent.offsetTop;\n       parent = parent.offsetParent;\n    }\n    return offset;\n}\n// http://stackoverflow.com/questions/3972014/get-caret-position-in-contenteditable-div\nexport function closest (el, predicate) {\n  /* eslint-disable */\n  do if (predicate(el)) return el;\n  while (el = el && el.parentNode);\n}\n// http://stackoverflow.com/questions/15157435/get-last-character-before-caret-position-in-javascript\n// 修复 \"空格+表情+空格+@\" range报错 应设(endContainer, 0)\n// stackoverflow上的这段代码有bug\nexport function getPrecedingRange() {\n  const r = getRange()\n  if (r) {\n    const range = r.cloneRange()\n    range.collapse(true)\n    // var el = closest(range.endContainer, d => d.contentEditable)\n    // range.setStart(el, 0)\n    range.setStart(range.endContainer, 0)\n    return range\n  }\n}\n/* eslint-enable */\n"
  },
  {
    "path": "test_umd.html",
    "content": "<meta charset=\"utf8\">\n\n<!-- for Vue2 -->\n<!-- <script src=\"//unpkg.com/vue@2\"></script> -->\n<!-- for Vue3 -->\n<script src=\"//unpkg.com/vue@3\"></script>\n\n<script src=\"dist/vue-at.umd.js\"></script>\n<script src=\"dist/vue-at-textarea.umd.js\"></script>\n\n<div id=\"app\">\n  <h2>UMD: Vue3 x Vue-At</h2>\n  <at v-model:value=\"html\">\n    <div contenteditable></div>\n  </at>\n  <at-ta v-model:value=\"text\">\n    <textarea></textarea>\n  </at-ta>\n  <at-ta>\n    <textarea v-model=\"text1\"></textarea>\n  </at-ta>\n  <br>\n  html: {{html}}\n  <br>\n  text: {{text}}\n  <br>\n  text1: {{text1}}\n</div>\n\n<script>\nVue.createApp({\n  components: { At, AtTa: AtTextarea },\n  data() {\n    return {\n      html: 'content-editable',\n      text: 'textarea',\n      text1: 'textarea'\n    }\n  }\n}).mount('#app')\n</script>\n"
  },
  {
    "path": "vue.config.js",
    "content": "\nmodule.exports = {\n  publicPath: './',\n  css: {\n    extract: false, // inline css into js\n    loaderOptions: {\n      sass: {},\n      scss: {}\n    }\n  },\n  chainWebpack: config => {\n    // config.resolve.alias.set('vue', '@vue/compat')\n\n    config.module\n      .rule('vue')\n      .use('vue-loader')\n      .tap(options => {\n        return {\n          ...options,\n          compilerOptions: {\n            compatConfig: {\n              MODE: 2\n            }\n          }\n        }\n      })\n  }\n}\n"
  }
]