├── .editorconfig ├── .gitignore ├── .release-it.json ├── LICENSE ├── README.md ├── example ├── App.vue ├── CodePreview.vue ├── Preview.vue ├── Props.vue ├── index.js └── sharedState.js ├── index.ejs ├── package-lock.json ├── package.json └── src ├── index.js └── index.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | dist 4 | docs 5 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "npm run build && npm run build:example", 3 | "src": { 4 | "beforeStartCommand": "npm run test", 5 | "afterReleaseCommand": "npm run publish-docs", 6 | "commitMessage": "Release v%s", 7 | "tagName": "v%s", 8 | "tagAnnotation": "Release v%s" 9 | }, 10 | "github": { 11 | "release": true, 12 | "releaseName": "v%s", 13 | "tokenRef": "GITHUB_TOKEN" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-chevron 2 | 3 | [![NPM version](https://img.shields.io/npm/v/vue-chevron.svg?style=flat)](https://npmjs.com/package/vue-chevron) [![NPM downloads](https://img.shields.io/npm/dm/vue-chevron.svg?style=flat)](https://npmjs.com/package/vue-chevron) 4 | [![NPM dependencies](https://img.shields.io/david/ispal/vue-chevron.svg?style=flat)](https://npmjs.com/package/vue-chevron) 5 | 6 | Animated chevron toggle component 7 | 8 | ## Demo 9 | [ispal.github.io/vue-chevron/](https://ispal.github.io/vue-chevron/) 10 | ## Install 11 | 12 | ```bash 13 | npm install vue-chevron 14 | ``` 15 | 16 | CDN: [UNPKG](https://unpkg.com/vue-chevron/) | [jsDelivr](https://cdn.jsdelivr.net/npm/vue-chevron/) (available as `window.VueChevron`) 17 | 18 | ## Usage 19 | 20 | ```vue 21 | 24 | 25 | 34 | ``` 35 | 36 | ## License 37 | 38 | MIT 39 | -------------------------------------------------------------------------------- /example/App.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 60 | 61 | 215 | -------------------------------------------------------------------------------- /example/CodePreview.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 88 | 89 | 95 | -------------------------------------------------------------------------------- /example/Preview.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 62 | 63 | 102 | -------------------------------------------------------------------------------- /example/Props.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 86 | 87 | 116 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | new Vue({ 5 | el: '#app', 6 | render: h => h(App) 7 | }) 8 | -------------------------------------------------------------------------------- /example/sharedState.js: -------------------------------------------------------------------------------- 1 | import eases from "eases"; 2 | const easings = Object.keys(eases).map(ease => { 3 | return { 4 | label: ease, 5 | value: ease 6 | }; 7 | }); 8 | 9 | export default { 10 | pointDown: true, 11 | thickness: 8, 12 | duration: 300, 13 | roundEdges: true, 14 | angle: 40, 15 | easings, 16 | selectedEasing: "linear", 17 | color: "#ffffff", 18 | fontSize: 80 19 | }; 20 | -------------------------------------------------------------------------------- /index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | <%= htmlWebpackPlugin.options.title %> 10 | 11 | <% if (htmlWebpackPlugin.options.description) { %> 12 | 13 | <% } %> 14 | 15 | 16 | 17 | 18 | 21 |
22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-chevron", 3 | "version": "0.1.0", 4 | "description": "Animated chevron toggle component", 5 | "repository": { 6 | "url": "https://github.com/ispal/vue-chevron", 7 | "type": "git" 8 | }, 9 | "keywords": [ 10 | "vue", 11 | "component", 12 | "svg" 13 | ], 14 | "main": "dist/vue-chevron.common.js", 15 | "module": "dist/vue-chevron.es.js", 16 | "files": [ 17 | "dist" 18 | ], 19 | "scripts": { 20 | "prepublishOnly": "npm test && npm run build", 21 | "test": "test-vue-app", 22 | "build": "bili --compress umd", 23 | "example": "poi", 24 | "build:example": "poi build", 25 | "commit": "git-cz", 26 | "publish-docs": "gh-pages -d docs/ -b gh-pages", 27 | "release": "release-it" 28 | }, 29 | "author": { 30 | "name": "Ispal", 31 | "email": "irpa.oss@gmail.com" 32 | }, 33 | "license": "MIT", 34 | "poi": { 35 | "entry": "example/index.js", 36 | "dist": "docs", 37 | "homepage": "./" 38 | }, 39 | "bili": { 40 | "moduleName": "VueChevron", 41 | "banner": true, 42 | "format": [ 43 | "cjs", 44 | "umd", 45 | "es" 46 | ], 47 | "plugins": [ 48 | "vue" 49 | ], 50 | "vue": { 51 | "css": "dist/vue-chevron.css" 52 | } 53 | }, 54 | "devDependencies": { 55 | "bili": "0.19.0", 56 | "commitizen": "2.9.6", 57 | "cz-conventional-changelog": "2.1.0", 58 | "eases": "1.0.8", 59 | "flexboxgrid-sass": "8.0.5", 60 | "gh-pages": "1.1.0", 61 | "node-sass": "4.13.1", 62 | "normalize.css": "7.0.0", 63 | "poi": "9.5.4", 64 | "prism-themes": "1.0.0", 65 | "prismjs": "1.25.0", 66 | "release-it": "5.0.0", 67 | "rollup-plugin-vue": "2.5.2", 68 | "sass-loader": "6.0.6", 69 | "test-vue-app": "1.0.0", 70 | "vue-prism-component": "1.0.1", 71 | "vue-switches": "2.0.1", 72 | "vue-test-utils": "1.0.0-beta.6" 73 | }, 74 | "config": { 75 | "commitizen": { 76 | "path": "./node_modules/cz-conventional-changelog" 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | let animationId; 2 | let lastTime = null; 3 | 4 | function linear(t) { 5 | return t; 6 | } 7 | 8 | function animate(duration, component) { 9 | lastTime = component.progress <= 1 ? performance.now() : lastTime; 10 | const animation = t => { 11 | let progress = (t - lastTime) / duration; 12 | if (progress >= 1) { 13 | window.cancelAnimationFrame(animationId); 14 | component.clickProgress = 1; 15 | component.progress = 1; 16 | return; 17 | } 18 | component.progress = component.easing(progress); 19 | animationId = window.requestAnimationFrame(animation); 20 | }; 21 | animationId = window.requestAnimationFrame(animation); 22 | } 23 | 24 | function calculatePosition( 25 | pointDown, 26 | progress, 27 | lastClickProgress, 28 | height, 29 | viewBoxCenterY 30 | ) { 31 | let progressWithClick = 32 | lastClickProgress === 1 ? progress : progress + (1 - lastClickProgress); 33 | 34 | if (progressWithClick >= 1) { 35 | progressWithClick = 1; 36 | } 37 | 38 | const topY = viewBoxCenterY + height / 2 - progressWithClick * height; 39 | const bottomY = viewBoxCenterY - height / 2 + progressWithClick * height; 40 | 41 | return pointDown ? topY : bottomY; 42 | } 43 | 44 | export default { 45 | name: "VueChevron", 46 | props: { 47 | pointDown: { 48 | type: Boolean, 49 | default: true 50 | }, 51 | duration: { 52 | type: Number, 53 | default: 500 54 | }, 55 | thickness: { 56 | type: Number, 57 | default: 4 58 | }, 59 | angle: { 60 | type: Number, 61 | default: 40 62 | }, 63 | roundEdges: { 64 | type: Boolean, 65 | default: true 66 | }, 67 | easing: { 68 | type: Function, 69 | default: linear 70 | } 71 | }, 72 | data() { 73 | return { 74 | progress: 1, 75 | clickProgress: 1, 76 | reverse: false, 77 | lineLength: 30 78 | }; 79 | }, 80 | computed: { 81 | path() { 82 | const progress = this.progress; 83 | const { width, height } = this.triangleSideLengths; 84 | const { x, y } = this.viewBoxCenter; 85 | const clickProgress = this.clickProgress; 86 | 87 | const sidesY = calculatePosition( 88 | this.pointDown, 89 | progress, 90 | clickProgress, 91 | height, 92 | y 93 | ); 94 | const centerY = calculatePosition( 95 | !this.pointDown, 96 | progress, 97 | clickProgress, 98 | height, 99 | y 100 | ); 101 | return `M${x - width},${sidesY}, ${x},${centerY} ${x + width},${sidesY}`; 102 | }, 103 | triangleSideLengths() { 104 | const height = this.lineLength * Math.sin(this.angle * (Math.PI / 180)); 105 | const width = this.lineLength * Math.cos(this.angle * (Math.PI / 180)); 106 | return { 107 | width, 108 | height 109 | }; 110 | }, 111 | viewBoxCenter() { 112 | const { width, height } = this.viewBoxSize; 113 | return { x: width / 2, y: height / 2 }; 114 | }, 115 | viewBoxSize() { 116 | const lineLength = this.lineLength; 117 | const thickness = this.thickness; 118 | 119 | const width = Math.ceil(lineLength * 2 + thickness * 2); 120 | const height = Math.ceil( 121 | lineLength * 2 * Math.sin(this.angle * (Math.PI / 180)) + thickness * 2 122 | ); 123 | return { width, height }; 124 | } 125 | }, 126 | watch: { 127 | pointDown: function() { 128 | this.clickProgress = this.progress; 129 | this.progress = 0; 130 | window.cancelAnimationFrame(animationId); 131 | animate(this.duration, this); 132 | } 133 | }, 134 | render(h) { 135 | const lineCapAndJoin = this.roundEdges ? "round" : "square"; 136 | const { width, height } = this.viewBoxSize; 137 | 138 | return h( 139 | "svg", 140 | { 141 | attrs: { 142 | height: 32, 143 | width: 32, 144 | xmlns: "http://www.w3.org/2000/svg", 145 | viewBox: `0 0 ${width} ${height}` 146 | } 147 | }, 148 | [ 149 | h("title", "vue-chevron"), 150 | h("path", { 151 | attrs: { 152 | d: this.path, 153 | fill: "none", 154 | "stroke-linecap": lineCapAndJoin, 155 | "stroke-width": this.thickness, 156 | "stroke-linejoin": lineCapAndJoin, 157 | stroke: "currentColor" 158 | } 159 | }) 160 | ] 161 | ); 162 | } 163 | }; 164 | -------------------------------------------------------------------------------- /src/index.test.js: -------------------------------------------------------------------------------- 1 | import { mount } from 'vue-test-utils' 2 | import VueChevron from './' 3 | 4 | test('it works', () => { 5 | const wrapper = mount(VueChevron) 6 | expect(wrapper.isVueInstance()).toBe(true) 7 | }) 8 | --------------------------------------------------------------------------------