Merge pull request #967 from Retrospring/feature/theme-stimulus

This commit is contained in:
Andreas Nedbal 2023-01-21 13:49:07 +01:00 committed by GitHub
commit 8736ea3af0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 154 additions and 143 deletions

View file

@ -0,0 +1,79 @@
import { Controller } from '@hotwired/stimulus';
import Coloris from '@melloware/coloris';
import {
THEME_MAPPING,
getColorForKey,
getHexColorFromThemeValue,
getIntegerFromHexColor
} from 'utilities/theme';
export default class extends Controller {
static targets = ['color'];
declare readonly colorTargets: HTMLInputElement[];
previewStyle = null;
previewTimeout = null;
setupPreviewElement(): void {
this.previewStyle = document.createElement('style');
this.previewStyle.setAttribute('data-preview-style', '');
document.body.appendChild(this.previewStyle);
}
convertColors(): void {
this.colorTargets.forEach((color) => {
color.value = `#${getHexColorFromThemeValue(color.value)}`;
});
}
connect(): void {
this.setupPreviewElement();
this.convertColors();
Coloris.init();
Coloris({
el: '.color',
wrap: false,
formatToggle: false,
alpha: false
});
}
updatePreview(): void {
clearTimeout(this.previewTimeout);
this.previewTimeout = setTimeout(this.previewTheme.bind(this), 1000);
}
previewTheme(): void {
const payload = {};
this.colorTargets.forEach((color) => {
const name = color.name.substring(6, color.name.length - 1);
payload[name] = parseInt(color.value.substr(1, 6), 16);
});
this.generateTheme(payload);
}
generateTheme(payload: Record<string, string>): void {
let body = ":root {\n";
Object.entries(payload).forEach(([key, value]) => {
if (THEME_MAPPING[key]) {
body += `--${THEME_MAPPING[key]}: ${getColorForKey(THEME_MAPPING[key], value)};\n`;
}
});
body += "}";
this.previewStyle.innerHTML = body;
}
submit(): void {
this.colorTargets.forEach((color) => {
color.value = String(getIntegerFromHexColor(color.value));
});
}
}

View file

