mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	feat(react/settings): port text formatting toolbar
This commit is contained in:
		| @@ -2,7 +2,7 @@ import { Tooltip } from "bootstrap"; | ||||
| import { useEffect, useRef, useMemo, useCallback } from "preact/hooks"; | ||||
| import { escapeQuotes } from "../../services/utils"; | ||||
| import { ComponentChildren } from "preact"; | ||||
| import { memo } from "preact/compat"; | ||||
| import { CSSProperties, memo } from "preact/compat"; | ||||
|  | ||||
| interface FormCheckboxProps { | ||||
|     name: string; | ||||
| @@ -14,9 +14,10 @@ interface FormCheckboxProps { | ||||
|     currentValue: boolean; | ||||
|     disabled?: boolean; | ||||
|     onChange(newValue: boolean): void; | ||||
|     containerStyle?: CSSProperties; | ||||
| } | ||||
|  | ||||
| const FormCheckbox = memo(({ name, disabled, label, currentValue, onChange, hint }: FormCheckboxProps) => { | ||||
| const FormCheckbox = memo(({ name, disabled, label, currentValue, onChange, hint, containerStyle }: FormCheckboxProps) => { | ||||
|     const labelRef = useRef<HTMLLabelElement>(null); | ||||
|  | ||||
|     // Fix: Move useEffect outside conditional | ||||
| @@ -46,7 +47,7 @@ const FormCheckbox = memo(({ name, disabled, label, currentValue, onChange, hint | ||||
|     const titleText = useMemo(() => hint ? escapeQuotes(hint) : undefined, [hint]); | ||||
|  | ||||
|     return ( | ||||
|         <div className="form-checkbox"> | ||||
|         <div className="form-checkbox" style={containerStyle}> | ||||
|             <label | ||||
|                 className="form-check-label tn-checkbox" | ||||
|                 style={labelStyle} | ||||
|   | ||||
| @@ -7,6 +7,7 @@ interface FormRadioProps { | ||||
|     values: { | ||||
|         value: string; | ||||
|         label: string | ComponentChildren; | ||||
|         inlineDescription?: string | ComponentChildren; | ||||
|     }[]; | ||||
|     onChange(newValue: string): void; | ||||
| } | ||||
| @@ -14,9 +15,14 @@ interface FormRadioProps { | ||||
| export default function FormRadioGroup({ values, ...restProps }: FormRadioProps) { | ||||
|     return ( | ||||
|         <> | ||||
|             {(values || []).map(({ value, label }) => ( | ||||
|             {(values || []).map(({ value, label, inlineDescription }) => ( | ||||
|                 <div className="form-checkbox"> | ||||
|                     <FormRadio value={value} label={label} {...restProps} labelClassName="form-check-label" /> | ||||
|                     <FormRadio | ||||
|                         value={value} | ||||
|                         label={label} inlineDescription={inlineDescription} | ||||
|                         labelClassName="form-check-label" | ||||
|                         {...restProps} | ||||
|                     /> | ||||
|                 </div> | ||||
|             ))} | ||||
|         </> | ||||
| @@ -31,7 +37,7 @@ export function FormInlineRadioGroup({ values, ...restProps }: FormRadioProps) { | ||||
|     ) | ||||
| } | ||||
|  | ||||
| function FormRadio({ name, value, label, currentValue, onChange, labelClassName }: Omit<FormRadioProps, "values"> & { value: string, label: ComponentChildren, labelClassName?: string }) { | ||||
| function FormRadio({ name, value, label, currentValue, onChange, labelClassName, inlineDescription }: Omit<FormRadioProps, "values"> & { value: string, label: ComponentChildren, inlineDescription?: ComponentChildren, labelClassName?: string }) { | ||||
|     return ( | ||||
|         <label className={`tn-radio ${labelClassName ?? ""}`}> | ||||
|             <input | ||||
| @@ -42,7 +48,9 @@ function FormRadio({ name, value, label, currentValue, onChange, labelClassName | ||||
|                 checked={value === currentValue} | ||||
|                 onChange={e => onChange((e.target as HTMLInputElement).value)} | ||||
|             /> | ||||
|             {label} | ||||
|             {inlineDescription ? | ||||
|                 <><strong>{label}</strong> - {inlineDescription}</> | ||||
|             : label} | ||||
|         </label> | ||||
|     ) | ||||
| } | ||||
| @@ -100,6 +100,16 @@ export function useSpacedUpdate(callback: () => Promise<void>, interval = 1000) | ||||
|     return spacedUpdateRef.current; | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Allows a React component to read and write a Trilium option, while also watching for external changes. | ||||
|  *  | ||||
|  * Conceptually, `useTriliumOption` works just like `useState`, but the value is also automatically updated if | ||||
|  * the option is changed somewhere else in the client. | ||||
|  *  | ||||
|  * @param name the name of the option to listen for. | ||||
|  * @param needsRefresh whether to reload the frontend whenever the value is changed. | ||||
|  * @returns an array where the first value is the current option value and the second value is the setter. | ||||
|  */ | ||||
| export function useTriliumOption(name: OptionNames, needsRefresh?: boolean): [string, (newValue: OptionValue) => Promise<void>] { | ||||
|     const initialValue = options.get(name); | ||||
|     const [ value, setValue ] = useState(initialValue); | ||||
| @@ -127,8 +137,8 @@ export function useTriliumOption(name: OptionNames, needsRefresh?: boolean): [st | ||||
|     ] | ||||
| } | ||||
|  | ||||
| export function useTriliumOptionBool(name: OptionNames): [boolean, (newValue: boolean) => Promise<void>] { | ||||
|     const [ value, setValue ] = useTriliumOption(name); | ||||
| export function useTriliumOptionBool(name: OptionNames, needsRefresh?: boolean): [boolean, (newValue: boolean) => Promise<void>] { | ||||
|     const [ value, setValue ] = useTriliumOption(name, needsRefresh); | ||||
|     return [ | ||||
|         (value === "true"), | ||||
|         (newValue) => setValue(newValue ? "true" : "false") | ||||
|   | ||||
| @@ -40,6 +40,7 @@ import BackupSettings from "./options/backup.js"; | ||||
| import SpellcheckSettings from "./options/spellcheck.js"; | ||||
| import PasswordSettings from "./options/password.jsx"; | ||||
| import ShortcutSettings from "./options/shortcuts.js"; | ||||
| import TextNoteSettings from "./options/text_notes.jsx"; | ||||
|  | ||||
| const TPL = /*html*/`<div class="note-detail-content-widget note-detail-printable"> | ||||
|     <style> | ||||
| @@ -69,16 +70,7 @@ export type OptionPages = "_optionsAppearance" | "_optionsShortcuts" | "_options | ||||
| const CONTENT_WIDGETS: Record<OptionPages | "_backendLog", ((typeof NoteContextAwareWidget)[] | JSX.Element)> = { | ||||
|     _optionsAppearance: <AppearanceSettings />, | ||||
|     _optionsShortcuts: <ShortcutSettings />, | ||||
|     _optionsTextNotes: [ | ||||
|         EditorOptions, | ||||
|         EditorFeaturesOptions, | ||||
|         HeadingStyleOptions, | ||||
|         CodeBlockOptions, | ||||
|         TableOfContentsOptions, | ||||
|         HighlightsListOptions, | ||||
|         TextAutoReadOnlySizeOptions, | ||||
|         DateTimeFormatOptions | ||||
|     ], | ||||
|     _optionsTextNotes: <TextNoteSettings />, | ||||
|     _optionsCodeNotes: [ | ||||
|         CodeEditorOptions, | ||||
|         CodeTheme, | ||||
|   | ||||
| @@ -112,11 +112,13 @@ function LayoutOrientation() { | ||||
|                 name="layout-orientation" | ||||
|                 values={[ | ||||
|                     { | ||||
|                         label: <><strong>{t("theme.layout-vertical-title")}</strong> - {t("theme.layout-vertical-description")}</>, | ||||
|                         label: t("theme.layout-vertical-title"), | ||||
|                         inlineDescription: t("theme.layout-vertical-description"), | ||||
|                         value: "vertical" | ||||
|                     }, | ||||
|                     { | ||||
|                         label: <><strong>{t("theme.layout-horizontal-title")}</strong> - {t("theme.layout-horizontal-description")}</>, | ||||
|                         label: t("theme.layout-horizontal-title"), | ||||
|                         inlineDescription: t("theme.layout-horizontal-description"), | ||||
|                         value: "horizontal" | ||||
|                     } | ||||
|                 ]} | ||||
|   | ||||
							
								
								
									
										46
									
								
								apps/client/src/widgets/type_widgets/options/text_notes.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								apps/client/src/widgets/type_widgets/options/text_notes.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| import { t } from "../../../services/i18n"; | ||||
| import FormCheckbox from "../../react/FormCheckbox"; | ||||
| import FormRadioGroup from "../../react/FormRadioGroup"; | ||||
| import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; | ||||
| import OptionsSection from "./components/OptionsSection"; | ||||
|  | ||||
| export default function TextNoteSettings() { | ||||
|     return ( | ||||
|         <> | ||||
|             <FormattingToolbar /> | ||||
|         </> | ||||
|     ) | ||||
| } | ||||
|  | ||||
| function FormattingToolbar() { | ||||
|     const [ textNoteEditorType, setTextNoteEditorType ] = useTriliumOption("textNoteEditorType", true); | ||||
|     const [ textNoteEditorMultilineToolbar, setTextNoteEditorMultilineToolbar ] = useTriliumOptionBool("textNoteEditorMultilineToolbar", true); | ||||
|  | ||||
|     return ( | ||||
|         <OptionsSection title={t("editing.editor_type.label")}> | ||||
|             <FormRadioGroup | ||||
|                 name="editor-type" | ||||
|                 currentValue={textNoteEditorType} onChange={setTextNoteEditorType} | ||||
|                 values={[ | ||||
|                     { | ||||
|                         value: "ckeditor-balloon", | ||||
|                         label: t("editing.editor_type.floating.title"), | ||||
|                         inlineDescription: t("editing.editor_type.floating.description") | ||||
|                     }, | ||||
|                     { | ||||
|                         value: "ckeditor-classic", | ||||
|                         label: t("editing.editor_type.fixed.title"), | ||||
|                         inlineDescription: t("editing.editor_type.fixed.description") | ||||
|                     } | ||||
|                 ]} | ||||
|             /> | ||||
|  | ||||
|             <FormCheckbox | ||||
|                 name="multiline-toolbar" | ||||
|                 label={t("editing.editor_type.multiline-toolbar")} | ||||
|                 currentValue={textNoteEditorMultilineToolbar} onChange={setTextNoteEditorMultilineToolbar} | ||||
|                 containerStyle={{ marginLeft: "1em" }} | ||||
|             /> | ||||
|         </OptionsSection> | ||||
|     ) | ||||
| } | ||||
| @@ -1,64 +0,0 @@ | ||||
| import type { OptionMap } from "@triliumnext/commons"; | ||||
| import { t } from "../../../../services/i18n.js"; | ||||
| import utils from "../../../../services/utils.js"; | ||||
| import OptionsWidget from "../options_widget.js"; | ||||
|  | ||||
| const TPL = /*html*/` | ||||
| <div class="options-section formatting-toolbar"> | ||||
|     <h4>${t("editing.editor_type.label")}</h4> | ||||
|  | ||||
|     <div> | ||||
|         <label class="tn-radio"> | ||||
|             <input type="radio" name="editor-type" value="ckeditor-balloon" /> | ||||
|             <strong>${t("editing.editor_type.floating.title")}</strong> | ||||
|             - ${t("editing.editor_type.floating.description")} | ||||
|         </label> | ||||
|     </div> | ||||
|  | ||||
|     <div> | ||||
|         <label class="tn-radio"> | ||||
|             <input type="radio" name="editor-type" value="ckeditor-classic" /> | ||||
|             <strong>${t("editing.editor_type.fixed.title")}</strong> | ||||
|             - ${t("editing.editor_type.fixed.description")} | ||||
|         </label> | ||||
|  | ||||
|         <div> | ||||
|             <label class="tn-checkbox"> | ||||
|                 <input type="checkbox" name="multiline-toolbar" /> | ||||
|                 ${t("editing.editor_type.multiline-toolbar")} | ||||
|             </label> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
| </div> | ||||
|  | ||||
| <style> | ||||
|     .formatting-toolbar div > div { | ||||
|         margin-left: 1em; | ||||
|     } | ||||
| </style> | ||||
| `; | ||||
|  | ||||
| export default class EditorOptions extends OptionsWidget { | ||||
|  | ||||
|     private $body!: JQuery<HTMLElement>; | ||||
|     private $multilineToolbarCheckbox!: JQuery<HTMLElement>; | ||||
|  | ||||
|     doRender() { | ||||
|         this.$widget = $(TPL); | ||||
|         this.$body = $("body"); | ||||
|         this.$widget.find(`input[name="editor-type"]`).on("change", async () => { | ||||
|             const newEditorType = this.$widget.find(`input[name="editor-type"]:checked`).val(); | ||||
|             await this.updateOption("textNoteEditorType", newEditorType); | ||||
|             utils.reloadFrontendApp("editor type change"); | ||||
|         }); | ||||
|  | ||||
|         this.$multilineToolbarCheckbox = this.$widget.find('input[name="multiline-toolbar"]'); | ||||
|         this.$multilineToolbarCheckbox.on("change", () => this.updateCheckboxOption("textNoteEditorMultilineToolbar", this.$multilineToolbarCheckbox)); | ||||
|     } | ||||
|  | ||||
|     async optionsLoaded(options: OptionMap) { | ||||
|         this.$widget.find(`input[name="editor-type"][value="${options.textNoteEditorType}"]`).prop("checked", "true"); | ||||
|         this.setCheckboxState(this.$multilineToolbarCheckbox, options.textNoteEditorMultilineToolbar); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user