Restructure editor user interface (#399)

* Replaced connection indicator in editor top bar with user-menu

* Added basic layout of bottom document bar

* Fixed margins between elements

* Reorganized document-bar

* Added dividers into toolbar

* Move files from task-bar to document-bar and remove test file

* moved connection-indicator components into its own folder

* moved document bar to the top

* moved connection-indicator once again

* Change design

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* New idea for timestamps

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Add css

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Revert "Add css"

This reverts commit 6780aa05

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Revert "New idea for timestamps"

This reverts commit bf2891e1

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* split import / export

* Made version input field to a common component

* added read-only modal
added document-time
added placeholder text for permissions

* remove flex-nowrap from editor toolbar

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Add codimd permission menu

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Move permission picker to the right

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* add use memo

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Add user-select-none to documenttime component

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* added status-bar

* fixed status-bar

* Add document info mock

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* changed published to share in i18n

* reordered document bar
moved share modal in it's own component

* changed the divider color in the toolbar

* Add details to document info

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Add pin mock button

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Restructure toolbar after rebase and extract EmojiPicker+Button into component

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Correct linue number output

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Add some space into status bar

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Cleanup code to make ESLint happy

* Fix Toc button position

* Added link to presentation mode button

* Cache codemirror props

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Fix code blocks not being completely visible

* Improve document info modal

- The document info timeline always wrapped the received moment.js-object into a new moment.js object instead of directly using the given one.
- The timestamps were configured to be displayed without suffix, but this is necessary to support valid translation grammar.
- There was no margin between the icons and the texts.

* Highlighted user name in document-info modal

* Add avatar icon to document-info modal

* Improved english translation of the share-info

* Improve performance of copyable-fields by using useCallback

* Add translation keys for pin-to-history button

* Forwarded note title to editor-menu for deletion modal info

* Add placeholders to translations

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* change translation

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Change permission dropdown to permission button

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Fix translations of emoji-picker and preferences

* remove unused imports

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Add alt attribute

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Fix share button and i18n files

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Fix use of i18n keys

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Use modal-body

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* useCallback

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Use more specific i18n key

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Add a new entry and move i18n key for usercontribution

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Fix i18nkey für shareLink

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* remove unused i18nkey

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Rename component DocumentInfo to DocumentInfoButton

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Extract revision button code into own component

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* wrap buttons in navbar-nav

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* organize imports

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* organize imports

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Added editor-preferences modal

* Added functionality to preferences modal

* Activated search and replace feature in CodeMirror

* pdf export unavailability notice (#403)

* added pdf export unavailability notice with link to FAQ
as many users ask all the time why this was removed and when they'll get it back, this seemed like a fine solution in the meantime.

Co-authored-by: Erik Michelson <github@erik.michelson.eu>

* Refactored editor-preferences to just use one generic select component

* Fixed warnings regarding duplicated controlId and missing useCb-deps

* Reorganized translation keys

* Fixed i18n indentation for POEditor.com

* Added translation key for 'avatar of ...'

* Remove fragment

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Use user-avatar in document-info-line.tsx

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Revert changes in user-avatar and solve the problem otherwise

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Removed unnecessary import

* Removed another unnecessary import

* Refactored EditorPreferenceSelect to use enum and automatic type conversions

* Remove unused CodeMirror reference

* Fix spacing problem

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* Increate size of image

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>

* fixed share-link's space around the copyable-field

Co-authored-by: Philip Molares <philip.molares@udo.edu>
Co-authored-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
Co-authored-by: Philip Molares <git@molar.es>
This commit is contained in:
Erik Michelson 2020-08-15 17:27:46 +02:00 committed by GitHub
parent 62e870828c
commit 8377722e1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 993 additions and 334 deletions

View file

@ -124,10 +124,10 @@
"image": "صورة",
"uploadImage": "تحميل صورة"
},
"menu": {
"documentBar": {
"menu": "القائمة",
"new": "جديد",
"publish": "انشر",
"shareLink": "",
"extra": "إضافي",
"revision": "مراجعة",
"slideMode": "نمط الشرائح التقديمية",

View file

@ -124,10 +124,10 @@
"image": "Imatge",
"uploadImage": "Pujar imatge"
},
"menu": {
"documentBar": {
"menu": "Menú",
"new": "Nou",
"publish": "Publicar",
"shareLink": "",
"extra": "Extra",
"revision": "Revisió",
"slideMode": "Mode presentació",

View file

@ -124,10 +124,10 @@
"image": "Obrázek",
"uploadImage": "Nahrát obrázek"
},
"menu": {
"documentBar": {
"menu": "Menu",
"new": "Nová",
"publish": "Publikovat",
"shareLink": "",
"extra": "Extra",
"revision": "Revize",
"slideMode": "Režim prezentace",

View file

@ -108,10 +108,10 @@
"image": "Billede",
"uploadImage": "Upload billede"
},
"menu": {
"documentBar": {
"menu": "Menu",
"new": "Ny",
"publish": "Publicér",
"shareLink": "",
"extra": "Ekstra",
"revision": "Revision",
"slideMode": "Præsentationstilstand",

View file

@ -124,10 +124,10 @@
"image": "Foto",
"uploadImage": "Foto hochladen"
},
"menu": {
"documentBar": {
"menu": "Menü",
"new": "Neu",
"publish": "Veröffentlichen",
"shareLink": "Link teilen",
"extra": "Extra",
"revision": "Version",
"slideMode": "Präsentationsmodus",

View file

@ -108,10 +108,10 @@
"image": "Εικόνα",
"uploadImage": "Ανέβασμα φωτογραφίας"
},
"menu": {
"documentBar": {
"menu": "Μενού",
"new": "Νέο",
"publish": "Δημοσίευση",
"shareLink": "",
"extra": "Επιπλέον",
"revision": "Αναθεώρηση",
"slideMode": "Λειτουργία με σύρσιμο",

View file

@ -219,21 +219,41 @@
"uploadImage": "Upload Image",
"table": "Table",
"line": "Horizontal line",
"comment": "Comment"
"comment": "Comment",
"preferences": "Editor settings",
"emoji": "Open emoji picker"
},
"menu": {
"documentBar": {
"menu": "Menu",
"import": "Import",
"export": "Export",
"new": "New",
"publish": "Publish",
"shareLink": "Share link",
"extra": "Extra",
"revision": "Revision",
"slideMode": "Slide Mode",
"download": "Download",
"help": "Help",
"deleteNote": "Delete note"
"deleteNote": "Delete note",
"permissions": "Permissions",
"documentInfo": "Document info",
"pinNoteToHistory": "Pin note to history",
"pinnedToHistory": "Pinned to history"
},
"statusBar": {
"cursor": "Line {{line}}, Columns {{columns}}",
"selection": {
"column": "Selected {{count}} columns",
"line": "Selected {{count}} lines"
},
"lines": "{{lines}} Lines",
"length": "Length {{length}}",
"lengthTooltip": "You can write up to 100000 characters in this document."
},
"export": {
"rawHtml": "Raw HTML"
"rawHtml": "Raw HTML",
"pdf": "PDF export is unavailable.",
"why": "Why?"
},
"import": {
"clipboard": "Clipboard"
@ -244,6 +264,13 @@
"selectProject": "Select From Available Projects",
"selectSnippet": "Select From Available Snippets"
},
"documentInfo": {
"title": "Document info",
"created": "<0></0> created this note <1></1> ago",
"edited": "<0></0> was the last editor <1></1> ago",
"usersContributed": "<0></0> users contributed to this document",
"revisions": "<0></0> revisions are saved"
},
"gistImport": {
"title": "Import from Gist",
"insertGistUrl": "Paste your gist url here…"
@ -265,6 +292,21 @@
"question": "Do you really want to delete this note?",
"warning": "All users will lose their connection. This process is irreversible.",
"button": "Delete note"
},
"permissions": {
"title": "Permissions"
},
"shareLink": {
"title": "Share link",
"viewOnlyDescription": "This link points to a read-only version of this note. You can use this e.g. for feedback from friends and colleagues."
},
"preferences": {
"title": "Preferences",
"theme": "Editor theme",
"keyMap": "Keymap",
"indentWithTabs": "Tab character",
"indentUnit": "Tab size (when using spaces)",
"spellChecker": "Spell checking"
}
},
"embeddings": {
@ -280,7 +322,8 @@
"close": "Close",
"save": "Save",
"or": "or",
"and": "and"
"and": "and",
"avatarOf": "avatar of '{{name}}'"
},
"login": {
"chooseMethod": "Choose method",

View file

@ -108,10 +108,10 @@
"image": "Bildo",
"uploadImage": "Alŝutu bildon"
},
"menu": {
"documentBar": {
"menu": "Menuo",
"new": "Nova",
"publish": "Dissendu",
"shareLink": "",
"extra": "Plia",
"revision": "Versio",
"slideMode": "Bildvica modo",

View file

@ -124,10 +124,10 @@
"image": "Imagen",
"uploadImage": "Subir imagen"
},
"menu": {
"documentBar": {
"menu": "Menú",
"new": "Nuevo",
"publish": "Publicar",
"shareLink": "",
"extra": "Extra",
"revision": "Revision",
"slideMode": "Modo presentación",

View file

@ -124,10 +124,10 @@
"image": "Image",
"uploadImage": "Téléverser une image"
},
"menu": {
"documentBar": {
"menu": "Menu",
"new": "Nouvelle",
"publish": "Publier",
"shareLink": "",
"extra": "Extra",
"revision": "Historique",
"slideMode": "Mode présentation",

View file

@ -108,10 +108,10 @@
"image": "तस्वीर",
"uploadImage": "तस्वीर डालिये"
},
"menu": {
"documentBar": {
"menu": "मेन्यू",
"new": "नया",
"publish": "प्रकाशित करें",
"shareLink": "",
"extra": "अतिरिक्त",
"revision": "संशोधन",
"slideMode": "स्लाइड मोड",

View file

@ -108,10 +108,10 @@
"image": "Slika",
"uploadImage": "Prenesi sliku"
},
"menu": {
"documentBar": {
"menu": "Meni",
"new": "Novo",
"publish": "Objavi",
"shareLink": "",
"extra": "Dodatno",
"revision": "Revizija",
"slideMode": "Način slajda",

View file

@ -124,10 +124,10 @@
"image": "Gambar",
"uploadImage": "Unggah Gambar"
},
"menu": {
"documentBar": {
"menu": "Menu",
"new": "Baru",
"publish": "Terbitkan",
"shareLink": "",
"extra": "Tambahan",
"revision": "Revisi",
"slideMode": "Mode Slide",

View file

@ -124,10 +124,10 @@
"image": "Immagine",
"uploadImage": "Carica Immagine"
},
"menu": {
"documentBar": {
"menu": "Menu",
"new": "Nuovo",
"publish": "Pubblica",
"shareLink": "",
"extra": "Extra",
"revision": "Revisione",
"slideMode": "Modalità slide",

View file

@ -124,10 +124,10 @@
"image": "画像",
"uploadImage": "画像をアップロード"
},
"menu": {
"documentBar": {
"menu": "メニュー",
"new": "新規作成",
"publish": "公開する",
"shareLink": "",
"extra": "その他",
"revision": "編集履歴",
"slideMode": "スライドモード",

View file

@ -120,10 +120,10 @@
"image": "이미지",
"uploadImage": "이미지 업로드"
},
"menu": {
"documentBar": {
"menu": "메뉴",
"new": "새",
"publish": "공개하기",
"shareLink": "",
"extra": "추가",
"revision": "기록",
"slideMode": "슬라이드 모드",

View file

@ -124,10 +124,10 @@
"image": "Afbeelding",
"uploadImage": "Afbeelding uploaden"
},
"menu": {
"documentBar": {
"menu": "Menu",
"new": "Nieuw",
"publish": "Publiceren",
"shareLink": "",
"extra": "Extra",
"revision": "Versie",
"slideMode": "Presentatiemodus",

View file

@ -124,10 +124,10 @@
"image": "Zdjęcie",
"uploadImage": "Prześlij zdjęcie"
},
"menu": {
"documentBar": {
"menu": "Menu",
"new": "Nowy",
"publish": "Publikuj",
"shareLink": "",
"extra": "Ekstra",
"revision": "Korekta",
"slideMode": "Tryb slajdów",

View file

@ -124,10 +124,10 @@
"image": "Imagem",
"uploadImage": "Carregar Imagem"
},
"menu": {
"documentBar": {
"menu": "Menu",
"new": "Novo",
"publish": "Publicar",
"shareLink": "",
"extra": "Extra",
"revision": "Revisão",
"slideMode": "Modo Apresentação",

View file

@ -124,10 +124,10 @@
"image": "Изображение",
"uploadImage": "Загрузить изображение"
},
"menu": {
"documentBar": {
"menu": "Меню",
"new": "Новая",
"publish": "Опубликовать",
"shareLink": "",
"extra": "Дополнительно",
"revision": "Изменения",
"slideMode": "Режим слайдера",

View file

@ -124,10 +124,10 @@
"image": "Obrázok",
"uploadImage": "Nahrať obrázok"
},
"menu": {
"documentBar": {
"menu": "Menu",
"new": "Nová",
"publish": "Publikovať",
"shareLink": "",
"extra": "Extra",
"revision": "Revízia",
"slideMode": "Prezentačný režim",

View file

@ -123,10 +123,10 @@
"image": "Слика",
"uploadImage": "Пошаљи слику"
},
"menu": {
"documentBar": {
"menu": "Мени",
"new": "Ново",
"publish": "Објави",
"shareLink": "",
"extra": "Додатно",
"revision": "Ревизија",
"slideMode": "Презентациони мод",

View file

@ -124,10 +124,10 @@
"image": "Bild",
"uploadImage": "Ladda upp bilder"
},
"menu": {
"documentBar": {
"menu": "Meny",
"new": "Ny",
"publish": "Publicera",
"shareLink": "",
"extra": "Extra",
"revision": "Revision",
"slideMode": "Slide Mode",

View file

@ -108,10 +108,10 @@
"image": "Resim",
"uploadImage": "Resim Yükle"
},
"menu": {
"documentBar": {
"menu": "Menü",
"new": "Yeni",
"publish": "Yayınla",
"shareLink": "",
"extra": "Ekstra",
"revision": "Sürüm",
"slideMode": "Slayt Modu",

View file

@ -108,10 +108,10 @@
"image": "Зображення",
"uploadImage": "Завантажити зображення"
},
"menu": {
"documentBar": {
"menu": "Меню",
"new": "Нова",
"publish": "Опублікувати",
"shareLink": "",
"extra": "Дотатково",
"revision": "Ревізія",
"slideMode": "Режим слайдера",

View file

@ -123,10 +123,10 @@
"image": "Ảnh",
"uploadImage": "Tải ảnh lên"
},
"menu": {
"documentBar": {
"menu": "Menu",
"new": "Mới",
"publish": "Xuất bản",
"shareLink": "",
"extra": "Extra",
"revision": "Sửa đổi",
"slideMode": "Chế độ slide",

View file

@ -124,10 +124,10 @@
"image": "图片",
"uploadImage": "上传图片"
},
"menu": {
"documentBar": {
"menu": "菜单",
"new": "新建",
"publish": "发表",
"shareLink": "",
"extra": "附加功能",
"revision": "修订版本",
"slideMode": "幻灯模式",

View file

@ -124,10 +124,10 @@
"image": "圖片",
"uploadImage": "上傳圖片"
},
"menu": {
"documentBar": {
"menu": "選單",
"new": "新增",
"publish": "發表",
"shareLink": "",
"extra": "增益",
"revision": "修訂版本",
"slideMode": "簡報模式",

View file

@ -1,40 +1,48 @@
import React, { Fragment, useRef, useState } from 'react'
import React, { Fragment, useCallback, useRef, useState } from 'react'
import { Button, FormControl, InputGroup, Overlay, Tooltip } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
export interface VersionInputFieldProps {
version: string
export interface CopyableFieldProps {
content: string
}
export const VersionInputField: React.FC<VersionInputFieldProps> = ({ version }) => {
export const CopyableField: React.FC<CopyableFieldProps> = ({ content }) => {
useTranslation()
const inputField = useRef<HTMLInputElement>(null)
const [showCopiedTooltip, setShowCopiedTooltip] = useState(false)
const copyToClipboard = (content: string) => {
const copyToClipboard = useCallback((content: string) => {
navigator.clipboard.writeText(content).then(() => {
setShowCopiedTooltip(true)
setTimeout(() => { setShowCopiedTooltip(false) }, 2000)
}).catch(() => {
console.error("couldn't copy")
})
}, [])
const selectContent = useCallback(() => {
if (!inputField.current) {
return
}
inputField.current.focus()
inputField.current.setSelectionRange(0, inputField.current.value.length)
}, [inputField])
return (
<Fragment>
<Overlay target={inputField} show={showCopiedTooltip} placement="top">
{(props) => (
<Tooltip id={'copied_' + version} {...props}>
<Tooltip id={'copied_' + content} {...props}>
<Trans i18nKey={'landing.versionInfo.successfullyCopied'}/>
</Tooltip>
)}
</Overlay>
<InputGroup className="mb-3">
<FormControl readOnly={true} ref={inputField} className={'text-center'} value={version} />
<InputGroup className="my-3">
<FormControl readOnly={true} ref={inputField} className={'text-center'} value={content} onMouseEnter={selectContent} />
<InputGroup.Append>
<Button variant="outline-secondary" onClick={() => copyToClipboard(version)} title={'Copy'}>
<Button variant="outline-secondary" onClick={() => copyToClipboard(content)} title={'Copy'}>
<ForkAwesomeIcon icon='files-o'/>
</Button>
</InputGroup.Append>

View file

@ -1,6 +1,6 @@
import React from 'react'
import { Dropdown } from 'react-bootstrap'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import { ActiveIndicatorStatus } from './active-indicator'
import './connection-indicator.scss'
import { UserLine } from './user-line'
@ -8,9 +8,9 @@ import { UserLine } from './user-line'
const ConnectionIndicator: React.FC = () => {
const userOnline = 2
return (
<Dropdown className="small" alignRight>
<Dropdown className="small mx-2" alignRight>
<Dropdown.Toggle id="connection-indicator" size="sm" variant="primary" className="upper-case">
<ForkAwesomeIcon icon="users"/> {userOnline} Online
<ForkAwesomeIcon icon="users" className={'mr-1'}/> {userOnline} Online
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item disabled={true} className="d-flex align-items-center p-0">

View file

@ -1,5 +1,5 @@
import React, { Fragment } from 'react'
import { UserAvatar } from '../../landing/layout/user-avatar/user-avatar'
import { UserAvatar } from '../../../landing/layout/user-avatar/user-avatar'
import { ActiveIndicator, ActiveIndicatorStatus } from './active-indicator'
import './user-line.scss'

View file

@ -0,0 +1,37 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { ConnectionIndicator } from './connection-indicator/connection-indicator'
import { DocumentInfoButton } from './document-info-button'
import { EditorMenu } from './editor-menu'
import { ExportMenu } from './export-menu'
import { ImportMenu } from './import-menu'
import { PermissionButton } from './permission-button'
import { PinToHistoryButton } from './pin-to-history-button'
import { ShareLinkButton } from './share-link-button'
import { RevisionButton } from './revision-button'
export interface DocumentBarProps {
title: string
}
export const DocumentBar: React.FC<DocumentBarProps> = ({ title }) => {
useTranslation()
return (
<div className={'navbar navbar-expand navbar-light bg-light'}>
<div className="navbar-nav">
<ShareLinkButton/>
<DocumentInfoButton/>
<RevisionButton/>
<PinToHistoryButton/>
<PermissionButton/>
</div>
<div className="ml-auto navbar-nav">
<ImportMenu/>
<ExportMenu/>
<EditorMenu noteTitle={title}/>
<ConnectionIndicator/>
</div>
</div>
)
}

View file

@ -0,0 +1,61 @@
import moment from 'moment'
import React, { Fragment, useState } from 'react'
import { Button, ListGroup, Modal } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import { CommonModal } from '../../common/modals/common-modal'
import { DocumentInfoLine } from './document-info-line'
import { DocumentInfoLineWithTimeMode, DocumentInfoTimeLine } from './document-info-time-line'
import { UnitalicBoldText } from './document-info-time-line-helper/unitalic-bold-text'
export const DocumentInfoButton: React.FC = () => {
const [showModal, setShowModal] = useState(false)
useTranslation()
return (
<Fragment>
<Button variant={'light'} className={'mx-1'} size={'sm'} onClick={() => setShowModal(true)}>
<ForkAwesomeIcon icon={'line-chart'} className={'mx-1'}/>
<Trans i18nKey={'editor.documentBar.documentInfo'}/>
</Button>
<CommonModal
show={showModal}
onHide={() => setShowModal(false)}
closeButton={true}
titleI18nKey={'editor.modal.documentInfo.title'}>
<Modal.Body>
<ListGroup>
<ListGroup.Item>
<DocumentInfoTimeLine
mode={DocumentInfoLineWithTimeMode.CREATED}
time={ moment().subtract(11, 'days') }
userName={'Tilman'}
profileImageSrc={'https://1.gravatar.com/avatar/767fc9c115a1b989744c755db47feb60?s=200&r=pg&d=mp'}/>
</ListGroup.Item>
<ListGroup.Item>
<DocumentInfoTimeLine
mode={DocumentInfoLineWithTimeMode.EDITED}
time={ moment().subtract(3, 'minutes') }
userName={'Philip'}
profileImageSrc={'https://1.gravatar.com/avatar/767fc9c115a1b989744c755db47feb60?s=200&r=pg&d=mp'}/>
</ListGroup.Item>
<ListGroup.Item>
<DocumentInfoLine icon={'users'}>
<Trans i18nKey='editor.modal.documentInfo.usersContributed'>
<UnitalicBoldText text={'42'}/>
</Trans>
</DocumentInfoLine>
</ListGroup.Item>
<ListGroup.Item>
<DocumentInfoLine icon={'history'}>
<Trans i18nKey='editor.modal.documentInfo.revisions'>
<UnitalicBoldText text={'192'}/>
</Trans>
</DocumentInfoLine>
</ListGroup.Item>
</ListGroup>
</Modal.Body>
</CommonModal>
</Fragment>
)
}

View file

@ -0,0 +1,17 @@
import React from 'react'
import { ForkAwesomeIcon, IconName } from '../../common/fork-awesome/fork-awesome-icon'
export interface DocumentInfoLineProps {
icon: IconName
}
export const DocumentInfoLine: React.FC<DocumentInfoLineProps> = ({ icon, children }) => {
return (
<span className={'d-flex align-items-center'}>
<ForkAwesomeIcon icon={icon} size={'2x'} fixedWidth={true} className={'mx-2'}/>
<i className={'d-flex align-items-center'}>
{children}
</i>
</span>
)
}

View file

@ -0,0 +1,12 @@
import { Moment } from 'moment'
import React from 'react'
export interface ItalicTime {
time: Moment
}
export const TimeFromNow: React.FC<ItalicTime> = ({ time }) => {
return (
<time className={'mx-1'} title={time.format('LLLL')} dateTime={time.format()}>{time.fromNow(true)}</time>
)
}

View file

@ -0,0 +1,3 @@
.font-style-normal {
font-style: normal;
}

View file

@ -0,0 +1,10 @@
import React from 'react'
import './unitalic-bold-text.scss'
export interface BoldTextProps {
text: string ;
}
export const UnitalicBoldText: React.FC<BoldTextProps> = ({ text }) => {
return <b className={'font-style-normal mr-1'}>{text}</b>
}

View file

@ -0,0 +1,4 @@
.document-info-avatar img {
height: 30px;
width: 30px;
}

View file

@ -0,0 +1,36 @@
import { Moment } from 'moment'
import React from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { IconName } from '../../common/fork-awesome/fork-awesome-icon'
import { DocumentInfoLine } from './document-info-line'
import './document-info-time-line.scss'
import { TimeFromNow } from './document-info-time-line-helper/time-from-now'
import { UserAvatar } from '../../landing/layout/user-avatar/user-avatar'
export interface DocumentInfoLineWithTimeProps {
time: Moment,
mode: DocumentInfoLineWithTimeMode
userName: string
profileImageSrc: string
}
export enum DocumentInfoLineWithTimeMode {
CREATED,
EDITED
}
export const DocumentInfoTimeLine: React.FC<DocumentInfoLineWithTimeProps> = ({ time, mode, userName, profileImageSrc }) => {
useTranslation()
const i18nKey = mode === DocumentInfoLineWithTimeMode.CREATED ? 'editor.modal.documentInfo.created' : 'editor.modal.documentInfo.edited'
const icon: IconName = mode === DocumentInfoLineWithTimeMode.CREATED ? 'plus' : 'pencil'
return (
<DocumentInfoLine icon={icon}>
<Trans i18nKey={i18nKey} >
<UserAvatar photo={profileImageSrc} additionalClasses={'document-info-avatar font-style-normal bold font-weight-bold'} name={userName}/>
<TimeFromNow time={time}/>
</Trans>
</DocumentInfoLine>
)
}

View file

@ -0,0 +1,34 @@
import React from 'react'
import { Dropdown } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { DropdownItemWithDeletionModal } from '../../landing/pages/history/common/entry-menu/dropdown-item-with-deletion-modal'
export interface EditorMenuProps {
noteTitle: string
}
export const EditorMenu: React.FC<EditorMenuProps> = ({ noteTitle }) => {
useTranslation()
return (
<Dropdown className={'small mx-1'} alignRight={true}>
<Dropdown.Toggle variant='light' size='sm' id='editor-menu'>
<Trans i18nKey={'editor.documentBar.menu'}/>
</Dropdown.Toggle>
<Dropdown.Menu>
<DropdownItemWithDeletionModal
itemI18nKey={'landing.history.menu.deleteNote'}
modalButtonI18nKey={'editor.modal.deleteNote.button'}
modalIcon={'trash'}
modalQuestionI18nKey={'editor.modal.deleteNote.question'}
modalTitleI18nKey={'editor.modal.deleteNote.title'}
modalWarningI18nKey={'editor.modal.deleteNote.warning'}
noteTitle={noteTitle}
className={'small'}
onConfirm={() => console.log('deleted')}/>
</Dropdown.Menu>
</Dropdown>
)
}

View file

@ -0,0 +1,59 @@
import React from 'react'
import { Dropdown } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import { TranslatedExternalLink } from '../../common/links/translated-external-link'
const ExportMenu: React.FC = () => {
useTranslation()
return (
<Dropdown className='small mx-1' alignRight={true}>
<Dropdown.Toggle variant='light' size='sm' id='editor-menu-export' className=''>
<Trans i18nKey='editor.documentBar.export'/>
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Header>
<Trans i18nKey='common.export'/>
</Dropdown.Header>
<Dropdown.Item className='small'>
<ForkAwesomeIcon icon='dropbox' className={'mx-2'}/>
Dropbox
</Dropdown.Item>
<Dropdown.Item className='small'>
<ForkAwesomeIcon icon='github' className={'mx-2'}/>
Gist
</Dropdown.Item>
<Dropdown.Divider/>
<Dropdown.Header>
<Trans i18nKey='editor.documentBar.download'/>
</Dropdown.Header>
<Dropdown.Item className='small'>
<ForkAwesomeIcon icon='file-text' className={'mx-2'}/>
Markdown
</Dropdown.Item>
<Dropdown.Item className='small'>
<ForkAwesomeIcon icon='file-code-o' className={'mx-2'}/>
HTML
</Dropdown.Item>
<Dropdown.Item className='small'>
<ForkAwesomeIcon icon='file-code-o' className={'mx-2'}/>
<Trans i18nKey='editor.export.rawHtml'/>
</Dropdown.Item>
<Dropdown.Divider/>
<Dropdown.Item className='small text-muted' dir={'auto'}>
<ForkAwesomeIcon icon='file-pdf-o' className={'mx-2'}/>
<Trans i18nKey={'editor.export.pdf'}/>
&nbsp;
<TranslatedExternalLink i18nKey={'editor.export.why'} href={'https://community.codimd.org/t/frequently-asked-questions/190'} className={'text-primary'}/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
)
}
export { ExportMenu }

View file

@ -0,0 +1,29 @@
import React from 'react'
import { Dropdown } from 'react-bootstrap'
import { Trans } from 'react-i18next'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
export const ImportMenu: React.FC = () => {
return (
<Dropdown className='small mx-1' alignRight={true}>
<Dropdown.Toggle variant='light' size='sm' id='editor-menu-import' className=''>
<Trans i18nKey='editor.documentBar.import'/>
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Item className='small'>
<ForkAwesomeIcon icon='dropbox' className={'mx-2'}/>
Dropbox
</Dropdown.Item>
<Dropdown.Item className='small'>
<ForkAwesomeIcon icon='github' className={'mx-2'}/>
Gist
</Dropdown.Item>
<Dropdown.Item className='small'>
<ForkAwesomeIcon icon='clipboard' className={'mx-2'}/>
<Trans i18nKey='editor.import.clipboard'/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
)
}

View file

@ -0,0 +1,27 @@
import React, { Fragment, useState } from 'react'
import { Trans } from 'react-i18next'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import { Button, Modal } from 'react-bootstrap'
import { CommonModal } from '../../common/modals/common-modal'
export const PermissionButton: React.FC = () => {
const [showReadOnly, setShowReadOnly] = useState(false)
return (
<Fragment>
<Button variant={'light'} className={'mx-1'} size={'sm'} onClick={() => setShowReadOnly(true)}>
<ForkAwesomeIcon icon={'lock'} className={'mx-1'}/>
<Trans i18nKey={'editor.documentBar.permissions'}/>
</Button>
<CommonModal
show={showReadOnly}
onHide={() => setShowReadOnly(false)}
closeButton={true}
titleI18nKey={'editor.modal.permissions.title'}>
<Modal.Body>
<img className={'w-100'} src={'https://thumbs.gfycat.com/ImpassionedDeliriousIndianpalmsquirrel-size_restricted.gif'} alt={'Placeholder'}/>
</Modal.Body>
</CommonModal>
</Fragment>
)
}

View file

@ -0,0 +1,18 @@
import React from 'react'
import { Button } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
export const PinToHistoryButton: React.FC = () => {
useTranslation()
const isPinned = true
const i18nKey = isPinned ? 'editor.documentBar.pinNoteToHistory' : 'editor.documentBar.pinnedToHistory'
return (
<Button variant={'light'} className={'mx-1'} size={'sm'}>
<ForkAwesomeIcon icon={'thumb-tack'} className={'mx-1'}/>
<Trans i18nKey={i18nKey}/>
</Button>
)
}

View file

@ -0,0 +1,13 @@
import React from 'react'
import { Button } from 'react-bootstrap'
import { Trans } from 'react-i18next'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
export const RevisionButton: React.FC = () => {
return (
<Button variant={'light'} className={'mx-1'} size={'sm'}>
<ForkAwesomeIcon icon={'history'} className={'mx-1'}/>
<Trans i18nKey={'editor.documentBar.revision'}/>
</Button>
)
}

View file

@ -0,0 +1,29 @@
import React, { Fragment, useState } from 'react'
import { Button, Modal } from 'react-bootstrap'
import { Trans } from 'react-i18next'
import { CopyableField } from '../../common/copyable-field/copyable-field'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import { CommonModal } from '../../common/modals/common-modal'
export const ShareLinkButton: React.FC = () => {
const [showReadOnly, setShowReadOnly] = useState(false)
return (
<Fragment>
<Button variant={'light'} className={'mx-1'} size={'sm'} onClick={() => setShowReadOnly(true)}>
<ForkAwesomeIcon icon={'share'} className={'mx-1'}/>
<Trans i18nKey={'editor.documentBar.shareLink'}/>
</Button>
<CommonModal
show={showReadOnly}
onHide={() => setShowReadOnly(false)}
closeButton={true}
titleI18nKey={'editor.modal.shareLink.title'}>
<Modal.Body>
<span className={'my-4'}><Trans i18nKey={'editor.modal.shareLink.viewOnlyDescription'}/></span>
<CopyableField content={'https://example.com'}/>
</Modal.Body>
</CommonModal>
</Fragment>
)
}

View file

@ -1,5 +1,7 @@
@import '../../../../node_modules/codemirror/lib/codemirror.css';
@import '../../../../node_modules/codemirror/addon/display/fullscreen.css';
@import '../../../../node_modules/codemirror/addon/dialog/dialog.css';
@import '../../../../node_modules/codemirror/theme/neat.css';
@import './one-dark.css';
@import 'hints';

View file

@ -1,5 +1,6 @@
import { Editor, EditorChange } from 'codemirror'
import { Editor, EditorChange, EditorConfiguration } from 'codemirror'
import 'codemirror/addon/comment/comment'
import 'codemirror/addon/dialog/dialog'
import 'codemirror/addon/display/autorefresh'
import 'codemirror/addon/display/fullscreen'
import 'codemirror/addon/display/placeholder'
@ -11,16 +12,21 @@ import 'codemirror/addon/edit/matchtags'
import 'codemirror/addon/fold/foldcode'
import 'codemirror/addon/fold/foldgutter'
import 'codemirror/addon/hint/show-hint'
import 'codemirror/addon/search/search'
import 'codemirror/addon/search/jump-to-line'
import 'codemirror/addon/search/match-highlighter'
import 'codemirror/addon/selection/active-line'
import 'codemirror/keymap/sublime.js'
import 'codemirror/mode/gfm/gfm.js'
import React, { useCallback, useState } from 'react'
import 'codemirror/keymap/sublime'
import 'codemirror/keymap/emacs'
import 'codemirror/keymap/vim'
import 'codemirror/mode/gfm/gfm'
import React, { useCallback, useMemo, useState } from 'react'
import { Controlled as ControlledCodeMirror } from 'react-codemirror2'
import { useTranslation } from 'react-i18next'
import './editor-window.scss'
import { emojiHints, emojiWordRegex, findWordAtCursor } from './hints/emoji'
import { defaultKeyMap } from './key-map'
import { createStatusInfo, defaultState, StatusBar, StatusBarInfo } from './status-bar/status-bar'
import { ToolBar } from './tool-bar/tool-bar'
export interface EditorWindowProps {
@ -45,30 +51,33 @@ const onChange = (editor: Editor) => {
export const EditorWindow: React.FC<EditorWindowProps> = ({ onContentChange, content }) => {
const { t } = useTranslation()
const [editor, setEditor] = useState<Editor>()
const [statusBarInfo, setStatusBarInfo] = useState<StatusBarInfo>(defaultState)
const [editorPreferences, setEditorPreferences] = useState<EditorConfiguration>({
theme: 'one-dark',
keyMap: 'sublime',
indentUnit: 4,
indentWithTabs: false
})
const onBeforeChange = useCallback((editor: Editor, data: EditorChange, value: string) => {
onContentChange(value)
}, [onContentChange])
return (
<div className={'d-flex flex-column h-100'}>
<ToolBar
editor={editor}
/>
<ControlledCodeMirror
className="overflow-hidden w-100 flex-fill"
value={content}
options={{
const onEditorDidMount = useCallback(mountedEditor => {
setStatusBarInfo(createStatusInfo(mountedEditor))
setEditor(mountedEditor)
}, [])
const onCursorActivity = useCallback((editorWithActivity) => {
setStatusBarInfo(createStatusInfo(editorWithActivity))
}, [])
const codeMirrorOptions: EditorConfiguration = useMemo<EditorConfiguration>(() => ({
...editorPreferences,
mode: 'gfm',
theme: 'one-dark',
keyMap: 'sublime',
viewportMargin: 20,
styleActiveLine: true,
lineNumbers: true,
lineWrapping: true,
showCursorWhenSelecting: true,
highlightSelectionMatches: true,
indentUnit: 4,
inputStyle: 'textarea',
matchBrackets: true,
autoCloseBrackets: true,
@ -87,13 +96,26 @@ export const EditorWindow: React.FC<EditorWindowProps> = ({ onContentChange, con
addModeClass: true,
autoRefresh: true,
// otherCursors: true,
placeholder: t('editor.placeholder'),
showHint: false,
hintOptions: hintOptions
}}
editorDidMount={mountedEditor => setEditor(mountedEditor)}
onBeforeChange={onBeforeChange}
placeholder: t('editor.placeholder')
}), [t, editorPreferences])
return (
<div className={'d-flex flex-column h-100'}>
<ToolBar
editor={editor}
onPreferencesChange={config => setEditorPreferences(config)}
editorPreferences={editorPreferences}
/>
<ControlledCodeMirror
className="overflow-hidden w-100 flex-fill"
value={content}
options={codeMirrorOptions}
onChange={onChange}
/></div>
onCursorActivity={onCursorActivity}
editorDidMount={onEditorDidMount}
onBeforeChange={onBeforeChange}
/>
<StatusBar {...statusBarInfo} />
</div>
)
}

View file

@ -0,0 +1,11 @@
.status-bar {
background: #1c1c1e;
border-top: 1px solid #343434;
color: #ccc;
position: relative;
display: block;
box-sizing: border-box;
font-size: 13px;
line-height: 25px;
height: 26px;
}

View file

@ -0,0 +1,53 @@
import { Editor, Position } from 'codemirror'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { ShowIf } from '../../../common/show-if/show-if'
import './status-bar.scss'
export interface StatusBarInfo {
position: Position
selectedColumns: number
selectedLines: number
linesInDocument: number
charactersInDocument: number
}
export const defaultState: StatusBarInfo = {
position: { line: 0, ch: 0 },
selectedColumns: 0,
selectedLines: 0,
linesInDocument: 0,
charactersInDocument: 0
}
export const createStatusInfo = (editor: Editor): StatusBarInfo => ({
position: editor.getCursor(),
charactersInDocument: editor.getValue().length,
linesInDocument: editor.lineCount(),
selectedColumns: editor.getSelection().length,
selectedLines: editor.getSelection().split('\n').length
})
export const StatusBar: React.FC<StatusBarInfo> = ({ position, selectedColumns, selectedLines, charactersInDocument, linesInDocument }) => {
const { t } = useTranslation()
return (
<div className="d-flex flex-row status-bar px-2">
<div>
<span>{t('editor.statusBar.cursor', { line: position.line + 1, columns: position.ch + 1 })}</span>
<ShowIf condition={selectedColumns !== 0 && selectedLines !== 0}>
<ShowIf condition={selectedLines === 1}>
<span>&nbsp;&nbsp;{t('editor.statusBar.selection.column', { count: selectedColumns })}</span>
</ShowIf>
<ShowIf condition={selectedLines > 1}>
<span>&nbsp;&nbsp;{t('editor.statusBar.selection.line', { count: selectedLines })}</span>
</ShowIf>
</ShowIf>
</div>
<div className="ml-auto">
<span>{t('editor.statusBar.lines', { lines: linesInDocument })}</span>
<span title={t('editor.statusBar.lengthTooltip')}>&nbsp;&nbsp;{t('editor.statusBar.length', { length: charactersInDocument })}</span>
</div>
</div>
)
}

View file

@ -0,0 +1,53 @@
import { EditorConfiguration } from 'codemirror'
import React, { ChangeEvent, useCallback, useState } from 'react'
import { Form } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
export enum EditorPreferenceProperty {
KEYMAP = 'keyMap',
THEME = 'theme',
INDENT_WITH_TABS = 'indentWithTabs',
INDENT_UNIT = 'indentUnit'
}
export interface EditorPreferenceSelectProps {
onChange: (config: EditorConfiguration) => void
preferences: EditorConfiguration
property: EditorPreferenceProperty
}
export const EditorPreferenceSelect: React.FC<EditorPreferenceSelectProps> = ({ property, onChange, preferences, children }) => {
useTranslation()
const [selected, setSelected] = useState(preferences[property])
const selectItem = useCallback((event: ChangeEvent<HTMLSelectElement>) => {
let selectedItem: string | boolean | number = event.target.value
if (property === EditorPreferenceProperty.INDENT_UNIT) {
selectedItem = parseInt(selectedItem)
}
setSelected(selectedItem)
if (property === EditorPreferenceProperty.INDENT_WITH_TABS) {
selectedItem = selectedItem === 'true'
}
onChange({
...preferences,
[property]: selectedItem
})
}, [preferences, property, setSelected, onChange])
return (
<Form.Group controlId={`editor-pref-${property}`}>
<Form.Label>
<Trans i18nKey={`editor.modal.preferences.${property}`}/>
</Form.Label>
<Form.Control
as={property === EditorPreferenceProperty.INDENT_UNIT ? 'input' : 'select'}
size='sm'
value={selected as string | number}
onChange={selectItem}
type={property === EditorPreferenceProperty.INDENT_UNIT ? 'number' : ''}>
{ children }
</Form.Control>
</Form.Group>
)
}

View file

@ -0,0 +1,73 @@
import { EditorConfiguration } from 'codemirror'
import React, { Fragment, useCallback, useState } from 'react'
import { Button, Form, ListGroup } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../../common/fork-awesome/fork-awesome-icon'
import { CommonModal } from '../../../../common/modals/common-modal'
import { EditorPreferenceProperty, EditorPreferenceSelect } from './editor-preference-select'
export interface EditorSettingsButtonProps {
preferences: EditorConfiguration
onPreferencesChange: (config: EditorConfiguration) => void
}
export const EditorPreferences: React.FC<EditorSettingsButtonProps> = ({ onPreferencesChange, preferences }) => {
const { t } = useTranslation()
const [showModal, setShowModal] = useState(false)
const sendPreferences = useCallback((newPreferences: EditorConfiguration) => {
onPreferencesChange(newPreferences)
}, [onPreferencesChange])
return (
<Fragment>
<Button variant='light' onClick={() => setShowModal(true)} title={t('editor.editorToolbar.preferences')}>
<ForkAwesomeIcon icon="wrench"/>
</Button>
<CommonModal
show={showModal}
onHide={() => setShowModal(false)}
titleI18nKey={'editor.modal.preferences.title'}
closeButton={true}
icon={'wrench'}>
<Form>
<ListGroup>
<ListGroup.Item>
<EditorPreferenceSelect onChange={sendPreferences} preferences={preferences} property={EditorPreferenceProperty.THEME}>
<option value='one-dark'>Dark</option>
<option value='neat'>Light</option>
</EditorPreferenceSelect>
</ListGroup.Item>
<ListGroup.Item>
<EditorPreferenceSelect onChange={sendPreferences} preferences={preferences} property={EditorPreferenceProperty.KEYMAP}>
<option value='sublime'>Sublime</option>
<option value='emacs'>Emacs</option>
<option value='vim'>Vim</option>
</EditorPreferenceSelect>
</ListGroup.Item>
<ListGroup.Item>
<EditorPreferenceSelect onChange={sendPreferences} preferences={preferences} property={EditorPreferenceProperty.INDENT_WITH_TABS}>
<option value='false'>Spaces</option>
<option value='true'>Tab</option>
</EditorPreferenceSelect>
</ListGroup.Item>
<ListGroup.Item>
<EditorPreferenceSelect onChange={sendPreferences} preferences={preferences} property={EditorPreferenceProperty.INDENT_UNIT}/>
</ListGroup.Item>
<ListGroup.Item>
<Form.Group controlId='editorSpellChecker'>
<Form.Label>
<Trans i18nKey='editor.modal.preferences.spellChecker'/>
</Form.Label>
<Form.Control as='select' size='sm' onChange={() => alert('This feature is not yet implemented.')}>
<option value='off'>off</option>
<option value='en'>English</option>
</Form.Control>
</Form.Group>
</ListGroup.Item>
</ListGroup>
</Form>
</CommonModal>
</Fragment>
)
}

View file

@ -0,0 +1,28 @@
import CodeMirror from 'codemirror'
import React, { Fragment, useState } from 'react'
import { Button } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import { EmojiPicker } from './emoji-picker/emoji-picker'
import { addEmoji } from './utils'
export interface EmojiPickerButtonProps {
editor: CodeMirror.Editor
}
export const EmojiPickerButton: React.FC<EmojiPickerButtonProps> = ({ editor }) => {
const { t } = useTranslation()
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
return (
<Fragment>
<EmojiPicker show={showEmojiPicker} onEmojiSelected={(emoji) => {
setShowEmojiPicker(false)
addEmoji(emoji, editor)
}} onDismiss={() => setShowEmojiPicker(false)}/>
<Button variant='light' onClick={() => setShowEmojiPicker(old => !old)} title={t('editor.editorToolbar.emoji')}>
<ForkAwesomeIcon icon="smile-o"/>
</Button>
</Fragment>
)
}

View file

@ -1,3 +1,9 @@
.btn-toolbar {
border: 1px solid #ededed;
}
.divider {
background-color: #e2e6ea;
width: 2px;
padding: 0.25rem 0;
}

View file

@ -1,14 +1,14 @@
import { Editor } from 'codemirror'
import React, { Fragment, useState } from 'react'
import { Button, ButtonToolbar } from 'react-bootstrap'
import { Editor, EditorConfiguration } from 'codemirror'
import React, { Fragment } from 'react'
import { Button, ButtonGroup, ButtonToolbar } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import { EmojiPicker } from './emoji-picker/emoji-picker'
import { EditorPreferences } from './editor-preferences/editor-preferences'
import { EmojiPickerButton } from './emoji-picker-button'
import './tool-bar.scss'
import {
addCodeFences,
addComment,
addEmoji,
addHeaderLevel,
addImage,
addLine,
@ -28,11 +28,12 @@ import {
export interface ToolBarProps {
editor: Editor | undefined
onPreferencesChange: (config: EditorConfiguration) => void
editorPreferences: EditorConfiguration
}
export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
export const ToolBar: React.FC<ToolBarProps> = ({ editor, onPreferencesChange, editorPreferences }) => {
const { t } = useTranslation()
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const notImplemented = () => {
alert('This feature is not yet implemented')
@ -44,7 +45,8 @@ export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
return (
<Fragment>
<ButtonToolbar className='flex-nowrap bg-light'>
<ButtonToolbar className='bg-light'>
<ButtonGroup className={'mx-2 flex-wrap'}>
<Button variant='light' onClick={() => makeSelectionBold(editor)} title={t('editor.editorToolbar.bold')}>
<ForkAwesomeIcon icon="bold"/>
</Button>
@ -63,6 +65,9 @@ export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
<Button variant='light' onClick={() => superscriptSelection(editor)} title={t('editor.editorToolbar.superscript')}>
<ForkAwesomeIcon icon="superscript"/>
</Button>
</ButtonGroup>
<span className={'divider'}>&nbsp;</span>
<ButtonGroup className={'mx-2 flex-wrap'}>
<Button variant='light' onClick={() => addHeaderLevel(editor)} title={t('editor.editorToolbar.header')}>
<ForkAwesomeIcon icon="header"/>
</Button>
@ -81,6 +86,9 @@ export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
<Button variant='light' onClick={() => addTaskList(editor)} title={t('editor.editorToolbar.checkList')}>
<ForkAwesomeIcon icon="check-square"/>
</Button>
</ButtonGroup>
<span className={'divider'}>&nbsp;</span>
<ButtonGroup className={'mx-2 flex-wrap'}>
<Button variant='light' onClick={() => addLink(editor)} title={t('editor.editorToolbar.link')}>
<ForkAwesomeIcon icon="link"/>
</Button>
@ -90,6 +98,9 @@ export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
<Button variant='light' onClick={notImplemented} title={t('editor.editorToolbar.uploadImage')}>
<ForkAwesomeIcon icon="upload"/>
</Button>
</ButtonGroup>
<span className={'divider'}>&nbsp;</span>
<ButtonGroup className={'mx-2 flex-wrap'}>
<Button variant='light' onClick={() => addTable(editor)} title={t('editor.editorToolbar.table')}>
<ForkAwesomeIcon icon="table"/>
</Button>
@ -99,13 +110,12 @@ export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
<Button variant='light' onClick={() => addComment(editor)} title={t('editor.editorToolbar.comment')}>
<ForkAwesomeIcon icon="comment"/>
</Button>
<EmojiPicker show={showEmojiPicker} onEmojiSelected={(emoji) => {
setShowEmojiPicker(false)
addEmoji(emoji, editor)
}} onDismiss={() => setShowEmojiPicker(false)}/>
<Button variant='light' onClick={() => setShowEmojiPicker(old => !old)} title={''}>
<ForkAwesomeIcon icon="smile-o"/>
</Button>
<EmojiPickerButton editor={editor}/>
</ButtonGroup>
<span className={'divider'}>&nbsp;</span>
<ButtonGroup className={'mx-2 flex-wrap'}>
<EditorPreferences onPreferencesChange={onPreferencesChange} preferences={editorPreferences}/>
</ButtonGroup>
</ButtonToolbar>
</Fragment>
)

View file

@ -7,6 +7,7 @@ import { setEditorModeConfig } from '../../redux/editor/methods'
import { DocumentTitle } from '../common/document-title/document-title'
import { Splitter } from '../common/splitter/splitter'
import { InfoBanner } from '../landing/layout/info-banner'
import { DocumentBar } from './document-bar/document-bar'
import { EditorWindow } from './editor-window/editor-window'
import { editorTestContent } from './editorTestContent'
import { MarkdownRenderWindow } from './renderer-window/markdown-render-window'
@ -14,6 +15,10 @@ import { EditorMode } from './task-bar/editor-view-mode'
import { TaskBar } from './task-bar/task-bar'
import { YAMLMetaData } from './yaml-metadata/yaml-metadata'
export interface EditorPathParams {
id: string
}
export const Editor: React.FC = () => {
const { t } = useTranslation()
const untitledNote = t('editor.untitledNote')
@ -61,11 +66,22 @@ export const Editor: React.FC = () => {
<DocumentTitle title={documentTitle}/>
<div className={'d-flex flex-column vh-100'}>
<TaskBar/>
<DocumentBar title={documentTitle}/>
<Splitter
showLeft={editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH}
left={<EditorWindow onContentChange={content => setMarkdownContent(content)} content={markdownContent}/>}
left={
<EditorWindow
onContentChange={content => setMarkdownContent(content)}
content={markdownContent}/>
}
showRight={editorMode === EditorMode.PREVIEW || (editorMode === EditorMode.BOTH)}
right={<MarkdownRenderWindow content={markdownContent} wide={editorMode === EditorMode.PREVIEW} onMetadataChange={onMetadataChange} onFirstHeadingChange={onFirstHeadingChange}/>}
right={
<MarkdownRenderWindow
content={markdownContent}
wide={editorMode === EditorMode.PREVIEW}
onMetadataChange={onMetadataChange}
onFirstHeadingChange={onFirstHeadingChange}/>
}
containerClassName={'overflow-hidden'}/>
</div>
</Fragment>

View file

@ -50,4 +50,8 @@
}
}
pre {
overflow: visible;
}
}

View file

@ -52,11 +52,11 @@
.markdown-toc-sidebar-button {
position: fixed;
right: 40px;
bottom: 20px;
bottom: 30px;
&>.dropup {
position: sticky;
bottom: 20px;
right: 0px;
right: 0;
}
}

View file

@ -1,75 +0,0 @@
import React from 'react'
import { Dropdown } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
const EditorMenu: React.FC = () => {
useTranslation()
return (
<Dropdown className="small" alignRight={true}>
<Dropdown.Toggle variant="light" size="sm" id="editor-menu" className="text-secondary">
<Trans i18nKey="editor.menu.menu"/>
</Dropdown.Toggle>
<Dropdown.Menu>
<Dropdown.Header>
<Trans i18nKey="editor.menu.extra"/>
</Dropdown.Header>
<Dropdown.Item className="small">
<ForkAwesomeIcon icon="history"/> <Trans i18nKey="editor.menu.revision"/>
</Dropdown.Item>
<Dropdown.Item className="small">
<ForkAwesomeIcon icon="television"/> <Trans i18nKey="editor.menu.slideMode"/>
</Dropdown.Item>
<Dropdown.Item className="small">
<ForkAwesomeIcon icon="trash"/> <Trans i18nKey="editor.menu.deleteNote"/>
</Dropdown.Item>
<Dropdown.Divider/>
<Dropdown.Header>
<Trans i18nKey="common.export"/>
</Dropdown.Header>
<Dropdown.Item className="small">
<ForkAwesomeIcon icon="dropbox"/> Dropbox
</Dropdown.Item>
<Dropdown.Item className="small">
<ForkAwesomeIcon icon="github"/> Gist
</Dropdown.Item>
<Dropdown.Divider/>
<Dropdown.Header>
<Trans i18nKey="common.import"/>
</Dropdown.Header>
<Dropdown.Item className="small">
<ForkAwesomeIcon icon="dropbox"/> Dropbox
</Dropdown.Item>
<Dropdown.Item className="small">
<ForkAwesomeIcon icon="github"/> Gist
</Dropdown.Item>
<Dropdown.Item className="small">
<ForkAwesomeIcon icon="clipboard"/> <Trans i18nKey="editor.import.clipboard"/>
</Dropdown.Item>
<Dropdown.Divider/>
<Dropdown.Header>
<Trans i18nKey="editor.menu.download"/>
</Dropdown.Header>
<Dropdown.Item className="small">
<ForkAwesomeIcon icon="file-text"/> Markdown
</Dropdown.Item>
<Dropdown.Item className="small">
<ForkAwesomeIcon icon="file-code-o"/> HTML
</Dropdown.Item>
<Dropdown.Item className="small">
<ForkAwesomeIcon icon="file-code-o"/> <Trans i18nKey='editor.export.rawHtml'/>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
)
}
export { EditorMenu }

View file

@ -12,14 +12,14 @@ export const HelpButton: React.FC = () => {
const handleClose = () => setShow(false)
return (
<Fragment>
<Button title={t('editor.menu.help')} className="ml-2 text-secondary" size="sm" variant="outline-light"
<Button title={t('editor.documentBar.help')} className="ml-2 text-secondary" size="sm" variant="outline-light"
onClick={handleShow}>
<ForkAwesomeIcon icon="question-circle"/>
</Button>
<Modal show={show} onHide={handleClose} animation={true} className="text-dark" size='lg'>
<Modal.Header closeButton>
<Modal.Title>
<ForkAwesomeIcon icon="question-circle"/> <Trans i18nKey={'editor.menu.help'}/>
<ForkAwesomeIcon icon="question-circle"/> <Trans i18nKey={'editor.documentBar.help'}/>
</Modal.Title>
</Modal.Header>
<Modal.Body className="text-dark">
@ -67,6 +67,7 @@ export const HelpButton: React.FC = () => {
</Card.Text>
</Card.Body>
</Card>
<br/>
<Card>
<Card.Header><Trans i18nKey='editor.help.documents.title'/></Card.Header>
<Card.Body>

View file

@ -1,17 +1,25 @@
import React from 'react'
import { Button, Nav, Navbar } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { useParams } from 'react-router'
import { Link } from 'react-router-dom'
import { ApplicationState } from '../../../redux'
import { Branding } from '../../common/branding/branding'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import { ConnectionIndicator } from './connection-indicator'
import { ShowIf } from '../../common/show-if/show-if'
import { SignInButton } from '../../landing/layout/navigation/sign-in-button'
import { UserDropdown } from '../../landing/layout/navigation/user-dropdown/user-dropdown'
import { EditorPathParams } from '../editor'
import { DarkModeButton } from './dark-mode-button'
import { EditorMenu } from './editor-menu'
import { EditorViewMode } from './editor-view-mode'
import { HelpButton } from './help-button'
const TaskBar: React.FC = () => {
useTranslation()
export const TaskBar: React.FC = () => {
const { t } = useTranslation()
const { id } = useParams<EditorPathParams>()
const user = useSelector((state: ApplicationState) => state.user)
return (
<Navbar bg={'light'}>
<Nav className="mr-auto d-flex align-items-center">
@ -24,24 +32,24 @@ const TaskBar: React.FC = () => {
</Navbar.Brand>
<EditorViewMode/>
<DarkModeButton/>
<Link to={`/p/${id}`} target='_blank'>
<Button title={t('editor.documentBar.slideMode')} className="ml-2 text-secondary" size="sm" variant="outline-light">
<ForkAwesomeIcon icon="television"/>
</Button>
</Link>
<HelpButton/>
</Nav>
<Nav className="d-flex align-items-center text-secondary">
<Button className="ml-2 text-secondary" size="sm" variant="outline-light">
<ForkAwesomeIcon icon="plus"/> <Trans i18nKey="editor.menu.new"/>
<Button className="mx-2" size="sm" variant="primary">
<ForkAwesomeIcon icon="plus"/> <Trans i18nKey="editor.documentBar.new"/>
</Button>
<Button className="ml-2 text-secondary" size="sm" variant="outline-light">
<ForkAwesomeIcon icon="share-square-o"/> <Trans i18nKey="editor.menu.publish"/>
</Button>
<div className="text-secondary">
<EditorMenu/>
</div>
<div className="mr-2">
<ConnectionIndicator/>
</div>
<ShowIf condition={!user}>
<SignInButton size={'sm'} />
</ShowIf>
<ShowIf condition={!!user}>
<UserDropdown />
</ShowIf>
</Nav>
</Navbar>
)
}
export { TaskBar }

View file

@ -1,22 +1,28 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { ShowIf } from '../../../common/show-if/show-if'
import './user-avatar.scss'
export interface UserAvatarProps {
name: string;
photo: string;
additionalClasses?: string;
showName?: boolean
}
const UserAvatar: React.FC<UserAvatarProps> = ({ name, photo, additionalClasses = '' }) => {
// ToDo: add Translation Key for Avatar of ${name}
const UserAvatar: React.FC<UserAvatarProps> = ({ name, photo, additionalClasses = '', showName = true }) => {
const { t } = useTranslation()
return (
<span className={'d-inline-flex align-items-center ' + additionalClasses}>
<img
src={photo}
className="user-avatar"
alt={`Avatar of ${name}`}
className="user-avatar rounded"
alt={t('common.avatarOf', { name })}
/>
<ShowIf condition={showName}>
<span className="mx-1 user-name">{name}</span>
</ShowIf>
</span>
)
}

View file

@ -7,7 +7,7 @@ import { ApplicationState } from '../../../../redux'
import frontendVersion from '../../../../version.json'
import { TranslatedExternalLink } from '../../../common/links/translated-external-link'
import { ShowIf } from '../../../common/show-if/show-if'
import { VersionInputField } from './version-input-field'
import { CopyableField } from '../../../common/copyable-field/copyable-field'
export const VersionInfo: React.FC = () => {
const [show, setShow] = useState(false)
@ -22,7 +22,7 @@ export const VersionInfo: React.FC = () => {
const column = (title: string, version: string, sourceCodeLink: string, issueTrackerLink: string) => (
<Col md={6} className={'flex-column'}>
<h5>{title}</h5>
<VersionInputField version={version}/>
<CopyableField content={version}/>
<ShowIf condition={!!sourceCodeLink}>
<TranslatedExternalLink i18nKey={'landing.versionInfo.sourceCode'} className={'btn btn-sm btn-primary d-block mb-2'} href={sourceCodeLink}/>
</ShowIf>

View file

@ -13,19 +13,20 @@ export interface DropdownItemWithDeletionModalProps {
modalQuestionI18nKey: string
modalWarningI18nKey: string
noteTitle: string
className?: string
}
export const DropdownItemWithDeletionModal: React.FC<DropdownItemWithDeletionModalProps> = ({
onConfirm, noteTitle,
modalTitleI18nKey, modalButtonI18nKey, itemI18nKey, modalIcon,
modalQuestionI18nKey, modalWarningI18nKey
modalQuestionI18nKey, modalWarningI18nKey, className
}) => {
useTranslation()
const [showDialog, setShowDialog] = useState(false)
return (
<Fragment>
<Dropdown.Item onClick={() => setShowDialog(true)}>
<Dropdown.Item onClick={() => setShowDialog(true)} className={className}>
<ForkAwesomeIcon icon={modalIcon} fixedWidth={true} className="mx-2"/>
<Trans i18nKey={itemI18nKey}/>
</Dropdown.Item>

View file

@ -4186,7 +4186,7 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
cypress@*, cypress@4.12.1:
cypress@4.12.1:
version "4.12.1"
resolved "https://registry.yarnpkg.com/cypress/-/cypress-4.12.1.tgz#0ead1b9f4c0917d69d8b57f996b6e01fe693b6ec"
integrity sha512-9SGIPEmqU8vuRA6xst2CMTYd9sCFCxKSzrHt0wr+w2iAQMCIIsXsQ5Gplns1sT6LDbZcmLv6uehabAOl3fhc9Q==