@ -1,13 +1,9 @@
import registerEvents from "utilities/registerEvents";
import { profileHeaderChangeHandler, profilePictureChangeHandler } from "./crop";
import { themeDocumentHandler, themeSubmitHandler } from "./theme";
import { userSubmitHandler } from "./password";
export default (): void => {
themeDocumentHandler();
registerEvents([
{ type: 'submit', target: document.querySelector('form.edit_theme, form.new_theme'), handler: themeSubmitHandler },
{ type: 'submit', target: document.querySelector('#edit_user'), handler: userSubmitHandler },
{ type: 'change', target: document.querySelector('#user_profile_picture[type=file]'), handler: profilePictureChangeHandler },
{ type: 'change', target: document.querySelector('#user_profile_header[type=file]'), handler: profileHeaderChangeHandler }

View file

@ -1,115 +0,0 @@
import Coloris from "@melloware/coloris";
let previewStyle = null;
let previewTimeout = null;
const previewTheme = (): void => {
const payload = {};
Array.from(document.querySelectorAll('#update .color')).forEach((color: HTMLInputElement) => {
const name = color.name.substring(6, color.name.length - 1);
payload[name] = parseInt(color.value.substr(1, 6), 16);
});
generateTheme(payload);
}
const generateTheme = (payload: Record<string, string>): void => {
const themeAttributeMap = {
'primary_color': 'primary',
'primary_text': 'primary-text',
'danger_color': 'danger',
'danger_text': 'danger-text',
'warning_color': 'warning',
'warning_text': 'warning-text',
'info_color': 'info',
'info_text': 'info-text',
'success_color': 'success',
'success_text': 'success-text',
'dark_color': 'dark',
'dark_text': 'dark-text',
'light_color': 'light',
'light_text': 'light-text',
'raised_background': 'raised-bg',
'raised_accent': 'raised-accent',
'background_color': 'background',
'body_text': 'body-text',
'input_color': 'input-bg',
'input_text': 'input-text',
'input_placeholder': 'input-placeholder',
'muted_text': 'muted-text'
};
let body = ":root {\n";
(Object.keys(payload)).forEach((payloadKey) => {
if (themeAttributeMap[payloadKey]) {
if (themeAttributeMap[payloadKey].includes('text') || themeAttributeMap[payloadKey].includes('placeholder')) {
const hex = getHexColorFromThemeValue(payload[payloadKey]);
body += `--${themeAttributeMap[payloadKey]}: ${getDecimalTripletsFromHex(hex)};\n`;
}
else {
body += `--${themeAttributeMap[payloadKey]}: #${getHexColorFromThemeValue(payload[payloadKey])};\n`;
}
}
});
body += "}";
previewStyle.innerHTML = body;
}
const getHexColorFromThemeValue = (themeValue: string): string => {
return ('000000' + parseInt(themeValue).toString(16)).substr(-6, 6);
}
const getDecimalTripletsFromHex = (hex: string): string => {
return hex.match(/.{1,2}/g).map((value) => parseInt(value, 16)).join(', ');
}
export function themeDocumentHandler(): void {
if (!document.querySelector('[action="/settings/theme"]')) return;
if (document.querySelector('#clr-picker')) return;
previewStyle = document.createElement('style');
previewStyle.setAttribute('data-preview-style', '');
document.body.appendChild(previewStyle);
Coloris.init();
Array.from(document.querySelectorAll('#update .color')).forEach((color: HTMLInputElement) => {
// If there already is a hex-color in the input, skip
if (color.value.startsWith('#')) return;
let colorValue;
// match for value="[digits]" to ALWAYS get a color value
// TODO: Fix this later with rethinking the entire lifecycle, or dropping Turbolinks
colorValue = color.outerHTML.match(/value="(\d+)"/)[1];
// matching failed, or no result was found, we just fallback to the input value
if (colorValue === null) {
colorValue = color.value;
}
color.value = `#${getHexColorFromThemeValue(colorValue)}`;
Coloris({
el: '.color',
wrap: false,
formatToggle: false,
alpha: false
});
color.addEventListener('input', () => {
clearTimeout(previewTimeout);
previewTimeout = setTimeout(previewTheme, 1000);
});
});
}
export function themeSubmitHandler(): void {
Array.from(document.querySelectorAll('#update .color')).forEach((color: HTMLInputElement) => {
color.value = String(parseInt(color.value.substr(1, 6), 16));
});
}

View file

@ -5,6 +5,7 @@ import CharacterCountController from "retrospring/controllers/character_count_co
import CharacterCountWarningController from "retrospring/controllers/character_count_warning_controller";
import FormatPopupController from "retrospring/controllers/format_popup_controller";
import CollapseController from "retrospring/controllers/collapse_controller";
import ThemeController from "retrospring/controllers/theme_controller";
import CapabilitiesController from "retrospring/controllers/capabilities_controller";
/**
@ -23,4 +24,5 @@ export default function (): void {
window['Stimulus'].register('character-count-warning', CharacterCountWarningController);
window['Stimulus'].register('collapse', CollapseController);
window['Stimulus'].register('format-popup', FormatPopupController);
window['Stimulus'].register('theme', ThemeController);
}

View file

@ -0,0 +1,46 @@
export const THEME_MAPPING = {
'primary_color': 'primary',
'primary_text': 'primary-text',
'danger_color': 'danger',
'danger_text': 'danger-text',
'warning_color': 'warning',
'warning_text': 'warning-text',
'info_color': 'info',
'info_text': 'info-text',
'success_color': 'success',
'success_text': 'success-text',
'dark_color': 'dark',
'dark_text': 'dark-text',
'light_color': 'light',
'light_text': 'light-text',
'raised_background': 'raised-bg',
'raised_accent': 'raised-accent',
'background_color': 'background',
'body_text': 'body-text',
'input_color': 'input-bg',
'input_text': 'input-text',
'input_placeholder': 'input-placeholder',
'muted_text': 'muted-text'
};
export const getHexColorFromThemeValue = (themeValue: string): string => {
return ('000000' + parseInt(themeValue).toString(16)).substr(-6, 6);
}
export const getDecimalTripletsFromHex = (hex: string): string => {
return hex.match(/.{1,2}/g).map((value) => parseInt(value, 16)).join(', ');
}
export const getIntegerFromHexColor = (hex: string): number => {
return parseInt(hex.substr(1, 6), 16);
}
export const getColorForKey = (key: string, color: string): string => {
const hex = getHexColorFromThemeValue(color);
if (key.includes('text') || key.includes('placeholder') || key.includes('rgb')) {
return getDecimalTripletsFromHex(hex);
} else {
return `#${hex}`;
}
};

View file

@ -7,7 +7,10 @@
- if current_user.theme
.pull-right
= link_to t(".delete"), settings_theme_path, data: { turbo_confirm: t("voc.confirm"), turbo_method: :delete }, tabindex: -1, class: "btn btn-danger"
= bootstrap_form_for(current_user.theme || Theme.new, html: { id: "update" }, method: :patch, data: { turbo: false }) do |f|
= bootstrap_form_for(current_user.theme || Theme.new,
html: { id: "update" },
method: :patch,
data: { turbo: false, controller: "theme", action: "theme#submit" }) do |f|
.card
.card-body
%h2= t(".general.heading")
@ -15,9 +18,9 @@
.row
.col-sm-6
= f.text_field :background_color, class: "color", data: { default: 0xF0EDF4 }
= f.text_field :background_color, class: "color", data: { default: 0xF0EDF4, theme_target: "color", action: "theme#updatePreview" }
.col-sm-6
= f.text_field :body_text, class: "color", data: { default: 0x000000 }
= f.text_field :body_text, class: "color", data: { default: 0x000000, theme_target: "color", action: "theme#updatePreview" }
.card
.card-body
%h2= t(".colors.heading")
@ -25,56 +28,56 @@
.row
.col-sm-6
= f.text_field :primary_color, class: "color", data: { default: 0x5E35B1 }
= f.text_field :primary_color, class: "color", data: { default: 0x5E35B1, theme_target: "color", action: "theme#updatePreview" }
.col-sm-6
= f.text_field :primary_text, class: "color", data: { default: 0xFFFFFF }
= f.text_field :primary_text, class: "color", data: { default: 0xFFFFFF, theme_target: "color", action: "theme#updatePreview" }
.col-sm-12
.alert.alert-primary= t(".colors.alert.example", type: t(".colors.alert.type.primary"))
.row
.col-sm-6
= f.text_field :danger_color, class: "color", data: { default: 0xDC3545 }
= f.text_field :danger_color, class: "color", data: { default: 0xDC3545, theme_target: "color", action: "theme#updatePreview" }
.col-sm-6
= f.text_field :danger_text, class: "color", data: { default: 0xFFFFFF }
= f.text_field :danger_text, class: "color", data: { default: 0xFFFFFF, theme_target: "color", action: "theme#updatePreview" }
.col-sm-12
.alert.alert-danger= t(".colors.alert.example", type: t(".colors.alert.type.danger"))
.row
.col-sm-6
= f.text_field :warning_color, class: "color", data: { default: 0xFFC107 }
= f.text_field :warning_color, class: "color", data: { default: 0xFFC107, theme_target: "color", action: "theme#updatePreview" }
.col-sm-6
= f.text_field :warning_text, class: "color", data: { default: 0x292929 }
= f.text_field :warning_text, class: "color", data: { default: 0x292929, theme_target: "color", action: "theme#updatePreview" }
.col-sm-12
.alert.alert-warning= t(".colors.alert.example", type: t(".colors.alert.type.warning"))
.row
.col-sm-6
= f.text_field :info_color, class: "color", data: { default: 0x17A2B8 }
= f.text_field :info_color, class: "color", data: { default: 0x17A2B8, theme_target: "color", action: "theme#updatePreview" }
.col-sm-6
= f.text_field :info_text, class: "color", data: { default: 0xFFFFFF }
= f.text_field :info_text, class: "color", data: { default: 0xFFFFFF, theme_target: "color", action: "theme#updatePreview" }
.col-sm-12
.alert.alert-info= t(".colors.alert.example", type: t(".colors.alert.type.info"))
.row
.col-sm-6
= f.text_field :success_color, class: "color", data: { default: 0x28A745 }
= f.text_field :success_color, class: "color", data: { default: 0x28A745, theme_target: "color", action: "theme#updatePreview" }
.col-sm-6
= f.text_field :success_text, class: "color", data: { default: 0xFFFFFF }
= f.text_field :success_text, class: "color", data: { default: 0xFFFFFF, theme_target: "color", action: "theme#updatePreview" }
.col-sm-12
.alert.alert-success= t(".colors.alert.example", type: t(".colors.alert.type.success"))
.row
.col-sm-6
= f.text_field :dark_color, class: "color", data: { default: 0x343A40 }
= f.text_field :dark_color, class: "color", data: { default: 0x343A40, theme_target: "color", action: "theme#updatePreview" }
.col-sm-6
= f.text_field :dark_text, class: "color", data: { default: 0xFFFFFF }
= f.text_field :dark_text, class: "color", data: { default: 0xFFFFFF, theme_target: "color", action: "theme#updatePreview" }
.col-sm-12
%a.btn.btn-dark.mb-3{ href: "#" }= t(".colors.button.example", type: t(".colors.button.type.dark"))
.row
.col-sm-6
= f.text_field :light_color, class: "color", data: { default: 0xF8F9FA }
= f.text_field :light_color, class: "color", data: { default: 0xF8F9FA, theme_target: "color", action: "theme#updatePreview" }
.col-sm-6
= f.text_field :light_text, class: "color", data: { default: 0xFFFFFF }
= f.text_field :light_text, class: "color", data: { default: 0xFFFFFF, theme_target: "color", action: "theme#updatePreview" }
.col-sm-12
%a.btn.btn-light.mb-3{ href: "#" }= t(".colors.button.example", type: t(".colors.button.type.light"))
.row
.col-sm-6
= f.text_field :muted_text, class: "color", data: { default: 0x6C757D }
= f.text_field :muted_text, class: "color", data: { default: 0x6C757D, theme_target: "color", action: "theme#updatePreview" }
.col-sm-6
%p.pt-4.text-muted= t(".colors.text.example")
.card
@ -84,16 +87,16 @@
.row
.col-sm-6
= f.text_field :input_color, class: "color", data: { default: 0xFFFFFF }
= f.text_field :input_color, class: "color", data: { default: 0xFFFFFF, theme_target: "color", action: "theme#updatePreview" }
.col-sm-6
= f.text_field :input_text, class: "color", data: { default: 0x000000 }
= f.text_field :input_text, class: "color", data: { default: 0x000000, theme_target: "color", action: "theme#updatePreview" }
.row
.col-sm-6
= f.text_field :input_placeholder, class: "color", data: { default: 0x6C757D }
= f.text_field :input_placeholder, class: "color", data: { default: 0x6C757D, theme_target: "color" }
.col-sm-6
.form-group
%label Example Input
%label.form-label Example Input
%input.form-control{ placeholder: "A test placeholder" }
.card
.card-body
@ -102,9 +105,9 @@
.row
.col-sm-6
= f.text_field :raised_background, class: "color", data: { default: 0xFFFFFF }
= f.text_field :raised_background, class: "color", data: { default: 0xFFFFFF, theme_target: "color" }
.col-sm-6
= f.text_field :raised_accent, class: "color", data: { default: 0xF7F7F7 }
= f.text_field :raised_accent, class: "color", data: { default: 0xF7F7F7, theme_target: "color" }
.card-footer
%p= t(".raised.accent.example")
.card