fix: color picker in style manager
This commit is contained in:
1 parent
24621b1450
commit
51584a98ea
2 files changed
+65
-32
No files matched your search
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { onDestroy, untrack } from 'svelte';
|
||||
import { onDestroy, tick, untrack } from 'svelte';
|
||||
import { t } from '../i18n/i18n.svelte';
|
||||
|
||||
let {
|
||||
@@ -77,6 +77,23 @@
|
||||
let stagedText = $derived(formatColor(pickerH, pickerS, pickerV, notation));
|
||||
let savedFrom: number | null = null;
|
||||
let savedTo: number | null = null;
|
||||
// The panels are `fixed` and placed from the button's rect, so a scrolling
|
||||
// ancestor (the style manager's field column) can't clip them.
|
||||
let split = $state<HTMLElement>();
|
||||
let panel = $state<HTMLElement>();
|
||||
let pos = $state({ top: 0, left: 0 });
|
||||
|
||||
async function placePanel() {
|
||||
const anchor = split?.getBoundingClientRect();
|
||||
if (!anchor) return;
|
||||
pos = { top: anchor.bottom + 3, left: anchor.left };
|
||||
await tick();
|
||||
const box = panel?.getBoundingClientRect();
|
||||
if (!box) return;
|
||||
const dx = Math.max(0, box.right + 8 - window.innerWidth);
|
||||
const dy = Math.max(0, box.bottom + 8 - window.innerHeight);
|
||||
if (dx || dy) pos = { top: Math.max(8, pos.top - dy), left: Math.max(8, pos.left - dx) };
|
||||
}
|
||||
|
||||
function clamp01(n: number): number {
|
||||
return n < 0 ? 0 : n > 1 ? 1 : n;
|
||||
@@ -153,35 +170,38 @@
|
||||
return { h: n[0], s: v === 0 ? 0 : 2 * (1 - l / v), v };
|
||||
}
|
||||
|
||||
// The range to restore when applying. No editor (style manager) = no range.
|
||||
function takeRange(): { from: number; to: number } {
|
||||
const from = savedFrom ?? editor?.state.selection.from ?? 0;
|
||||
const to = savedTo ?? editor?.state.selection.to ?? 0;
|
||||
savedFrom = null;
|
||||
savedTo = null;
|
||||
return { from, to };
|
||||
}
|
||||
|
||||
function openPicker() {
|
||||
if (!editor) return;
|
||||
onOpen?.();
|
||||
savedFrom = editor.state.selection.from;
|
||||
savedTo = editor.state.selection.to;
|
||||
savedFrom = editor?.state.selection.from ?? null;
|
||||
savedTo = editor?.state.selection.to ?? null;
|
||||
if (open) {
|
||||
open = false;
|
||||
} else {
|
||||
view = 'palette';
|
||||
open = true;
|
||||
placePanel();
|
||||
}
|
||||
}
|
||||
|
||||
function applyColor(color: string) {
|
||||
if (!editor) return;
|
||||
open = false;
|
||||
view = 'palette';
|
||||
lastColor = color;
|
||||
const from = savedFrom ?? editor.state.selection.from;
|
||||
const to = savedTo ?? editor.state.selection.to;
|
||||
savedFrom = null;
|
||||
savedTo = null;
|
||||
onApply(color, { from, to });
|
||||
onApply(color, takeRange());
|
||||
}
|
||||
|
||||
function quickApplyColor() {
|
||||
if (!editor) return;
|
||||
// Save selection here too because the main button doesn't go through openPicker.
|
||||
if (savedFrom === null) {
|
||||
if (savedFrom === null && editor) {
|
||||
savedFrom = editor.state.selection.from;
|
||||
savedTo = editor.state.selection.to;
|
||||
}
|
||||
@@ -189,14 +209,9 @@
|
||||
}
|
||||
|
||||
function clearColor() {
|
||||
if (!editor) return;
|
||||
open = false;
|
||||
view = 'palette';
|
||||
const from = savedFrom ?? editor.state.selection.from;
|
||||
const to = savedTo ?? editor.state.selection.to;
|
||||
savedFrom = null;
|
||||
savedTo = null;
|
||||
onClear({ from, to });
|
||||
onClear(takeRange());
|
||||
}
|
||||
|
||||
function openMoreColors() {
|
||||
@@ -206,6 +221,7 @@
|
||||
pickerS = hsv.s;
|
||||
pickerV = hsv.v;
|
||||
view = 'custom';
|
||||
placePanel();
|
||||
}
|
||||
|
||||
function confirmMoreColors() {
|
||||
@@ -396,7 +412,7 @@
|
||||
</script>
|
||||
|
||||
<div class="color-picker" use:colorPickerClickOutside>
|
||||
<div class="color-split">
|
||||
<div class="color-split" bind:this={split}>
|
||||
<button class="color-main" onclick={quickApplyColor} {title}>
|
||||
{@render icon()}
|
||||
<span class="color-bar" style="background: {lastColor}"></span>
|
||||
@@ -408,7 +424,7 @@
|
||||
</button>
|
||||
</div>
|
||||
{#if open && view === 'palette'}
|
||||
<div class="color-dropdown">
|
||||
<div class="color-dropdown" bind:this={panel} style="top: {pos.top}px; left: {pos.left}px">
|
||||
<div class="color-title">{title}</div>
|
||||
<button
|
||||
class="color-automatic"
|
||||
@@ -433,6 +449,7 @@
|
||||
</div>
|
||||
<div class="color-extras">
|
||||
<button class="color-more-trigger" onclick={openMoreColors}>{t().color.moreColors}</button>
|
||||
{#if editor || eyeDropperSupported}
|
||||
<button
|
||||
class="color-pipette-trigger"
|
||||
onclick={pickFromPage}
|
||||
@@ -447,11 +464,13 @@
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if open && view === 'custom'}
|
||||
<div class="color-window" role="dialog" aria-label={t().color.chooseColor}>
|
||||
<div class="color-window" role="dialog" aria-label={t().color.chooseColor}
|
||||
bind:this={panel} style="top: {pos.top}px; left: {pos.left}px">
|
||||
<div class="color-window-body">
|
||||
<div
|
||||
class="sv-square"
|
||||
@@ -554,9 +573,7 @@
|
||||
}
|
||||
|
||||
.color-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 3px);
|
||||
left: 0;
|
||||
position: fixed;
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
@@ -689,9 +706,7 @@
|
||||
}
|
||||
|
||||
.color-window {
|
||||
position: absolute;
|
||||
top: calc(100% + 3px);
|
||||
left: 0;
|
||||
position: fixed;
|
||||
width: 210px;
|
||||
padding: 8px;
|
||||
display: flex;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import { activeCharacterStyle } from '../editor/extensions/characterStyle';
|
||||
import { CANDIDATE_FONTS, detectAvailableFonts } from '../utils/fontDetect';
|
||||
import AlignIcon, { type AlignValue } from './AlignIcon.svelte';
|
||||
import ColorPicker from './ColorPicker.svelte';
|
||||
|
||||
// LibreOffice's style manager: pick a style, edit its properties, or make a new one
|
||||
// from the cursor's formatting. Edits apply live — every block using the style follows.
|
||||
@@ -318,10 +319,28 @@
|
||||
placeholder={inherited(resolved.text.fontSizePt)}
|
||||
onchange={(e) => edit({ text: { fontSizePt: num(e.currentTarget.value) } })} />
|
||||
</label>
|
||||
<label>{t().styles.color}
|
||||
<input type="color" value={style.text.color ?? resolved.text.color ?? '#000000'}
|
||||
onchange={(e) => edit({ text: { color: e.currentTarget.value.toUpperCase() } })} />
|
||||
</label>
|
||||
<div class="field">
|
||||
<span class="field-label">{t().styles.color}</span>
|
||||
<!-- No editor: the picker sets a style property, not a document range. -->
|
||||
{#key style.name}
|
||||
<ColorPicker
|
||||
editor={null}
|
||||
currentColor={style.text.color ?? null}
|
||||
defaultColor={style.text.color ?? resolved.text.color ?? '#000000'}
|
||||
title={t().styles.color}
|
||||
chevronTitle={t().styles.color}
|
||||
clearLabel={inherited(resolved.text.color) || '—'}
|
||||
onApply={(c) => edit({ text: { color: c } })}
|
||||
onClear={() => edit({ text: { color: undefined } })}
|
||||
>
|
||||
{#snippet icon()}
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<text x="8" y="14" text-anchor="middle" font-size="15" font-family="sans-serif" fill="currentColor">A</text>
|
||||
</svg>
|
||||
{/snippet}
|
||||
</ColorPicker>
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row toggles">
|
||||
@@ -557,7 +576,6 @@
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
input[type='color'] { padding: 0.1rem; height: 1.9rem; }
|
||||
|
||||
footer {
|
||||
display: flex;
|
||||
|
||||
Reference in new issue
Block a user