Add custom color picker to sidebar editor

Signed-off-by: Georg Ehrke <developer@georgehrke.com>
This commit is contained in:
Georg Ehrke 2020-02-25 13:02:51 +01:00
parent f2577d4e2f
commit ccfa924035
No known key found for this signature in database
GPG Key ID: 9D98FD9380A1CB43
12 changed files with 329 additions and 12 deletions

View File

@ -467,6 +467,7 @@
.property-text,
.property-select,
.property-color,
.property-select-multiple,
.property-title {
display: flex;
@ -524,6 +525,18 @@
}
}
.property-color {
&__color-preview {
border-radius: var(--border-radius);
height: 34px !important;
width: 34px !important;
}
&__info {
margin-top: 0;
}
}
.property-text {
&__input {
textarea {

70
package-lock.json generated
View File

@ -7171,6 +7171,27 @@
"is-regexp": "^2.0.0"
}
},
"closest-css-color": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/closest-css-color/-/closest-css-color-0.1.1.tgz",
"integrity": "sha512-Vor9mkq3sXlEO8FTTjxVhV8jlmh2dQ9oVsrhcSan2f4qikBgnqZnJQXSvxrYrGWEp5UHUIAEEtPZ1OLeyfMa5w==",
"requires": {
"colour-proximity": "0.0.2",
"css-color-names": "0.0.4",
"hex-rgb": "^2.0.0",
"lodash.merge": "^4.6.1",
"lodash.pick": "^4.4.0",
"lodash.sortby": "^4.7.0",
"lodash.uniqby": "^4.7.0"
},
"dependencies": {
"css-color-names": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz",
"integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA="
}
}
},
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@ -7223,6 +7244,29 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"color-string": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-0.1.3.tgz",
"integrity": "sha1-6GXS4+WfZlw68N4UOD9r8HBWhfM=",
"requires": {
"color-convert": "0.2.x"
},
"dependencies": {
"color-convert": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.2.1.tgz",
"integrity": "sha1-NjyrI8lLMaDWTbcQSLjGqUD4xow="
}
}
},
"colour-proximity": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/colour-proximity/-/colour-proximity-0.0.2.tgz",
"integrity": "sha1-E5rTr+zzAbyAO42mmPMslyl0dpw=",
"requires": {
"color-string": "~0.1.2"
}
},
"combined-stream": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
@ -7533,6 +7577,11 @@
}
}
},
"css-color-names": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-1.0.1.tgz",
"integrity": "sha512-/loXYOch1qU1biStIFsHH8SxTmOseh1IJqFvy8IujXOm1h+QjUdDhkzOrR5HG8K8mlxREj0yfi8ewCHx0eMxzA=="
},
"css-loader": {
"version": "3.4.2",
"resolved": "http://registry.npmjs.org/css-loader/-/css-loader-3.4.2.tgz",
@ -10112,6 +10161,11 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
"hex-rgb": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-2.0.0.tgz",
"integrity": "sha512-JDPETuFBgYKFWufLB9tk7SaLoia5nSsOSa2DWzbLvK0TXxQglIQ4FBxrO9QmfUKenJJjBfFQ0jJ6jSIK2468kQ=="
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@ -13513,14 +13567,17 @@
"lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
},
"lodash.pick": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz",
"integrity": "sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM="
},
"lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
"dev": true
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg="
},
"lodash.throttle": {
"version": "4.1.1",
@ -13533,6 +13590,11 @@
"integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=",
"dev": true
},
"lodash.uniqby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz",
"integrity": "sha1-2ZwHpmnp5tJOE2Lf4mbGdhavEwI="
},
"log-symbols": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz",

View File

