diff --git a/frontend/components/FrontmatterInput.js b/frontend/components/FrontmatterInput.js index 3ca04e3d24..a60f4b5b6a 100644 --- a/frontend/components/FrontmatterInput.js +++ b/frontend/components/FrontmatterInput.js @@ -1,10 +1,12 @@ import { html, Component, useRef, useLayoutEffect, useState, useEffect } from "../imports/Preact.js" import { has_ctrl_or_cmd_pressed } from "../common/KeyboardShortcuts.js" +import _ from "../imports/lodash.js" import "https://cdn.jsdelivr.net/gh/fonsp/rebel-tag-input@1.0.6/lib/rebel-tag-input.mjs" //@ts-ignore import dialogPolyfill from "https://cdn.jsdelivr.net/npm/dialog-polyfill@0.5.6/dist/dialog-polyfill.esm.min.js" +import immer from "../imports/immer.js" /** * @param {{ @@ -23,9 +25,12 @@ export const FrontMatterInput = ({ remote_frontmatter, set_remote_frontmatter }) // console.log("New frontmatter:", frontmatter) // }, [frontmatter]) - const fm_setter = (key) => (value) => { - set_frontmatter((fm) => ({ ...fm, [key]: value })) - } + const fm_setter = (key) => (value) => + set_frontmatter( + immer((fm) => { + _.set(fm, key, value) + }) + ) const dialog_ref = useRef(/** @type {HTMLDialogElement?} */ (null)) useLayoutEffect(() => { @@ -42,10 +47,26 @@ export const FrontMatterInput = ({ remote_frontmatter, set_remote_frontmatter }) close() } const submit = () => { - set_remote_frontmatter(frontmatter).then(() => alert("Frontmatter synchronized ✔\n\nThese parameters will be used in future exports.")) + set_remote_frontmatter(clean_data(frontmatter) ?? {}).then(() => + alert("Frontmatter synchronized ✔\n\nThese parameters will be used in future exports.") + ) close() } + const clean_data = (obj) => { + let a = _.isPlainObject(obj) + ? Object.fromEntries( + Object.entries(obj) + .map(([key, val]) => [key, clean_data(val)]) + .filter(([key, val]) => val != null) + ) + : _.isArray(obj) + ? obj.map(clean_data).filter((x) => x != null) + : obj + + return _.isEmpty(a) ? null : a + } + useLayoutEffect(() => { window.addEventListener("open pluto frontmatter", open) return () => { @@ -68,43 +89,91 @@ export const FrontMatterInput = ({ remote_frontmatter, set_remote_frontmatter }) description: null, date: null, tags: [], + author: [{}], ...frontmatter, } - return html` -

Frontmatter

-

- If you are publishing this notebook on the web, you can set the parameters below to provide HTML metadata. This is useful for search engines and - social media. -

-
- ${Object.entries(frontmatter_with_defaults).map(([key, value]) => { - let id = `fm-${key}` - return html` - - <${Input} type=${field_type(key)} id=${id} value=${value} on_value=${fm_setter(key)} /> - - ` - })} + const show_entry = ([key, value]) => !((_.isArray(value) && field_type(key) !== "tags") || _.isPlainObject(value)) + + const entries_input = (data, base_path) => { + return html` + ${Object.entries(data) + .filter(show_entry) + .map(([key, value]) => { + let path = `${base_path}${key}` + let id = `fm-${path}` + return html` + + <${Input} type=${field_type(key)} id=${id} value=${value} on_value=${fm_setter(path)} /> + + ` + })} + ` + } + + return html` +

Frontmatter

+

+ If you are publishing this notebook on the web, you can set the parameters below to provide HTML metadata. This is useful for search engines and + social media. +

+
+ ${entries_input(frontmatter_with_defaults, ``)} + ${!_.isArray(frontmatter_with_defaults.author) + ? null + : frontmatter_with_defaults.author.map((author, i) => { + let author_with_defaults = { + name: null, + url: null, + ...author, + } + + return html` +
+ Author ${i + 1} + + ${entries_input(author_with_defaults, `author[${i}].`)} +
+ ` + })} + ${!_.isArray(frontmatter_with_defaults.author) + ? null + : html``}
diff --git a/frontend/editor.css b/frontend/editor.css index 7d92b004a4..2f6b6b2e4a 100644 --- a/frontend/editor.css +++ b/frontend/editor.css @@ -3445,6 +3445,10 @@ pluto-cell.hooked_up pluto-output { margin-top: 0.5em; } +.pluto-frontmatter fieldset { + grid-column: 1/4; +} + .pluto-frontmatter .final { display: flex; margin-top: 2rem;