diff --git a/package.json b/package.json index 1f94c49de8..ec6715c92a 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@typescript-eslint/parser": "4.22.0", "abcjs": "6.0.0-beta.32", "bootstrap": "4.6.0", + "codemirror": "5.60.0", "copy-webpack-plugin": "6.4.1", "d3-graphviz": "3.2.0", @@ -56,6 +57,7 @@ "i18next": "20.2.1", "i18next-browser-languagedetector": "6.1.0", "i18next-http-backend": "1.2.1", + "iso-639-1": "2.1.8", "js-yaml": "4.0.0", "katex": "0.13.2", "luxon": "1.26.0", diff --git a/public/locales/en.json b/public/locales/en.json index 808582e2e4..b2a7f5d788 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -339,26 +339,45 @@ "modal": { "snippetImport": { "title": "Import from Snippet", - "selectProject": "Select From Available Projects", - "selectSnippet": "Select From Available Snippets" + "selectProject": "Select From Available Projects", + "selectSnippet": "Select From Available Snippets" }, - "documentInfo": { - "title": "Document info", - "created": "<0> created this note <1>", - "edited": "<0> was the last editor <1>", - "usersContributed": "<0> users contributed to this document", - "revisions": "<0> revisions are saved" - }, - "gistImport": { - "title": "Import from Gist", - "insertGistUrl": "Paste your gist url here…" - }, - "snippetExport": { - "title": "Export to Snippet", - "visibilityLevel": "Select Visibility Level" - }, - "revision": { - "title": "Revisions", + "documentInfo": { + "title": "Document info", + "created": "<0> created this note <1>", + "edited": "<0> was the last editor <1>", + "usersContributed": "<0> users contributed to this document", + "revisions": "<0> revisions are saved" + }, + "metadataEditor": { + "title": "Edit Metadata", + "labels": { + "title": "Title", + "type": "Document type", + "description": "Description", + "tags": "Tags", + "lang": "Language", + "dir": "Text direction", + "breaks": "New line style", + "robots": "Robots", + "GA": "Google Analytics", + "disqus": "Disqus", + "LTR": "left to right", + "RTL": "right to left", + "breaksOn": "hedgedoc style", + "breaksOff": "markdown style" + } + }, + "gistImport": { + "title": "Import from Gist", + "insertGistUrl": "Paste your gist url here…" + }, + "snippetExport": { + "title": "Export to Snippet", + "visibilityLevel": "Select Visibility Level" + }, + "revision": { + "title": "Revisions", "revertButton": "Revert", "error": "An error occurred while fetching the revisions of this note.", "length": "Length", diff --git a/src/components/common/fork-awesome/fork-awesome-icon.tsx b/src/components/common/fork-awesome/fork-awesome-icon.tsx index 4b7c5ab7dd..9c4a306aa6 100644 --- a/src/components/common/fork-awesome/fork-awesome-icon.tsx +++ b/src/components/common/fork-awesome/fork-awesome-icon.tsx @@ -1,10 +1,10 @@ /* - SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) - - SPDX-License-Identifier: AGPL-3.0-only + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only */ -import React from 'react' +import React, { MouseEventHandler } from 'react' import { IconName, IconSize } from './types' export interface ForkAwesomeIconProps { @@ -13,14 +13,15 @@ export interface ForkAwesomeIconProps { fixedWidth?: boolean size?: IconSize stacked?: boolean + onClick?: MouseEventHandler } -export const ForkAwesomeIcon: React.FC = ({ icon, fixedWidth = false, size, className, stacked = false }) => { +export const ForkAwesomeIcon: React.FC = ({ icon, fixedWidth = false, size, className, stacked = false, onClick }) => { const fixedWithClass = fixedWidth ? 'fa-fw' : '' const sizeClass = size ? `-${ size }` : (stacked ? '-1x' : '') const stackClass = stacked ? '-stack' : '' const extraClasses = `${ className ?? '' } ${ sizeClass || stackClass ? `fa${ stackClass }${ sizeClass }` : '' }` return ( - + ) } diff --git a/src/components/editor-page/document-bar/share/share-modal.tsx b/src/components/editor-page/document-bar/share/share-modal.tsx index 7bc4c254d1..9c132bd065 100644 --- a/src/components/editor-page/document-bar/share/share-modal.tsx +++ b/src/components/editor-page/document-bar/share/share-modal.tsx @@ -45,7 +45,7 @@ export const ShareModal: React.FC = ({ show, onHide }) => { - + diff --git a/src/components/editor-page/metadata-editor/breaks-metadata-input.tsx b/src/components/editor-page/metadata-editor/breaks-metadata-input.tsx new file mode 100644 index 0000000000..d8d996a2b9 --- /dev/null +++ b/src/components/editor-page/metadata-editor/breaks-metadata-input.tsx @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import React, { useCallback } from 'react' +import { ToggleButton, ToggleButtonGroup } from 'react-bootstrap' +import { Trans, useTranslation } from 'react-i18next' +import { MetadataInputFieldProps } from './metadata-editor' + +enum ButtonState { + ON = 1, + OFF = 0 +} + +export const BreaksMetadataInput: React.FC> = ({ frontmatterKey, content, onContentChange }) => { + const { t } = useTranslation() + + const toggleButtonClick = useCallback((value: ButtonState) => { + onContentChange({ breaks: value === ButtonState.ON }) + }, [onContentChange]) + + return ( + + + + + + + + + ) +} diff --git a/src/components/editor-page/metadata-editor/datalist-metadata-input.tsx b/src/components/editor-page/metadata-editor/datalist-metadata-input.tsx new file mode 100644 index 0000000000..968d81dd3b --- /dev/null +++ b/src/components/editor-page/metadata-editor/datalist-metadata-input.tsx @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import React, { useCallback } from 'react' +import { MetadataInputFieldProps, SelectMetadataOptions } from './metadata-editor' +import { Dropdown, DropdownButton } from 'react-bootstrap' + +export const DatalistMetadataInput: React.FC & SelectMetadataOptions> = ({ frontmatterKey, content, onContentChange, options }) => { + const onSelect = useCallback((eventKey: string | null) => { + if (eventKey === null) { + return + } + onContentChange({ [frontmatterKey]: eventKey }) + }, [frontmatterKey, onContentChange]) + + return ( + + { + options.map((option) => { option }) + } + + ) +} +/* + + + */ diff --git a/src/components/editor-page/metadata-editor/input-label.scss b/src/components/editor-page/metadata-editor/input-label.scss new file mode 100644 index 0000000000..3e087ae3a7 --- /dev/null +++ b/src/components/editor-page/metadata-editor/input-label.scss @@ -0,0 +1,9 @@ +/*! + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +.tighter { + margin-bottom: -0.5px !important; +} diff --git a/src/components/editor-page/metadata-editor/input-label.tsx b/src/components/editor-page/metadata-editor/input-label.tsx new file mode 100644 index 0000000000..0ba42acb8f --- /dev/null +++ b/src/components/editor-page/metadata-editor/input-label.tsx @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import React from 'react' +import './input-label.scss' +import { Form } from 'react-bootstrap' + +export interface InputLabelProps { + id: string + label: string +} + +export const InputLabel: React.FC = ({ id, label, children }) => { + return ( + + + { children } + + ) +} diff --git a/src/components/editor-page/metadata-editor/metadata-editor.tsx b/src/components/editor-page/metadata-editor/metadata-editor.tsx new file mode 100644 index 0000000000..e25e84be21 --- /dev/null +++ b/src/components/editor-page/metadata-editor/metadata-editor.tsx @@ -0,0 +1,119 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import ISO from 'iso-639-1' +import React, { useCallback } from 'react' +import { Col, Modal, Row } from 'react-bootstrap' +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' +import { ApplicationState } from '../../../redux' +import { replaceFrontmatterInMarkdownContentAction } from '../../../redux/note-details/methods' +import { CommonModal } from '../../common/modals/common-modal' +import { NoteType, RawNoteFrontmatter } from '../note-frontmatter/note-frontmatter' +import { BreaksMetadataInput } from './breaks-metadata-input' +import { DatalistMetadataInput } from './datalist-metadata-input' +import { InputLabel } from './input-label' +import { StringMetadataInput } from './string-metadata-input' +import { StringMetadataTextarea } from './string-metadata-textarea' +import { TagsMetadataInput } from './tags-metadata-input' +import { TextDirectionMetadataInput } from './text-direction-metadata-input' + +export interface MetadataEditorProps { + show: boolean, + onHide: () => void +} + +export interface MetadataInputFieldProps { + content: T + frontmatterKey: keyof RawNoteFrontmatter + onContentChange: (frontmatter: RawNoteFrontmatter) => void +} + +export interface SelectMetadataOptions { + options: T[] +} + +export const MetadataEditor: React.FC = ({ show, onHide }) => { + const { t } = useTranslation() + const yamlMetadata = useSelector((state: ApplicationState) => state.noteDetails.frontmatter) + /*const [yamlMetadata, setNoteFrontmatter] = useState>({ + title: "Test Title", + description: "Test Description\nwith two lines", + tags: ["tag1", "tag2"], + robots: "", + lang: "de-at", + dir: TextDirection.LTR, + breaks: false, + GA: "test GA string", + disqus: "test disqus string", + type: '', + deprecatedTagsSyntax: false + })*/ + + const updateFrontmatter = useCallback((frontmatter: RawNoteFrontmatter): void => { + replaceFrontmatterInMarkdownContentAction(frontmatter) + }, []) + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/src/components/editor-page/metadata-editor/string-metadata-input.tsx b/src/components/editor-page/metadata-editor/string-metadata-input.tsx new file mode 100644 index 0000000000..aa684a472d --- /dev/null +++ b/src/components/editor-page/metadata-editor/string-metadata-input.tsx @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import React, { useCallback } from 'react' +import { MetadataInputFieldProps } from './metadata-editor' + +export const StringMetadataInput: React.FC> = ({ content, onContentChange, frontmatterKey }) => { + const onChange = useCallback((event: React.ChangeEvent) => { + onContentChange({ [frontmatterKey]: event.currentTarget.value }) + }, [frontmatterKey, onContentChange]) + + return ( + + ) +} diff --git a/src/components/editor-page/metadata-editor/string-metadata-textarea.tsx b/src/components/editor-page/metadata-editor/string-metadata-textarea.tsx new file mode 100644 index 0000000000..7a661e9812 --- /dev/null +++ b/src/components/editor-page/metadata-editor/string-metadata-textarea.tsx @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import React, { useCallback } from 'react' +import { MetadataInputFieldProps } from './metadata-editor' + +export const StringMetadataTextarea: React.FC> = ({ frontmatterKey, content, onContentChange }) => { + const onChange = useCallback((event: React.ChangeEvent) => { + onContentChange({ [frontmatterKey]: event.currentTarget.value }) + }, [frontmatterKey, onContentChange]) + + return ( +