@thinkpixellab-public/px-vue 4.0.8 → 4.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/README.md +7 -0
- package/.storybook/main.js +34 -0
- package/.storybook/manager-head.html +0 -0
- package/.storybook/manager.js +6 -0
- package/.storybook/preview-head.html +3 -0
- package/.storybook/preview.js +30 -0
- package/.storybook/preview.scss +44 -0
- package/.storybook/px-vue.storybook.theme.js +34 -0
- package/README.md +15 -7
- package/bases/PxBaseItemsSelect.vue +4 -4
- package/bases/PxBaseMobileAware.vue +25 -60
- package/bases/PxBaseResize.vue +22 -3
- package/bases/PxBaseToggle.vue +1 -0
- package/bases/PxBaseVariants.vue +19 -4
- package/bases/PxBaseVariantsOverride.vue +1 -1
- package/components/PxArrow.vue +127 -0
- package/components/PxArrowScroller.vue +41 -9
- package/components/PxAutoClamp.vue +8 -1
- package/components/PxBalancedText.vue +8 -0
- package/components/PxBaseActiveTab.vue +50 -0
- package/components/PxBaseMobileProvide.vue +79 -0
- package/components/PxCalendar.vue +2 -2
- package/components/PxCardGrid.vue +13 -0
- package/components/PxClock.vue +1 -1
- package/components/PxContain.vue +4 -4
- package/components/PxDatePicker.vue +1 -1
- package/components/PxFlip.vue +95 -0
- package/components/PxFloat.vue +23 -1
- package/components/PxGsapScrubber.vue +146 -0
- package/components/PxIconButton.vue +7 -0
- package/components/PxMeasureContainer.vue +83 -0
- package/components/PxMenu.vue +240 -0
- package/components/PxMenuItem.vue +143 -0
- package/components/PxModal.vue +1 -1
- package/components/PxPivot.vue +101 -47
- package/components/PxPopup.vue +1 -1
- package/components/PxRadioButton.vue +1 -1
- package/components/PxScrubber.vue +140 -0
- package/components/PxSideDrawer.vue +46 -12
- package/components/PxSlideTransition.vue +77 -17
- package/components/PxSlides.vue +104 -29
- package/components/PxSpinner.vue +18 -9
- package/components/PxSticky.vue +112 -0
- package/components/PxSvg.vue +32 -1
- package/components/PxTable.vue +1 -1
- package/components/PxToggle.vue +1 -1
- package/components/PxToggleButton.vue +30 -18
- package/components/PxTransition.vue +241 -0
- package/components/PxTransitionItem.vue +97 -0
- package/components/PxValidator.vue +4 -7
- package/components/PxVueTest.vue +11 -15
- package/dev/demos/Demo-PxCardGrid.vue +0 -4
- package/dev/demos/Demo-PxContain.vue +0 -4
- package/dev/demos/Demo-PxFlip.vue +104 -0
- package/dev/demos/Demo-PxFloat.vue +5 -5
- package/dev/demos/Demo-PxMenu.vue +91 -0
- package/dev/demos/Demo-PxPivot.vue +0 -3
- package/dev/demos/Demo-PxSideDrawer.vue +0 -3
- package/dev/demos/Demo-PxSlideTransition.vue +0 -2
- package/dev/demos/Demo-PxStylesPreview.vue +0 -3
- package/dev/demos/DemoPicker.vue +2 -4
- package/dev/dev.vue +5 -1
- package/package.json +32 -28
- package/plugins/common.js +9 -2
- package/plugins/filters/striphtml.js +1 -1
- package/stories/PxAspect.stories.js +8 -4
- package/stories/PxBalancedText.stories.js +4 -2
- package/stories/PxButton.stories.js +9 -5
- package/stories/PxDropdown.stories.js +4 -2
- package/stories/PxExpander.stories.js +5 -3
- package/stories/PxFloat.stories.js +4 -2
- package/stories/PxIcon.mdx +9 -13
- package/stories/PxIcon.stories.js +14 -8
- package/stories/PxPivot.stories.js +8 -4
- package/stories/PxRadioButton.stories.js +4 -2
- package/stories/PxSvg.stories.js +4 -2
- package/stories/PxToggleButton.stories.js +8 -4
- package/stories/PxToggleButtonList.stories.js +9 -4
- package/utils/balanceText.js +2 -2
- package/utils/bem.js +2 -1
- package/utils/cssStr.js +181 -13
- package/utils/drag.js +1 -1
- package/utils/lethargy.js +11 -2
- package/utils/points.js +110 -0
- package/utils/textWidth.js +1 -0
- package/utils/uniqueId.js +5 -14
- package/utils/utils.js +398 -204
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** @type { import('@storybook/vue3-vite').StorybookConfig } */
|
|
2
|
+
const config = {
|
|
3
|
+
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|ts|tsx)'],
|
|
4
|
+
addons: [
|
|
5
|
+
'@storybook/addon-links',
|
|
6
|
+
'@storybook/addon-essentials',
|
|
7
|
+
'@storybook/addon-interactions',
|
|
8
|
+
{
|
|
9
|
+
name: '@storybook/addon-docs',
|
|
10
|
+
options: {
|
|
11
|
+
mdxPluginOptions: {
|
|
12
|
+
// mdxCompileOptions: {
|
|
13
|
+
// remarkPlugins: [remarkGfm],
|
|
14
|
+
// },
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
'@storybook/addon-mdx-gfm'
|
|
19
|
+
],
|
|
20
|
+
|
|
21
|
+
framework: {
|
|
22
|
+
name: '@storybook/vue3-vite',
|
|
23
|
+
options: {},
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
docs: {
|
|
27
|
+
autodocs: true,
|
|
28
|
+
defaultName: 'Documentation',
|
|
29
|
+
},
|
|
30
|
+
features: {
|
|
31
|
+
interactionsDebugger: true,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
export default config;
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { setup } from '@storybook/vue3';
|
|
2
|
+
import common from '../plugins/common.js';
|
|
3
|
+
|
|
4
|
+
import './preview.scss';
|
|
5
|
+
|
|
6
|
+
// px-vue common plugins
|
|
7
|
+
setup(app => {
|
|
8
|
+
app.use(common);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const preview = {
|
|
12
|
+
parameters: {
|
|
13
|
+
actions: { argTypesRegex: '^on[A-Z].*' },
|
|
14
|
+
controls: {
|
|
15
|
+
matchers: {
|
|
16
|
+
color: /(background|color)$/i,
|
|
17
|
+
date: /Date$/,
|
|
18
|
+
string: /(itemsKey|itemsDisabled)$/i,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
// theme
|
|
23
|
+
docs: {
|
|
24
|
+
// requires import { themes } from '@storybook/theming';
|
|
25
|
+
//theme: themes.dark,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default preview;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600&family=Public+Sans:wght@300;400;500;600&family=Work+Sans:wght@400;600&display=swap');
|
|
2
|
+
|
|
3
|
+
// .sbdocs-h2,
|
|
4
|
+
// .sbdocs-h3,
|
|
5
|
+
// .sbdocs-h4,
|
|
6
|
+
// .sbdocs-h5,
|
|
7
|
+
// .sbdocs h1:where(:not(.sb-unstyled h1)),
|
|
8
|
+
// .sbdocs h2:where(:not(.sb-unstyled h2)),
|
|
9
|
+
// .sbdocs h3:where(:not(.sb-unstyled h3)),
|
|
10
|
+
// .sbdocs h4:where(:not(.sb-unstyled h4)),
|
|
11
|
+
// .sbdocs h5:where(:not(.sb-unstyled h5)),
|
|
12
|
+
// .sbdocs-title,
|
|
13
|
+
// .sbdocs-content p,
|
|
14
|
+
// .docblock-argstable,
|
|
15
|
+
// .docblock-argstable tr,
|
|
16
|
+
// .docblock-argstable th,
|
|
17
|
+
// .docblock-argstable td,
|
|
18
|
+
// .docblock-argstable td button {
|
|
19
|
+
// font-family: $font !important;
|
|
20
|
+
// }
|
|
21
|
+
|
|
22
|
+
.story-bounds {
|
|
23
|
+
display: inline-block;
|
|
24
|
+
height: auto;
|
|
25
|
+
width: auto;
|
|
26
|
+
padding: 0;
|
|
27
|
+
margin: 0;
|
|
28
|
+
display: block;
|
|
29
|
+
border: 1px dashed rgba(black, 0.33);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.story-circle {
|
|
33
|
+
background-color: tomato;
|
|
34
|
+
width: 6em;
|
|
35
|
+
height: 6em;
|
|
36
|
+
border-radius: 100%;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.story-fill {
|
|
40
|
+
background-color: tomato;
|
|
41
|
+
width: 100%;
|
|
42
|
+
height: 100%;
|
|
43
|
+
border-radius: 8px;
|
|
44
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// https://storybook.js.org/docs/7.0/vue/configure/theming
|
|
2
|
+
|
|
3
|
+
import { create } from '@storybook/theming/create';
|
|
4
|
+
|
|
5
|
+
export default create({
|
|
6
|
+
// base: 'light',
|
|
7
|
+
// // Typography
|
|
8
|
+
// fontBase: '"Noto Sans", sans-serif',
|
|
9
|
+
// fontCode: 'monospace',
|
|
10
|
+
// brandTitle: 'px-vue',
|
|
11
|
+
// brandUrl: 'https://example.com',
|
|
12
|
+
// brandImage: 'https://storybook.js.org/images/placeholders/350x150.png',
|
|
13
|
+
// brandTarget: '_self',
|
|
14
|
+
// //
|
|
15
|
+
// colorPrimary: '#3A10E5',
|
|
16
|
+
// colorSecondary: '#585C6D',
|
|
17
|
+
// // UI
|
|
18
|
+
// appBg: '#ffffff',
|
|
19
|
+
// appContentBg: '#ffffff',
|
|
20
|
+
// appBorderColor: '#585C6D',
|
|
21
|
+
// appBorderRadius: 4,
|
|
22
|
+
// // Text colors
|
|
23
|
+
// textColor: '#10162F',
|
|
24
|
+
// textInverseColor: '#ffffff',
|
|
25
|
+
// // Toolbar default and active colors
|
|
26
|
+
// barTextColor: '#9E9E9E',
|
|
27
|
+
// barSelectedColor: '#585C6D',
|
|
28
|
+
// barBg: '#ffffff',
|
|
29
|
+
// // Form colors
|
|
30
|
+
// inputBg: '#ffffff',
|
|
31
|
+
// inputBorder: '#10162F',
|
|
32
|
+
// inputTextColor: '#10162F',
|
|
33
|
+
// inputBorderRadius: 2,
|
|
34
|
+
});
|
package/README.md
CHANGED
|
@@ -1,40 +1,48 @@
|
|
|
1
1
|
# px-vue
|
|
2
2
|
|
|
3
|
+
## Storybook
|
|
4
|
+
|
|
5
|
+
Basic component preview and testing can be done with storybook. Run storybook with `npm run dev-px`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Default README
|
|
10
|
+
|
|
3
11
|
This template should help get you started developing with Vue 3 in Vite.
|
|
4
12
|
|
|
5
|
-
|
|
13
|
+
### Recommended IDE Setup
|
|
6
14
|
|
|
7
15
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
|
8
16
|
|
|
9
|
-
|
|
17
|
+
### Customize configuration
|
|
10
18
|
|
|
11
19
|
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
### Project Setup
|
|
14
22
|
|
|
15
23
|
```sh
|
|
16
24
|
npm install
|
|
17
25
|
```
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
#### Compile and Hot-Reload for Development
|
|
20
28
|
|
|
21
29
|
```sh
|
|
22
30
|
npm run dev
|
|
23
31
|
```
|
|
24
32
|
|
|
25
|
-
|
|
33
|
+
#### Compile and Minify for Production
|
|
26
34
|
|
|
27
35
|
```sh
|
|
28
36
|
npm run build
|
|
29
37
|
```
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
#### Run Unit Tests with [Vitest](https://vitest.dev/)
|
|
32
40
|
|
|
33
41
|
```sh
|
|
34
42
|
npm run test:unit
|
|
35
43
|
```
|
|
36
44
|
|
|
37
|
-
|
|
45
|
+
#### Lint with [ESLint](https://eslint.org/)
|
|
38
46
|
|
|
39
47
|
```sh
|
|
40
48
|
npm run lint
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<script>
|
|
6
6
|
import PxBaseItems from './PxBaseItems.vue';
|
|
7
|
-
import
|
|
7
|
+
import { arraySafeIndex } from '../utils/utils.js';
|
|
8
8
|
|
|
9
9
|
// helper to define what an "empty" key or set of keys looks like
|
|
10
10
|
const isKeySetEmpty = keyOrKeys => {
|
|
@@ -356,7 +356,7 @@ export default {
|
|
|
356
356
|
const replace =
|
|
357
357
|
replaceSelection == 'auto' ? this.updateMode == 'single' : replaceSelection;
|
|
358
358
|
|
|
359
|
-
index =
|
|
359
|
+
index = arraySafeIndex(this.items, index, 0, wrap);
|
|
360
360
|
this.selectAt(index, replace);
|
|
361
361
|
},
|
|
362
362
|
|
|
@@ -364,11 +364,11 @@ export default {
|
|
|
364
364
|
if (this.selectedIndex === null) {
|
|
365
365
|
this.selectAt(0);
|
|
366
366
|
} else {
|
|
367
|
-
const index =
|
|
367
|
+
const index = arraySafeIndex(
|
|
368
368
|
this.items,
|
|
369
369
|
this.selectedIndex,
|
|
370
370
|
offsetFromSelectedIndex,
|
|
371
|
-
wrap
|
|
371
|
+
wrap,
|
|
372
372
|
);
|
|
373
373
|
this.selectAt(index, true);
|
|
374
374
|
}
|
|
@@ -1,73 +1,38 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
#VUE3 Todo
|
|
3
|
-
|
|
4
|
-
Use for global mobile awareness in javascript. This doesn't actually handle mobile detection
|
|
5
|
-
but provides a consistent interface for communicating between a single component that does
|
|
6
|
-
the detection and other components that subscribe to an eventbus and update a property called
|
|
7
|
-
isMobile.
|
|
8
|
-
|
|
9
|
-
Sample usage using a css variable for detecting mobile (this keeps css and js in sync):
|
|
10
|
-
|
|
11
|
-
### MyLayout.vue (or other global component)
|
|
12
|
-
|
|
13
|
-
// add PxBaseMobileAware as a mixin
|
|
14
|
-
import PxBaseMobileAware from '@thinkpixellab-public/px-vue/bases/PxBaseMobileAware.vue';
|
|
15
|
-
mixins: [PxBaseMobileAware],
|
|
16
|
-
|
|
17
|
-
// add a resize handler and call setGlobalIsMobile
|
|
18
|
-
resize() {
|
|
19
|
-
if (this.$el) {
|
|
20
|
-
const cssMobile = getComputedStyle(this.$el).getPropertyValue('--is-mobile');
|
|
21
|
-
this.setGlobalIsMobile(cssMobile.toLowerCase().trim() == 'true');
|
|
22
|
-
}
|
|
23
|
-
},
|
|
24
|
-
|
|
25
|
-
// add a css variable that changes with the viewport
|
|
26
|
-
|
|
27
|
-
.my-layout {
|
|
28
|
-
--is-mobile: false;
|
|
29
|
-
@include media-until-mobile() {
|
|
30
|
-
--is-mobile: true;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
### MyComponent.vue
|
|
35
|
-
|
|
36
|
-
// use PxBaseMobileAware as a mixin
|
|
37
|
-
import PxBaseMobileAware from '@thinkpixellab-public/px-vue/bases/PxBaseMobileAware.vue';
|
|
38
|
-
mixins: [PxBaseMobileAware]
|
|
39
|
-
|
|
40
|
-
// use the isMobile property as needed in a template
|
|
41
|
-
<div> This is mobile: {{isMobile ? 'yup' : 'nope' }} </div>
|
|
42
|
-
|
|
43
|
-
// or in code
|
|
44
|
-
watch: {
|
|
45
|
-
isMobile(nv) {
|
|
46
|
-
// do something when isMobile changes
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
3
|
+
Must be used in conjuction with PxBaseMobileProvide. See notes in that file for details.
|
|
51
4
|
-->
|
|
52
5
|
|
|
53
6
|
<script>
|
|
54
7
|
export default {
|
|
55
|
-
|
|
56
|
-
|
|
8
|
+
inject: {
|
|
9
|
+
mediaQuery: {
|
|
10
|
+
default: { isMobile: null, currentBreakpoint: null, breakpoints: {} },
|
|
11
|
+
},
|
|
57
12
|
},
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
this
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
13
|
+
computed: {
|
|
14
|
+
isMobile() {
|
|
15
|
+
return this.mediaQuery.isMobile;
|
|
16
|
+
},
|
|
17
|
+
currentBreakpoint() {
|
|
18
|
+
return this.mediaQuery.currentBreakpoint;
|
|
19
|
+
},
|
|
64
20
|
},
|
|
21
|
+
|
|
65
22
|
methods: {
|
|
66
|
-
|
|
67
|
-
if (this
|
|
68
|
-
|
|
69
|
-
this.isMobile = isMobile;
|
|
23
|
+
isBreakpointOrSmaller(breakpoint) {
|
|
24
|
+
if (this.currentBreakpoint === null) {
|
|
25
|
+
return false;
|
|
70
26
|
}
|
|
27
|
+
|
|
28
|
+
if (this.currentBreakpoint === breakpoint) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
this.mediaQuery.breakpoints[breakpoint] >=
|
|
34
|
+
this.mediaQuery.breakpoints[this.currentBreakpoint]
|
|
35
|
+
);
|
|
71
36
|
},
|
|
72
37
|
},
|
|
73
38
|
};
|
package/bases/PxBaseResize.vue
CHANGED
|
@@ -31,7 +31,11 @@ export default {
|
|
|
31
31
|
const resizeDebouncedFn =
|
|
32
32
|
this.resizeDebounceValue && this.resize
|
|
33
33
|
? debounce(this.resizeDebounceValue, e => {
|
|
34
|
-
|
|
34
|
+
try {
|
|
35
|
+
this.resize(e);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error('Resize: ' + e);
|
|
38
|
+
}
|
|
35
39
|
this.resizing = false;
|
|
36
40
|
})
|
|
37
41
|
: null;
|
|
@@ -51,13 +55,28 @@ export default {
|
|
|
51
55
|
// regular version
|
|
52
56
|
else {
|
|
53
57
|
if (this.resize) {
|
|
54
|
-
|
|
58
|
+
try {
|
|
59
|
+
this.resize();
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error('Resize: ' + e);
|
|
62
|
+
}
|
|
55
63
|
}
|
|
56
64
|
}
|
|
57
65
|
};
|
|
58
66
|
|
|
59
67
|
this.resizeObserver = new ResizeObserver(resizeFn);
|
|
60
|
-
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
this.observeElementValue &&
|
|
71
|
+
(this.observeElementValue instanceof Element ||
|
|
72
|
+
this.observeElementValue instanceof SVGElement)
|
|
73
|
+
) {
|
|
74
|
+
this.resizeObserver.observe(this.observeElementValue);
|
|
75
|
+
} else {
|
|
76
|
+
console.warn(
|
|
77
|
+
`Could not call ResizeObserver.observe because observeElementValue is not an Element: ${this.observeElementValue}`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
61
80
|
},
|
|
62
81
|
beforeUnmount() {
|
|
63
82
|
this.resizeObserver.disconnect();
|
package/bases/PxBaseToggle.vue
CHANGED
package/bases/PxBaseVariants.vue
CHANGED
|
@@ -45,12 +45,27 @@ export default {
|
|
|
45
45
|
},
|
|
46
46
|
},
|
|
47
47
|
methods: {
|
|
48
|
-
// true if the given variant
|
|
49
|
-
isVariant(
|
|
50
|
-
|
|
48
|
+
// true if the given variant (string) or variants (array) are present
|
|
49
|
+
isVariant(val) {
|
|
50
|
+
if (this.variants && this.variants.length && val) {
|
|
51
|
+
if (typeof val == 'string') {
|
|
52
|
+
return this.variants?.includes(val);
|
|
53
|
+
}
|
|
54
|
+
if (Array.isArray(val) && val.length) {
|
|
55
|
+
for (let i = 0; i < val.length; i++) {
|
|
56
|
+
if (this.variants?.includes(val[i])) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
51
63
|
},
|
|
52
64
|
|
|
53
|
-
//
|
|
65
|
+
// Find the first matching variant key in a map and return the correspond value
|
|
66
|
+
// Example:
|
|
67
|
+
// <MyComponent variant="red" />
|
|
68
|
+
// variantsToValue({'red': '#F00', 'blue': '#0F0'}) => '#F00'
|
|
54
69
|
variantsToValue(valueMap, fallback) {
|
|
55
70
|
for (let key in valueMap) {
|
|
56
71
|
if (this.isVariant(key)) {
|
|
@@ -20,7 +20,7 @@ export default {
|
|
|
20
20
|
variantValue() {
|
|
21
21
|
return (
|
|
22
22
|
(this.variantBase ? `${this.variantBase} ` : '') +
|
|
23
|
-
(this.
|
|
23
|
+
(this.variant && this.variant !== 'default' ? this.variant : this.variantDefault)
|
|
24
24
|
);
|
|
25
25
|
},
|
|
26
26
|
},
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Generates an SVG arrow based on props.
|
|
3
|
+
-->
|
|
4
|
+
<template>
|
|
5
|
+
<svg
|
|
6
|
+
version="1.1"
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
9
|
+
overflow="visible"
|
|
10
|
+
:viewBox="svgData.viewBox"
|
|
11
|
+
>
|
|
12
|
+
<g
|
|
13
|
+
:stroke-width="svgData.strokeWidth"
|
|
14
|
+
stroke="currentColor"
|
|
15
|
+
fill="none"
|
|
16
|
+
fill-rule="evenodd"
|
|
17
|
+
stroke-linecap="round"
|
|
18
|
+
>
|
|
19
|
+
<polyline :points="svgData.pointer"></polyline>
|
|
20
|
+
<polyline v-if="stemVisible" :points="svgData.stem"></polyline>
|
|
21
|
+
</g>
|
|
22
|
+
</svg>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script>
|
|
26
|
+
import PxBaseResize from '@thinkpixellab-public/px-vue/bases/PxBaseResize.vue';
|
|
27
|
+
import { getBoundingBox, rotatePoints } from '../utils/points.js';
|
|
28
|
+
import { contain } from '@thinkpixellab-public/px-vue/utils/fit.js';
|
|
29
|
+
|
|
30
|
+
export default {
|
|
31
|
+
name: 'px-arrow',
|
|
32
|
+
mixins: [PxBaseResize],
|
|
33
|
+
props: {
|
|
34
|
+
// the aspect of the arrow "rectangle"
|
|
35
|
+
pointerAspect: { type: [Number, String], default: 2 },
|
|
36
|
+
|
|
37
|
+
// the relative height of the arrow
|
|
38
|
+
pointerSize: { type: Number, default: 0.4 },
|
|
39
|
+
|
|
40
|
+
// the relative size of the stem
|
|
41
|
+
stemVisible: { type: Boolean, default: true },
|
|
42
|
+
|
|
43
|
+
// the angle in which the arrow is pointing (in degrees)
|
|
44
|
+
angle: { type: Number, default: 0 },
|
|
45
|
+
|
|
46
|
+
// the stroke thickness of the arrow
|
|
47
|
+
strokeWidth: { type: Number, default: 1 },
|
|
48
|
+
|
|
49
|
+
// when true, we scale the strokeWidth in the internal SVG to keep it at the specified value
|
|
50
|
+
// regardless of render size
|
|
51
|
+
scaleStrokeWidth: { type: Boolean, default: true },
|
|
52
|
+
},
|
|
53
|
+
data() {
|
|
54
|
+
return {
|
|
55
|
+
elementSize: { width: 100, height: 100 },
|
|
56
|
+
};
|
|
57
|
+
},
|
|
58
|
+
computed: {
|
|
59
|
+
svgData() {
|
|
60
|
+
const pointerHeight = this.pointerSize;
|
|
61
|
+
const pointerWidth = this.pointerAspect * pointerHeight;
|
|
62
|
+
|
|
63
|
+
// basic arrow (bottom left corner anchored to origin)
|
|
64
|
+
let points = [
|
|
65
|
+
// pointer
|
|
66
|
+
{ x: 0, y: pointerHeight },
|
|
67
|
+
{ x: pointerWidth / 2, y: 0 },
|
|
68
|
+
{ x: pointerWidth, y: pointerHeight },
|
|
69
|
+
|
|
70
|
+
// stem
|
|
71
|
+
{ x: pointerWidth / 2, y: 0 },
|
|
72
|
+
{ x: pointerWidth / 2, y: 1 },
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
// rotate
|
|
76
|
+
points = rotatePoints(points, this.angle);
|
|
77
|
+
|
|
78
|
+
// scale by 100 so the viewbox isn't ridiculous
|
|
79
|
+
points = points.map(p => {
|
|
80
|
+
return {
|
|
81
|
+
x: p.x * 100,
|
|
82
|
+
y: p.y * 100,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// get viewbox bounds
|
|
87
|
+
const bounds = getBoundingBox(points);
|
|
88
|
+
|
|
89
|
+
// get stroke scale
|
|
90
|
+
const strokeScale = this.scaleStrokeWidth ? contain(this.elementSize, bounds).scale : 1;
|
|
91
|
+
|
|
92
|
+
// convert points to a string
|
|
93
|
+
points = points.map(p => {
|
|
94
|
+
return `${p.x} ${p.y}`;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const svgData = {
|
|
98
|
+
viewBox: [bounds.x, bounds.y, bounds.width, bounds.height]
|
|
99
|
+
.map(n => Math.round(n))
|
|
100
|
+
.join(' '),
|
|
101
|
+
pointer: [points[0], points[1], points[2]].join(' '),
|
|
102
|
+
stem: [points[3], points[4]].join(' '),
|
|
103
|
+
strokeWidth: this.strokeWidth * strokeScale,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return svgData;
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
methods: {
|
|
110
|
+
resize() {
|
|
111
|
+
if (this.$el) {
|
|
112
|
+
const r = this.$el.getBoundingClientRect();
|
|
113
|
+
this.elementSize = {
|
|
114
|
+
width: r.width,
|
|
115
|
+
height: r.height,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<style lang="scss">
|
|
124
|
+
.px-arrow {
|
|
125
|
+
position: relative;
|
|
126
|
+
}
|
|
127
|
+
</style>
|