Skip to content

Commit 223746a

Browse files
committed
support custom colors
1 parent 30902af commit 223746a

31 files changed

Lines changed: 332 additions & 119 deletions

src/components/desktop/ColorSelect.vue

Lines changed: 83 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
persistent-placeholder
88
:disabled="disabled"
99
:label="label"
10+
:menu-props="{ contentClass: 'color-select-menu' }"
1011
v-model="color"
1112
@update:menu="onMenuStateChanged"
1213
>
@@ -17,25 +18,37 @@
1718
</template>
1819

1920
<template #no-data>
20-
<div class="color-select-dropdown px-2" ref="dropdownMenu">
21-
<div class="color-item" :class="{ 'row-has-selected-item': hasSelectedIcon(row) }"
22-
:style="`grid-template-columns: repeat(${itemPerRow}, minmax(0, 1fr));`"
23-
:key="idx" v-for="(row, idx) in allColorRows">
24-
<div class="text-center" :key="colorInfo.color" v-for="colorInfo in row">
25-
<div class="cursor-pointer" @click="color = colorInfo.color">
26-
<v-icon class="ma-2" size="28"
27-
:icon="mdiSquareRounded" :color="getDisplayColor(colorInfo.color)"
28-
v-if="!modelValue || modelValue !== colorInfo.color" />
29-
<v-badge class="right-bottom-icon" color="primary"
30-
offset-x="8" offset-y="8"
31-
:location="`bottom ${textDirection === TextDirection.LTR ? 'right' : 'left'}`"
32-
:icon="mdiCheck"
33-
v-if="modelValue && modelValue === colorInfo.color">
34-
<v-icon class="ma-2" size="28" :icon="mdiSquareRounded" :color="getDisplayColor(colorInfo.color)" />
35-
</v-badge>
21+
<div ref="dropdownMenu" class="color-select-dropdown px-2" :class="{ 'color-select-dropdown-custom': currentTab === 'custom' }">
22+
<div class="color-select-tabs-container">
23+
<v-tabs grow density="compact" v-model="currentTab">
24+
<v-tab value="system">{{ tt('System Colors') }}</v-tab>
25+
<v-tab value="custom">{{ tt('Custom Color') }}</v-tab>
26+
</v-tabs>
27+
</div>
28+
<div v-if="currentTab === 'system'">
29+
<div class="color-item" :class="{ 'row-has-selected-item': hasSelectedColor(row) }"
30+
:style="`grid-template-columns: repeat(${itemPerRow}, minmax(0, 1fr));`"
31+
:key="idx" v-for="(row, idx) in allColorRows">
32+
<div class="text-center" :key="colorInfo.color" v-for="colorInfo in row">
33+
<div class="cursor-pointer" @click="color = colorInfo.color">
34+
<v-icon class="ma-2" size="28"
35+
:icon="mdiSquareRounded" :color="getDisplayColor(colorInfo.color)"
36+
v-if="!modelValue || modelValue !== colorInfo.color" />
37+
<v-badge class="right-bottom-icon" color="primary"
38+
offset-x="8" offset-y="8"
39+
:location="`bottom ${textDirection === TextDirection.LTR ? 'right' : 'left'}`"
40+
:icon="mdiCheck"
41+
v-if="modelValue && modelValue === colorInfo.color">
42+
<v-icon class="ma-2" size="28" :icon="mdiSquareRounded" :color="getDisplayColor(colorInfo.color)" />
43+
</v-badge>
44+
</div>
3645
</div>
3746
</div>
3847
</div>
48+
<div class="d-flex justify-center" v-if="currentTab === 'custom'">
49+
<v-color-picker hide-input-labels elevation="0" :width="Math.max(320, dropdownMenuWidth - 16)"
50+
hide-alpha mode="hex" :modes="['hex']" v-model="customColor" />
51+
</div>
3952
</div>
4053
</template>
4154
</v-select>
@@ -63,36 +76,56 @@ const props = defineProps<{
6376
disabled?: boolean;
6477
label?: string;
6578
columnCount?: number;
66-
allColorInfos: ColorValue[];
79+
allSystemColorInfos: ColorValue[];
6780
}>();
6881
6982
const emit = defineEmits<{
7083
(e: 'update:modelValue', value: ColorValue): void;
7184
}>();
7285
73-
const { getCurrentLanguageTextDirection } = useI18n();
86+
const { tt, getCurrentLanguageTextDirection } = useI18n();
7487
7588
const dropdownMenu = useTemplateRef<HTMLElement>('dropdownMenu');
89+
90+
const currentTab = ref<'system' | 'custom'>(isSystemColor(props.modelValue) ? 'system' : 'custom');
7691
const itemPerRow = ref<number>(props.columnCount || 7);
92+
const dropdownMenuWidth = ref<number>(320);
7793
7894
const textDirection = computed<TextDirection>(() => getCurrentLanguageTextDirection());
79-
const allColorRows = computed<ColorInfo[][]>(() => getColorsInRows(props.allColorInfos, itemPerRow.value));
95+
const allColorRows = computed<ColorInfo[][]>(() => getColorsInRows(props.allSystemColorInfos, itemPerRow.value));
8096
8197
const color = computed<ColorValue>({
8298
get: () => props.modelValue,
8399
set: (value: ColorValue) => emit('update:modelValue', value)
84100
});
85101
86-
function hasSelectedIcon(row: ColorInfo[]): boolean {
87-
return arrayContainsFieldValue(row, 'id', props.modelValue);
102+
const customColor = computed<string>({
103+
get: () => `#${props.modelValue}`,
104+
set: (value: string) => emit('update:modelValue', value.replace(/^#/, '').substring(0, 6).toLowerCase())
105+
});
106+
107+
function isSystemColor(value: ColorValue): boolean {
108+
return props.allSystemColorInfos.includes(value);
109+
}
110+
111+
function hasSelectedColor(row: ColorInfo[]): boolean {
112+
return arrayContainsFieldValue(row, 'color', props.modelValue);
88113
}
89114
90115
function onMenuStateChanged(state: boolean): void {
91116
if (state) {
117+
currentTab.value = isSystemColor(props.modelValue) ? 'system' : 'custom';
118+
92119
nextTick(() => {
93120
if (dropdownMenu.value && dropdownMenu.value.parentElement) {
94121
scrollToSelectedItem(dropdownMenu.value.parentElement, null, null, '.row-has-selected-item');
95122
}
123+
124+
window.requestAnimationFrame(() => {
125+
if (dropdownMenu.value) {
126+
dropdownMenuWidth.value = Math.floor(dropdownMenu.value.clientWidth);
127+
}
128+
});
96129
});
97130
}
98131
}
@@ -104,7 +137,34 @@ function onMenuStateChanged(state: boolean): void {
104137
opacity: 1;
105138
}
106139
107-
.color-select-dropdown .color-item {
108-
display: grid;
140+
.color-select-menu {
141+
.color-select-dropdown {
142+
.color-select-tabs-container {
143+
position: sticky;
144+
top: -8px;
145+
z-index: 1;
146+
margin: -8px -8px 8px;
147+
padding: 8px 8px 0;
148+
background-color: rgb(var(--v-theme-surface));
149+
}
150+
151+
.color-item {
152+
display: grid;
153+
}
154+
155+
.v-color-picker__controls {
156+
padding: 4px;
157+
158+
.v-color-picker-edit {
159+
margin-top: 4px;
160+
161+
> .v-color-picker-edit__input {
162+
> input {
163+
margin-bottom: 0;
164+
}
165+
}
166+
}
167+
}
168+
}
109169
}
110170
</style>
Lines changed: 110 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
<template>
22
<f7-sheet swipe-to-close swipe-handler=".swipe-handler"
3-
:opened="show"
3+
:style="sheetStyle" :opened="show"
44
@sheet:open="onSheetOpen" @sheet:closed="onSheetClosed">
55
<f7-toolbar class="toolbar-with-swipe-handler">
66
<div class="swipe-handler"></div>
77
<div class="left">
88
<f7-link sheet-close icon-f7="xmark"></f7-link>
99
</div>
10+
<f7-segmented strong round class="width-100">
11+
<f7-button :active="currentTab === 'system'" @click="showSystemColors">{{ tt('System Colors') }}</f7-button>
12+
<f7-button :active="currentTab === 'custom'" @click="showCustomColor">{{ tt('Custom Color') }}</f7-button>
13+
</f7-segmented>
1014
</f7-toolbar>
1115
<f7-page-content>
12-
<f7-block class="margin-vertical no-padding">
16+
<f7-block class="margin-vertical no-padding" v-if="currentTab === 'system'">
1317
<div class="grid padding-vertical-half padding-horizontal-half"
14-
:class="{ 'row-has-selected-item': hasSelectedIcon(row) }"
18+
:class="{ 'row-has-selected-item': hasSelectedColor(row) }"
1519
:style="`grid-template-columns: repeat(${itemPerRow}, minmax(0, 1fr));`"
1620
:key="idx" v-for="(row, idx) in allColorRows">
1721
<div class="text-align-center" :key="colorInfo.color" v-for="colorInfo in row">
@@ -23,51 +27,147 @@
2327
</div>
2428
</div>
2529
</f7-block>
30+
<f7-block class="no-margin no-padding" v-if="currentTab === 'custom'">
31+
<div class="custom-color-picker" ref="customColorPickerContainer"></div>
32+
</f7-block>
2633
</f7-page-content>
2734
</f7-sheet>
2835
</template>
2936

3037
<script setup lang="ts">
31-
import { ref, computed } from 'vue';
38+
import { ref, computed, useTemplateRef, nextTick, onBeforeUnmount } from 'vue';
39+
import type { ColorPicker } from 'framework7/types';
3240
41+
import { useI18n } from '@/locales/helpers.ts';
3342
import type { ColorValue, ColorInfo } from '@/core/color.ts';
3443
import { arrayContainsFieldValue } from '@/lib/common.ts';
3544
import { getColorsInRows } from '@/lib/color.ts';
3645
import { scrollToSelectedItem } from '@/lib/ui/common.ts';
37-
import { type Framework7Dom } from '@/lib/ui/mobile.ts';
46+
import { type Framework7Dom, createInlineColorPicker } from '@/lib/ui/mobile.ts';
3847
3948
const props = defineProps<{
4049
modelValue: ColorValue;
4150
show: boolean;
4251
columnCount?: number;
43-
allColorInfos: ColorValue[];
52+
allSystemColorInfos: ColorValue[];
4453
}>();
4554
4655
const emit = defineEmits<{
4756
(e: 'update:modelValue', value: ColorValue): void;
4857
(e: 'update:show', value: boolean): void;
4958
}>();
5059
60+
const { tt } = useI18n();
61+
62+
const customColorPickerContainer = useTemplateRef<HTMLElement>('customColorPickerContainer');
63+
64+
let customColorPicker: ColorPicker.ColorPicker | null = null;
65+
66+
const currentTab = ref<'system' | 'custom'>(isSystemColor(props.modelValue) ? 'system' : 'custom');
5167
const currentValue = ref<ColorValue>(props.modelValue);
5268
const itemPerRow = ref<number>(props.columnCount || 7);
69+
const customSheetHeight = ref<number>(420);
70+
71+
const allColorRows = computed<ColorInfo[][]>(() => getColorsInRows(props.allSystemColorInfos, itemPerRow.value));
72+
73+
const sheetStyle = computed<Record<string, string> | undefined>(() => {
74+
const style: Record<string, string> = {};
75+
76+
if (currentTab.value === 'custom' && customSheetHeight.value) {
77+
style['height'] = `${customSheetHeight.value}px`;
78+
}
5379
54-
const allColorRows = computed<ColorInfo[][]>(() => getColorsInRows(props.allColorInfos, itemPerRow.value));
80+
return style;
81+
});
5582
5683
function onColorClicked(colorInfo: ColorInfo): void {
5784
currentValue.value = colorInfo.color;
5885
emit('update:modelValue', currentValue.value);
5986
}
6087
61-
function hasSelectedIcon(row: ColorInfo[]): boolean {
62-
return arrayContainsFieldValue(row, 'id', currentValue.value);
88+
function isSystemColor(value: ColorValue): boolean {
89+
return props.allSystemColorInfos.includes(value);
90+
}
91+
92+
function hasSelectedColor(row: ColorInfo[]): boolean {
93+
return arrayContainsFieldValue(row, 'color', currentValue.value);
94+
}
95+
96+
function createCustomColorPicker(): void {
97+
nextTick(() => {
98+
if (!customColorPickerContainer.value || customColorPicker) {
99+
return;
100+
}
101+
102+
customColorPicker = createInlineColorPicker(
103+
customColorPickerContainer.value,
104+
currentValue.value,
105+
(value: ColorValue) => {
106+
currentValue.value = value;
107+
emit('update:modelValue', currentValue.value);
108+
}
109+
);
110+
111+
nextTick(() => {
112+
const pageContent = customColorPickerContainer.value?.closest('.page-content');
113+
114+
if (pageContent) {
115+
customSheetHeight.value = Math.ceil(pageContent.scrollHeight);
116+
}
117+
});
118+
});
119+
}
120+
121+
function destroyCustomColorPicker(): void {
122+
if (customColorPicker) {
123+
customColorPicker.destroy();
124+
customColorPicker = null;
125+
}
126+
127+
if (customColorPickerContainer.value) {
128+
customColorPickerContainer.value.innerHTML = '';
129+
}
130+
}
131+
132+
function showSystemColors(): void {
133+
currentTab.value = 'system';
134+
destroyCustomColorPicker();
135+
}
136+
137+
function showCustomColor(): void {
138+
currentTab.value = 'custom';
139+
createCustomColorPicker();
63140
}
64141
65142
function onSheetOpen(event: { $el: Framework7Dom }): void {
66143
currentValue.value = props.modelValue;
67-
scrollToSelectedItem(event.$el[0], '.sheet-modal-inner', '.page-content', '.row-has-selected-item');
144+
currentTab.value = isSystemColor(currentValue.value) ? 'system' : 'custom';
145+
146+
if (currentTab.value === 'custom') {
147+
createCustomColorPicker();
148+
} else {
149+
scrollToSelectedItem(event.$el[0], '.sheet-modal-inner', '.page-content', '.row-has-selected-item');
150+
}
68151
}
69152
70153
function onSheetClosed(): void {
154+
destroyCustomColorPicker();
71155
emit('update:show', false);
72156
}
157+
158+
onBeforeUnmount(() => {
159+
destroyCustomColorPicker();
160+
});
73161
</script>
162+
163+
<style>
164+
.custom-color-picker {
165+
.color-picker {
166+
width: 100%;
167+
168+
.color-picker-hex-value {
169+
width: 100%;
170+
}
171+
}
172+
}
173+
</style>

src/desktop-main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { VBtnToggle } from 'vuetify/components/VBtnToggle';
1414
import { VCard, VCardActions, VCardItem, VCardSubtitle, VCardText, VCardTitle } from 'vuetify/components/VCard';
1515
import { VCheckbox, VCheckboxBtn } from 'vuetify/components/VCheckbox';
1616
import { VChip } from 'vuetify/components/VChip';
17+
import { VColorPicker } from 'vuetify/components/VColorPicker';
1718
import { VDataTable } from 'vuetify/components/VDataTable';
1819
import { VDialog } from 'vuetify/components/VDialog';
1920
import { VDivider } from 'vuetify/components/VDivider';
@@ -158,6 +159,7 @@ const vuetify = createVuetify({
158159
VCheckbox,
159160
VCheckboxBtn,
160161
VChip,
162+
VColorPicker,
161163
VDataTable,
162164
VDialog,
163165
VDivider,

0 commit comments

Comments
 (0)