@thinkpixellab-public/px-vue 3.0.101 → 3.0.103

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.
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 24 24"><g transform="translate(4 11)" fill="currentColor" fill-rule="evenodd"><circle cx="1.5" cy="1.5" r="1.5"/><circle cx="8" cy="1.5" r="1.5"/><circle cx="14.5" cy="1.5" r="1.5"/></g></svg>
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 24 24"><g fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke="currentColor" stroke-width="2"><path d="M16.716 2.498a3.067 3.067 0 0 1 2.962-.793c1.059.283 2.654 1.878 2.937 2.937a3.067 3.067 0 0 1-.793 2.962l-4.152 4.152-.538-.537-9.72 9.72L1.6 22.72l1.78-5.811 9.721-9.721-.537-.538 4.152-4.152ZM11.315 5.402l7.526 7.526"/></g></svg>
@@ -19,6 +19,9 @@
19
19
  {{ text }}
20
20
  </slot>
21
21
  </component>
22
+
23
+ <!-- after -->
24
+ <slot name="after"></slot>
22
25
  </component>
23
26
  </template>
24
27
 
@@ -0,0 +1,186 @@
1
+ <script>
2
+ import tinycolor from 'tinycolor2';
3
+ import {
4
+ hsvaEnsureObject,
5
+ hsvaIsEqual,
6
+ hsvaToHsla,
7
+ hslaToStr,
8
+ hsvaContrastColor,
9
+ } from '../utils/color.js';
10
+
11
+ const TINY_FORMATS = ['rgb', 'prgb', 'hex6', 'hex3', 'hex8', 'name', 'hsl', 'hsv'];
12
+
13
+ export default {
14
+ name: 'px-color-picker',
15
+
16
+ props: {
17
+ /**
18
+ * The color value.
19
+ */
20
+ color: { default: '#FF0000' },
21
+
22
+ /**
23
+ * The format of the color value (used for emitting events, v-model, and the default format
24
+ * for getColor). Allowed values are the same as those available from the tinycolor library
25
+ * (rgb, prgb, hex6, hex3, hex8, name, hsl, hsv) as well as 'auto' (which will attempt to
26
+ * use the same format as the last value set with the color prop) and 'tiny' (which will
27
+ * return a tinycolor object) and 'hsva' which will return hsva values as an object. The
28
+ * format has the advantage of not requiring a tinycolor object to be created when the color
29
+ * changes.
30
+ *
31
+ * @values rgb, prgb, hex6, hex3, hex8, name, hsl, hsv, auto, tiny
32
+ */
33
+ format: { type: String, default: 'hsva' },
34
+
35
+ /**
36
+ * Whether alpha colors are enabled.
37
+ */
38
+ alphaEnabled: { type: Boolean, default: false },
39
+ },
40
+
41
+ data() {
42
+ return { autoFormat: this.format, h: 0, s: 0, v: 0, a: 1 };
43
+ },
44
+
45
+ computed: {
46
+ hsva() {
47
+ return { h: this.h, s: this.s, v: this.v, a: this.alphaEnabled ? this.a : 1 };
48
+ },
49
+ hsla() {
50
+ return hsvaToHsla(this.hsva);
51
+ },
52
+ hslStr() {
53
+ return hslaToStr(this.hsla, false);
54
+ },
55
+ hslaStr() {
56
+ return hslaToStr(this.hsla, true);
57
+ },
58
+ hslaTransparentStr() {
59
+ return hslaToStr({ ...this.hsla, a: 0 }, true);
60
+ },
61
+ hueStr() {
62
+ return `hsl(${this.h}, 100%, 50%)`;
63
+ },
64
+ contrastStr() {
65
+ return hsvaContrastColor(this.hsva);
66
+ },
67
+ eyedropperSupported() {
68
+ return window?.EyeDropper;
69
+ },
70
+ },
71
+
72
+ watch: {
73
+ color(nv) {
74
+ // update colorValue when color changes
75
+ this.setColor(nv);
76
+ },
77
+ hsla() {
78
+ // emit change when this changes as a proxy for changes to hsva as individual props
79
+ this.emitChange();
80
+ },
81
+ },
82
+
83
+ mounted() {
84
+ if (this.color) {
85
+ this.setColor(this.color);
86
+ }
87
+
88
+ if (this.format) {
89
+ if (!TINY_FORMATS.includes(this.format)) {
90
+ console.warn(
91
+ `[PxBaseColor] Invalid format: "${this.format}". See comments in PxBaseColor.vue`,
92
+ );
93
+ }
94
+ }
95
+ },
96
+ methods: {
97
+ setColor(strOrHsva) {
98
+ if (typeof strOrHsva === 'string') {
99
+ this.setColorStr(strOrHsva);
100
+ }
101
+
102
+ if (typeof strOrHsva === 'object') {
103
+ this.setColorHsva(strOrHsva);
104
+ }
105
+ },
106
+
107
+ setColorStr(str) {
108
+ // this is for efficiency but also to prevent color creep as we convert between formats
109
+ if (this.lastEmit == str) {
110
+ return;
111
+ }
112
+
113
+ const tiny = tinycolor(str);
114
+ this.autoFormat = tiny.getFormat();
115
+
116
+ const hsva = tiny.toHsv();
117
+ this.setColorHsva(hsva);
118
+ },
119
+
120
+ setColorHsva(hsva) {
121
+ if (this.isCurrentHsva(hsva)) {
122
+ return;
123
+ }
124
+
125
+ this.h = hsva.h;
126
+ this.s = hsva.s;
127
+ this.v = hsva.v;
128
+ this.a = hsva.a;
129
+ },
130
+
131
+ emitChange() {
132
+ const clr = this.getColor(this.format);
133
+
134
+ this.lastEmit = clr;
135
+
136
+ this.$emit('update:color', clr);
137
+ this.$emit('change', clr);
138
+ },
139
+
140
+ getColor(format = null) {
141
+ format = format || this.format;
142
+
143
+ if (format == 'auto') {
144
+ format = this.autoFormat;
145
+ }
146
+
147
+ let clr = { h: this.h, s: this.s, v: this.v, a: this.a };
148
+
149
+ if (format == 'hsva') {
150
+ return clr;
151
+ }
152
+
153
+ if (format && [...TINY_FORMATS, 'tiny'].includes(format)) {
154
+ const tiny = tinycolor(clr);
155
+
156
+ if (format == 'tiny') {
157
+ clr = tiny;
158
+ } else {
159
+ clr = tiny.toString(format);
160
+ }
161
+ }
162
+
163
+ return clr;
164
+ },
165
+
166
+ colorStrToHsva(str) {
167
+ const tiny = tinycolor(str);
168
+ return tiny.toHsv();
169
+ },
170
+
171
+ isCurrentHsva(hOrObj, s, v, a) {
172
+ const hsvaCompare = hsvaEnsureObject(hOrObj, s, v, a);
173
+ return hsvaIsEqual(this.hsva, hsvaCompare);
174
+ },
175
+
176
+ setColorFromEyedropper() {
177
+ if (this.eyedropperSupported) {
178
+ const eyeDropper = new EyeDropper();
179
+ eyeDropper.open().then(result => {
180
+ this.setColorStr(result.sRGBHex);
181
+ });
182
+ }
183
+ },
184
+ },
185
+ };
186
+ </script>
@@ -0,0 +1,118 @@
1
+ <!--
2
+ Describe this component...
3
+ -->
4
+ <template>
5
+ <div :class="bem()">
6
+ <PxColorPaletteButton
7
+ v-for="(color, index) in calcPalette"
8
+ :key="index"
9
+ :hsva="color.hsva"
10
+ :selected="!(pickerActive && isCurrentHsva(pickerHsva)) && isCurrentHsva(color.hsva)"
11
+ @click.native="onPaletteClick(color)"
12
+ ></PxColorPaletteButton>
13
+ <PxColorPicker
14
+ v-if="showPicker"
15
+ v-bind="calcPickerAttrs"
16
+ :color="pickerHsva"
17
+ :selected="pickerActive && isCurrentHsva(pickerHsva)"
18
+ :pending="pickerPending"
19
+ @click="onPickerClick"
20
+ @change="onPickerChange"
21
+ />
22
+ </div>
23
+ </template>
24
+
25
+ <script>
26
+ import PxBaseColor from './PxBaseColor.vue';
27
+ import tinycolor from 'tinycolor2';
28
+ import PxColorPaletteButton from './PxColorPaletteButton.vue';
29
+ import PxColorPicker from './PxColorPicker.vue';
30
+
31
+ export default {
32
+ name: 'px-color-palette',
33
+ components: { PxColorPaletteButton, PxColorPicker },
34
+ mixins: [PxBaseColor],
35
+ props: {
36
+ /**
37
+ * Array of colors to display in the palette
38
+ */
39
+ palette: { type: Array, default: null },
40
+
41
+ // none | picker | eyedropper | eyedropper-only
42
+ pickerMode: { type: String, default: 'none' },
43
+ pickerAttrs: { type: Object, default: () => {} },
44
+ pickerInitColor: { type: String, default: 'blue' },
45
+ pickerInitSelected: { type: Boolean, default: false },
46
+ },
47
+ data() {
48
+ return {
49
+ pickerActive: this.pickerInitSelected,
50
+ pickerPending: true,
51
+ pickerHsva: this.colorStrToHsva(this.pickerInitColor),
52
+ };
53
+ },
54
+ computed: {
55
+ calcPalette() {
56
+ return this.palette.map(color => {
57
+ const tiny = tinycolor(color);
58
+ return {
59
+ hsva: tiny.toHsv(),
60
+ css: tiny.toString(this.alphaEnabled ? 'hex6' : 'rgb'),
61
+ };
62
+ });
63
+ },
64
+ calcPickerAttrs() {
65
+ const eyedropperMode = {
66
+ none: 'none',
67
+ picker: 'none',
68
+ eyedropper: 'popup',
69
+ 'eyedropper-only': 'only',
70
+ }[this.pickerMode];
71
+
72
+ const iconFallback = {
73
+ none: null,
74
+ picker: 'ellipsis',
75
+ eyedropper: 'ellipsis',
76
+ 'eyedropper-only': 'eyedropper',
77
+ }[this.pickerMode];
78
+
79
+ return {
80
+ iconFallback,
81
+ eyedropperMode,
82
+ alphaEnabled: this.alphaEnabled,
83
+ ...this.pickerAttrs,
84
+ };
85
+ },
86
+ showPicker() {
87
+ return this.pickerMode != 'none';
88
+ },
89
+ },
90
+ // watch: {},
91
+ mounted() {},
92
+ methods: {
93
+ onPaletteClick(color) {
94
+ this.pickerActive = false;
95
+ this.setColorHsva(color.hsva);
96
+ },
97
+ onPickerClick() {
98
+ this.pickerActive = true;
99
+ },
100
+ onPickerChange(e) {
101
+ if (this.pickerActive) {
102
+ this.pickerPending = false;
103
+ this.pickerHsva = e;
104
+ this.setColorHsva(e);
105
+ }
106
+ },
107
+ },
108
+ };
109
+ </script>
110
+
111
+ <style lang="scss">
112
+ @use '../styles/px.scss' as *;
113
+
114
+ .px-color-palette {
115
+ display: flex;
116
+ gap: 0.5em;
117
+ }
118
+ </style>
@@ -0,0 +1,140 @@
1
+ <!--
2
+ Describe this component...
3
+ -->
4
+ <template>
5
+ <button :class="bem({ selected, pending })" :style="{ color: hslaStr }">
6
+ <span />
7
+ <slot />
8
+ </button>
9
+ </template>
10
+
11
+ <script>
12
+ // import { mapState } from 'vuex';
13
+ // import Component from '~/components/Component.vue';
14
+ // import Mixin from '~/components/Mixin.vue';
15
+ import { hsvaToHsla, hslaToStr } from '../utils/color.js';
16
+
17
+ export default {
18
+ name: 'px-color-palette-button',
19
+ props: {
20
+ hsva: {
21
+ type: Object,
22
+ default: () => {
23
+ return { h: 0, s: 0, v: 0, a: 0 };
24
+ },
25
+ },
26
+ selected: { type: Boolean, default: false },
27
+ pending: { type: Boolean, default: false },
28
+ },
29
+ // data() { return { b: 0 }; },
30
+ computed: {
31
+ hslaStr() {
32
+ return hslaToStr(hsvaToHsla(this.hsva), true);
33
+ },
34
+ },
35
+ // watch: {},
36
+ // mounted() {},
37
+ // methods: {},
38
+ };
39
+ </script>
40
+
41
+ <style lang="scss">
42
+ @use '../styles/px.scss' as *;
43
+
44
+ @function color-wheel-gradient($count, $blend: true, $sat: 100%, $light: 50%) {
45
+ $grad: 'conic-gradient(';
46
+ @for $i from 1 through $count {
47
+ $color: hsl(360 - divide(360, $count) * $i, $sat, $light);
48
+ $start: percentage(divide($i - 1, $count));
49
+ $stop: percentage(divide($i, $count));
50
+ $end: if($i == $count, ')', ',');
51
+
52
+ @if $blend {
53
+ $grad: '#{$grad} #{$color} #{$start}#{$end}';
54
+ } @else {
55
+ $grad: '#{$grad} #{$color} #{$start}, #{$color} #{$stop}#{$end}';
56
+ }
57
+ }
58
+
59
+ @return unquote($grad);
60
+ }
61
+
62
+ .px-color-palette-button {
63
+ @include control-reset();
64
+
65
+ position: relative;
66
+ aspect-ratio: 1;
67
+ width: 2em;
68
+ border-radius: 50%;
69
+ cursor: pointer;
70
+ display: inline-flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+
74
+ @include transition(transform, $dur: 0.2s);
75
+
76
+ @include checkered-background(
77
+ $width: 0.25em,
78
+ $color: rgba(black, 0.1),
79
+ $color-alt: transparent
80
+ );
81
+
82
+ > span {
83
+ @include abs();
84
+ border-radius: inherit;
85
+ background-color: currentColor;
86
+ }
87
+
88
+ &:before,
89
+ &:after {
90
+ border-radius: inherit;
91
+ @include transition(all, $dur: 0.3s);
92
+ }
93
+
94
+ @include before() {
95
+ @include abs(-2px);
96
+ border: 2px solid black;
97
+ opacity: 0;
98
+ }
99
+ @include after() {
100
+ @include abs();
101
+ border: 1.5px solid white;
102
+ opacity: 0;
103
+ }
104
+
105
+ &:hover {
106
+ transform: scale(1.1);
107
+ }
108
+
109
+ &:active {
110
+ transform: scale(1) !important;
111
+ }
112
+
113
+ &--selected {
114
+ &:before,
115
+ &:after {
116
+ opacity: 1;
117
+ }
118
+ }
119
+
120
+ &--pending > span {
121
+ @include abs(-0.5px);
122
+ background-color: transparent;
123
+ background-image: color-wheel-gradient(8, false, $light: 50%);
124
+ @include after() {
125
+ @include abs(4px);
126
+ background-color: rgba(white, 0.8);
127
+ border: 0px solid white;
128
+ border-radius: 50%;
129
+ }
130
+ }
131
+
132
+ &--pending {
133
+ color: black !important;
134
+ // &:before {
135
+ // border: 1px solid rgba(black, 0.5);
136
+ // opacity: 1;
137
+ // }
138
+ }
139
+ }
140
+ </style>
@@ -0,0 +1,267 @@
1
+ <template>
2
+ <div
3
+ :class="bem()"
4
+ :style="{
5
+ '--px-color-panel-hsl': hslStr,
6
+ '--px-color-panel-hsla': hslaStr,
7
+ '--px-color-panel-hsla-transparent': hslaTransparentStr,
8
+ '--px-color-panel-hsl-hue': hueStr,
9
+ '--px-color-panel-contrast': contrastStr,
10
+ }"
11
+ >
12
+ <div ref="satval" :class="[bem('satval'), satValPanelClass]">
13
+ <div
14
+ :class="bem('sel', { satval: true })"
15
+ :style="{
16
+ left: cssPcnt(s),
17
+ top: cssPcnt(1 - v),
18
+ }"
19
+ ></div>
20
+ </div>
21
+ <div :class="bem('sliders')">
22
+ <div :class="[bem('hue'), hueSliderClass]" ref="hue">
23
+ <div
24
+ :class="bem('sel', { hue: true })"
25
+ :style="{
26
+ left: cssPcnt(h / 360),
27
+ }"
28
+ ></div>
29
+ </div>
30
+ <div v-if="alphaEnabled" :class="[bem('alpha'), alphaSliderClass]" ref="alpha">
31
+ <div
32
+ :class="bem('sel', { alpha: true })"
33
+ :style="{
34
+ left: cssPcnt(a),
35
+ }"
36
+ ></div>
37
+ </div>
38
+ </div>
39
+ <div :class="bem('swatch')"></div>
40
+ <PxIconButton
41
+ v-if="eyedropperEnabled && eyedropperSupported"
42
+ :class="[bem('eyedropper'), eyedropperClass]"
43
+ @click="setColorFromEyedropper"
44
+ :src="EyeDropperSvg"
45
+ :size="0.85"
46
+ />
47
+ </div>
48
+ </template>
49
+
50
+ <script>
51
+ import Drag from '../utils/drag.js';
52
+ import PxBaseColor from './PxBaseColor.vue';
53
+ import PxIconButton from './PxIconButton.vue';
54
+ import EyeDropperSvg from '../assets/icons/eyedropper.svg';
55
+
56
+ export default {
57
+ name: 'px-color-panel',
58
+ mixins: [PxBaseColor],
59
+ components: { PxIconButton },
60
+ props: {
61
+ hueSliderClass: { type: String, default: '' },
62
+ alphaSliderClass: { type: String, default: '' },
63
+ satValPanelClass: { type: String, default: '' },
64
+ eyedropperClass: { type: String, default: '' },
65
+ eyedropperEnabled: { type: Boolean, default: true },
66
+ eyedropperIcon: { type: String, default: EyeDropperSvg },
67
+ swatchVisible: { type: Boolean, default: true },
68
+ },
69
+
70
+ data() {
71
+ return {
72
+ EyeDropperSvg,
73
+ };
74
+ },
75
+
76
+ // watch: {},
77
+ mounted() {
78
+ if (this.$refs.satval) {
79
+ this.satvalDrag = new Drag(this.$refs.satval, {
80
+ startThreshold: 0,
81
+ lock: null,
82
+ onStart: this.onSatvalDrag,
83
+ onDrag: this.onSatvalDrag,
84
+ excludeSelector: this.dragExcludeSelector,
85
+ });
86
+ }
87
+
88
+ if (this.$refs.hue) {
89
+ this.hueDrag = new Drag(this.$refs.hue, {
90
+ startThreshold: 0,
91
+ lock: null,
92
+ onStart: this.onHueDrag,
93
+ onDrag: this.onHueDrag,
94
+ excludeSelector: this.dragExcludeSelector,
95
+ });
96
+ }
97
+
98
+ if (this.alphaEnabled && this.$refs.alpha) {
99
+ this.alphaDrag = new Drag(this.$refs.alpha, {
100
+ startThreshold: 0,
101
+ lock: null,
102
+ onStart: this.onAlphaDrag,
103
+ onDrag: this.onAlphaDrag,
104
+ excludeSelector: this.dragExcludeSelector,
105
+ });
106
+ }
107
+ },
108
+ beforeDestroy() {
109
+ if (this.satvalDrag) {
110
+ this.satvalDrag.destroy();
111
+ }
112
+
113
+ if (this.hueDrag) {
114
+ this.hueDrag.destroy();
115
+ }
116
+
117
+ if (this.alphaDrag) {
118
+ this.alphaDrag.destroy();
119
+ }
120
+ },
121
+ methods: {
122
+ onSatvalDrag(e) {
123
+ this.s = Math.max(0, Math.min(1, e.elementXPos));
124
+ this.v = Math.max(0, Math.min(1, 1 - e.elementYPos));
125
+ },
126
+ onHueDrag(e) {
127
+ this.h = 360 * Math.max(0, Math.min(1, e.elementXPos));
128
+ },
129
+ onAlphaDrag(e) {
130
+ this.a = Math.max(0, Math.min(1, e.elementXPos));
131
+ },
132
+ },
133
+ };
134
+ </script>
135
+
136
+ <style lang="scss">
137
+ @use '../styles/px.scss' as *;
138
+
139
+ @at-root {
140
+ :root {
141
+ --px-color-panel-aspect: 1;
142
+ --px-color-panel-gap: 0.5em;
143
+ --px-color-panel-radius: 0.5em;
144
+ --px-color-panel-slider-height: 0.5em;
145
+ }
146
+ }
147
+
148
+ .px-color-panel {
149
+ width: 20em;
150
+ display: flex;
151
+ flex-direction: column;
152
+ aspect-ratio: var(--px-color-panel-aspect);
153
+ gap: var(--px-color-panel-gap) 0;
154
+
155
+ // prettier-ignore
156
+ @include grid-art(
157
+ (
158
+ 'auto | #(0, 1fr) |',
159
+ 'satval | satval | #(0,1fr)',
160
+ 'swatch | sliders | auto',
161
+ )
162
+ );
163
+
164
+ &__satval {
165
+ grid-area: satval;
166
+ flex: 1;
167
+ position: relative;
168
+ display: block;
169
+ width: 100%;
170
+ border-radius: var(--px-color-panel-radius);
171
+
172
+ background-image: linear-gradient(rgba(0, 0, 0, 0), #000),
173
+ linear-gradient(90deg, #fff, var(--px-color-panel-hsl-hue));
174
+ }
175
+
176
+ &__sliders {
177
+ grid-area: sliders;
178
+ display: flex;
179
+ flex-direction: column;
180
+ justify-content: center;
181
+ gap: var(--px-color-panel-gap);
182
+ }
183
+
184
+ &__hue,
185
+ &__alpha {
186
+ position: relative;
187
+ display: block;
188
+ width: 100%;
189
+ height: 0.66em;
190
+ border-radius: var(--px-color-panel-radius);
191
+ flex: none;
192
+ align-self: center;
193
+ }
194
+
195
+ &__hue {
196
+ grid-area: hue;
197
+ background-image: linear-gradient(90deg, red, yellow, lime, aqua, blue, fuchsia, red);
198
+ }
199
+
200
+ &__alpha {
201
+ grid-area: alpha;
202
+
203
+ @include checkered-background(
204
+ $width: 0.25em,
205
+ $color: rgba(black, 0.1),
206
+ $color-alt: transparent
207
+ );
208
+
209
+ @include after() {
210
+ @include abs();
211
+ border-radius: inherit;
212
+ background-image: linear-gradient(
213
+ to right,
214
+ var(--px-color-panel-hsla-transparent) 0%,
215
+ var(--px-color-panel-hsl) 100%
216
+ );
217
+ }
218
+ }
219
+
220
+ &__sel {
221
+ position: absolute;
222
+ top: 100%;
223
+ left: 100%;
224
+ transform: translate(-50%, -50%);
225
+ width: 1px;
226
+ height: 1px;
227
+ width: 0.66em;
228
+ height: 0.66em;
229
+ background-color: var(--px-color-panel-hsl);
230
+ border: 1px solid white;
231
+ border-radius: 50%;
232
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
233
+ z-index: 1;
234
+
235
+ &:hover {
236
+ z-index: 2;
237
+ }
238
+
239
+ &--hue {
240
+ top: 50%;
241
+ background-color: var(--px-color-panel-hsl-hue);
242
+ }
243
+
244
+ &--alpha {
245
+ top: 50%;
246
+ background-color: var(--px-color-panel-hsla);
247
+ }
248
+ }
249
+
250
+ &__swatch {
251
+ grid-area: swatch;
252
+ aspect-ratio: 1;
253
+ border-radius: 50%;
254
+ background-color: var(--px-color-panel-hsla);
255
+ margin-inline-end: var(--px-color-panel-gap);
256
+ border: 1px solid rgba(black, 0.1);
257
+ min-height: 1.66em;
258
+ }
259
+
260
+ &__eyedropper {
261
+ grid-area: swatch;
262
+ margin-inline-end: var(--px-color-panel-gap);
263
+ color: var(--px-color-panel-contrast);
264
+ aspect-ratio: 1;
265
+ }
266
+ }
267
+ </style>
@@ -0,0 +1,142 @@
1
+ <template>
2
+ <div :class="bem()">
3
+ <PxColorPaletteButton
4
+ @click.native="click"
5
+ :hsva="hsva"
6
+ :selected="selected"
7
+ :pending="pending"
8
+ >
9
+ <PxIcon
10
+ :class="bem('icon')"
11
+ v-bind="calcIconAttrs"
12
+ :style="{ color: calcIconColor }"
13
+ :size="1"
14
+ />
15
+ </PxColorPaletteButton>
16
+ <PxFloat
17
+ :popupClass="bem('popup')"
18
+ :visible.sync="popupVisible"
19
+ variant="none"
20
+ placement="bottom-center"
21
+ >
22
+ <PxColorPanel
23
+ :class="bem('panel')"
24
+ :color="hsva"
25
+ :alphaEnabled="alphaEnabled"
26
+ :eyedropperEnabled="eyedropperMode === 'popup'"
27
+ format="hsva"
28
+ @change="panelColorChange"
29
+ ></PxColorPanel>
30
+ </PxFloat>
31
+ </div>
32
+ </template>
33
+
34
+ <script>
35
+ import PxFloat from './PxFloat.vue';
36
+ import PxColorPanel from './PxColorPanel.vue';
37
+ import PxColorPaletteButton from './PxColorPaletteButton.vue';
38
+ import PxBaseColor from './PxBaseColor.vue';
39
+ import PxIcon from './PxIcon.vue';
40
+
41
+ const PxColorPicker = {
42
+ name: 'px-color-picker',
43
+ components: { PxFloat, PxColorPanel, PxColorPaletteButton, PxIcon },
44
+ mixins: [PxBaseColor],
45
+ props: {
46
+ /**
47
+ * An icon to display inside the button. If a string is provided, it will be interpreted as
48
+ * the src of the icon. If an object is provided, it will be passed directly to the icon
49
+ * component as props (with a filter applied to ensure no fallback attributes).
50
+ */
51
+ icon: { type: [String, Object], default: null },
52
+
53
+ /**
54
+ * Convenience prop for setting the icon if the icon prop hasn't been set. Allowed values
55
+ * are 'eyedropper' and 'ellipsis'.
56
+ */
57
+ iconFallback: { type: String, default: null },
58
+
59
+ /**
60
+ * Describes how to integrate the eyedropper into the color picker. Allowed values are:
61
+ * 'none' - don't show the eyedropper
62
+ * 'popup' - show the eyedropper in the popup
63
+ * 'only' - only show the eyedropper (instead of the standard dialog)
64
+ */
65
+ eyedropperMode: { type: String, default: 'none' },
66
+
67
+ /**
68
+ * Makes the color picker appear selected (passed to the PxColorPaletteButton component)
69
+ */
70
+ selected: { type: Boolean, default: false },
71
+
72
+ /**
73
+ * Gives the color picker a pending appearance (passed to the PxColorPaletteButton component)
74
+ */
75
+ pending: { type: Boolean, default: false },
76
+ },
77
+ data() {
78
+ return { popupVisible: false };
79
+ },
80
+ computed: {
81
+ calcIconAttrs() {
82
+ if (this.icon) {
83
+ return this.icon;
84
+ } else if (this.iconFallback) {
85
+ switch (this.iconFallback) {
86
+ case 'eyedropper':
87
+ return { src: require('../assets/icons/eyedropper.svg'), size: 0.9 };
88
+ case 'ellipsis':
89
+ return { src: require('../assets/icons/ellipsis.svg') };
90
+ }
91
+ }
92
+ },
93
+ calcIconColor() {
94
+ if (this.pending) {
95
+ return null;
96
+ }
97
+ return this.contrastStr;
98
+ },
99
+ },
100
+ // watch: {},
101
+ // mounted() {},
102
+ methods: {
103
+ panelColorChange(hsva) {
104
+ this.setColorHsva(hsva);
105
+ },
106
+ click() {
107
+ this.$emit('click');
108
+
109
+ if (this.eyedropperMode === 'only') {
110
+ this.setColorFromEyedropper();
111
+ } else {
112
+ this.popupVisible = true;
113
+ }
114
+ },
115
+ },
116
+ };
117
+
118
+ export default PxColorPicker;
119
+ </script>
120
+
121
+ <style lang="scss">
122
+ @use '../styles/px.scss' as *;
123
+
124
+ @at-root {
125
+ :root {
126
+ --px-color-picker-popup-bg: #{clr(page-bg)};
127
+ }
128
+ }
129
+ .px-color-picker {
130
+ &__panel {
131
+ width: 10em;
132
+ }
133
+
134
+ &__popup {
135
+ padding: 0.66em;
136
+ border-radius: 0.75em;
137
+
138
+ background-color: var(--px-color-picker-popup-bg);
139
+ @include depth-shadow(20, 0.15);
140
+ }
141
+ }
142
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinkpixellab-public/px-vue",
3
- "version": "3.0.101",
3
+ "version": "3.0.103",
4
4
  "description": "General purpose Vue components and helpers that can be used across projects.",
5
5
  "author": "Pixel Lab",
6
6
  "license": "MIT",
@@ -25,6 +25,7 @@
25
25
  "qrcode": "^1.5.1",
26
26
  "tiny-date-picker": "^3.2.8",
27
27
  "tiny-swiper": "^2.2.0",
28
+ "tinycolor2": "^1.6.0",
28
29
  "vue-inline-svg": "^2.1.0"
29
30
  },
30
31
  "devDependencies": {
package/utils/color.js ADDED
@@ -0,0 +1,50 @@
1
+ function hsvaEnsureObject(hOrObj, s, v, a) {
2
+ if (typeof hOrObj === 'object') {
3
+ return hOrObj;
4
+ }
5
+
6
+ return { h: hOrObj, s, v, a };
7
+ }
8
+
9
+ function hsvaIsEqual(hsva1, hsva2) {
10
+ return hsva1.h === hsva2.h && hsva1.s === hsva2.s && hsva1.v === hsva2.v && hsva1.a === hsva2.a;
11
+ }
12
+
13
+ function hsvaToHsla(hOrObj, s, v, a) {
14
+ const hsva = hsvaEnsureObject(hOrObj, s, v, a);
15
+
16
+ let l = ((2 - hsva.s) * hsva.v) / 2;
17
+ s = hsva.s;
18
+
19
+ if (l != 0) {
20
+ if (l == 1) {
21
+ s = 0;
22
+ } else if (l < 0.5) {
23
+ s = (s * hsva.v) / (l * 2);
24
+ } else {
25
+ s = (s * hsva.v) / (2 - l * 2);
26
+ }
27
+ }
28
+
29
+ return { h: hsva.h, s: s, l: l, a: hsva.a };
30
+ }
31
+
32
+ function hslaToStr(hsla, alphaEnabled = true) {
33
+ if (alphaEnabled) {
34
+ return `hsla(${hsla.h}, ${hsla.s * 100}%, ${hsla.l * 100}%, ${hsla.a})`;
35
+ } else {
36
+ return `hsl(${hsla.h}, ${hsla.s * 100}%, ${hsla.l * 100}%)`;
37
+ }
38
+ }
39
+
40
+ function hsvaContrastColor(hsva, light = '#FFF', dark = '#000') {
41
+ const THRESHOLD = 0.5;
42
+ const hsla = hsvaToHsla(hsva);
43
+ if (hsla.a < THRESHOLD) {
44
+ return dark;
45
+ }
46
+
47
+ return hsla.l > THRESHOLD ? dark : light;
48
+ }
49
+
50
+ export { hsvaEnsureObject, hsvaIsEqual, hsvaToHsla, hslaToStr, hsvaContrastColor };
package/utils/drag.js CHANGED
@@ -267,13 +267,26 @@ function Drag(element, options) {
267
267
  coords.y = nativeEvent.clientY;
268
268
  }
269
269
 
270
+ const x = this.thresholdMet && this.startEv && this.lock == 'y' ? this.startEv.x : coords.x;
271
+ const y = this.thresholdMet && this.startEv && this.lock == 'x' ? this.startEv.y : coords.y;
272
+
270
273
  const ev = {
271
- x: this.thresholdMet && this.startEv && this.lock == 'y' ? this.startEv.x : coords.x,
272
- y: this.thresholdMet && this.startEv && this.lock == 'x' ? this.startEv.y : coords.y,
274
+ x,
275
+ y,
273
276
  native: nativeEvent,
274
277
  lock: this.lock,
275
278
  };
276
279
 
280
+ if (this.thresholdMet && this.element) {
281
+ const rect = element.getBoundingClientRect();
282
+ ev.elementX = x - rect.left;
283
+ ev.elementY = y - rect.top;
284
+ ev.elementWidth = rect.width;
285
+ ev.elementHeight = rect.height;
286
+ ev.elementXPos = ev.elementX / rect.width;
287
+ ev.elementYPos = ev.elementY / rect.height;
288
+ }
289
+
277
290
  return ev;
278
291
  };
279
292