mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 15:19:01 +02:00
feat(react/settings): port code block settings
This commit is contained in:
parent
3ba0bcea4e
commit
234d3997b1
@ -14,10 +14,12 @@ export default function FormGroup({ label, title, className, children, descripti
|
|||||||
return (
|
return (
|
||||||
<div className={`form-group ${className} ${disabled ? "disabled" : ""}`} title={title}
|
<div className={`form-group ${className} ${disabled ? "disabled" : ""}`} title={title}
|
||||||
style={{ "margin-bottom": "15px" }}>
|
style={{ "margin-bottom": "15px" }}>
|
||||||
<label style={{ width: "100%" }} ref={labelRef}>
|
{ label
|
||||||
|
? <label style={{ width: "100%" }} ref={labelRef}>
|
||||||
{label && <div style={{ "margin-bottom": "10px" }}>{label}</div> }
|
{label && <div style={{ "margin-bottom": "10px" }}>{label}</div> }
|
||||||
{children}
|
{children}
|
||||||
</label>
|
</label>
|
||||||
|
: children}
|
||||||
|
|
||||||
{description && <small className="form-text">{description}</small>}
|
{description && <small className="form-text">{description}</small>}
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,7 +2,7 @@ import type { ComponentChildren } from "preact";
|
|||||||
|
|
||||||
type OnChangeListener = (newValue: string) => void;
|
type OnChangeListener = (newValue: string) => void;
|
||||||
|
|
||||||
interface FormSelectGroup<T> {
|
export interface FormSelectGroup<T> {
|
||||||
title: string;
|
title: string;
|
||||||
items: T[];
|
items: T[];
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ function getProps({ className, html, style }: RawHtmlProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHtml(html: string | HTMLElement | JQuery<HTMLElement>) {
|
export function getHtml(html: string | HTMLElement | JQuery<HTMLElement>) {
|
||||||
if (typeof html === "object" && "length" in html) {
|
if (typeof html === "object" && "length" in html) {
|
||||||
html = html[0];
|
html = html[0];
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,20 @@
|
|||||||
import { useEffect } from "preact/hooks";
|
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
||||||
import { t } from "../../../services/i18n";
|
import { t } from "../../../services/i18n";
|
||||||
import FormCheckbox from "../../react/FormCheckbox";
|
import FormCheckbox from "../../react/FormCheckbox";
|
||||||
import FormRadioGroup from "../../react/FormRadioGroup";
|
import FormRadioGroup from "../../react/FormRadioGroup";
|
||||||
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
|
||||||
import OptionsSection from "./components/OptionsSection";
|
import OptionsSection from "./components/OptionsSection";
|
||||||
import { toggleBodyClass } from "../../../services/utils";
|
import { toggleBodyClass } from "../../../services/utils";
|
||||||
|
import FormGroup from "../../react/FormGroup";
|
||||||
|
import Column from "../../react/Column";
|
||||||
|
import { FormSelectGroup, FormSelectWithGroups } from "../../react/FormSelect";
|
||||||
|
import { Themes, type Theme } from "@triliumnext/highlightjs";
|
||||||
|
import { ensureMimeTypesForHighlighting, loadHighlightingTheme } from "../../../services/syntax_highlight";
|
||||||
|
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
|
||||||
|
import { ComponentChildren } from "preact";
|
||||||
|
import RawHtml, { getHtml } from "../../react/RawHtml";
|
||||||
|
import { ParentComponent } from "../../react/ReactBasicWidget";
|
||||||
|
import { CSSProperties } from "preact/compat";
|
||||||
|
|
||||||
export default function TextNoteSettings() {
|
export default function TextNoteSettings() {
|
||||||
return (
|
return (
|
||||||
@ -12,6 +22,7 @@ export default function TextNoteSettings() {
|
|||||||
<FormattingToolbar />
|
<FormattingToolbar />
|
||||||
<EditorFeatures />
|
<EditorFeatures />
|
||||||
<HeadingStyle />
|
<HeadingStyle />
|
||||||
|
<CodeBlockStyle />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -91,3 +102,132 @@ function HeadingStyle() {
|
|||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CodeBlockStyle() {
|
||||||
|
const themes = useMemo(() => groupThemesByLightOrDark(), []);
|
||||||
|
const [ codeBlockTheme, setCodeBlockTheme ] = useTriliumOption("codeBlockTheme");
|
||||||
|
const [ codeBlockWordWrap, setCodeBlockWordWrap ] = useTriliumOptionBool("codeBlockWordWrap");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OptionsSection title={t("highlighting.title")}>
|
||||||
|
<FormGroup className="row">
|
||||||
|
<Column md={6}>
|
||||||
|
<label>{t("highlighting.color-scheme")}</label>
|
||||||
|
<FormSelectWithGroups
|
||||||
|
values={themes}
|
||||||
|
keyProperty="val" titleProperty="title"
|
||||||
|
currentValue={codeBlockTheme} onChange={(newTheme) => {
|
||||||
|
loadHighlightingTheme(newTheme);
|
||||||
|
setCodeBlockTheme(newTheme);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column md={6} className="side-checkbox">
|
||||||
|
<FormCheckbox
|
||||||
|
name="word-wrap"
|
||||||
|
label={t("code_block.word_wrapping")}
|
||||||
|
currentValue={codeBlockWordWrap} onChange={setCodeBlockWordWrap}
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<CodeBlockPreview theme={codeBlockTheme} wordWrap={codeBlockWordWrap} />
|
||||||
|
</OptionsSection>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SAMPLE_LANGUAGE = normalizeMimeTypeForCKEditor("application/javascript;env=frontend");
|
||||||
|
const SAMPLE_CODE = `\
|
||||||
|
const n = 10;
|
||||||
|
greet(n); // Print "Hello World" for n times
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a "Hello World!" message for a given amount of times, on the standard console. The "Hello World!" text will be displayed once per line.
|
||||||
|
*
|
||||||
|
* @param {number} times The number of times to print the \`Hello World!\` message.
|
||||||
|
*/
|
||||||
|
function greet(times) {
|
||||||
|
for (let i = 0; i++; i < times) {
|
||||||
|
console.log("Hello World!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function CodeBlockPreview({ theme, wordWrap }: { theme: string, wordWrap: boolean }) {
|
||||||
|
const [ code, setCode ] = useState<string>(SAMPLE_CODE);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (theme !== "none") {
|
||||||
|
import("@triliumnext/highlightjs").then(async (hljs) => {
|
||||||
|
await ensureMimeTypesForHighlighting();
|
||||||
|
const highlightedText = hljs.highlight(SAMPLE_CODE, {
|
||||||
|
language: SAMPLE_LANGUAGE
|
||||||
|
});
|
||||||
|
if (highlightedText) {
|
||||||
|
setCode(highlightedText.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setCode(SAMPLE_CODE);
|
||||||
|
}
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
const codeStyle = useMemo<CSSProperties>(() => {
|
||||||
|
if (wordWrap) {
|
||||||
|
return { whiteSpace: "pre-wrap" };
|
||||||
|
} else {
|
||||||
|
return { whiteSpace: "pre"};
|
||||||
|
}
|
||||||
|
}, [ wordWrap ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="note-detail-readonly-text-content ck-content code-sample-wrapper">
|
||||||
|
<pre className="hljs" style={{ marginBottom: 0 }}>
|
||||||
|
<code className="code-sample" style={codeStyle} dangerouslySetInnerHTML={getHtml(code)} />
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ThemeData {
|
||||||
|
val: string;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupThemesByLightOrDark() {
|
||||||
|
const darkThemes: ThemeData[] = [];
|
||||||
|
const lightThemes: ThemeData[] = [];
|
||||||
|
|
||||||
|
for (const [ id, theme ] of Object.entries(Themes)) {
|
||||||
|
const data: ThemeData = {
|
||||||
|
val: "default:" + id,
|
||||||
|
title: theme.name
|
||||||
|
};
|
||||||
|
|
||||||
|
if (theme.name.includes("Dark")) {
|
||||||
|
darkThemes.push(data);
|
||||||
|
} else {
|
||||||
|
lightThemes.push(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const output: FormSelectGroup<ThemeData>[] = [
|
||||||
|
{
|
||||||
|
title: "",
|
||||||
|
items: [{
|
||||||
|
val: "none",
|
||||||
|
title: t("code_block.theme_none")
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("code_block.theme_group_light"),
|
||||||
|
items: lightThemes
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("code_block.theme_group_dark"),
|
||||||
|
items: darkThemes
|
||||||
|
}
|
||||||
|
];
|
||||||
|
return output;
|
||||||
|
}
|
@ -1,165 +0,0 @@
|
|||||||
import { normalizeMimeTypeForCKEditor, type OptionMap } from "@triliumnext/commons";
|
|
||||||
import { t } from "../../../../services/i18n.js";
|
|
||||||
import server from "../../../../services/server.js";
|
|
||||||
import OptionsWidget from "../options_widget.js";
|
|
||||||
import { ensureMimeTypesForHighlighting, loadHighlightingTheme } from "../../../../services/syntax_highlight.js";
|
|
||||||
import { Themes, type Theme } from "@triliumnext/highlightjs";
|
|
||||||
|
|
||||||
const SAMPLE_LANGUAGE = normalizeMimeTypeForCKEditor("application/javascript;env=frontend");
|
|
||||||
const SAMPLE_CODE = `\
|
|
||||||
const n = 10;
|
|
||||||
greet(n); // Print "Hello World" for n times
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays a "Hello World!" message for a given amount of times, on the standard console. The "Hello World!" text will be displayed once per line.
|
|
||||||
*
|
|
||||||
* @param {number} times The number of times to print the \`Hello World!\` message.
|
|
||||||
*/
|
|
||||||
function greet(times) {
|
|
||||||
for (let i = 0; i++; i < times) {
|
|
||||||
console.log("Hello World!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="options-section">
|
|
||||||
<h4>${t("highlighting.title")}</h4>
|
|
||||||
|
|
||||||
<div class="form-group row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="highlighting-color-scheme-select">${t("highlighting.color-scheme")}</label>
|
|
||||||
<select id="highlighting-color-scheme-select" class="theme-select form-select"></select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 side-checkbox">
|
|
||||||
<label class="form-check tn-checkbox">
|
|
||||||
<input type="checkbox" class="word-wrap form-check-input" />
|
|
||||||
${t("code_block.word_wrapping")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="note-detail-readonly-text-content ck-content code-sample-wrapper">
|
|
||||||
<pre class="hljs"><code class="code-sample">${SAMPLE_CODE}</code></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.code-sample-wrapper {
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-sample-wrapper pre {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains appearance settings for code blocks within text notes, such as the theme for the syntax highlighter.
|
|
||||||
*/
|
|
||||||
export default class CodeBlockOptions extends OptionsWidget {
|
|
||||||
|
|
||||||
private $themeSelect!: JQuery<HTMLElement>;
|
|
||||||
private $wordWrap!: JQuery<HTMLElement>;
|
|
||||||
private $sampleEl!: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
this.$themeSelect = this.$widget.find(".theme-select");
|
|
||||||
|
|
||||||
// Populate the list of themes.
|
|
||||||
const themeGroups = groupThemesByLightOrDark();
|
|
||||||
for (const [key, themes] of Object.entries(themeGroups)) {
|
|
||||||
const $group = key ? $("<optgroup>").attr("label", key) : null;
|
|
||||||
|
|
||||||
for (const theme of themes) {
|
|
||||||
const option = $("<option>")
|
|
||||||
.attr("value", theme.val)
|
|
||||||
.text(theme.title);
|
|
||||||
|
|
||||||
if ($group) {
|
|
||||||
$group.append(option);
|
|
||||||
} else {
|
|
||||||
this.$themeSelect.append(option);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($group) {
|
|
||||||
this.$themeSelect.append($group);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$themeSelect.on("change", async () => {
|
|
||||||
const newTheme = String(this.$themeSelect.val());
|
|
||||||
loadHighlightingTheme(newTheme);
|
|
||||||
await server.put(`options/codeBlockTheme/${newTheme}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$wordWrap = this.$widget.find("input.word-wrap");
|
|
||||||
this.$wordWrap.on("change", () => this.updateCheckboxOption("codeBlockWordWrap", this.$wordWrap));
|
|
||||||
|
|
||||||
// Set up preview
|
|
||||||
this.$sampleEl = this.$widget.find(".code-sample");
|
|
||||||
}
|
|
||||||
|
|
||||||
#setupPreview(shouldEnableSyntaxHighlight: boolean) {
|
|
||||||
const text = SAMPLE_CODE;
|
|
||||||
if (shouldEnableSyntaxHighlight) {
|
|
||||||
import("@triliumnext/highlightjs").then(async (hljs) => {
|
|
||||||
await ensureMimeTypesForHighlighting();
|
|
||||||
const highlightedText = hljs.highlight(text, {
|
|
||||||
language: SAMPLE_LANGUAGE
|
|
||||||
});
|
|
||||||
if (highlightedText) {
|
|
||||||
this.$sampleEl.html(highlightedText.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.$sampleEl.text(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async optionsLoaded(options: OptionMap) {
|
|
||||||
this.$themeSelect.val(options.codeBlockTheme);
|
|
||||||
this.setCheckboxState(this.$wordWrap, options.codeBlockWordWrap);
|
|
||||||
this.$widget.closest(".note-detail-printable").toggleClass("word-wrap", options.codeBlockWordWrap === "true");
|
|
||||||
|
|
||||||
this.#setupPreview(options.codeBlockTheme !== "none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ThemeData {
|
|
||||||
val: string;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function groupThemesByLightOrDark() {
|
|
||||||
const darkThemes: ThemeData[] = [];
|
|
||||||
const lightThemes: ThemeData[] = [];
|
|
||||||
|
|
||||||
for (const [ id, theme ] of Object.entries(Themes)) {
|
|
||||||
const data: ThemeData = {
|
|
||||||
val: "default:" + id,
|
|
||||||
title: theme.name
|
|
||||||
};
|
|
||||||
|
|
||||||
if (theme.name.includes("Dark")) {
|
|
||||||
darkThemes.push(data);
|
|
||||||
} else {
|
|
||||||
lightThemes.push(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const output: Record<string, ThemeData[]> = {
|
|
||||||
"": [
|
|
||||||
{
|
|
||||||
val: "none",
|
|
||||||
title: t("code_block.theme_none")
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
output[t("code_block.theme_group_light")] = lightThemes;
|
|
||||||
output[t("code_block.theme_group_dark")] = darkThemes;
|
|
||||||
return output;
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user