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": "صورة", "image": "صورة",
"uploadImage": "تحميل صورة" "uploadImage": "تحميل صورة"
}, },
"menu": { "documentBar": {
"menu": "القائمة", "menu": "القائمة",
"new": "جديد", "new": "جديد",
"publish": "انشر", "shareLink": "",
"extra": "إضافي", "extra": "إضافي",
"revision": "مراجعة", "revision": "مراجعة",
"slideMode": "نمط الشرائح التقديمية", "slideMode": "نمط الشرائح التقديمية",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -219,21 +219,41 @@
"uploadImage": "Upload Image", "uploadImage": "Upload Image",
"table": "Table", "table": "Table",
"line": "Horizontal line", "line": "Horizontal line",
"comment": "Comment" "comment": "Comment",
"preferences": "Editor settings",
"emoji": "Open emoji picker"
}, },
"menu": { "documentBar": {
"menu": "Menu", "menu": "Menu",
"import": "Import",
"export": "Export",
"new": "New", "new": "New",
"publish": "Publish", "shareLink": "Share link",
"extra": "Extra", "extra": "Extra",
"revision": "Revision", "revision": "Revision",
"slideMode": "Slide Mode", "slideMode": "Slide Mode",
"download": "Download", "download": "Download",
"help": "Help", "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": { "export": {
"rawHtml": "Raw HTML" "rawHtml": "Raw HTML",
"pdf": "PDF export is unavailable.",
"why": "Why?"
}, },
"import": { "import": {
"clipboard": "Clipboard" "clipboard": "Clipboard"
@ -244,6 +264,13 @@
"selectProject": "Select From Available Projects", "selectProject": "Select From Available Projects",
"selectSnippet": "Select From Available Snippets" "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": { "gistImport": {
"title": "Import from Gist", "title": "Import from Gist",
"insertGistUrl": "Paste your gist url here…" "insertGistUrl": "Paste your gist url here…"
@ -265,6 +292,21 @@
"question": "Do you really want to delete this note?", "question": "Do you really want to delete this note?",
"warning": "All users will lose their connection. This process is irreversible.", "warning": "All users will lose their connection. This process is irreversible.",
"button": "Delete note" "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": { "embeddings": {
@ -280,7 +322,8 @@
"close": "Close", "close": "Close",
"save": "Save", "save": "Save",
"or": "or", "or": "or",
"and": "and" "and": "and",
"avatarOf": "avatar of '{{name}}'"
}, },
"login": { "login": {
"chooseMethod": "Choose method", "chooseMethod": "Choose method",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -124,10 +124,10 @@
"image": "圖片", "image": "圖片",
"uploadImage": "上傳圖片" "uploadImage": "上傳圖片"
}, },
"menu": { "documentBar": {
"menu": "選單", "menu": "選單",
"new": "新增", "new": "新增",
"publish": "發表", "shareLink": "",
"extra": "增益", "extra": "增益",
"revision": "修訂版本", "revision": "修訂版本",
"slideMode": "簡報模式", "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 { Button, FormControl, InputGroup, Overlay, Tooltip } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' 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 { export interface CopyableFieldProps {
version: string content: string
} }
export const VersionInputField: React.FC<VersionInputFieldProps> = ({ version }) => { export const CopyableField: React.FC<CopyableFieldProps> = ({ content }) => {
useTranslation() useTranslation()
const inputField = useRef<HTMLInputElement>(null) const inputField = useRef<HTMLInputElement>(null)
const [showCopiedTooltip, setShowCopiedTooltip] = useState(false) const [showCopiedTooltip, setShowCopiedTooltip] = useState(false)
const copyToClipboard = (content: string) => { const copyToClipboard = useCallback((content: string) => {
navigator.clipboard.writeText(content).then(() => { navigator.clipboard.writeText(content).then(() => {
setShowCopiedTooltip(true) setShowCopiedTooltip(true)
setTimeout(() => { setShowCopiedTooltip(false) }, 2000) setTimeout(() => { setShowCopiedTooltip(false) }, 2000)
}).catch(() => { }).catch(() => {
console.error("couldn't copy") 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 ( return (
<Fragment> <Fragment>
<Overlay target={inputField} show={showCopiedTooltip} placement="top"> <Overlay target={inputField} show={showCopiedTooltip} placement="top">
{(props) => ( {(props) => (
<Tooltip id={'copied_' + version} {...props}> <Tooltip id={'copied_' + content} {...props}>
<Trans i18nKey={'landing.versionInfo.successfullyCopied'}/> <Trans i18nKey={'landing.versionInfo.successfullyCopied'}/>
</Tooltip> </Tooltip>
)} )}
</Overlay> </Overlay>
<InputGroup className="mb-3"> <InputGroup className="my-3">
<FormControl readOnly={true} ref={inputField} className={'text-center'} value={version} /> <FormControl readOnly={true} ref={inputField} className={'text-center'} value={content} onMouseEnter={selectContent} />
<InputGroup.Append> <InputGroup.Append>
<Button variant="outline-secondary" onClick={() => copyToClipboard(version)} title={'Copy'}> <Button variant="outline-secondary" onClick={() => copyToClipboard(content)} title={'Copy'}>
<ForkAwesomeIcon icon='files-o'/> <ForkAwesomeIcon icon='files-o'/>
</Button> </Button>
</InputGroup.Append> </InputGroup.Append>

View file

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

View file

@ -1,5 +1,5 @@
import React, { Fragment } from 'react' 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 { ActiveIndicator, ActiveIndicatorStatus } from './active-indicator'
import './user-line.scss' 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/lib/codemirror.css';
@import '../../../../node_modules/codemirror/addon/display/fullscreen.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 './one-dark.css';
@import 'hints'; @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/comment/comment'
import 'codemirror/addon/dialog/dialog'
import 'codemirror/addon/display/autorefresh' import 'codemirror/addon/display/autorefresh'
import 'codemirror/addon/display/fullscreen' import 'codemirror/addon/display/fullscreen'
import 'codemirror/addon/display/placeholder' import 'codemirror/addon/display/placeholder'
@ -11,16 +12,21 @@ import 'codemirror/addon/edit/matchtags'
import 'codemirror/addon/fold/foldcode' import 'codemirror/addon/fold/foldcode'
import 'codemirror/addon/fold/foldgutter' import 'codemirror/addon/fold/foldgutter'
import 'codemirror/addon/hint/show-hint' 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/search/match-highlighter'
import 'codemirror/addon/selection/active-line' import 'codemirror/addon/selection/active-line'
import 'codemirror/keymap/sublime.js' import 'codemirror/keymap/sublime'
import 'codemirror/mode/gfm/gfm.js' import 'codemirror/keymap/emacs'
import React, { useCallback, useState } from 'react' import 'codemirror/keymap/vim'
import 'codemirror/mode/gfm/gfm'
import React, { useCallback, useMemo, useState } from 'react'
import { Controlled as ControlledCodeMirror } from 'react-codemirror2' import { Controlled as ControlledCodeMirror } from 'react-codemirror2'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import './editor-window.scss' import './editor-window.scss'
import { emojiHints, emojiWordRegex, findWordAtCursor } from './hints/emoji' import { emojiHints, emojiWordRegex, findWordAtCursor } from './hints/emoji'
import { defaultKeyMap } from './key-map' import { defaultKeyMap } from './key-map'
import { createStatusInfo, defaultState, StatusBar, StatusBarInfo } from './status-bar/status-bar'
import { ToolBar } from './tool-bar/tool-bar' import { ToolBar } from './tool-bar/tool-bar'
export interface EditorWindowProps { export interface EditorWindowProps {
@ -45,30 +51,33 @@ const onChange = (editor: Editor) => {
export const EditorWindow: React.FC<EditorWindowProps> = ({ onContentChange, content }) => { export const EditorWindow: React.FC<EditorWindowProps> = ({ onContentChange, content }) => {
const { t } = useTranslation() const { t } = useTranslation()
const [editor, setEditor] = useState<Editor>() 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) => { const onBeforeChange = useCallback((editor: Editor, data: EditorChange, value: string) => {
onContentChange(value) onContentChange(value)
}, [onContentChange]) }, [onContentChange])
const onEditorDidMount = useCallback(mountedEditor => {
return ( setStatusBarInfo(createStatusInfo(mountedEditor))
<div className={'d-flex flex-column h-100'}> setEditor(mountedEditor)
<ToolBar }, [])
editor={editor} const onCursorActivity = useCallback((editorWithActivity) => {
/> setStatusBarInfo(createStatusInfo(editorWithActivity))
<ControlledCodeMirror }, [])
className="overflow-hidden w-100 flex-fill" const codeMirrorOptions: EditorConfiguration = useMemo<EditorConfiguration>(() => ({
value={content} ...editorPreferences,
options={{
mode: 'gfm', mode: 'gfm',
theme: 'one-dark',
keyMap: 'sublime',
viewportMargin: 20, viewportMargin: 20,
styleActiveLine: true, styleActiveLine: true,
lineNumbers: true, lineNumbers: true,
lineWrapping: true, lineWrapping: true,
showCursorWhenSelecting: true, showCursorWhenSelecting: true,
highlightSelectionMatches: true, highlightSelectionMatches: true,
indentUnit: 4,
inputStyle: 'textarea', inputStyle: 'textarea',
matchBrackets: true, matchBrackets: true,
autoCloseBrackets: true, autoCloseBrackets: true,
@ -87,13 +96,26 @@ export const EditorWindow: React.FC<EditorWindowProps> = ({ onContentChange, con
addModeClass: true, addModeClass: true,
autoRefresh: true, autoRefresh: true,
// otherCursors: true, // otherCursors: true,
placeholder: t('editor.placeholder'), placeholder: t('editor.placeholder')
showHint: false, }), [t, editorPreferences])
hintOptions: hintOptions
}} return (
editorDidMount={mountedEditor => setEditor(mountedEditor)} <div className={'d-flex flex-column h-100'}>
onBeforeChange={onBeforeChange} <ToolBar
editor={editor}
onPreferencesChange={config => setEditorPreferences(config)}
editorPreferences={editorPreferences}
/>
<ControlledCodeMirror
className="overflow-hidden w-100 flex-fill"
value={content}
options={codeMirrorOptions}
onChange={onChange} 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 { .btn-toolbar {
border: 1px solid #ededed; 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 { Editor, EditorConfiguration } from 'codemirror'
import React, { Fragment, useState } from 'react' import React, { Fragment } from 'react'
import { Button, ButtonToolbar } from 'react-bootstrap' import { Button, ButtonGroup, ButtonToolbar } from 'react-bootstrap'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon' 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 './tool-bar.scss'
import { import {
addCodeFences, addCodeFences,
addComment, addComment,
addEmoji,
addHeaderLevel, addHeaderLevel,
addImage, addImage,
addLine, addLine,
@ -28,11 +28,12 @@ import {
export interface ToolBarProps { export interface ToolBarProps {
editor: Editor | undefined 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 { t } = useTranslation()
const [showEmojiPicker, setShowEmojiPicker] = useState(false)
const notImplemented = () => { const notImplemented = () => {
alert('This feature is not yet implemented') alert('This feature is not yet implemented')
@ -44,7 +45,8 @@ export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
return ( return (
<Fragment> <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')}> <Button variant='light' onClick={() => makeSelectionBold(editor)} title={t('editor.editorToolbar.bold')}>
<ForkAwesomeIcon icon="bold"/> <ForkAwesomeIcon icon="bold"/>
</Button> </Button>
@ -63,6 +65,9 @@ export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
<Button variant='light' onClick={() => superscriptSelection(editor)} title={t('editor.editorToolbar.superscript')}> <Button variant='light' onClick={() => superscriptSelection(editor)} title={t('editor.editorToolbar.superscript')}>
<ForkAwesomeIcon icon="superscript"/> <ForkAwesomeIcon icon="superscript"/>
</Button> </Button>
</ButtonGroup>
<span className={'divider'}>&nbsp;</span>
<ButtonGroup className={'mx-2 flex-wrap'}>
<Button variant='light' onClick={() => addHeaderLevel(editor)} title={t('editor.editorToolbar.header')}> <Button variant='light' onClick={() => addHeaderLevel(editor)} title={t('editor.editorToolbar.header')}>
<ForkAwesomeIcon icon="header"/> <ForkAwesomeIcon icon="header"/>
</Button> </Button>
@ -81,6 +86,9 @@ export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
<Button variant='light' onClick={() => addTaskList(editor)} title={t('editor.editorToolbar.checkList')}> <Button variant='light' onClick={() => addTaskList(editor)} title={t('editor.editorToolbar.checkList')}>
<ForkAwesomeIcon icon="check-square"/> <ForkAwesomeIcon icon="check-square"/>
</Button> </Button>
</ButtonGroup>
<span className={'divider'}>&nbsp;</span>
<ButtonGroup className={'mx-2 flex-wrap'}>
<Button variant='light' onClick={() => addLink(editor)} title={t('editor.editorToolbar.link')}> <Button variant='light' onClick={() => addLink(editor)} title={t('editor.editorToolbar.link')}>
<ForkAwesomeIcon icon="link"/> <ForkAwesomeIcon icon="link"/>
</Button> </Button>
@ -90,6 +98,9 @@ export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
<Button variant='light' onClick={notImplemented} title={t('editor.editorToolbar.uploadImage')}> <Button variant='light' onClick={notImplemented} title={t('editor.editorToolbar.uploadImage')}>
<ForkAwesomeIcon icon="upload"/> <ForkAwesomeIcon icon="upload"/>
</Button> </Button>
</ButtonGroup>
<span className={'divider'}>&nbsp;</span>
<ButtonGroup className={'mx-2 flex-wrap'}>
<Button variant='light' onClick={() => addTable(editor)} title={t('editor.editorToolbar.table')}> <Button variant='light' onClick={() => addTable(editor)} title={t('editor.editorToolbar.table')}>
<ForkAwesomeIcon icon="table"/> <ForkAwesomeIcon icon="table"/>
</Button> </Button>
@ -99,13 +110,12 @@ export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
<Button variant='light' onClick={() => addComment(editor)} title={t('editor.editorToolbar.comment')}> <Button variant='light' onClick={() => addComment(editor)} title={t('editor.editorToolbar.comment')}>
<ForkAwesomeIcon icon="comment"/> <ForkAwesomeIcon icon="comment"/>
</Button> </Button>
<EmojiPicker show={showEmojiPicker} onEmojiSelected={(emoji) => { <EmojiPickerButton editor={editor}/>
setShowEmojiPicker(false) </ButtonGroup>
addEmoji(emoji, editor) <span className={'divider'}>&nbsp;</span>
}} onDismiss={() => setShowEmojiPicker(false)}/> <ButtonGroup className={'mx-2 flex-wrap'}>
<Button variant='light' onClick={() => setShowEmojiPicker(old => !old)} title={''}> <EditorPreferences onPreferencesChange={onPreferencesChange} preferences={editorPreferences}/>
<ForkAwesomeIcon icon="smile-o"/> </ButtonGroup>
</Button>
</ButtonToolbar> </ButtonToolbar>
</Fragment> </Fragment>
) )

View file

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

View file

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

View file

@ -52,11 +52,11 @@
.markdown-toc-sidebar-button { .markdown-toc-sidebar-button {
position: fixed; position: fixed;
right: 40px; right: 40px;
bottom: 20px; bottom: 30px;
&>.dropup { &>.dropup {
position: sticky; position: sticky;
bottom: 20px; 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) const handleClose = () => setShow(false)
return ( return (
<Fragment> <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}> onClick={handleShow}>
<ForkAwesomeIcon icon="question-circle"/> <ForkAwesomeIcon icon="question-circle"/>
</Button> </Button>
<Modal show={show} onHide={handleClose} animation={true} className="text-dark" size='lg'> <Modal show={show} onHide={handleClose} animation={true} className="text-dark" size='lg'>
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title> <Modal.Title>
<ForkAwesomeIcon icon="question-circle"/> <Trans i18nKey={'editor.menu.help'}/> <ForkAwesomeIcon icon="question-circle"/> <Trans i18nKey={'editor.documentBar.help'}/>
</Modal.Title> </Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body className="text-dark"> <Modal.Body className="text-dark">
@ -67,6 +67,7 @@ export const HelpButton: React.FC = () => {
</Card.Text> </Card.Text>
</Card.Body> </Card.Body>
</Card> </Card>
<br/>
<Card> <Card>
<Card.Header><Trans i18nKey='editor.help.documents.title'/></Card.Header> <Card.Header><Trans i18nKey='editor.help.documents.title'/></Card.Header>
<Card.Body> <Card.Body>

View file

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

View file

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

View file

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

View file

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

View file

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