mirror of
https://github.com/Retrospring/retrospring.git
synced 2025-02-13 21:33:20 +01:00
Merge pull request #967 from Retrospring/feature/theme-stimulus
This commit is contained in:
commit
8736ea3af0
6 changed files with 154 additions and 143 deletions
79
app/javascript/retrospring/controllers/theme_controller.ts
Normal file
79
app/javascript/retrospring/controllers/theme_controller.ts
Normal 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));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
46
app/javascript/retrospring/utilities/theme.ts
Normal file
46
app/javascript/retrospring/utilities/theme.ts
Normal 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}`;
|
||||
}
|
||||
};
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue