├── .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 | [](https://npmjs.com/package/vue-chevron) [](https://npmjs.com/package/vue-chevron)
4 | [](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 |
22 |
23 |
24 |
25 |
34 | ```
35 |
36 | ## License
37 |
38 | MIT
39 |
--------------------------------------------------------------------------------
/example/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Vue Chevron
6 |
14 |
25 |
28 |
36 |
37 |
38 |
39 |
40 |
60 |
61 |
215 |
--------------------------------------------------------------------------------
/example/CodePreview.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Install
6 |
7 |
8 | npm install vue-chevron --save
9 |
10 |
11 |
Code example
12 |
13 |
14 | <template>
15 | <div class="example" @click="toggle">
16 | <vue-chevron
17 | :point-down="pointDown"
18 | :duration="duration"
19 | :thickness="thickness"
20 | :angle="angle"
21 | :round-edges="roundEdges"
22 | />
23 | </div>
24 | </template>
25 |
26 | <script>
27 | import VueChevron from 'vue-chevron';
28 |
29 | export default {
30 | name: 'App',
31 | components: {
32 | VueChevron
33 | },
34 | data() {
35 | return {
36 | pointDown: {{ sharedState.pointDown}},
37 | thickness: {{ sharedState.thickness }},
38 | duration: {{ sharedState.duration }},
39 | angle: {{ sharedState.angle }},
40 | roundEdges: {{ sharedState.roundEdges }},
41 | easing: {{ easing }}
42 | };
43 | },
44 | methods: {
45 | toggle() {
46 | this.pointDown = !this.pointDown;
47 | }
48 | }
49 | }
50 | </script>
51 |
52 | <style>
53 | .example {
54 | color: {{ sharedState.color }};
55 | font-size: {{ sharedState.fontSize }}px;
56 | }
57 | </style>
58 |
59 |
60 |
61 |
62 |
63 |
88 |
89 |
95 |
--------------------------------------------------------------------------------
/example/Preview.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
62 |
63 |
102 |
--------------------------------------------------------------------------------
/example/Props.vue:
--------------------------------------------------------------------------------
1 |
2 |
68 |
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 |
--------------------------------------------------------------------------------