@ -76,6 +76,12 @@ export default {
},
},
methods: {
/**
* Emits the select calendar event
*
* // TODO: this should emit the calendar id instead
* @param {Object} value The calendar Object
*/
selectCalendar(value) {
this.$emit('selectCalendar', value)
},

View File

@ -0,0 +1,138 @@
<!--
- @copyright Copyright (c) 2020 Georg Ehrke <oc.list@georgehrke.com>
-
- @author Georg Ehrke <oc.list@georgehrke.com>
-
- @license GNU AGPL version 3 or any later version
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-->
<template>
<div class="property-color">
<div
class="property-color__icon"
:class="icon"
:title="readableName" />
<div
v-if="isReadOnly"
class="property-color__input property-color__input--readonly">
<!-- eslint-disable-next-line vue/singleline-html-element-content-newline -->
<div
class="property-color__color-preview"
:style="{'background-color': selectedColor }" />
</div>
<div
v-else
class="property-color__input">
<ColorPicker
:value="selectedColor"
:open.sync="isColorPickerOpen"
@input="changeColor">
<button class="property-color__color-preview"
:style="{'background-color': selectedColor }" />
</ColorPicker>
</div>
<div
v-if="!isReadOnly"
class="property-color__info">
<Actions>
<ActionButton
icon="icon-close"
@click.prevent.stop="deleteColor">
{{ $t('calendar', 'Remove color') }}
</ActionButton>
</Actions>
</div>
</div>
</template>
<script>
import PropertyMixin from '../../../mixins/PropertyMixin'
import { Actions } from '@nextcloud/vue/dist/Components/Actions'
import { ActionButton } from '@nextcloud/vue/dist/Components/ActionButton'
import { ColorPicker } from '@nextcloud/vue/dist/Components/ColorPicker'
import debounce from 'debounce'
export default {
name: 'PropertyColor',
components: {
Actions,
ActionButton,
ColorPicker,
},
mixins: [
PropertyMixin,
],
props: {
/**
* The color of the calendar
* this event is in
*/
calendarColor: {
type: String,
default: null,
},
},
data() {
return {
isColorPickerOpen: false,
}
},
computed: {
/**
* The selected color is either custom or
* defaults to the color of the calendar
*
* @returns {String}
*/
selectedColor() {
return this.value || this.calendarColor
},
},
methods: {
/**
* Changes / Sets the custom color of this event
*
* The problem we are facing here is that the
* color-picker component uses normal hex colors,
* but the RFC 7986 property COLOR requires
* css-color-names.
*
* The color-space of css-color-names is smaller
* than the one of hex colors. Hence the color-
* picker (especially in the custom color-picker)
* will jump after the color changed. To prevent
* flickering, we only update the color after the
* user stopped moving the color-picker and not
* immediately.
*
* @param {String} newColor The new Color as HEX
*/
changeColor: debounce(function(newColor) {
this.$emit('update:value', newColor)
}, 500),
/**
* Removes the custom color from this event,
* defaulting the color back to the calendar-color
*/
deleteColor() {
this.$emit('update:value', null)
},
},
}
</script>

View File

@ -44,6 +44,10 @@ export default {
},
},
methods: {
/**
* TODO: this should emit the calendar id instead
* @param {Object} newCalendar The selected calendar
*/
change(newCalendar) {
if (!newCalendar) {
return

View File

@ -24,7 +24,7 @@ import {
hexToRGB,
isLight,
generateTextColorForHex,
getHexForColorName
getHexForColorName,
} from '../utils/color.js'
import logger from '../utils/logger.js'
@ -94,7 +94,6 @@ export function eventSourceFunction(calendarObjects, calendar, start, end, timez
if (object.color) {
const customColor = getHexForColorName(object.color)
console.debug(customColor)
if (customColor) {
fcEvent.backgroundColor = customColor
fcEvent.borderColor = customColor

View File

@ -177,6 +177,14 @@ export default {
backgroundImage() {
return getIllustrationForTitle(this.title)
},
/**
* Returns the color the illustration should be colored in
*
* @returns {String}
*/
illustrationColor() {
return this.color || this.selectedCalendarColor
},
/**
* Returns the color of the calendar selected by the user
* This is used to color illustration
@ -195,6 +203,18 @@ export default {
return this.selectedCalendar.color
},
/**
* Returns the custom color of this event
*
* @returns {null|String}
*/
color() {
if (!this.calendarObjectInstance) {
return null
}
return this.calendarObjectInstance.customColor
},
/**
* Returns whether or not to display event details
*

View File

@ -23,6 +23,7 @@ import { getDateFromDateTimeValue } from '../utils/date.js'
import DurationValue from 'calendar-js/src/values/durationValue.js'
import { getWeekDayFromDate } from '../utils/recurrence.js'
import { getAmountAndUnitForTimedEvents, getAmountHoursMinutesAndUnitForAllDayEvents } from '../utils/alarms.js'
import { getHexForColorName } from '../utils/color.js'
/**
* Creates a complete calendar-object-instance-object based on given props
@ -129,6 +130,13 @@ export const mapEventComponentToCalendarObjectInstanceObject = (eventComponent)
calendarObjectInstanceObject.attendees = getAttendeesFromEventComponent(eventComponent)
calendarObjectInstanceObject.alarms = getAlarmsFromEventComponent(eventComponent)
if (eventComponent.hasProperty('COLOR')) {
const hexColor = getHexForColorName(eventComponent.getFirstPropertyFirstValue('COLOR'))
if (hexColor !== null) {
calendarObjectInstanceObject.customColor = hexColor
}
}
return calendarObjectInstanceObject
}

View File

@ -39,6 +39,10 @@ import {
getAmountHoursMinutesAndUnitForAllDayEvents,
getTotalSecondsFromAmountAndUnitForTimedEvents, getTotalSecondsFromAmountHourMinutesAndUnitForAllDayEvents,
} from '../utils/alarms.js'
import {
getClosestCSS3ColorNameForHex,
getHexForColorName,
} from '../utils/color.js'
const state = {
isNew: null,
@ -355,11 +359,29 @@ const mutations = {
* @param {Object} state The Vuex state
* @param {Object} data The destructuring object
* @param {Object} data.calendarObjectInstance The calendarObjectInstance object
* @param {String} data.customColor New color to set
* @param {String|null} data.customColor New color to set
*/
changeCustomColor(state, { calendarObjectInstance, customColor }) {
calendarObjectInstance.eventComponent.customColor = customColor
calendarObjectInstance.customColor = customColor
if (customColor === null) {
calendarObjectInstance.eventComponent.deleteAllProperties('COLOR')
Vue.set(calendarObjectInstance, 'customColor', null)
return
}
const cssColorName = getClosestCSS3ColorNameForHex(customColor)
const hexColorOfCssName = getHexForColorName(cssColorName)
// Abort if either is undefined
if (!cssColorName || !hexColorOfCssName) {
console.error('Setting custom color failed')
console.error('customColor: ', customColor)
console.error('cssColorName: ', cssColorName)
console.error('hexColorOfCssName: ', hexColorOfCssName)
return
}
calendarObjectInstance.eventComponent.color = cssColorName
Vue.set(calendarObjectInstance, 'customColor', hexColorOfCssName)
},
/**

View File

@ -51,7 +51,7 @@
</template>
<template v-slot:header>
<IllustrationHeader :color="selectedCalendarColor" :illustration-url="backgroundImage" />
<IllustrationHeader :color="illustrationColor" :illustration-url="backgroundImage" />
</template>
<template v-slot:secondary-actions>
@ -124,6 +124,13 @@
:value="categories"
@addSingleValue="addCategory"
@removeSingleValue="removeCategory" />
<PropertyColor
:calendar-color="selectedCalendarColor"
:is-read-only="isReadOnly"
:prop-model="rfcProps.color"
:value="color"
@update:value="updateColor" />
</div>
<SaveButtons
v-if="!isLoading && !isReadOnly"
@ -248,10 +255,12 @@ import PropertyTitleTimePickerLoadingPlaceholder
from '../components/Editor/Properties/PropertyTitleTimePickerLoadingPlaceholder.vue'
import SaveButtons from '../components/Editor/SaveButtons.vue'
import PropertySelectMultiple from '../components/Editor/Properties/PropertySelectMultiple.vue'
import PropertyColor from '../components/Editor/Properties/PropertyColor.vue'
export default {
name: 'EditSidebar',
components: {
PropertyColor,
PropertySelectMultiple,
SaveButtons,
PropertyTitleTimePickerLoadingPlaceholder,
@ -367,6 +376,17 @@ export default {
category,
})
},
/**
* Updates the color of the event
*
* @param {String} customColor The new color
*/
updateColor(customColor) {
this.$store.commit('changeCustomColor', {
calendarObjectInstance: this.calendarObjectInstance,
customColor,
})
},
},
}
</script>

View File

@ -50,7 +50,7 @@
<IllustrationHeader
v-if="!isLoading"
:color="selectedCalendarColor"
:color="illustrationColor"
:illustration-url="backgroundImage" />
<PropertyTitle

View File

@ -20,18 +20,33 @@
*
*/
import { eventSourceFunction } from '../../../../src/fullcalendar/eventSourceFunction.js'
import {
hexToRGB,
isLight,
generateTextColorForHex,
getHexForColorName,
} from '../../../../src/utils/color.js'
import { translate } from '@nextcloud/l10n'
jest.mock('@nextcloud/l10n')
jest.mock('../../../../src/utils/color.js')
describe('fullcalendar/eventSourceFunction test suite', () => {
beforeEach(() => {
translate.mockClear()
getHexForColorName.mockClear()
generateTextColorForHex.mockClear()
})
it('should provide fc-events', () => {
translate
.mockImplementation((app, str) => str)
getHexForColorName
.mockImplementation(() => '#ff0000')
generateTextColorForHex
.mockImplementation(() => '#eeeeee')
isLight
.mockImplementation(() => false)
const event11Start = new Date(2020, 1, 1, 10, 0, 0, 0);
const event11End = new Date(2020, 1, 1, 15, 0, 0, 0);
@ -131,6 +146,7 @@ describe('fullcalendar/eventSourceFunction test suite', () => {
})
},
hasComponent: jest.fn().mockReturnValue(false),
color: 'red',
}]
const calendarObjects = [{
@ -249,7 +265,10 @@ describe('fullcalendar/eventSourceFunction test suite', () => {
calendarName: 'Calendar displayname',
calendarOrder: 1337,
darkText: false,
}
},
backgroundColor: '#ff0000',
borderColor: '#ff0000',
textColor: '#eeeeee',
}
])
@ -285,6 +304,12 @@ describe('fullcalendar/eventSourceFunction test suite', () => {
expect(translate).toHaveBeenNthCalledWith(4, 'calendar', 'Untitled event')
expect(translate).toHaveBeenNthCalledWith(5, 'calendar', 'Untitled event')
expect(getHexForColorName).toHaveBeenCalledTimes(1)
expect(getHexForColorName).toHaveBeenNthCalledWith(1, 'red')
expect(generateTextColorForHex).toHaveBeenCalledTimes(1)
expect(generateTextColorForHex).toHaveBeenNthCalledWith(1, '#ff0000')
// Make sure the following dates have not been touched
expect(event11Start.getFullYear()).toEqual(2020)
expect(event11Start.getMonth()).toEqual(1)