1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-14 10:16:26 +00:00

Compare commits

..

647 Commits

Author SHA1 Message Date
Junyoung Choi
1195c77f7a v0.14.0 2020-01-03 11:28:19 -05:00
Flexo013
c373c207c0 Added link to downloads 2019-12-29 07:21:13 +09:00
Junyoung Choi
65e83e7017 Merge pull request #2585 from daiyam/fix-mermaid-height
fix height of mermaid diagrams
2019-12-25 05:07:07 -05:00
Abner Soares Alves Junior
3c12e0d119 Fix emoji render on notes list 2019-12-24 06:54:20 +09:00
Abner Soares Alves Junior
60d6c68e48 Add some translations 2019-12-24 06:53:26 +09:00
Abner Soares Alves Junior
d8605965a8 Add Ok button to export confirmation box 2019-12-24 06:53:26 +09:00
Yuki Furukawa
6d455fc286 remove redundant conditions of macOS in main-menu 2019-12-24 06:39:18 +09:00
Mayke
2882667e94 rebuilding test snapshots 2019-12-24 06:36:53 +09:00
Mayke
7fa578880e refactoring to use the new .markdownIt-TOC-wrapper div 2019-12-24 06:36:53 +09:00
Mayke
3f465df1cd configuring a div wrapper for TOC plugin to use overflow-y in <ul> and still use &:before on parent element 2019-12-24 06:36:53 +09:00
Mayke
c423784cc5 adding TOC UI 2019-12-24 06:36:53 +09:00
Nicholas Browning
ce853a7e3a Export: CSS: adjusted relative path 2019-12-24 06:23:51 +09:00
Nicholas Browning
099ebad06b Export: uses markdown preview dom. Supports diagrams 2019-12-24 06:23:51 +09:00
Junyoung Choi
75a1347ae1 Update readme and contributing 2019-12-23 10:02:41 +09:00
Baptiste Augrain
b83d3e5c33 Merge branch 'master' into fix-mermaid-height 2019-12-22 23:46:47 +01:00
Junyoung Choi
1a7c719a4e Update new BoostNote info 2019-11-26 14:03:57 +09:00
Jack Hsieh
d78f6b7aba Ability to stop auto update 2019-11-22 05:42:55 +09:00
dependabot[bot]
7d4d176bf4 Bump js-yaml from 3.12.0 to 3.13.1
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.12.0 to 3.13.1.
- [Release notes](https://github.com/nodeca/js-yaml/releases)
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.12.0...3.13.1)

Signed-off-by: dependabot[bot] <support@github.com>
2019-11-22 03:57:10 +09:00
hikerpig
52ea44ceaa Remove overcomplicated solution for scrollbar styling. 2019-11-22 03:55:39 +09:00
Nicholas Browning
132d04326b TOC: Scrolling: Fixed TOC links and scrolling 2019-11-22 03:55:39 +09:00
nam
9996b5d686 bump mermaid version to support state diagrams 2019-11-22 03:47:42 +09:00
Rafael Gonzaga
e9d9f49ff3 minor changes
just to improve reading file
2019-11-22 03:47:03 +09:00
Nicholas Browning
95300546dc Delete Dialog: typo for the "type" property 2019-11-22 03:46:45 +09:00
Itai Braude
489fc6578b changed webpack config back to original 2019-11-22 03:46:22 +09:00
Itai Braude
edebba6680 added more admonition styles 2019-11-22 03:46:22 +09:00
dependabot[bot]
72c2a20a74 Bump lodash from 4.17.10 to 4.17.13
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.10 to 4.17.13.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.10...4.17.13)

Signed-off-by: dependabot[bot] <support@github.com>
2019-11-22 03:30:33 +09:00
PetrTodorov
abef6c5fee Added Czech translation (#3264)
* Added Czech translation

* Update locales/de.json

Fixed german translation as suggested.

Co-Authored-By: Simon <simon81186@aol.com>
2019-11-04 14:44:09 +09:00
Junyoung Choi
662ae73637 Upgrade electron to v4 2019-10-29 13:28:51 +09:00
roottool
3ef485548a Removed a single quotation 2019-10-28 18:16:59 +09:00
roottool
a90c10ef3e removed single quotation 2019-10-28 18:16:59 +09:00
roottool
d43fe8db75 #3147 fix: added script tag and stylesheet 2019-10-28 18:16:59 +09:00
AWolf81
1d84cac922 Rephrase error messages 2019-10-28 18:10:21 +09:00
AWolf81
5280b6ed63 Add error handling 2019-10-28 18:10:21 +09:00
hikerpig
77833ff980 Fix preview-window's scroll behavior, #3289 2019-10-28 18:09:29 +09:00
Junyoung Choi
d010c5532d v0.13.0 2019-10-16 20:18:47 +09:00
hikerpig
f2dc8b8020 optimize: npm 'test' script should contain jest tests 2019-10-15 13:24:44 +09:00
Junyoung Choi
1798353eac Merge pull request #3173 from hikerpig/feature/toc
Suppport auto generating toc content for the [TOC] tag
2019-10-15 13:24:30 +09:00
Kazumasa Yokomizo
772a8b2383 Updated the readme 2019-10-14 13:34:27 +09:00
hikerpig
5690c8361a 🔥 remove obsolete snap file 2019-10-11 10:41:38 +08:00
hikerpig
6d09cf227c Merge remote-tracking branch 'origin/master' into feature/toc
Fix conflicts
2019-10-10 18:23:39 +08:00
Junyoung Choi
8736666e91 Fix default prettier hotkey 2019-10-10 18:01:27 +09:00
Junyoung Choi
d1fd5cfb45 Set prettier external deps 2019-10-10 18:01:09 +09:00
hikerpig
3eabf95fb3 optimize implementation for a0c15182 2019-10-10 16:34:28 +09:00
hikerpig
8ea920ef91 fix: Can't open external browser in Markdown Preview with external link containing '#', close #3213 2019-10-10 16:34:28 +09:00
jhdcruz
3c0f20f364 Single Instance fix #3241
Fixes single instance depreciation
2019-10-10 16:33:21 +09:00
alwxkxk
59f8425c97 fix #2935 2019-10-10 16:29:55 +09:00
hikerpig
f181a7e459 more strict regex pattern in pathname matching, fix #3183 2019-10-10 16:29:27 +09:00
MSSandroid
6b1c595f87 add test for PlantUml Ditaa 2019-10-10 16:26:57 +09:00
MSSandroid
0003de8f08 remove code redundancy in parsing of PlantUml 2019-10-10 16:26:57 +09:00
MSSandroid
5357d8dc04 remove code redundancy in parsing of PlantUml 2019-10-10 16:26:57 +09:00
MSSandroid
d069722bf9 require(\'markdown-it-plantuml\') only once 2019-10-10 16:26:57 +09:00
MSSandroid
3f4dd49a8f added missing newline at end of document 2019-10-10 16:26:57 +09:00
Michael Schuldes
be06b3f7e8 Added plantUML Gantt support 2019-10-10 16:26:57 +09:00
Michael Schuldes
5044bdda00 Added plantUML wbs support 2019-10-10 16:26:57 +09:00
Michael Schuldes
fbeffb0b5d Added plantUML mindmap support 2019-10-10 16:26:57 +09:00
Olcod
ef0af39aa7 Added package-lock file to the gitignore 2019-10-10 16:13:45 +09:00
Olcod
0697bc0a74 Add ability to sort lines with a hot key combination 2019-10-10 16:13:45 +09:00
Aleksei Seletskiy
43d8ebb3c4 Dracula theme buttons in storage settings fix 2019-09-14 13:20:50 +09:00
Robert Weber
68175cd71b Add sidebar collapse button to sidebar while viewing the tags list
Fixes #2097
2019-09-14 13:20:37 +09:00
amedora
f4d87f64ae Fix #3190 - App blanks out after setting HotKey (#3193)
* fix lack of hotkey properties

* Update HotkeyTab.js
2019-09-03 02:24:09 +09:00
Junyoung Choi
68b3077651 Merge pull request #3099 from AWolf81/html-to-md
Html to md feature
2019-09-03 02:03:51 +09:00
Junyoung Choi
1332b337f3 Merge pull request #3136 from hikerpig/feature/scrollbarAppearance
tweak MarkdownPreview style to optimize overflow scrollbar display
2019-09-03 02:03:23 +09:00
hikerpig
e9975d1ea5 fix: HotkeyTab accidentally set incomplete hotkey, related #3190 2019-09-03 01:59:59 +09:00
Thamara Andrade
2c103aca3d Fix #888 - Wrong word count due splitting 2019-09-03 01:54:53 +09:00
Thamara Andrade
c0a5eb0d2b Fix #888 - Wrong word count due splitting 2019-09-03 01:54:53 +09:00
霸气千秋
ff7c4495f0 format locale files 2019-09-03 01:53:45 +09:00
minbaby.zhang
35fe639cd2 optimize translate 2019-09-03 01:53:45 +09:00
minbaby.zhang
59add8982e update lang 2019-09-03 01:53:45 +09:00
minbaby.zhang
8d9c514097 update lang 2019-09-03 01:53:45 +09:00
minbaby.zhang
6f880d0f02 更新翻译 2019-09-03 01:53:45 +09:00
AWolf81
ec47ee8110 Remove manual script tag filter and use turndown remove filter 2019-08-31 21:35:09 +02:00
Nguyễn Việt Hưng
28b8141c6b fixed test 2019-08-30 12:46:40 +09:00
Nguyễn Việt Hưng
5b0b309c49 added test for getAttachmentsPathAndStatus 2019-08-30 12:46:40 +09:00
Nguyễn Việt Hưng
0b84a372f6 re-organize attachment functions and updated comments doc 2019-08-30 12:46:40 +09:00
Nguyễn Việt Hưng
8355e1e006 updated function name and return type 2019-08-30 12:46:40 +09:00
Nguyễn Việt Hưng
c7d33fbd83 Allow user to view attachments and clear unused attachments 2019-08-30 12:46:40 +09:00
ehhc
cf324d93fe Add option to disable the automiatic deletion of un-referenced attachments -> might fix #3203 2019-08-30 12:46:40 +09:00
hikerpig
9a704a2bcb Merge branch 'master' into feature/scrollbarAppearance 2019-08-26 10:38:26 +08:00
hikerpig
1e00651541 feature/toc: upgrade "@hikerpig/markdown-it-toc-and-anchor" package to avoid default anchor lowercase casting 2019-08-25 18:25:17 +08:00
AWolf81
857e75594d Disable Javascript for printout window 2019-08-24 13:43:36 +09:00
AWolf81
2f1dadfc3e Change drag disable styles to be more specific 2019-08-24 13:43:14 +09:00
AWolf81
7d0404657e Fix routing for tag filtering 2019-08-24 13:42:09 +09:00
Junyoung Choi
b9dd651fc1 Merge pull request #3093 from nathan-castlehow/feat-run-prettier-on-markdown
Feat run prettier on markdown
2019-08-24 13:41:41 +09:00
hikerpig
25ef456af2 feat: should scroll to top after selecting another note, also fix #3023 2019-08-24 13:39:28 +09:00
hikerpig
084decaa85 improvement: MarkdownPreview, rewriteIframe attempt can be combined to one call 2019-08-24 13:39:28 +09:00
hikerpig
330a444fc5 optimize: should highlight any non-empty search query, fix #3164 2019-08-24 13:38:28 +09:00
alwxkxk
a47dac2854 fix #3159 2019-08-24 13:38:09 +09:00
Ronald Walker
08070f3e2d fix #3144 2019-08-24 13:37:43 +09:00
hikerpig
2352c78cb6 Add CodeEditor::setLineContent method to manipulate line contents, related #3163
Avoid changing all CodeMirror instance's contents
2019-08-24 13:36:56 +09:00
Antonio Cangiano
6ef9c3865f Fix JavaScript hello world example
The current snippet note example references a non-existent element with id `enjoy`. I updated it to reference the correct id (i.e., `hello`).
2019-08-24 13:36:29 +09:00
sirrah23
ff9789b5a7 Fix 3060
Right now there are only two export types that are using a special
output formatter, pdf and html. Both of these formatters currently populate the
`/html/head/base` portion of the associated html document with the name
of the target directory for the file that the user is exporting.

In order for internal links within the exported document to work
correctly, the value of base must also include the filename. This fix
removes the call to `path.dirname`, which gets rid of the necessary
filename.
2019-08-24 13:36:05 +09:00
Jack Hsieh
f09297f406 Fix 2636 (#3206)
* Fix 2636 Can't scroll to bottom of editor pane

* Fix minor lint issues
2019-08-11 23:22:53 +09:00
nathan-castlehow
2d3c69d178 Fixed eslint issue 2019-08-01 20:13:46 +08:00
nathan-castlehow
b837653cf1 Merged Master into feature branch and fixed conflicts 2019-08-01 20:12:58 +08:00
nathan-castlehow
eeca031c86 Merge upstream into master 2019-08-01 19:56:38 +08:00
nathan-castlehow
918a8627e9 Merge upstream into master 2019-08-01 19:55:21 +08:00
nathan-castlehow
86370edd1e Merge branch 'feat-run-prettier-on-markdown' of https://github.com/nathan-castlehow/Boostnote into feat-run-prettier-on-markdown 2019-08-01 18:36:40 +08:00
nathan-castlehow
1173631255 feat(prettierOnMarkdown): Forced prettier options to always have parser set to markdown when used. 2019-08-01 18:36:22 +08:00
nathan-castlehow
911fd9a004 feat(prettierOnMarkdown): Changed Prettier require to use import 2019-08-01 18:36:21 +08:00
nathan-castlehow
0ad3da5bbc feat(prettierOnMarkdown): Changed default hotkey value 2019-08-01 18:36:21 +08:00
nathan-castlehow
89ae2a9516 feat(prettierOnMarkdown):Fixed incorrect options passed to code mirror instance 2019-08-01 18:36:20 +08:00
nathan-castlehow
70892cae05 feat(prettierOnMarkdown):Tweaked spacing on default Prettier Config Value 2019-08-01 18:36:19 +08:00
nathan-castlehow
de0af153bc feat(prettierOnMarkdown):Added prettier config default to config manager 2019-08-01 18:36:19 +08:00
nathan-castlehow
33161e46e6 feat(prettierOnMarkdown): Added support for prettyifing markdown as well as added hot key option. Partial Implementation of Prettier config in configuration screen. TODO Fix defaulting of prettier configuration 2019-08-01 18:36:18 +08:00
nathan-castlehow
7e3c662374 feat(prettierOnMarkdown): Added Reference to prettier in Code Editor and created config file 2019-08-01 18:36:17 +08:00
nathan-castlehow
a39e9c2da6 feat(prettierOnMarkdown): Added Reference To Prettier 2019-08-01 18:36:16 +08:00
AWolf81
72b8d56245 Merge remote-tracking branch 'upstream/master' into html-to-md 2019-07-28 15:49:16 +02:00
AWolf81
0d36f59036 Create turndown service & use gfm turndown plugin 2019-07-28 15:02:17 +02:00
AWolf81
a3f7d2287a Add dracula theme styles 2019-07-28 11:00:40 +02:00
hikerpig
8edfbd28ed feat: suppport auto generating toc content for the '[TOC]' placeholder, related #3022 2019-07-27 16:55:32 +08:00
amedora
606be4304d Fix 3007 (#3028)
* fix code fences never sanitized

* fix mermaid xss

* Revert "fix mermaid xss"

This reverts commit 1ff179a1bd.

* configuable mermaid HTML label

* add locales for mermaid configuration
2019-07-27 12:39:12 +09:00
Junyoung Choi
329066719e 0.12.1 2019-07-27 11:51:40 +09:00
Junyoung Choi
93b8ef35f7 0.12.1-1 2019-07-26 13:50:44 +09:00
Junyoung Choi
484b003b34 Fix theme paths 2019-07-26 13:49:40 +09:00
Junyoung Choi
ed427130a9 0.12.1-0 2019-07-26 11:51:51 +09:00
Junyoung Choi
807feae540 Merge pull request #3165 from BoostIO/discard-warnings
Discard warnings
2019-07-26 11:45:29 +09:00
Junyoung Choi
4b62e93257 Fix more style warnings 2019-07-26 11:38:05 +09:00
Junyoung Choi
e425417d68 Load bfm mode info script only once 2019-07-26 11:33:05 +09:00
Junyoung Choi
bc1e837466 Set alias to stylus mode inof 2019-07-26 11:32:41 +09:00
Junyoung Choi
95321e33a0 Fix style of FolderItem 2019-07-26 11:32:07 +09:00
Junyoung Choi
5720b313a3 Merge branch 'master' into discard-warnings 2019-07-26 09:12:18 +09:00
hikerpig
f3e2205e69 fix several propType errors raised by 'react.development.js'
some are caused by typo, some are caused by unused propType declarations
2019-07-26 09:11:00 +09:00
Junyoung Choi
410b611b14 Discard all style warnings 2019-07-26 09:03:32 +09:00
Junyoung Choi
1c8af47bac Fix warnings in ToggleModeButton 2019-07-26 08:50:12 +09:00
Junyoung Choi
c8a2baca3c Use default value prop rather than value prop 2019-07-26 08:49:52 +09:00
Junyoung Choi
ac5a323115 Discard unused props 2019-07-26 08:49:21 +09:00
Junyoung Choi
fa65e7feef Fix isTagActive 2019-07-26 08:49:07 +09:00
amedora
71d27d0e55 MarkdownEditor and MarkdownSplitEditor always wrap lines 2019-07-26 07:50:53 +09:00
hikerpig
972d053c83 fix: array access error when token.map is null, fix #3123 2019-07-26 07:49:59 +09:00
Matt Gabriel
7797661489 Fixed permissions 2019-07-26 07:40:18 +09:00
Junyoung Choi
9f9e036c68 0.12.0 2019-07-24 16:57:04 +09:00
Tobias Doll
e940253caf Using single quotes for empty string 2019-07-22 16:43:21 +09:00
Tobias Doll
2b4d20b94e Dont highlight search term if search field is emptied 2019-07-22 16:43:21 +09:00
nathan-castlehow
f88fc23e58 Removed Paste / Cut options from preview context menu as they are not relevant in preview mode 2019-07-22 16:42:57 +09:00
nathan-castlehow
caf1f92fef Removed SetTimeout on Markdown Preview Context menu 2019-07-22 16:42:57 +09:00
nathan-castlehow
f2a02a25a7 Fixed Test Fail Issue due to incorrect Require for context menu items 2019-07-22 16:42:57 +09:00
nathan-castlehow
e85767b4a0 feat: Added Context Menu for markdown preview mode and copy url when hyperlink 2019-07-22 16:42:57 +09:00
amedora
c83e5cc7d8 add locales 2019-07-22 16:42:08 +09:00
amedora
71f565f66b fix UI change handler 2019-07-22 16:42:08 +09:00
amedora
769407b3df add "Wrap Line" button 2019-07-22 16:42:08 +09:00
amedora
e7615ed6d7 add "Wrap Line" button handlers 2019-07-22 16:42:08 +09:00
amedora
fe508307b2 make lineWrapping configurable 2019-07-22 16:42:08 +09:00
hikerpig
c2a26a8547 improvement: refactor buildStyle to NamedParameters style, and add some jsdoc 2019-07-21 15:28:12 +08:00
hikerpig
addf9b920f tweak MarkdownPreview style to optimize overflow scrollbar display, fix #2902 2019-07-21 15:28:12 +08:00
Baptiste Augrain
4e30d4b8fb fix URLs by using the correct path separator ('/' for an url and not path.sep) 2019-07-20 00:58:51 +09:00
Baptiste Augrain
850561613b fix gallery on windows 2019-07-20 00:58:51 +09:00
AWolf81
782d71ddb0 path fix for MacOs 2019-07-17 17:25:13 +09:00
AWolf81
a0799d19f8 fix app path 2019-07-17 17:25:13 +09:00
AWolf81
ba6eb4f26f fix path Linux 2019-07-17 17:25:13 +09:00
AWolf81
38fcee35c2 fix test for Windows (fs.rmdir throws dir not empty error) 2019-07-17 17:25:13 +09:00
AWolf81
4af7106e01 change theme path usage and remove relative path 2019-07-17 17:25:13 +09:00
AWolf81
bd52226ae2 change process.env check 2019-07-17 17:25:13 +09:00
ehhc
cb7ac77c61 Update attachmentManagement.js
Deleting unneeded stupid log-message
2019-07-10 11:42:00 +09:00
ehhc
55a7ee1f91 Debounce deletion of un-referenced attachments --> don't fixes but mitigates the problems of #3103 2019-07-10 11:42:00 +09:00
amorist
d37210a0d0 fix chinese language new note tab style 2019-07-08 13:14:56 +09:00
amedora
0c7a1e8f17 apply theme color to slider in MarkdownSplitEditor 2019-07-08 13:14:30 +09:00
Kazumi Harada
3d7ab40674 [update] apply the method of fixLocalURLS before exporting to pdf 2019-07-08 13:14:10 +09:00
Kazumi Harada
b8de51be57 [update] adjust code style 2019-07-08 13:13:52 +09:00
Kazumi Harada
1ce72b91ca [update] by force, insert the default value to the customCSS config 2019-07-08 13:13:52 +09:00
Kazumi Harada
3f96587a70 [update] move default value of the customCSS field to ConfigManager 2019-07-08 13:13:52 +09:00
Huachao Mao
c012bbd54a Update lock/unlock icons in editor #1760 2019-07-08 13:13:30 +09:00
David Thomason
a50852306e Loading insert day hotkey triggers from config.hotkey 2019-07-08 13:13:06 +09:00
David Thomason
db396ec107 Added insert date & time info to HotkeyTab settings 2019-07-08 13:13:06 +09:00
David Thomason
6b868658aa Changed "insert date & time" hotkey on mac from Shift-Cmd-/ to Alt-Cmd-/ 2019-07-08 13:13:06 +09:00
JianXu
9fe9e1a1c4 Add menu item "About" to "Help" 2019-07-08 13:12:22 +09:00
AWolf81
aeb77e5a40 Remove package-lock file & use startsWith for https check 2019-07-08 00:05:26 +02:00
nathan-castlehow
1d59d89588 feat(prettierOnMarkdown): Forced prettier options to always have parser set to markdown when used. 2019-07-03 09:28:36 +08:00
nathan-castlehow
bde357f952 feat(prettierOnMarkdown): Changed Prettier require to use import 2019-07-03 09:03:24 +08:00
AWolf81
558c091205 fix linting 2019-06-30 00:18:52 +02:00
AWolf81
f67175e628 fix test 2019-06-30 00:03:54 +02:00
AWolf81
390f6d545f fix PropTypes 2019-06-30 00:03:25 +02:00
AWolf81
44efb0178c Merge branch 'master' into html-to-md
# Conflicts:
#	browser/main/modals/NewNoteModal.js
#	package-lock.json
#	package.json
#	yarn.lock
2019-06-29 23:25:52 +02:00
AWolf81
37eee26bdf fix linting & routing 2019-06-29 23:21:32 +02:00
Junyoung Choi
b4251a793b Merge pull request #3037 from AWolf81/fix-search-issue-unicode
Simplified search input & fixed chinese character input
2019-06-29 00:41:24 +09:00
roottool
6736a08240 Fixes that TOC hasn't id attribute when the title is all 2-byte characters (#2994)
* Fix: 2-byte character support of slug

* Fix: Decodes slug to display slug

* Fix: Removed a logic of replaceDiacritics

* Fix: Fixed slugify to pass tests

* Fix: Fixed not to remove underscore

* Adds the test for slugify.js

* Fix: Fix to jump to heading

* Added a comment

* Fix: Created click event only linking to heading

* Fix: Fix to use handleLinkClick(e)

* Fix: Changed the regex rule

* Fix: Changed the regex rule of extractId
2019-06-29 00:40:16 +09:00
nathan-castlehow
ed4a670f0a feat(prettierOnMarkdown): Changed default hotkey value 2019-06-23 13:54:17 +08:00
nathan-castlehow
fbb9afe34f feat(prettierOnMarkdown):Fixed incorrect options passed to code mirror instance 2019-06-23 13:42:14 +08:00
nathan-castlehow
020bc11bd7 feat(prettierOnMarkdown):Tweaked spacing on default Prettier Config Value 2019-06-23 13:41:52 +08:00
nathan-castlehow
ae0837e29b feat(prettierOnMarkdown):Added prettier config default to config manager 2019-06-23 13:41:34 +08:00
nathan-castlehow
f0380ef733 feat(prettierOnMarkdown): Added support for prettyifing markdown as well as added hot key option. Partial Implementation of Prettier config in configuration screen. TODO Fix defaulting of prettier configuration 2019-06-23 13:40:20 +08:00
nathan-castlehow
25bdaf9f00 feat(prettierOnMarkdown): Added Reference to prettier in Code Editor and created config file 2019-06-23 13:34:47 +08:00
nathan-castlehow
ef1809305c feat(prettierOnMarkdown): Added Reference To Prettier 2019-06-23 13:34:47 +08:00
nathan-castlehow
090b5c32f0 feat: Added Context Menu for markdown preview mode and copy url when hyperlink 2019-06-09 13:28:53 +08:00
KZ
49c75e3599 Merge pull request #3059 from BoostIO/update-funding-yml
Update FUNDING.yml
2019-06-06 10:44:34 +09:00
KZ
05765642d9 Update FUNDING.yml 2019-06-06 10:39:09 +09:00
amedora
f62eba9d7b Fix #2922 (#2998)
* update katex to 0.10.1

* Update snapshot for markdown-test
2019-06-03 13:57:16 +09:00
amedora
bc27fd0acc disable fuzzy link 2019-06-03 13:54:40 +09:00
Ryota Kusano
244a28c7d2 Adjust continuous quotation pattern 2019-05-29 22:55:05 +09:00
Ryota Kusano
edac4d3fed Fix to remove unnecessary escape charactor erorr on IDE 2019-05-29 22:55:05 +09:00
Ryota Kusano
e402929cca Fix regular expression related checkbox logic 2019-05-29 22:55:05 +09:00
AWolf81
639bfbe549 apply search term on enter key 2019-05-29 09:02:34 +02:00
AWolf81
60e841e5a2 fix focus loss by checking switchPrieview prop change 2019-05-29 07:52:22 +02:00
AWolf81
25d055e560 Merge branch 'master' into fix-search-issue-unicode
# Conflicts:
#	browser/main/TopBar/index.js
#	yarn.lock
2019-05-29 07:42:20 +02:00
Morten Lautrup
052fb3df5b Specify wich images should not be draggable
Make only images and spans inside buttons undraggable
2019-05-29 13:02:37 +09:00
Morten Lautrup
929f475354 Make buttons undraggable 2019-05-29 13:02:37 +09:00
AWolf81
1afa02bbb3 fix local link detection by creating a link tag to parse the input string 2019-05-29 12:20:49 +09:00
AWolf81
3c39dc3cec change redirecting to connected-react-router 2019-05-29 12:20:49 +09:00
AWolf81
7e8f46c4f3 remove commented imports 2019-05-29 12:20:49 +09:00
AWolf81
333b0584a4 address review comments - add production/dev main.html & remove comments 2019-05-29 12:20:49 +09:00
AWolf81
f7a648903e fix auto-scroll 2019-05-29 12:20:49 +09:00
AWolf81
6ec687ef15 add React & Redux devtools 2019-05-29 12:20:49 +09:00
AWolf81
b6212f4bfe Update dependencies & change to React-router v5 2019-05-29 12:20:49 +09:00
AWolf81
9794149fae Merge branch 'master' into fix-search-issue-unicode 2019-05-28 20:18:13 +02:00
AWolf81
edc9d8bd4d use composition input 2019-05-28 20:16:02 +02:00
Junyoung Choi
76335f78ac v0.11.17 2019-05-26 18:49:44 +09:00
Junyoung Choi
56192f0758 Merge pull request #3034 from AWolf81/enable-markdownlint-option
Enable Markdown lint option
2019-05-26 18:38:30 +09:00
Junyoung Choi
63eb8584e7 Hide markdown lint settings when markdown lint is disabled 2019-05-26 18:32:24 +09:00
Zubata SMRTKA
aa4d06fb1e formatting 2019-05-26 18:22:04 +09:00
AWolf81
c70cca2776 remove console.log 2019-05-26 10:07:43 +02:00
AWolf81
1a38771f1a remove console.logs & improve error handling 2019-05-26 09:57:40 +02:00
AWolf81
25728e51d2 simplified search input & fixed focus loss issue 2019-05-25 21:01:51 +02:00
AWolf81
02576c48b6 move enableMarkdownLint checkbox to customMarkdownLintConfig area (like at allow custom css) 2019-05-25 07:31:21 +02:00
AWolf81
4263309d89 Merge branch 'add-markdownlint-rules-form' of github.com:roottool/Boostnote into enable-markdownlint-option 2019-05-25 07:21:26 +02:00
AWolf81
b68b367b3c Toggle linting gutter with document.querySelector and style.display 2019-05-25 07:16:03 +02:00
tool root
2cffb50884 Fix: Changed height of form 2019-05-25 08:16:25 +09:00
tool root
3b473a5f7a Fix: Changed the function name 2019-05-25 08:04:40 +09:00
tool root
8b82c448af Fix: Changed the function name 2019-05-25 07:58:43 +09:00
AWolf81
270a015514 WIP: Add MarkdownLint enable setting. Gutter toggle not working as expected. 2019-05-24 19:06:15 +02:00
Junyoung Choi
f02125e411 Update FUNDING.yml 2019-05-24 10:10:29 +09:00
KZ
b0f2694745 Create FUNDING.yml 2019-05-24 10:09:49 +09:00
roottool
2497bdb124 Fix: Removed changes to debug the app 2019-05-22 22:45:09 +09:00
Junyoung Choi
d5d564f789 v0.11.16 2019-05-22 15:20:34 +09:00
Junyoung Choi
49e48f7adc Update yarn.lock 2019-05-22 12:23:28 +09:00
Darío Hereñú
079aaec21e Fixed typo on string 137
* plus some translations added
2019-05-13 14:50:53 +09:00
AWolf81
1601292db7 add react hooks section 2019-05-13 14:05:57 +09:00
roottool
c82dbddc74 Fix markdownlint result desplay works properly 2019-05-12 13:13:32 +09:00
David Dreher
9a6ee9d2ef change return handling of sortByAlphabetical 2019-05-09 15:45:54 +09:00
David Dreher
2cbbe7aeda Check if the float is quals (abs value is greather 0.01) and return the sub value when not. 2019-05-09 15:45:54 +09:00
David Dreher
19fc1fd674 rename const 2019-05-09 15:45:54 +09:00
David Dreher
5b63bedc0d fix issue #2894: sort alphabetical will now parse float values starting at all titles and compare these. 2019-05-09 15:45:54 +09:00
amedora
12229a1719 allow to expand snippets following $ character 2019-05-09 15:41:33 +09:00
roottool
0d6c952721 Added a newline 2019-05-09 05:30:46 +09:00
roottool
c42eb41fb3 Fix: Fixed that default rule was shifted by indent 2019-05-09 05:29:43 +09:00
roottool
2da4f1df32 Fix: Rewrote the code to inline 2019-05-09 04:11:05 +09:00
roottool
69a62d1b73 Fix: Changed variable name 2019-05-09 04:06:27 +09:00
roottool
61e054024b Fix: Removed unnecessary css code 2019-05-07 04:44:23 +09:00
roottool
0a5c4c092a Fix: Improved for the app not to need to reload 2019-05-07 04:22:03 +09:00
roottool
f1597f8e84 Fix: Poped up the lint tooptip 2019-05-07 03:26:47 +09:00
roottool
f3ca893aea Ajusted markdownlint config editor to code editor 2019-05-07 02:30:41 +09:00
roottool
ecfeedeff3 Fix: Removed unnecessary caution 2019-05-07 00:44:27 +09:00
roottool
a162bab591 Fix: Improved for the app not to need to reload 2019-05-07 00:42:55 +09:00
roottool
79a29c3461 Merge branch 'master' into add-markdownlint-rules-form 2019-05-06 20:53:59 +09:00
Junyoung Choi
377901606e Merge pull request #2846 from roottool/IntroduceMarkdownLint#864
Introduce markdownlint #864
2019-05-06 18:31:01 +09:00
Nguyễn Việt Hưng
8cd24a5734 fixed open empty link 2 2019-05-06 18:30:20 +09:00
Baptiste Augrain
ba913b77e7 use constants and fix cursor color 2019-05-06 18:28:57 +09:00
Baptiste Augrain
6012fc929e add Nord theme to CodeMirror 2019-05-06 18:28:57 +09:00
roottool
33d1700548 Change styleName of caution 2019-04-29 10:01:55 +09:00
roottool
8b3beb45c6 Added a caution of the custom markdownlint rules 2019-04-29 09:17:25 +09:00
roottool
4ba4e68833 Added markdownlint rules form 2019-04-29 08:54:28 +09:00
roottool
11bed72bed Introduced jsonlint-mod 2019-04-29 08:49:19 +09:00
roottool
7fb22f3f07 Translated from English to Japanese 2019-04-29 03:24:16 +09:00
roottool
788900e31a Added markdownlint rules form 2019-04-29 03:23:25 +09:00
roottool
03b1adca12 Merge master branch into this branch 2019-04-29 01:57:16 +09:00
Charles Wang
aecf2eb08d Bug fix check in. 2019-04-28 12:31:24 -04:00
Christian Dimas
895aee3e6c add default value to custom CSS editor 2019-04-28 12:30:38 -04:00
David Dreher
c667e1465d After select an other note now scroll to top 2019-04-28 12:30:13 -04:00
David Dreher
3c9d614e1f Change the save botton at the about tab to the same style as the 'See IssueHunt' button at the crowdfunding tab 2019-04-28 12:29:33 -04:00
David Dreher
971e0cb61e change missing font color for the themes at the preferences headlines 2019-04-28 12:29:33 -04:00
David Dreher
67d78f6603 Replace the width calc with a fixed value 2019-04-28 12:29:33 -04:00
David Dreher
ed0b370d49 Change paragraph headlines at the crowdfunding tab 2019-04-28 12:29:33 -04:00
David Dreher
e5b8f4d372 Use same styles for all preferences tabs 2019-04-28 12:29:33 -04:00
Junyoung Choi
2999e490ad Merge pull request #2509 from lvarado/mojave-dark-mode
enable dark mode support for macOS mojave
2019-04-28 12:26:28 -04:00
Nguyễn Việt Hưng
156a2c6521 added check empty for safety reason 2019-04-23 22:58:38 -04:00
Nguyễn Việt Hưng
48bb9d3242 fixed empty link point to main file 2019-04-23 22:58:38 -04:00
steve-o
b2ee4f0c66 update electron-packager to 12.2.0 2019-04-22 10:02:19 -05:00
steve-o
9b68f34a31 remove hashes from lockfile (except those present in master branch) 2019-04-22 10:00:46 -05:00
alvarado
f8944eb8b3 Merge branch 'master' into mojave-dark-mode 2019-04-22 09:45:16 -05:00
David Dreher
3634194e4d Fix for #2836, Initialize storage ignores now tags from skipped notes 2019-04-15 10:57:20 +09:00
Abner Soares Alves Junior
038b87cf70 Fix test moving from Ava to JEST 2019-04-15 10:56:35 +09:00
Abner Soares Alves Junior
5ca6bbea11 Fix some linter issues 2019-04-15 10:56:35 +09:00
Abner Soares Alves Junior
7b9b4d56a7 Fix the position of title mark when replacing 2019-04-15 10:56:35 +09:00
Abner Soares Alves Junior
d81e69bf00 Add parse and fetch pasted markdown titles with url 2019-04-15 10:56:35 +09:00
linxiuda
5e134f990e fix newNoteModal create note twice after double click quickly 2019-04-15 10:55:26 +09:00
David Dreher
1675e04f90 Fix for #2782: CodeEditor.handleChange will now update the toc if available when a headline was modified 2019-04-15 10:54:45 +09:00
Todt
0a72acd899 Modified style 2019-04-15 10:44:16 +09:00
amedora
aa6c9680da Increase arrow size on sidebar 2019-04-15 10:43:52 +09:00
milotodt
d41663143b Repush due to pipeline issues 2019-04-15 10:43:10 +09:00
milotodt
9db363865c Slight refactor 2019-04-15 10:43:10 +09:00
milotodt
9b03a32eec removed uneeded line of code 2019-04-15 10:43:10 +09:00
milotodt
eeec1b12b5 adjusted handleAttachmentDrop 2019-04-15 10:43:10 +09:00
Milo Todt
e655c2078b Update MarkdownPreview.js 2019-04-12 18:02:23 +09:00
Milo Todt
3426191e59 Update MarkdownPreview.js 2019-04-12 18:02:23 +09:00
Todt
b1c77ae59c Changes and refactor of HandleMouseDown 2019-04-12 18:02:23 +09:00
Jamie Luckett
d17ff4afba Update productDescription
Fixed grammatical error
2019-04-12 18:01:40 +09:00
Garfield Lee
0131bbbbed Update all locales copyright year (#2881)
* Update all locales copyright year

* Fix js-sequence-diagrams
2019-04-12 18:01:05 +09:00
Boris Pruessmann
7f55fc4b56 Removed erroneous parameter 2019-04-12 14:33:38 +09:00
amedora
aea9673b78 Add "Ctrl + /" and "Shift + Ctrl + /" to insert today's date and time (#1765) 2019-04-12 14:32:09 +09:00
anasasilva
89e9a84ea6 fixing package.json 2019-04-08 21:10:42 +09:00
anasasilva
fba9afd6f5 Shorter code 2019-04-08 21:10:42 +09:00
anasasilva
c474d972cb Supports relative and absolute path 2019-04-08 21:10:42 +09:00
anasasilva
3d6f670e8d regex fixed again 2019-04-08 21:10:42 +09:00
anasasilva
9d47f319a0 absolute path 2019-04-08 21:10:42 +09:00
anasasilva
7106f042da Regex fixed 2019-04-08 21:10:42 +09:00
anasasilva
ea6e56842f Error message when the image path is wrong 2019-04-08 21:10:42 +09:00
anasasilva
65926fea73 fixing identation and regex 2019-04-08 21:10:42 +09:00
anasasilva
85cb94d99d importAttachments 2019-04-08 21:10:42 +09:00
Ryo Shibayama
1be208d96b Fix locale json format, add some translations 2019-04-02 14:09:39 +09:00
Max Schmitt
22d494d3f1 README: removed mobile app mention (#2828) 2019-03-25 11:10:09 +09:00
Nguyen Viet Hung
5b99132f66 Adjust find function
Co-Authored-By: dredav <dredav@users.noreply.github.com>
2019-03-25 11:06:13 +09:00
David Dreher
91d04b99d1 change filter function to find, find will match the first note with requested key 2019-03-25 11:06:13 +09:00
David Dreher
70e57cf738 jumpNoteByHashHandler will now try to find the location for the note that will be selected 2019-03-25 11:06:13 +09:00
Ryo Shibayama
2f00cec52b Upgrade Travis CI node and npm versions 2019-03-25 11:04:05 +09:00
Junyoung Choi
fee966996f Fix wrong binding (#2940) 2019-03-22 15:33:29 +09:00
Nguyen Viet Hung
bff081a263 fixed copy button (#2914) 2019-03-21 01:27:51 +09:00
Nguyen Viet Hung
b5b56f7af1 Refactor code editor by moving the expand snippet out to a separate file (#2864)
* refactored CodeEditor by moving Snippet out

* fixed typo
2019-03-21 01:26:53 +09:00
Junyoung Choi
3f7f0e677d Merge pull request #2819 from ZeroX-DG/feature/edit-from-preview
[Feature] Edit from preview
2019-03-21 01:23:11 +09:00
Junyoung Choi
2b29d96d61 Merge pull request #2678 from AgentEpsilon/export-pdf
Export a Markdown note as PDF
2019-03-21 01:20:12 +09:00
Junyoung Choi
9f13645127 Merge pull request #2545 from Danmou/patch-1
Add ~ and _ to autoclosing brackets
2019-03-21 01:19:25 +09:00
Junyoung Choi
bbbbe9a121 Merge pull request #2493 from opw0011/zh-locale
Add missing Chinese translation in preference page
2019-03-21 01:18:36 +09:00
Junyoung Choi
46ecf0af88 Merge pull request #2868 from dredav/issue-2859
[Fix] Cloning a note will now clone more note properties
2019-03-21 01:18:09 +09:00
Evan Miller
2a0906d88e Rehid printout BrowserWindow 2019-03-18 10:02:20 -04:00
Daniel Mouritzen
116244384e Add ~ and _ to autoclosing brackets 2019-03-04 08:37:29 +01:00
Daniel Mouritzen
29775040d1 Merge branch 'master' into patch-1 2019-03-04 08:33:29 +01:00
Evan Miller
177888b159 disable webSecurity to render files to pdf 2019-02-11 11:15:08 -05:00
Evan Miller
bc24acd057 add targetDir parameter to outputFormatter 2019-02-11 11:15:08 -05:00
Evan Miller
4d727b0af7 destroy window after printing 2019-02-11 11:15:08 -05:00
Junyoung Choi
78c00b1722 v0.11.15 2019-02-08 20:33:20 +09:00
Junyoung Choi
2ff655d2dc Check update if the app is packaged 2019-02-08 19:30:46 +09:00
Junyoung Choi
e53717cd87 v0.11.14 2019-02-08 16:46:29 +09:00
David Dreher
d144a5884a fix for issue #2859: Cloning a note will now also copy the properties description, snippets, tags and isStarred 2019-02-06 22:11:35 +01:00
Junyoung Choi
8b8d915ab7 Merge pull request #2642 from daiyam/drop-browser-image
handle all dropped images
2019-02-05 17:55:18 +09:00
Junyoung Choi
54de57ee7b Merge pull request #2863 from zzdjk6/master
FIX #2853 Allow "#" in title
2019-02-05 11:25:21 +09:00
Baptiste Augrain
e0d9cf7f5c fix dropping image from Firefox on Linux 2019-02-04 18:22:10 +01:00
Baptiste Augrain
10ea5d00eb avoid converting SVG 2019-02-04 14:09:11 +01:00
Baptiste Augrain
de76f55fe2 fix gif 2019-02-04 13:54:45 +01:00
Baptiste Augrain
cd301d514c Merge branch 'master' into drop-browser-image 2019-02-04 13:36:32 +01:00
Shenghan Chen
0f232b3d86 FIX #2853 Allow "#" in title
- Only strip the leading # in the title
- Make the finding title logic more straightforward
- Add unit test
2019-02-04 20:07:33 +13:00
Junyoung Choi
53ff693e95 Merge pull request #2852 from Aaron-Bird/bug-gif
[Fix] GIFs don't animate
2019-02-04 12:25:28 +09:00
roottool
c15cc2ecfc added to check Markdown Note is opening 2019-02-02 14:59:04 +09:00
roottool
59b441e524 removed markdown mode 2019-02-02 03:07:59 +09:00
Baptiste Augrain
215484c19a add tests 2019-02-01 16:27:47 +01:00
Junyoung Choi
885f656d34 Merge pull request #2856 from K-Sato1995/fix/grammer-error#2844
[Fix  #2844] Grammar Error In Notes
2019-02-01 16:44:12 +09:00
Baptiste Augrain
851d3ba159 fix image path when the dropped image's url contains a query 2019-01-31 23:26:31 +01:00
Katsuki
62ab444b29 Fix grammer error 2019-01-31 19:33:43 +09:00
roottool
b84ebfa7b3 Merge branch 'master' into IntroduceMarkdownLint#864 2019-01-31 12:44:07 +09:00
Junyoung Choi
f1b929c13b Merge pull request #2848 from daiyam/fix-gallery
fix image path due to bad regex
2019-01-31 10:58:20 +09:00
Aaron-Bird
806139091c fix: GIFs don't animate 2019-01-31 01:08:07 +08:00
Junyoung Choi
6960c8b2d6 Merge pull request #2835 from ehhc/fix_broken_attachments
Strange url-handling reverted + tests modified so that they might fin…
2019-01-30 10:32:20 +09:00
Baptiste Augrain
b1c6c0442f fix guardrails errors 2019-01-29 16:49:51 +01:00
Baptiste Augrain
a85a27f225 - fix bad regex
- improve test
- fix missing 'e' in some functions name
2019-01-29 16:05:30 +01:00
roottool
d5629651d1 modified from require to import 2019-01-29 21:26:57 +09:00
roottool
afbc8eec75 changed variable name 2019-01-29 21:00:47 +09:00
roottool
3a96164c27 display lint result on CodeEditor #864 2019-01-29 20:42:24 +09:00
roottool
e393b5a2ea Add markdownlint #864 2019-01-29 20:41:29 +09:00
Junyoung Choi
950d31ada8 Merge pull request #2809 from roottool/zoom-in-and-out-image#1448
Zoom in and out image #1448
2019-01-28 11:39:14 +09:00
Junyoung Choi
543c31cec6 Merge pull request #2831 from roottool/change_default_editor_font#1995
Change default editor font #1995
2019-01-28 10:02:19 +09:00
Benny O
5920b3515e Merge branch 'master' into zh-locale 2019-01-27 23:04:27 +08:00
roottool
5e87ec2627 Specified MarkdownPreview class 2019-01-27 22:26:31 +09:00
ehhc
7165c4550b possibly fix for the broken test... 2019-01-26 18:16:39 +01:00
ehhc
472496d59c possibly fix for the broken test... 2019-01-26 17:53:31 +01:00
ehhc
127da40256 possibly fix for the broken test... 2019-01-26 17:37:04 +01:00
ehhc
a113b99de0 Strange url-handling reverted + tests modified so that they might find that issue in the future + test modified so that they both contain tests for posix and windows path separator -> fixes #2834 2019-01-26 17:21:53 +01:00
Junyoung Choi
74535c9cba Update yarn.lock 2019-01-26 12:30:22 +09:00
Junyoung Choi
79254a562f Update contributors 2019-01-26 12:29:20 +09:00
Junyoung Choi
4b1469748b v0.11.13 2019-01-26 12:25:05 +09:00
roottool
1683d63f33 Modified indent 2019-01-26 01:03:37 +09:00
roottool
4cce52f9ce changed default editor font #1995 2019-01-25 23:17:05 +09:00
roottool
33fb03066e Reduced the count of calculation 2019-01-25 20:06:43 +09:00
Junyoung Choi
a1deb15db8 Merge pull request #2651 from ZeroX-DG/fix-delete-note
Fixed delete note not navigating to the next note
2019-01-23 14:58:50 +09:00
Junyoung Choi
96ab8ec958 Merge pull request #2661 from miguelalexbt/bugfix-2121
Fixed lock button behavior and display
2019-01-23 14:50:08 +09:00
Junyoung Choi
c0f68dce25 Merge pull request #2798 from MiloTodt/disable-updates-in-dev
Disable updates in dev
2019-01-23 13:03:14 +09:00
Junyoung Choi
2b6c38083c Merge pull request #2774 from gurungrahul2/2194
#2194 - fixes Hotkey not working
2019-01-23 13:01:45 +09:00
Milo Todt
667ece7d3f Update main-app.js 2019-01-22 19:59:27 -08:00
Nguyễn Việt Hưng
5d38937f34 scroll selected line to middle of the editor 2019-01-23 00:20:50 +07:00
Junyoung Choi
9270e59508 Merge pull request #2786 from Foo-x/2232
Fix bullet position of LaTeX equation in list
2019-01-22 18:09:09 +09:00
Nguyen Viet Hung
b32865488e fixed app body background (#2791) 2019-01-22 18:07:44 +09:00
Junyoung Choi
e43c7e9a6a Merge pull request #2598 from richardtks/fix-number-list
Fix the auto-updating numbered list to the middle of a list
2019-01-22 17:47:26 +09:00
Milo Todt
c3a980836a change to isDev 2019-01-20 21:04:56 -08:00
Junyoung Choi
304b83be89 Merge pull request #2801 from MiloTodt/issue-2799-add_storage
Removal of USB drive will no longer cause irreparable damage.
2019-01-21 09:53:14 +09:00
Nguyễn Việt Hưng
a2e050b8c5 added jump to line on click preview 2019-01-19 17:03:59 +07:00
Nguyễn Việt Hưng
a7ad56be98 Merge branch 'master' of https://github.com/BoostIO/Boostnote 2019-01-19 16:28:36 +07:00
Milo Todt
eea01f10ac Added catch for exceptions, removed uneeded duplicate test. 2019-01-17 17:00:07 -08:00
Junyoung Choi
3d9b85dc6d Merge pull request #2811 from MiloTodt/Add_cheatsheets_to_menu
Added cheatsheets to menu
2019-01-17 17:40:05 +09:00
Junyoung Choi
743a9009de Merge pull request #2797 from MiloTodt/patch-1
Update copywrite year
2019-01-17 17:39:22 +09:00
Junyoung Choi
a0da4f9dd0 Merge pull request #2795 from MiloTodt/update-build.md
Updated build.md to include directions for checking out PRs
2019-01-17 17:38:57 +09:00
Milo Todt
3fe45e9cbb Added cheatsheets to menu 2019-01-16 16:09:07 -08:00
roottool
294bf742cd change variable name #1448 2019-01-16 23:14:21 +09:00
roottool
ea5970ab1c change a magnification by image size #1448 2019-01-16 22:47:42 +09:00
roottool
72df418953 DRY my code #1448 2019-01-16 18:10:28 +09:00
roottool
f566b567be add zoom-in and zoom-out action of images #1448 2019-01-16 01:54:54 +09:00
Milo Todt
8d817066e8 Added a few more checks in related methods to prevent simular errors 2019-01-13 18:35:24 -08:00
Milo Todt
99b53f4a55 added filter. 2019-01-13 18:21:12 -08:00
Milo Todt
038154c441 Removed uneeded "electron-is-dev" package 2019-01-13 17:21:30 -08:00
Milo Todt
951a126d63 Update LICENSE 2019-01-12 18:51:03 -08:00
Milo Todt
fa77cda0b4 whitespace 2019-01-12 17:59:04 -08:00
Milo Todt
a0bfd9e497 Disables updates in development mode 2019-01-12 17:58:19 -08:00
Milo Todt
a8e601e5e0 Update InfoTab.js 2019-01-12 16:09:25 -08:00
Milo Todt
47d7cef214 Update copywrite year
2018->2019
2019-01-12 15:50:33 -08:00
Milo Todt
e298739cb9 fixed typo 2019-01-12 12:34:25 -08:00
Milo Todt
6fb72bd44a minor typo 2019-01-12 09:46:51 -08:00
Milo Todt
7b8fb56440 fixed error in url 2019-01-12 08:38:18 -08:00
Milo Todt
5e9bd2fd2d escaped angle brackets 2019-01-12 08:34:14 -08:00
Milo Todt
aac13dcdca Updated build.md to include directions for checking out PRs 2019-01-12 08:26:31 -08:00
Nguyễn Việt Hưng
c6c5e33ee6 Merge branch 'master' of https://github.com/BoostIO/Boostnote 2019-01-11 10:36:59 +07:00
Foo-x
406e230ed3 Fix bullet position of LaTeX equation in list 2019-01-10 23:26:47 +09:00
Junyoung Choi
5a9de1a95d Merge pull request #2759 from richardtks/toggle-menu-bar-settings
allow menu bar visibility to be set in the settings
2019-01-08 08:27:41 +09:00
Junyoung Choi
0289caad67 Merge pull request #2712 from roottool/fix-issue#2644-and-#2662
fix issues #2644 and #2662
2019-01-07 14:15:48 +09:00
Junyoung Choi
99cb6fa9ed Merge pull request #2720 from richardtks/fix-tag-allow-overflow
show the scroll bar when the tag list is overflow
2019-01-07 14:15:21 +09:00
Junyoung Choi
fe011e87d1 Merge pull request #2758 from elfman/colorTag
add feature: colored tags
2019-01-07 14:14:40 +09:00
Junyoung Choi
5a5563f00a Merge pull request #2770 from vienai8d/update_snippet_note
Replace current fullscreen button of SnippetNote to FullscreenButton module
2019-01-07 14:12:15 +09:00
Junyoung Choi
5a8f076d85 Merge pull request #2762 from coliff/patch-1
Update mousetrap
2019-01-07 14:11:45 +09:00
Junyoung Choi
45deb5ba7f Merge pull request #2771 from caina/patch-1
typo on pt-BR.json
2019-01-07 14:11:24 +09:00
Junyoung Choi
bcea3eb7c1 Merge pull request #2764 from vienai8d/translate_ja
Add Japanese translation
2019-01-07 10:30:37 +09:00
Junyoung Choi
9ca5fa6144 Merge pull request #2761 from abnersajr/ptBr-translations
Pt br translations
2019-01-07 10:30:20 +09:00
Junyoung Choi
d9809318fc Merge pull request #2772 from vienai8d/localize_tooltips_ja
Localize tooltips for Japanese
2019-01-07 10:27:35 +09:00
Nguyễn Việt Hưng
9ad0f58095 Merge branch 'master' of https://github.com/BoostIO/Boostnote 2019-01-06 09:35:01 +07:00
Junyoung Choi
04ae8a81a5 Merge pull request #2768 from elfman/detect
new feature: auto detect snippet language
2019-01-06 09:58:13 +09:00
HarlanLuo
082a078b51 check config before auto detect language 2019-01-04 21:55:58 +08:00
Rahul Gurung
04fdb67fc9 #2194 - fixes Hotkey not working 2019-01-02 12:38:26 +05:30
vienai8d
e6a927e5af localize ToggleModeButton for Japanese 2019-01-02 11:10:07 +09:00
vienai8d
b05ba64db8 localize TrashButton for Japanese 2019-01-02 11:07:15 +09:00
vienai8d
d0f5ec8ada fix FullscreenButton 2019-01-02 11:03:40 +09:00
vienai8d
caa6c8d4b9 localize FullscreenButton for Japanese 2019-01-02 11:01:37 +09:00
vienai8d
5cf4a0e09d localize StarButton for Japanese 2019-01-02 10:59:06 +09:00
vienai8d
0a0c1c45a1 translate tooltip texts into Japanese 2019-01-02 10:56:27 +09:00
Douglas Caina
fce89fe8be Update pt-BR.json
## Description
There was a misspelling on the word "escluir", the correct one is "excluir", as you can see on [google translate](https://translate.google.com/?source=osdd#view=home&op=translate&sl=pt&tl=en&text=excluir)


## Type of changes

-  Bug fix (Change that fixed an issue)
-  Breaking change (Change that can cause existing functionality to change)
-  Improvement (Change that improves the code. Maybe performance or development improvement)
-  Feature (Change that adds new functionality)
-  Documentation change (Change that modifies documentation. Maybe typo fixes)

## Checklist:

-  My code follows [the project code style](docs/code_style.md)
-  I have written test for my code and it has been tested
-  All existing tests have been passed
-  I have attached a screenshot/video to visualize my change if possible
2019-01-01 22:20:23 +01:00
vienai8d
2bbe39120a use FullscreenButton instead of current button 2019-01-01 23:11:39 +09:00
vienai8d
2a5da746c7 add translation about content menu 2019-01-01 18:39:39 +09:00
HarlanLuo
deb2cd0156 new feature: auto detect snippet language
only try to detect after pasting and mode has not been set and default snippet language is "Auto Detect"
2019-01-01 17:01:49 +08:00
HarlanLuo
b4e4d7055f improve style of color-picker 2018-12-30 20:49:26 +08:00
HarlanLuo
a9feddf6f6 fix bug missing param colored tags in sorted list 2018-12-30 19:23:43 +08:00
tool root
071f7cb035 rewrote the function code to MarkdownPreview.js #2644 #2662 2018-12-30 16:20:19 +09:00
tool root
a11b0f1665 Merge branch 'master' into fix-issue#2644-and-#2662 2018-12-30 15:57:31 +09:00
HarlanLuo
39442bcafe invert icon-x color when tag text color is inverted 2018-12-29 22:23:48 +08:00
richardtks8@gmail.com
48a905bf6f decrease the height of tags scrollbar 2018-12-29 15:34:36 +08:00
vienai8d
440b50b4e8 add translation about interface in preferences 2018-12-29 15:08:03 +09:00
HarlanLuo
0cf6487cad invert text color in colored tags 2018-12-28 22:12:51 +08:00
Abner Soares Alves Junior
74825dddbf Minor tweaks on portuguese translation 2018-12-28 09:59:22 -02:00
HarlanLuo
699006a3e9 Improve ui of colored tags 2018-12-28 14:09:35 +08:00
HarlanLuo
6367be213f rename config.tag to config.coloredTags 2018-12-28 12:30:23 +08:00
Christian Oliff
4e97ac3b8c Update mousetrap 2018-12-28 10:11:44 +09:00
Abner Soares Alves Junior
57817fd90c Add pt_BR translation to debug documentation 2018-12-27 23:07:18 -02:00
Abner Soares Alves Junior
5b79d0439c Add pt_BR translation to build documentation 2018-12-27 22:43:17 -02:00
richardtks8@gmail.com
6d4cee0041 allow menu bar to be set in the settings 2018-12-28 02:15:24 +08:00
HarlanLuo
3e645db324 add feature: colored tags 2018-12-27 23:22:28 +08:00
Junyoung Choi
05da826c24 Merge pull request #2755 from empeje/patch-1
Create FAQ.md
2018-12-27 20:16:29 +09:00
Junyoung Choi
70e16d853e Replace heading 1 with heading 2 2018-12-27 15:36:20 +09:00
mpj
9ebf949890 Update FAQ.md 2018-12-27 13:20:44 +07:00
mpj
a06bdced8a Update FAQ.md 2018-12-27 13:19:54 +07:00
Junyoung Choi
3ab506ea94 Merge pull request #2756 from vienai8d/saveTagsAlphabetically
Fix feature 'saveTagsAlphabetically'
2018-12-27 13:45:28 +09:00
mpj
0340402dc1 Create FAQ.md 2018-12-27 11:12:36 +07:00
vienai8d
6e8fe7308c implement feature 'saveTagsAlphabeticall' 2018-12-27 12:09:56 +09:00
Junyoung Choi
13c2f471aa Merge pull request #2747 from daiyam/fix-paste-image
fix paste image
2018-12-25 01:07:37 +09:00
Junyoung Choi
604f17fbfd Merge pull request #2586 from GuilhermeJSilva/feature/autoBracketMatching
Feature/auto bracket matching
2018-12-25 00:24:37 +09:00
Baptiste Augrain
50669f65bb update preferences' labels 2018-12-24 11:04:17 +01:00
Guilherme Silva
21c61121b0 Fixing code style probllems 2018-12-24 09:46:25 +00:00
Baptiste Augrain
a58b6f1b49 Merge branch 'master' into fix-mermaid-height 2018-12-24 10:06:15 +01:00
Baptiste Augrain
073a5d4d68 Ctrl+V can paste an image 2018-12-24 09:50:14 +01:00
Junyoung Choi
7a3cab8947 Merge pull request #2455 from daiyam/fix-notelist
fix scrolling in note list
2018-12-24 17:20:05 +09:00
Junyoung Choi
aec79c4eeb Merge pull request #2591 from daiyam/drop-image-preview
drag image into preview
2018-12-24 17:19:41 +09:00
Junyoung Choi
5f385e4c03 Merge pull request #2465 from daiyam/gallery
add image gallery
2018-12-24 17:19:03 +09:00
Junyoung Choi
b018502079 Merge pull request #2456 from daiyam/precommit-command
lint before commit
2018-12-24 17:17:53 +09:00
Junyoung Choi
47e0a82caf Merge pull request #2746 from BoostIO/fix-go-to-eol-on-mac
Fix Cmd+Left to go to end of line on Mac
2018-12-24 17:02:51 +09:00
Junyoung Choi
d848ee5d5f Merge pull request #2692 from Brunovsky/fix-2557
Fix issue 2557 katex alignment in display math
2018-12-24 17:00:11 +09:00
Junyoung Choi
2df0f1bcb8 Merge pull request #2682 from duartefrazao/master
Feature - Line highlighting within code block #2469
2018-12-24 16:56:59 +09:00
Junyoung Choi
1ae141492a Merge pull request #2743 from daiyam/fix-notelist-width
fix min width of note list
2018-12-24 16:54:37 +09:00
Junyoung Choi
7232d07b1c Merge pull request #2704 from jarvisuser90/Update-tabs-ui
Update tabs ui
2018-12-24 16:53:59 +09:00
Ryan Scott
64abd564b4 Fix Cmd+Left to go to end of line on Mac 2018-12-24 16:48:16 +09:00
Junyoung Choi
483ea77d14 Merge pull request #2710 from daiyam/fix-toc
fix toc
2018-12-24 16:41:41 +09:00
Junyoung Choi
cca5abdc8f Merge pull request #2741 from roottool/fix-issue#2729
fixed issue #2729
2018-12-24 16:38:35 +09:00
Junyoung Choi
17b3b02ac5 Merge pull request #2739 from coliff/patch-1
Https link to EditorConfig.org
2018-12-24 16:37:56 +09:00
Junyoung Choi
ce4e203c14 Merge pull request #2730 from joaocastro/vscode-debug-windows
Fixed windows debug path
2018-12-24 16:37:34 +09:00
Junyoung Choi
011defc1f7 Merge pull request #2745 from BoostIO/change-issuehunt-image
Change IssueHunt image
2018-12-24 15:03:01 +09:00
kazup01
5ccb9bde28 Change IssueHunt image 2018-12-24 14:10:52 +09:00
Baptiste Augrain
30e262d8ac fix min width of note list 2018-12-23 17:01:46 +01:00
roottool
692f6779d6 fixed the checkboxes are too far right #2729 2018-12-23 01:13:57 +09:00
duartefrazao
e93bf1cfe7 Removed bind in MarkdownEditor and MarkdownSplitEditor 2018-12-22 11:44:30 +00:00
Christian Oliff
8d769d4c4b Https link 2018-12-22 12:00:19 +09:00
MiguelPedrosa
b539ac6335 Fixed windows debug path 2018-12-19 20:57:27 +00:00
Duarte-Frazao
72906b3ee7 Corrections to make line highlighting robust, added tests
Lines now save correctly with different inputs, making sure that different inputs like enter, delete, paste and where it's deleted stay consistent when saving.
Included in the create/update  snippet/note tests the structure from lines highlighting saved to the files.
2018-12-19 16:34:16 +00:00
Miguel Teixeira
7f6d4acf90 Fixed lock button not appearing 2018-12-19 15:16:55 +00:00
richardtks
bb892f7e78 Updated the overflow-x to auto 2018-12-18 18:34:49 +08:00
Baptiste Augrain
ead6bb09dc Merge branch 'master' into fix-notelist 2018-12-18 11:07:49 +01:00
richardtks
0b1ec3f29f show the scroll bar when the tag list is overflow 2018-12-18 16:32:30 +08:00
Guilherme Silva
e77db372bd Merge branch 'master' into feature/autoBracketMatching 2018-12-17 15:39:03 +00:00
Junyoung Choi
64ca875cfd v0.11.12 2018-12-17 14:56:16 +09:00
Baptiste Augrain
256653677e fix lint errors and remove unused dependencies 2018-12-16 19:45:18 +01:00
Baptiste Augrain
b99980fda1 improve slug by replacing diacritics and removing unwanted characters 2018-12-16 19:38:04 +01:00
roottool
13857d4313 Modified position of escapeHtmlCharactersInCodeTag definition 2018-12-17 00:37:25 +09:00
roottool
fe1ab73818 fix #2644 and #2662 2018-12-16 22:08:55 +09:00
Baptiste Augrain
d5a2aa6d6d fix missing bullets 2018-12-15 10:41:47 +01:00
Baptiste Augrain
4f9b37433c fix toc by sharing slugify() 2018-12-15 10:11:04 +01:00
Nguyễn Việt Hưng
b5763ec89d Merge branch 'master' of https://github.com/BoostIO/Boostnote 2018-12-15 13:40:03 +07:00
John Ciprian
ec506e71a4 Adding support for dracula interface theme 2018-12-13 20:56:47 -05:00
John Ciprian
cfcaa58b71 Optimizing css for dark and solarized-dark themes 2018-12-13 20:56:47 -05:00
John Ciprian
29cf4769f5 Adding support for monokai interface theme 2018-12-13 20:56:47 -05:00
John Ciprian
58fd2273ea Adding support for solarized-dark interface theme 2018-12-13 20:56:47 -05:00
John Ciprian
b52616c64d Changing tabs from material design to traditional
- Adding support for “default” interface theme.
- Adding support for “white” interface theme.
- Adding support for “dark” interface theme.
2018-12-13 20:56:47 -05:00
Duarte-Frazao
ac1ce6043b Fixed legacy default Markdown/Snippet notes bug
Fixed a bug where a note or snippet is created before the pull request and you ran Boostnote for the first time after the pr and you firstly created a note or markdown and only then returned to the old default notes and you couldn't highlight
2018-12-13 20:19:02 +00:00
Miguel Teixeira
6631f98c43 Removed debug log 2018-12-13 19:13:12 +00:00
Miguel Teixeira
37340d0445 Fixed issue with double and right click 2018-12-13 19:09:41 +00:00
Junyoung Choi
743c97940e Merge pull request #2641 from daiyam/fix-paste-code
fix pasting into fenced code block
2018-12-14 01:26:49 +09:00
Duarte-Frazao
f2a0f59b08 Fixed error on call to bind. 2018-12-13 13:27:20 +00:00
Duarte-Frazao
4fb11b68e4 Markdown functionality 2018-12-13 13:13:01 +00:00
Guilherme Silva
ed742c7e16 Merge branch 'master' into feature/autoBracketMatching 2018-12-13 11:58:45 +00:00
Miguel Teixeira
3b110bcd4b WIP 2018-12-13 11:55:04 +00:00
Baptiste Augrain
d4dd74e820 Merge branch 'master' into fix-paste-code 2018-12-13 08:20:36 +01:00
Junyoung Choi
3f77cb2214 Merge pull request #2684 from GuilhermeJSilva/fix/highlight-folder-on-hover
Dragged note highlighting
2018-12-13 13:59:10 +09:00
Junyoung Choi
7c839a1df9 Merge pull request #2685 from jhdcruz/patch-1
Fixed Build Badge Alignment w/ Fixed link
2018-12-13 13:58:29 +09:00
Miguel Teixeira
c5de940946 Fixed bug where icon was hidden 2018-12-12 18:19:41 +00:00
Miguel Teixeira
2943c5fafb Sync with upstream 2018-12-12 18:09:46 +00:00
Junyoung Choi
fddeaa966d Merge pull request #2690 from vienai8d/translate_ja
Add Japanese translation
2018-12-13 00:27:54 +09:00
Junyoung Choi
e706ec0ffe Merge pull request #2643 from daiyam/fix-contextmenu
fix editor's context menu
2018-12-13 00:26:05 +09:00
Brunovsky
e65c48be33 Fix issue 2557 katex alignment in display math 2018-12-11 00:41:51 +00:00
Nguyễn Việt Hưng
a095e8b25c Merge branch 'master' of https://github.com/BoostIO/Boostnote 2018-12-10 11:56:13 +07:00
vienai8d
d44a76bae3 add translation about interface in preferences 2018-12-10 07:20:02 +09:00
vienai8d
4488de9add add translation about hotkeys in preferences 2018-12-09 10:58:06 +09:00
Duarte-Frazao
62609a2918 Fixed last nonfunctional changes made earlier
Now iterates in the SnippetNoteDetail constructor the snippets and if linesHighlighted is not defined assigns an empty array
2018-12-08 17:22:57 +00:00
Duarte-Frazao
492294e11d Reset of linesHighlighted 2018-12-08 12:19:42 +00:00
Duarte-Frazao
f483f8fdf0 Fix so that linesHighlighted defaults to [] when does't find it 2018-12-08 12:07:28 +00:00
Guilherme Silva
4061866042 Moving prevent default 2018-12-08 09:06:55 +00:00
Duarte-Frazao
191295b6de Added array of linesHighlighted to default snippet
This makes the default snippet also handle highlight on the lines, because this snippet is created in the code without the normal snippet constructor
2018-12-07 23:41:51 +00:00
Joshua Dela Cruz
6d4aa27e15 Fixed Build Badge Alignment w/ Fixed link
Related to PR #2584
2018-12-07 17:22:01 +08:00
Junyoung Choi
d70d3b439f Merge pull request #2672 from arkist/handle-encoded-uri-on-copyfile
Handle encoded uri path on `copyFile`
2018-12-07 14:30:35 +09:00
Junyoung Choi
56851eb91f Merge pull request #2666 from yougotwill/menu_items_overhaul
Menu items overhaul
2018-12-07 12:41:22 +09:00
Baptiste Augrain
2cfe8de030 Merge branch 'master' into precommit-command 2018-12-06 15:48:27 +01:00
Duarte-Frazao
b5604ba0a9 Changes to optimize initial highlighting
Now iterates over highlighted lines instead of all lines of the snippet
2018-12-06 13:23:23 +00:00
Duarte-Frazao
1a0e15e04c Fixed bug when switching to markdown notes 2018-12-05 14:12:29 +00:00
Evan Miller
b546b9cbe7 InfoPanel automatically adjusts its width 2018-12-03 17:04:04 -05:00
Evan Miller
ce9f76fa63 fixed code style error 2018-12-03 16:55:05 -05:00
Evan Miller
dceed7d84d Fixes exportFolder by making it actually wait for each exportNote 2018-12-03 16:51:59 -05:00
Evan Miller
660a27850f Generate PDF through an Electron BrowserWindow 2018-12-03 16:39:41 -05:00
Evan Miller
0a7fd0288c Use promises for outputFormatter 2018-12-03 16:14:28 -05:00
Evan Miller
33d0a9d3b3 extract html contentformatter 2018-12-03 15:49:41 -05:00
Evan Miller
c1deeaf5f7 Added PDF error to SnippetNoteDetail 2018-12-03 14:40:52 -05:00
Evan Miller
7c1cd50def Add structure for exporting PDFs 2018-12-03 13:12:22 -05:00
Junyoung Choi (Sai)
b224c72e98 Merge pull request #2637 from miguelalexbt/master
Warning when printing snippet
2018-12-03 17:16:50 +09:00
Junyoung Choi (Sai)
6c57ac8f01 Merge pull request #2383 from ehhc/exportMDexportsAttachments
export folder should also export the attachments -> fixes #2374
2018-12-03 17:15:02 +09:00
Jinwoo Oh
cf35dc5345 Handle encoded uri path on copyFile
fix #2578
2018-12-03 14:04:13 +09:00
William Grant
102d13bbae you can't have multiple accelerators on a single menu item. Bye bye delete key :( 2018-12-02 16:29:10 +02:00
William Grant
faf2aff959 added in the shift key for deleting a note based on https://github.com/BoostIO/Boostnote/pull/2452 2018-12-02 16:02:29 +02:00
William Grant
5d54ad3342 Cleaned up menu items ordering and separators, added clone note menu item and shortcut, updated focus note shortcut to be mac friendly, made delete note shortcut use either command+backspace or ctrl+backspace and the delete key on windows and linux machines 2018-12-02 15:59:47 +02:00
William Grant
0f5d753910 Removed the old ctrl+d shortcut for deleting a note since we now have the super+shift+backspace shortcut which can be changed in the hotkey settings 2018-12-02 15:21:01 +02:00
Duarte-Frazao
a9442a019f Changes to pass tests and lint code 2018-11-30 19:20:40 +00:00
Duarte-Frazao
1668ef6bb4 Small changes 2018-11-30 18:32:14 +00:00
Duarte-Frazao
45436f65af Issue #2469 almost done, missing refactor to reduce calls on code mirror 2018-11-30 18:03:23 +00:00
Guilherme Silva
b8a295713c Dragged note highlighting 2018-11-30 08:42:27 +00:00
Nguyễn Việt Hưng
2ccb541b7f Merge branch 'master' of https://github.com/BoostIO/Boostnote 2018-11-30 10:04:25 +07:00
Nguyễn Việt Hưng
e4a6ff4c70 Merge branch 'master' of https://github.com/ZeroX-DG/Boostnote 2018-11-30 10:04:16 +07:00
Miguel Teixeira
aa20bc769c Fixed lock button behaviour and display 2018-11-29 17:06:28 +00:00
Guilherme Silva
9bc291e618 Merge branch 'master' into feature/autoBracketMatching 2018-11-29 11:47:07 +00:00
Baptiste Augrain
fdb54b5cdc fix expanding snippet 2018-11-28 16:47:19 +01:00
Baptiste Augrain
df20662005 remove console.log 2018-11-28 16:42:10 +01:00
Baptiste Augrain
900f20f164 handle all dropped images 2018-11-28 15:58:58 +01:00
Baptiste Augrain
df3b2cd8fe fix double paste when pasting attachement links 2018-11-28 15:12:18 +01:00
Baptiste Augrain
c2e4bae9dd fix regex to correctly match the src attribute when there is a data-src attribute 2018-11-28 15:00:29 +01:00
Miguel Teixeira
a39da481e0 Changed message. 2018-11-28 09:08:58 +00:00
Junyoung Choi (Sai)
830ade9596 Merge pull request #2399 from Pudge601/bug/2241
Clear search when a new note is created
2018-11-28 11:36:46 +09:00
Baptiste Augrain
2aa296ff33 fix lint error 2018-11-28 00:45:20 +01:00
Baptiste Augrain
9050035c74 - add option to enable/disable smart paste
- add shortcut to paste smartly
- use electron's clipboard
2018-11-28 00:44:15 +01:00
Miguel Teixeira
c245855bbf Improved messages. 2018-11-27 20:28:57 +00:00
Miguel Teixeira
ef66e71feb Solved some errors in identation 2018-11-27 18:02:59 +00:00
Miguel Teixeira
c33058ae2b Added custom warning messages. 2018-11-27 17:42:04 +00:00
Nguyễn Việt Hưng
629d4a82ae fixed delete note not navigate to next note 2018-11-27 13:58:06 +07:00
Baptiste Augrain
d95d282f39 disable editor's context menu when switch preview is using right click 2018-11-25 17:09:54 +01:00
Baptiste Augrain
64f7233bfc fix regex to match :storage reference, added comment to explain what it does. 2018-11-25 16:58:11 +01:00
Baptiste Augrain
9d81e4be2f undo change 2018-11-25 16:17:01 +01:00
Baptiste Augrain
0a1ee86baf Merge branch 'master' into gallery 2018-11-25 16:05:27 +01:00
Baptiste Augrain
2908884202 drag and drop image from browser 2018-11-25 15:56:11 +01:00
Baptiste Augrain
aac075be06 fix lint error 2018-11-25 14:57:03 +01:00
Baptiste Augrain
bf288fdaeb fix pasting into fenced code block 2018-11-25 14:55:29 +01:00
Junyoung Choi (Sai)
a6eddb5798 Merge pull request #2592 from enyaxu/bug-2581
Fixed duplicate TOC Title jump error
2018-11-25 15:49:06 +09:00
Junyoung Choi (Sai)
78ae7b847c Merge branch 'master' into bug-2581 2018-11-25 15:39:39 +09:00
Junyoung Choi (Sai)
c7bae93b48 Merge pull request #2601 from yougotwill/zoom_controls
Zoom Controls
2018-11-25 15:37:29 +09:00
Junyoung Choi (Sai)
938cf238ff Merge pull request #2604 from arcturus140/fixGUI
fix GUI style element for default theme
2018-11-25 15:36:17 +09:00
Junyoung Choi (Sai)
5188846e2e Merge pull request #2611 from gregueiras/issue2199
Ability to go from 'editor with preview' mode to 'preview' using mouse
2018-11-25 15:35:53 +09:00
Junyoung Choi (Sai)
d874a3a493 Merge pull request #2620 from arcturus140/fix2618
fix2618
2018-11-25 15:29:37 +09:00
Junyoung Choi (Sai)
f95284622e Merge pull request #2621 from mehr-licht/master
included shortcut for Info Panel (issue 2533)
2018-11-25 15:28:25 +09:00
Junyoung Choi (Sai)
0b6c0e6b94 Merge pull request #2622 from daiyam/fix-fence
fix code blocks
2018-11-25 15:26:48 +09:00
Junyoung Choi (Sai)
b021bb73ed Merge pull request #2498 from daiyam/create-note-with-tags
create note with selected tags
2018-11-25 15:23:04 +09:00
Junyoung Choi (Sai)
c76b653737 Merge pull request #2338 from ehhc/spellchecker
Spellchecker
2018-11-25 15:21:04 +09:00
Miguel Teixeira
3414e2daf0 Added warning when trying to print from InfoPanel 2018-11-22 12:22:49 +00:00
mehr-licht
92e2cd102e deleted ctr+l and ctrl+t 2018-11-19 19:00:51 +00:00
Junyoung Choi (Sai)
b703c42ee3 Merge pull request #2504 from ZeroX-DG/pr-template
Added PR Template and update docs
2018-11-19 15:14:02 +09:00
Baptiste Augrain
94f7533ee7 fix code blocks 2018-11-18 23:53:46 +01:00
mehr-licht
101e5d5035 included shortcut for Info Panel 2018-11-18 19:22:05 +00:00
Arcturus
ab78af0691 fix2618 2018-11-18 17:36:42 +00:00
Gonçalo Santos
af14b682b1 User can now go back to previous mode 2018-11-15 12:51:53 +00:00
Junyoung Choi
a26d4fb499 Update yarn.lock 2018-11-15 19:40:52 +09:00
Arcturus
7717cda52a fix GUI style element for default theme 2018-11-12 20:50:09 +00:00
Baptiste Augrain
c13746f10e Merge branch 'master' into create-note-with-tags 2018-11-12 19:16:50 +01:00
ehhc
49abfac98e don't do spellcheck if disabled... 2018-11-12 17:47:45 +01:00
ehhc
aa26e5ac2a fix linter errors 2018-11-12 17:40:37 +01:00
ehhc
336f007fa1 Merge branch 'spellchecker' of https://github.com/ehhc/Boostnote into spellchecker 2018-11-12 17:36:36 +01:00
ehhc
a3e59d43a7 hiding spellcheck by default and adding a interface option to enable it 2018-11-12 17:35:33 +01:00
William Grant
d3a6ff6b6a updated zoom out accelerator for consistency 2018-11-12 06:53:10 +02:00
William Grant
e2c7a8c384 updated the zoom logic for the zoom in/out menu items so that if you zoom it reflects in the status bar and the config. Also added an actual size / zoom reset menu item for convenience. 2018-11-11 12:59:50 +02:00
Ivan Chen
0904c62a2d Update locales/zh-CN.json
Co-Authored-By: opw0011 <i.mpeople@gmail.com>
2018-11-11 11:19:38 +08:00
Ivan Chen
817b74cc7f Update locales/zh-TW.json
Co-Authored-By: opw0011 <i.mpeople@gmail.com>
2018-11-11 11:18:57 +08:00
richardtks
01891d46b3 Added the auto-updating numbered list to the middle of a list 2018-11-11 00:15:22 +08:00
JianXu
849104f530 Update Snapshots test 2018-11-10 16:19:42 +08:00
JianXu
3a4bc33d53 Fixed duplicate TOC Title jump error 2018-11-10 15:56:21 +08:00
Baptiste Augrain
3679fbe3ea fix lint errors 2018-11-10 00:18:06 +01:00
Baptiste Augrain
b1d2c25ce5 when dropping an image, switch to editor and add it at the end of the file 2018-11-10 00:05:45 +01:00
Baptiste Augrain
b1a7f0fd64 Merge branch 'master' into gallery 2018-11-08 23:52:31 +01:00
Baptiste Augrain
70b86907f3 Merge branch 'master' into create-note-with-tags 2018-11-08 18:31:32 +01:00
Baptiste Augrain
d0d813552c add option to tag the new notes with the filtering tags, or not... 2018-11-08 18:22:52 +01:00
Guilherme Silva
707dace3d0 removing spaces after = 2018-11-08 14:11:43 +00:00
Guilherme Silva
8361106660 Removing trailing spaces and added spaces in config 2018-11-08 14:07:35 +00:00
Guilherme Silva
a46c519459 Fixing indentations 2018-11-08 14:03:21 +00:00
Baptiste Augrain
b6b29e02f3 fix lint error 2018-11-08 14:41:25 +01:00
Guilherme Silva
441c70b388 Removing checkmark and fixed Code Editor indentation 2018-11-08 13:25:22 +00:00
Guilherme Silva
ab65fb7a5c Choosing which characters to match and explode 2018-11-08 13:18:13 +00:00
Baptiste Augrain
2fc37d54f2 fix height of mermaid's diagrams 2018-11-08 13:19:46 +01:00
Guilherme Silva
59d31c9a18 Deleted useless console log 2018-11-08 12:01:12 +00:00
Guilherme Silva
db97ab51ac Bracket matching option added to config 2018-11-08 11:44:03 +00:00
ehhc
c6f1f97a57 Merge branch 'master' into spellchecker 2018-11-08 11:19:46 +01:00
ehhc
6c7ed82fa9 Merge branch 'master' into spellchecker 2018-11-06 08:36:59 +01:00
ehhc
88ac2a98e8 Merge branch 'master' into spellchecker 2018-11-02 10:41:30 +01:00
Daniel Mouritzen
d6fe0df24f Add ~ and _ to autoclosing brackets 2018-10-27 18:22:03 +02:00
Daniel Mouritzen
7fb1a06e1e Add ~ and _ to autoclosing brackets 2018-10-27 18:18:51 +02:00
Nguyễn Việt Hưng
4550d888bb updated code style with class property style 2018-10-27 09:47:15 +07:00
Nguyễn Việt Hưng
6cb6cd3f26 updated docs and pull request template 2018-10-26 00:06:06 +07:00
Nguyễn Việt Hưng
c5554e8f1e update pull request template 2018-10-25 23:36:31 +07:00
Nguyễn Việt Hưng
3b7bedbbe8 update pull request message 2018-10-21 10:25:28 +07:00
steve-o
b004247478 update electron-packager; enable dark mode support for macOS mojave 2018-10-19 12:16:27 -05:00
Nguyễn Việt Hưng
83243b61a6 smaller header font size 2018-10-18 12:39:57 +07:00
Nguyễn Việt Hưng
80c4601fdc fixed grammar in PR template 2018-10-18 12:36:21 +07:00
Nguyễn Việt Hưng
fa3700df7c updated pr template 2018-10-18 12:33:54 +07:00
Nguyễn Việt Hưng
6244e44033 added pr template 2018-10-18 12:29:47 +07:00
Nguyễn Việt Hưng
43bd0b0dd5 Merge branch 'master' of https://github.com/BoostIO/Boostnote 2018-10-18 12:07:40 +07:00
Baptiste Augrain
d75dd874ca create note with selected tags 2018-10-15 08:24:21 +02:00
Benny O
28007a33a0 Add missing translation for zh-cn 2018-10-13 19:19:48 +08:00
Benny O
4242e0d329 Add missing translation for zh-tw 2018-10-13 19:18:47 +08:00
Baptiste Augrain
c5f6ace332 Merge branch 'chart-yaml' into gallery 2018-10-11 12:59:53 +02:00
Baptiste Augrain
5d9b1abe82 allow markdown image syntax 2018-10-09 01:18:19 +02:00
Baptiste Augrain
7a5a821f8a fix failing test 2018-10-09 00:47:37 +02:00
Baptiste Augrain
39eaed260a display correctly attached image 2018-10-08 15:57:42 +02:00
Baptiste Augrain
f308836264 add new fenced block language: gallery 2018-10-02 23:48:07 +02:00
Baptiste Augrain
e52bcf33c5 add precommit command 2018-10-01 18:54:50 +02:00
Baptiste Augrain
fa6c504b34 fix lint error 2018-10-01 18:42:18 +02:00
Baptiste Augrain
e9dac8c8f3 fix scrolling in note list 2018-10-01 18:16:26 +02:00
Martin Price
6510152138 Always redirect to /home when jumping to a note by hash
The note which we are jumping to may not be available in the note list for a number of reasons (e.g. if there is an active search, or if another storage folder is selected, or if the note list is showing starred notes).

This affects both when we are creating a new note (which may not match the current search criteria), and when jumping to a note via a link in another note (and the linked note may not be available for any of the above reasons).

2241
2018-09-17 11:55:01 +01:00
ehhc
d79b6e094a export folder should also export the attachments -> fixes #2374 2018-09-09 17:41:51 +02:00
ehhc
786675a99b Merge branch 'master' into spellchecker 2018-09-04 11:36:27 +02:00
ehhc
54717ea6f2 linter errors fixed 2018-08-25 18:45:13 +02:00
ehhc
ceca4c98a3 linter errors fixed 2018-08-25 18:39:29 +02:00
ehhc
39b4287c5e linter errors fixed 2018-08-25 18:34:21 +02:00
ehhc
734db58d85 Spellcheck - first try to fix #2176 2018-08-22 16:48:10 +02:00
ehhc
4307db11c5 Merge branch 'master' of https://github.com/BoostIO/Boostnote into spell_check
# Conflicts:
#	browser/components/CodeEditor.js
#	locales/fr.json
#	package.json
2018-07-03 09:05:47 +02:00
ehhc
83f8151ca4 spellcheck -> context menu with spelling suggestions 2018-07-02 17:27:47 +02:00
ehhc
342575a576 Spellcheck - Dropdown & localisation 2018-06-23 19:29:22 +02:00
ehhc
785272540e Spellcheck - liveSpellcheck 2018-06-23 18:16:39 +02:00
ehhc
82178055af Spellcheck - initialisation and first draft onChange 2018-06-17 17:13:44 +02:00
Storm Burpee
18aae8cf7b getting very close 2018-05-28 22:12:04 +09:30
Storm Burpee
4a9bc69ac2 starting to write a test 2018-05-28 19:45:09 +09:30
Storm Burpee
d97e62f864 Import note from url with markdown 2018-05-26 17:00:12 +09:30
207 changed files with 230445 additions and 5189 deletions

View File

@@ -7,7 +7,7 @@
"test": {
"presets": ["env" ,"react", "es2015"],
"plugins": [
[ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ]
[ "babel-plugin-webpack-alias", { "config": "<rootDir>/webpack.config.js" } ]
]
}
}

View File

@@ -1,4 +1,4 @@
# EditorConfig is awesome: http://EditorConfig.org
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true

View File

@@ -18,7 +18,9 @@
"globals": {
"FileReader": true,
"localStorage": true,
"fetch": true
"fetch": true,
"Image": true,
"MutationObserver": true
},
"env": {
"jest": true

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
issuehunt: BoostIo/Boostnote

3
.gitignore vendored
View File

@@ -9,4 +9,5 @@ node_modules/*
/secret
*.log
.idea
.vscode
.vscode
package-lock.json

View File

@@ -1,10 +1,9 @@
language: node_js
node_js:
- 7
- 8
script:
- npm run lint && npm run test
- yarn jest
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && grunt pre-build; fi'
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@6.4 && grunt pre-build; fi'
after_success:
- openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv
-in .snapcraft/travis_snapcraft.cfg -out .snapcraft/snapcraft.cfg -d

2
.vscode/launch.json vendored
View File

@@ -17,7 +17,7 @@
"${workspaceFolder}/index.js"
],
"windows": {
"runtimeExecutable": "${workspaceFolder}/node_modeules/.bin/electron.cmd"
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
}
},
{

View File

@@ -1,72 +0,0 @@
<h1 align="center">Sponsors &amp; Backers</h1>
Boostnote is an open source project. It's an independent project with its ongoing development made possible entirely thanks to the support by these awesome backers. If you'd like to join them, please consider:
- [Become a backer or sponsor on Open Collective.](https://opencollective.com/boostnoteio)
---
## Backers via OpenCollective
### [Gold Sponsors / $1,000 per month](https://opencollective.com/boostnoteio/order/2259)
- Get your logo on our Readme.md on GitHub and the frontpage of https://boostnote.io/.
### [Silver Sponsors / $250 per month](https://opencollective.com/boostnoteio/order/2257)
- Get your logo on our Readme.md on GitHub and the frontpage of https://boostnote.io/.
### [Bronze Sponsors / $50 per month](https://opencollective.com/boostnoteio/order/2258)
- Get your name and Url (or E-mail) on Readme.md on GitHub.
### [Backers3 / $10 per month](https://opencollective.com/boostnoteio/order/2176)
- [Ralph03](https://opencollective.com/ralph03)
- [Nikolas Dan](https://opencollective.com/nikolas-dan)
### [Backers2 / $5 per month](https://opencollective.com/boostnoteio/order/2175)
- [Yeojong Kim](https://twitter.com/yeojoy)
- [Scotia Draven](https://opencollective.com/scotia-draven)
- [A. J. Vargas](https://opencollective.com/aj-vargas)
### [Backers1](https://opencollective.com/boostnoteio/order/2563) and One-time sponsors
- Ryosuke Tamura - $30
- tatoosh11 - $10
- Alexander Borovkov - $10
- spoonhoop - $5
- Drew Williams - $2
- Andy Shaw - $2
- mysafesky -$2
---
## Backers via Bountysource
https://salt.bountysource.com/teams/boostnote
- Kuzz - $65
- Intense Raiden - $45
- ravy22 - $25
- trentpolack - $20
- hikariru - $10
- kolchan11 - $10
- RonWalker22 - $10
- hocchuc - $5
- Adam - $5
- Steve - $5
- evmin - $5

29
FAQ.md Normal file
View File

@@ -0,0 +1,29 @@
# Frequently Asked Questions
<details><summary>Allowing dangerous HTML tags</summary>
Sometimes it is useful to allow dangerous HTML tags to add interactivity to your notebook. One of the example is to use details/summary as a way to expand/collaps your todo-list.
* How to enable:
* Go to **Preferences****Interface****Sanitization****Allow dangerous html tags**
* Example note: Multiple todo-list
* Create new notes
* Paste the below code, and you'll see that you can expand/collaps the todo-list, and you can have multiple todo-list in your note.
```html
<details><summary>What I want to do</summary>
- [x] Create an awesome feature X
- [ ] Do my homework
</details>
```
</details>
## Other questions
You can ask [here][ISSUES]
[ISSUES]: https://github.com/BoostIO/Boostnote/issues

View File

@@ -2,7 +2,7 @@ GPL-3.0
Boostnote - an open source note-taking app made for programmers just like you.
Copyright (C) 2017 - 2018 BoostIO
Copyright (C) 2017 - 2019 BoostIO
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by

36
PULL_REQUEST_TEMPLATE.md Normal file
View File

@@ -0,0 +1,36 @@
<!--
Before submitting this PR, please make sure that:
- You have read and understand the contributing.md
- You have checked docs/code_style.md for information on code style
-->
## Description
<!--
Tell us what your PR does.
Please attach a screenshot/ video/gif image describing your PR if possible.
-->
## Issue fixed
<!--
Please list out all issue fixed with this PR here.
-->
<!--
Please make sure you fill in these checkboxes,
your PR will be reviewed faster if we know exactly what it does.
Change :white_circle: to :radio_button: in all the options that apply
-->
## Type of changes
- :white_circle: Bug fix (Change that fixed an issue)
- :white_circle: Breaking change (Change that can cause existing functionality to change)
- :white_circle: Improvement (Change that improves the code. Maybe performance or development improvement)
- :white_circle: Feature (Change that adds new functionality)
- :white_circle: Documentation change (Change that modifies documentation. Maybe typo fixes)
## Checklist:
- :white_circle: My code follows [the project code style](docs/code_style.md)
- :white_circle: I have written test for my code and it has been tested
- :white_circle: All existing tests have been passed
- :white_circle: I have attached a screenshot/video to visualize my change if possible

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
.codeEditor-typo
text-decoration underline wavy red
.spellcheck-select
border: none

View File

@@ -0,0 +1,68 @@
import React from 'react'
import PropTypes from 'prop-types'
import { SketchPicker } from 'react-color'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ColorPicker.styl'
const componentHeight = 330
class ColorPicker extends React.Component {
constructor (props) {
super(props)
this.state = {
color: this.props.color || '#939395'
}
this.onColorChange = this.onColorChange.bind(this)
this.handleConfirm = this.handleConfirm.bind(this)
}
componentWillReceiveProps (nextProps) {
this.onColorChange(nextProps.color)
}
onColorChange (color) {
this.setState({
color
})
}
handleConfirm () {
this.props.onConfirm(this.state.color)
}
render () {
const { onReset, onCancel, targetRect } = this.props
const { color } = this.state
const clientHeight = document.body.clientHeight
const alignX = targetRect.right + 4
let alignY = targetRect.top
if (targetRect.top + componentHeight > clientHeight) {
alignY = targetRect.bottom - componentHeight
}
return (
<div styleName='colorPicker' style={{top: `${alignY}px`, left: `${alignX}px`}}>
<div styleName='cover' onClick={onCancel} />
<SketchPicker color={color} onChange={this.onColorChange} />
<div styleName='footer'>
<button styleName='btn-reset' onClick={onReset}>Reset</button>
<button styleName='btn-cancel' onClick={onCancel}>Cancel</button>
<button styleName='btn-confirm' onClick={this.handleConfirm}>Confirm</button>
</div>
</div>
)
}
}
ColorPicker.propTypes = {
color: PropTypes.string,
targetRect: PropTypes.object,
onConfirm: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
onReset: PropTypes.func.isRequired
}
export default CSSModules(ColorPicker, styles)

View File

@@ -0,0 +1,39 @@
.colorPicker
position fixed
z-index 2
display flex
flex-direction column
.cover
position fixed
top 0
right 0
bottom 0
left 0
.footer
display flex
justify-content center
z-index 2
align-items center
& > button + button
margin-left 10px
.btn-cancel,
.btn-confirm,
.btn-reset
vertical-align middle
height 25px
margin-top 2.5px
border-radius 2px
border none
padding 0 5px
background-color $default-button-background
&:hover
background-color $default-button-background--hover
.btn-confirm
background-color #1EC38B
&:hover
background-color darken(#1EC38B, 25%)

View File

@@ -7,6 +7,7 @@ import MarkdownPreview from 'browser/components/MarkdownPreview'
import eventEmitter from 'browser/main/lib/eventEmitter'
import { findStorage } from 'browser/lib/findStorage'
import ConfigManager from 'browser/main/lib/ConfigManager'
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
class MarkdownEditor extends React.Component {
constructor (props) {
@@ -19,10 +20,10 @@ class MarkdownEditor extends React.Component {
this.supportMdSelectionBold = [16, 17, 186]
this.state = {
status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'PREVIEW',
status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'CODE',
renderValue: props.value,
keyPressed: new Set(),
isLocked: false
isLocked: props.isLocked
}
this.lockEditorCode = () => this.handleLockEditor()
@@ -31,6 +32,7 @@ class MarkdownEditor extends React.Component {
componentDidMount () {
this.value = this.refs.code.value
eventEmitter.on('editor:lock', this.lockEditorCode)
eventEmitter.on('editor:focus', this.focusEditor.bind(this))
}
componentDidUpdate () {
@@ -46,6 +48,15 @@ class MarkdownEditor extends React.Component {
componentWillUnmount () {
this.cancelQueue()
eventEmitter.off('editor:lock', this.lockEditorCode)
eventEmitter.off('editor:focus', this.focusEditor.bind(this))
}
focusEditor () {
this.setState({
status: 'CODE'
}, () => {
this.refs.code.focus()
})
}
queueRendering (value) {
@@ -75,6 +86,7 @@ class MarkdownEditor extends React.Component {
}
handleContextMenu (e) {
if (this.state.isLocked) return
const { config } = this.props
if (config.editor.switchPreview === 'RIGHTCLICK') {
const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW'
@@ -107,7 +119,7 @@ class MarkdownEditor extends React.Component {
status: 'PREVIEW'
}, () => {
this.refs.preview.focus()
this.refs.preview.scrollTo(cursorPosition.line)
this.refs.preview.scrollToRow(cursorPosition.line)
})
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
}
@@ -147,22 +159,25 @@ class MarkdownEditor extends React.Component {
e.preventDefault()
e.stopPropagation()
const idMatch = /checkbox-([0-9]+)/
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i
const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/
const checkReplace = /\[x]/i
const uncheckReplace = /\[ ]/
if (idMatch.test(e.target.getAttribute('id'))) {
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
const lines = this.refs.code.value
.split('\n')
const targetLine = lines[lineIndex]
let newLine = targetLine
if (targetLine.match(checkedMatch)) {
lines[lineIndex] = targetLine.replace(checkedMatch, '- [ ]')
newLine = targetLine.replace(checkReplace, '[ ]')
}
if (targetLine.match(uncheckedMatch)) {
lines[lineIndex] = targetLine.replace(uncheckedMatch, '- [x]')
newLine = targetLine.replace(uncheckReplace, '[x]')
}
this.refs.code.setValue(lines.join('\n'))
this.refs.code.setLineContent(lineIndex, newLine)
}
}
@@ -219,6 +234,28 @@ class MarkdownEditor extends React.Component {
this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`)
}
handleDropImage (dropEvent) {
dropEvent.preventDefault()
const { storageKey, noteKey } = this.props
this.setState({
status: 'CODE'
}, () => {
this.refs.code.focus()
this.refs.code.editor.execCommand('goDocEnd')
this.refs.code.editor.execCommand('goLineEnd')
this.refs.code.editor.execCommand('newlineAndIndent')
attachmentManagement.handleAttachmentDrop(
this.refs.code,
storageKey,
noteKey,
dropEvent
)
})
}
handleKeyUp (e) {
const keyPressed = this.state.keyPressed
keyPressed.delete(e.keyCode)
@@ -230,7 +267,7 @@ class MarkdownEditor extends React.Component {
}
render () {
const {className, value, config, storageKey, noteKey} = this.props
const {className, value, config, storageKey, noteKey, linesHighlighted} = this.props
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
@@ -268,13 +305,26 @@ class MarkdownEditor extends React.Component {
enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers}
displayLineNumbers={config.editor.displayLineNumbers}
lineWrapping
matchingPairs={config.editor.matchingPairs}
matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs}
scrollPastEnd={config.editor.scrollPastEnd}
storageKey={storageKey}
noteKey={noteKey}
fetchUrlTitle={config.editor.fetchUrlTitle}
enableTableEditor={config.editor.enableTableEditor}
linesHighlighted={linesHighlighted}
onChange={(e) => this.handleChange(e)}
onBlur={(e) => this.handleBlur(e)}
spellCheck={config.editor.spellcheck}
enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey}
switchPreview={config.editor.switchPreview}
enableMarkdownLint={config.editor.enableMarkdownLint}
customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
prettierConfig={config.editor.prettierConfig}
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
/>
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
? 'preview'
@@ -294,6 +344,7 @@ class MarkdownEditor extends React.Component {
smartArrows={config.preview.smartArrows}
breaks={config.preview.breaks}
sanitize={config.preview.sanitize}
mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
ref='preview'
onContextMenu={(e) => this.handleContextMenu(e)}
onDoubleClick={(e) => this.handleDoubleClick(e)}
@@ -308,6 +359,7 @@ class MarkdownEditor extends React.Component {
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox}
onDrop={(e) => this.handleDropImage(e)}
/>
</div>
)

View File

@@ -8,7 +8,7 @@ import consts from 'browser/lib/consts'
import Raphael from 'raphael'
import flowchart from 'flowchart'
import mermaidRender from './render/MermaidRender'
import SequenceDiagram from 'js-sequence-diagrams'
import SequenceDiagram from '@rokt33r/js-sequence-diagrams'
import Chart from 'chart.js'
import eventEmitter from 'browser/main/lib/eventEmitter'
import htmlTextHelper from 'browser/lib/htmlTextHelper'
@@ -18,12 +18,14 @@ import mdurl from 'mdurl'
import exportNote from 'browser/main/lib/dataApi/exportNote'
import { escapeHtmlCharacters } from 'browser/lib/utils'
import yaml from 'js-yaml'
import context from 'browser/lib/context'
import { render } from 'react-dom'
import Carousel from 'react-image-carousel'
import ConfigManager from '../main/lib/ConfigManager'
import i18n from 'browser/lib/i18n'
import fs from 'fs'
const { remote, shell } = require('electron')
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
const buildMarkdownPreviewContextMenu = require('browser/lib/contextMenuBuilder').buildMarkdownPreviewContextMenu
const { app } = remote
const path = require('path')
@@ -31,27 +33,39 @@ const fileUrl = require('file-url')
const dialog = remote.dialog
const uri2path = require('file-uri-to-path')
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const appPath = fileUrl(
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
)
const CSS_FILES = [
`${appPath}/node_modules/katex/dist/katex.min.css`,
`${appPath}/node_modules/codemirror/lib/codemirror.css`
`${appPath}/node_modules/codemirror/lib/codemirror.css`,
`${appPath}/node_modules/react-image-carousel/lib/css/main.min.css`
]
function buildStyle (
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
) {
/**
* @param {Object} opts
* @param {String} opts.fontFamily
* @param {Numberl} opts.fontSize
* @param {String} opts.codeBlockFontFamily
* @param {String} opts.theme
* @param {Boolean} [opts.lineNumber] Should show line number
* @param {Boolean} [opts.scrollPastEnd]
* @param {Boolean} [opts.allowCustomCSS] Should add custom css
* @param {String} [opts.customCSS] Will be added to bottom, only if `opts.allowCustomCSS` is truthy
* @returns {String}
*/
function buildStyle (opts) {
const {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = opts
return `
@font-face {
font-family: 'Lato';
@@ -81,12 +95,17 @@ function buildStyle (
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
}
${markdownStyle}
body {
font-family: '${fontFamily.join("','")}';
font-size: ${fontSize}px;
${scrollPastEnd && 'padding-bottom: 90vh;'}
${scrollPastEnd ? `
padding-bottom: 90vh;
box-sizing: border-box;
`
: ''}
}
@media print {
body {
@@ -162,6 +181,10 @@ const scrollBarStyle = `
::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.15);
}
::-webkit-scrollbar-track-piece {
background-color: inherit;
}
`
const scrollBarDarkStyle = `
::-webkit-scrollbar {
@@ -171,6 +194,10 @@ const scrollBarDarkStyle = `
::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.3);
}
::-webkit-scrollbar-track-piece {
background-color: inherit;
}
`
const OSX = global.process.platform === 'darwin'
@@ -188,6 +215,19 @@ const defaultCodeBlockFontFamily = [
'source-code-pro',
'monospace'
]
// return the line number of the line that used to generate the specified element
// return -1 if the line is not found
function getSourceLineNumberByElement (element) {
let isHasLineNumber = element.dataset.line !== undefined
let parent = element
while (!isHasLineNumber && parent.parentElement !== null) {
parent = parent.parentElement
isHasLineNumber = parent.dataset.line !== undefined
}
return parent.dataset.line !== undefined ? parseInt(parent.dataset.line) : -1
}
export default class MarkdownPreview extends React.Component {
constructor (props) {
super(props)
@@ -204,9 +244,11 @@ export default class MarkdownPreview extends React.Component {
this.saveAsTextHandler = () => this.handleSaveAsText()
this.saveAsMdHandler = () => this.handleSaveAsMd()
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
this.saveAsPdfHandler = () => this.handleSaveAsPdf()
this.printHandler = () => this.handlePrint()
this.resizeHandler = _.throttle(this.handleResize.bind(this), 100)
this.linkClickHandler = this.handlelinkClick.bind(this)
this.linkClickHandler = this.handleLinkClick.bind(this)
this.initMarkdown = this.initMarkdown.bind(this)
this.initMarkdown()
}
@@ -231,30 +273,12 @@ export default class MarkdownPreview extends React.Component {
}
handleContextMenu (event) {
// If a contextMenu handler was passed to us, use it instead of the self-defined one -> return
if (_.isFunction(this.props.onContextMenu)) {
const menu = buildMarkdownPreviewContextMenu(this, event)
const switchPreview = ConfigManager.get().editor.switchPreview
if (menu != null && switchPreview !== 'RIGHTCLICK') {
menu.popup(remote.getCurrentWindow())
} else if (_.isFunction(this.props.onContextMenu)) {
this.props.onContextMenu(event)
return
}
// No contextMenu was passed to us -> execute our own link-opener
if (event.target.tagName.toLowerCase() === 'a') {
const href = event.target.href
const isLocalFile = href.startsWith('file:')
if (isLocalFile) {
const absPath = uri2path(href)
try {
if (fs.lstatSync(absPath).isFile()) {
context.popup([
{
label: i18n.__('Show in explorer'),
click: (e) => shell.showItemInFolder(absPath)
}
])
}
} catch (e) {
console.log('Error while evaluating if the file is locally available', e)
}
}
}
}
@@ -263,14 +287,28 @@ export default class MarkdownPreview extends React.Component {
}
handleMouseDown (e) {
if (e.target != null) {
switch (e.target.tagName) {
case 'A':
case 'INPUT':
return null
const config = ConfigManager.get()
const clickElement = e.target
const targetTag = clickElement.tagName // The direct parent HTML of where was clicked ie "BODY" or "DIV"
const lineNumber = getSourceLineNumberByElement(clickElement) // Line location of element clicked.
if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') {
eventEmitter.emit('topbar:togglemodebutton', 'CODE')
}
if (e.ctrlKey) {
if (config.editor.type === 'SPLIT') {
if (lineNumber !== -1) {
eventEmitter.emit('line:jump', lineNumber)
}
} else {
if (lineNumber !== -1) {
eventEmitter.emit('editor:focus')
eventEmitter.emit('line:jump', lineNumber)
}
}
}
if (this.props.onMouseDown != null) this.props.onMouseDown(e)
if (this.props.onMouseDown != null && targetTag === 'BODY') this.props.onMouseDown(e)
}
handleMouseUp (e) {
@@ -286,95 +324,84 @@ export default class MarkdownPreview extends React.Component {
}
handleSaveAsMd () {
this.exportAsDocument('md', (noteContent, exportTasks) => {
let result = noteContent
if (this.props && this.props.storagePath && this.props.noteKey) {
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
this.props.storagePath
)
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: attachmentManagement.DESTINATION_FOLDER
})
})
result = attachmentManagement.removeStorageAndNoteReferences(
noteContent,
this.props.noteKey
)
}
return result
this.exportAsDocument('md')
}
htmlContentFormatter (noteContent, exportTasks, targetDir) {
const {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = this.getStyleParams()
const inlineStyles = buildStyle({
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
})
let body = this.refs.root.contentWindow.document.body.innerHTML
body = attachmentManagement.fixLocalURLS(
body,
this.props.storagePath
)
const files = [this.getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
files.forEach(file => {
if (global.process.platform === 'win32') {
file = file.replace('file:///', '')
} else {
file = file.replace('file://', '')
}
exportTasks.push({
src: file,
dst: 'css'
})
})
let styles = ''
files.forEach(file => {
styles += `<link rel="stylesheet" href="../css/${path.basename(file)}">`
})
return `<html>
<head>
<base href="file://${targetDir}/">
<meta charset="UTF-8">
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
<style id="style">${inlineStyles}</style>
${styles}
</head>
<body>${body}</body>
</html>`
}
handleSaveAsHtml () {
this.exportAsDocument('html', (noteContent, exportTasks) => {
const {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = this.getStyleParams()
this.exportAsDocument('html', (noteContent, exportTasks, targetDir) => Promise.resolve(this.htmlContentFormatter(noteContent, exportTasks, targetDir)))
}
const inlineStyles = buildStyle(
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
)
let body = this.markdown.render(noteContent)
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
this.props.storagePath
)
files.forEach(file => {
if (global.process.platform === 'win32') {
file = file.replace('file:///', '')
} else {
file = file.replace('file://', '')
}
exportTasks.push({
src: file,
dst: 'css'
handleSaveAsPdf () {
this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => {
const printout = new remote.BrowserWindow({show: false, webPreferences: {webSecurity: false, javascript: false}})
printout.loadURL('data:text/html;charset=UTF-8,' + this.htmlContentFormatter(noteContent, exportTasks, targetDir))
return new Promise((resolve, reject) => {
printout.webContents.on('did-finish-load', () => {
printout.webContents.printToPDF({}, (err, data) => {
if (err) reject(err)
else resolve(data)
printout.destroy()
})
})
})
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: attachmentManagement.DESTINATION_FOLDER
})
})
body = attachmentManagement.removeStorageAndNoteReferences(
body,
this.props.noteKey
)
let styles = ''
files.forEach(file => {
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
})
return `<html>
<head>
<meta charset="UTF-8">
<meta name = "viewport" content = "width = device-width, initial-scale = 1, maximum-scale = 1">
<style id="style">${inlineStyles}</style>
${styles}
</head>
<body>${body}</body>
</html>`
})
}
@@ -392,12 +419,14 @@ export default class MarkdownPreview extends React.Component {
if (filename) {
const content = this.props.value
const storage = this.props.storagePath
const nodeKey = this.props.noteKey
exportNote(storage, content, filename, contentFormatter)
exportNote(nodeKey, storage, content, filename, contentFormatter)
.then(res => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: `Exported to ${filename}`
message: `Exported to ${filename}`,
buttons: [i18n.__('Ok')]
})
})
.catch(err => {
@@ -423,6 +452,31 @@ export default class MarkdownPreview extends React.Component {
}
}
/**
* @description Convert special characters between three ```
* @param {string[]} splitWithCodeTag Array of HTML strings separated by three ```
* @returns {string} HTML in which special characters between three ``` have been converted
*/
escapeHtmlCharactersInCodeTag (splitWithCodeTag) {
for (let index = 0; index < splitWithCodeTag.length; index++) {
const codeTagRequired = (splitWithCodeTag[index] !== '\`\`\`' && index < splitWithCodeTag.length - 1)
if (codeTagRequired) {
splitWithCodeTag.splice((index + 1), 0, '\`\`\`')
}
}
let inCodeTag = false
let result = ''
for (let content of splitWithCodeTag) {
if (content === '\`\`\`') {
inCodeTag = !inCodeTag
} else if (inCodeTag) {
content = escapeHtmlCharacters(content)
}
result += content
}
return result
}
getScrollBarStyle () {
const { theme } = this.props
@@ -438,6 +492,8 @@ export default class MarkdownPreview extends React.Component {
}
componentDidMount () {
const { onDrop } = this.props
this.refs.root.setAttribute('sandbox', 'allow-scripts')
this.refs.root.contentWindow.document.body.addEventListener(
'contextmenu',
@@ -475,7 +531,7 @@ export default class MarkdownPreview extends React.Component {
)
this.refs.root.contentWindow.document.addEventListener(
'drop',
this.preventImageDroppedHandler
onDrop || this.preventImageDroppedHandler
)
this.refs.root.contentWindow.document.addEventListener(
'dragover',
@@ -485,13 +541,20 @@ export default class MarkdownPreview extends React.Component {
'scroll',
this.scrollHandler
)
this.refs.root.contentWindow.addEventListener(
'resize',
this.resizeHandler
)
eventEmitter.on('export:save-text', this.saveAsTextHandler)
eventEmitter.on('export:save-md', this.saveAsMdHandler)
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
eventEmitter.on('export:save-pdf', this.saveAsPdfHandler)
eventEmitter.on('print', this.printHandler)
}
componentWillUnmount () {
const { onDrop } = this.props
this.refs.root.contentWindow.document.body.removeEventListener(
'contextmenu',
this.contextMenuHandler
@@ -510,7 +573,7 @@ export default class MarkdownPreview extends React.Component {
)
this.refs.root.contentWindow.document.removeEventListener(
'drop',
this.preventImageDroppedHandler
onDrop || this.preventImageDroppedHandler
)
this.refs.root.contentWindow.document.removeEventListener(
'dragover',
@@ -520,23 +583,31 @@ export default class MarkdownPreview extends React.Component {
'scroll',
this.scrollHandler
)
this.refs.root.contentWindow.removeEventListener(
'resize',
this.resizeHandler
)
eventEmitter.off('export:save-text', this.saveAsTextHandler)
eventEmitter.off('export:save-md', this.saveAsMdHandler)
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
eventEmitter.off('export:save-pdf', this.saveAsPdfHandler)
eventEmitter.off('print', this.printHandler)
}
componentDidUpdate (prevProps) {
if (prevProps.value !== this.props.value) this.rewriteIframe()
// actual rewriteIframe function should be called only once
let needsRewriteIframe = false
if (prevProps.value !== this.props.value) needsRewriteIframe = true
if (
prevProps.smartQuotes !== this.props.smartQuotes ||
prevProps.sanitize !== this.props.sanitize ||
prevProps.mermaidHTMLLabel !== this.props.mermaidHTMLLabel ||
prevProps.smartArrows !== this.props.smartArrows ||
prevProps.breaks !== this.props.breaks ||
prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox
) {
this.initMarkdown()
this.rewriteIframe()
needsRewriteIframe = true
}
if (
prevProps.fontFamily !== this.props.fontFamily ||
@@ -551,8 +622,17 @@ export default class MarkdownPreview extends React.Component {
prevProps.customCSS !== this.props.customCSS
) {
this.applyStyle()
needsRewriteIframe = true
}
if (needsRewriteIframe) {
this.rewriteIframe()
}
// Should scroll to top after selecting another note
if (prevProps.noteKey !== this.props.noteKey) {
this.scrollTo(0, 0)
}
}
getStyleParams () {
@@ -608,8 +688,8 @@ export default class MarkdownPreview extends React.Component {
this.getWindow().document.getElementById(
'codeTheme'
).href = this.GetCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
).href = this.getCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle({
fontFamily,
fontSize,
codeBlockFontFamily,
@@ -618,17 +698,15 @@ export default class MarkdownPreview extends React.Component {
theme,
allowCustomCSS,
customCSS
)
})
}
GetCodeThemeLink (theme) {
theme = consts.THEMES.some(_theme => _theme === theme) &&
theme !== 'default'
? theme
: 'elegant'
return theme.startsWith('solarized')
? `${appPath}/node_modules/codemirror/theme/solarized.css`
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
getCodeThemeLink (name) {
const theme = consts.THEMES.find(theme => theme.name === name)
return theme != null
? theme.path
: `${appPath}/node_modules/codemirror/theme/elegant.css`
}
rewriteIframe () {
@@ -653,11 +731,17 @@ export default class MarkdownPreview extends React.Component {
indentSize,
showCopyNotification,
storagePath,
noteKey
noteKey,
sanitize,
mermaidHTMLLabel
} = this.props
let { value, codeBlockTheme } = this.props
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
if (sanitize === 'NONE') {
const splitWithCodeTag = value.split('```')
value = this.escapeHtmlCharactersInCodeTag(splitWithCodeTag)
}
const renderedHTML = this.markdown.render(value)
attachmentManagement.migrateAttachments(value, storagePath, noteKey)
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(
@@ -681,9 +765,9 @@ export default class MarkdownPreview extends React.Component {
}
)
codeBlockTheme = consts.THEMES.some(_theme => _theme === codeBlockTheme)
? codeBlockTheme
: 'default'
codeBlockTheme = consts.THEMES.find(theme => theme.name === codeBlockTheme)
const codeBlockThemeClassName = codeBlockTheme ? codeBlockTheme.className : 'cm-s-default'
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.code code'),
@@ -696,6 +780,8 @@ export default class MarkdownPreview extends React.Component {
copyIcon.innerHTML =
'<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
copyIcon.onclick = e => {
e.preventDefault()
e.stopPropagation()
copy(content)
if (showCopyNotification) {
this.notify('Saved to Clipboard!', {
@@ -704,14 +790,11 @@ export default class MarkdownPreview extends React.Component {
})
}
}
el.parentNode.appendChild(copyIcon)
el.innerHTML = ''
if (codeBlockTheme.indexOf('solarized') === 0) {
const [refThema, color] = codeBlockTheme.split(' ')
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
} else {
el.parentNode.className += ` cm-s-${codeBlockTheme}`
}
el.parentNode.className += ` ${codeBlockThemeClassName}`
CodeMirror.runMode(content, syntax.mime, el, {
tabSize: indentSize
})
@@ -782,6 +865,7 @@ export default class MarkdownPreview extends React.Component {
canvas.height = height.value + 'vh'
}
// eslint-disable-next-line no-unused-vars
const chart = new Chart(canvas, chartConfig)
} catch (e) {
el.className = 'chart-error'
@@ -792,7 +876,141 @@ export default class MarkdownPreview extends React.Component {
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
el => {
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme)
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme, mermaidHTMLLabel)
}
)
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.gallery'),
el => {
const images = el.innerHTML.split(/\n/g).filter(i => i.length > 0)
el.innerHTML = ''
const height = el.attributes.getNamedItem('data-height')
if (height && height.value !== 'undefined') {
el.style.height = height.value + 'vh'
}
let autoplay = el.attributes.getNamedItem('data-autoplay')
if (autoplay && autoplay.value !== 'undefined') {
autoplay = parseInt(autoplay.value, 10) || 0
} else {
autoplay = 0
}
render(
<Carousel
images={images}
autoplay={autoplay}
/>,
el
)
}
)
const markdownPreviewIframe = document.querySelector('.MarkdownPreview')
const rect = markdownPreviewIframe.getBoundingClientRect()
const config = { attributes: true, subtree: true }
const imgObserver = new MutationObserver((mutationList) => {
for (const mu of mutationList) {
if (mu.target.className === 'carouselContent-enter-done') {
this.setImgOnClickEventHelper(mu.target, rect)
break
}
}
})
const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('img')
for (const img of imgList) {
const parentEl = img.parentElement
this.setImgOnClickEventHelper(img, rect)
imgObserver.observe(parentEl, config)
}
const aList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('a')
for (const a of aList) {
a.removeEventListener('click', this.linkClickHandler)
a.addEventListener('click', this.linkClickHandler)
}
}
setImgOnClickEventHelper (img, rect) {
img.onclick = () => {
const widthMagnification = document.body.clientWidth / img.width
const heightMagnification = document.body.clientHeight / img.height
const baseOnWidth = widthMagnification < heightMagnification
const magnification = baseOnWidth ? widthMagnification : heightMagnification
const zoomImgWidth = img.width * magnification
const zoomImgHeight = img.height * magnification
const zoomImgTop = (document.body.clientHeight - zoomImgHeight) / 2
const zoomImgLeft = (document.body.clientWidth - zoomImgWidth) / 2
const originalImgTop = img.y + rect.top
const originalImgLeft = img.x + rect.left
const originalImgRect = {
top: `${originalImgTop}px`,
left: `${originalImgLeft}px`,
width: `${img.width}px`,
height: `${img.height}px`
}
const zoomInImgRect = {
top: `${baseOnWidth ? zoomImgTop : 0}px`,
left: `${baseOnWidth ? 0 : zoomImgLeft}px`,
width: `${zoomImgWidth}px`,
height: `${zoomImgHeight}px`
}
const animationSpeed = 300
const zoomImg = document.createElement('img')
zoomImg.src = img.src
zoomImg.style = `
position: absolute;
top: ${baseOnWidth ? zoomImgTop : 0}px;
left: ${baseOnWidth ? 0 : zoomImgLeft}px;
width: ${zoomImgWidth};
height: ${zoomImgHeight}px;
`
zoomImg.animate([
originalImgRect,
zoomInImgRect
], animationSpeed)
const overlay = document.createElement('div')
overlay.style = `
background-color: rgba(0,0,0,0.5);
cursor: zoom-out;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: ${document.body.clientHeight}px;
z-index: 100;
`
overlay.onclick = () => {
zoomImg.style = `
position: absolute;
top: ${originalImgTop}px;
left: ${originalImgLeft}px;
width: ${img.width}px;
height: ${img.height}px;
`
const zoomOutImgAnimation = zoomImg.animate([
zoomInImgRect,
originalImgRect
], animationSpeed)
zoomOutImgAnimation.onfinish = () => overlay.remove()
}
overlay.appendChild(zoomImg)
document.body.appendChild(overlay)
}
}
handleResize () {
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll('svg[ratio]'),
el => {
el.setAttribute('height', el.clientWidth / el.getAttribute('ratio'))
}
)
}
@@ -805,7 +1023,11 @@ export default class MarkdownPreview extends React.Component {
return this.refs.root.contentWindow
}
scrollTo (targetRow) {
/**
* @public
* @param {Number} targetRow
*/
scrollToRow (targetRow) {
const blocks = this.getWindow().document.querySelectorAll(
'body>[data-line]'
)
@@ -815,12 +1037,21 @@ export default class MarkdownPreview extends React.Component {
const row = parseInt(block.getAttribute('data-line'))
if (row > targetRow || index === blocks.length - 1) {
block = blocks[index - 1]
block != null && this.getWindow().scrollTo(0, block.offsetTop)
block != null && this.scrollTo(0, block.offsetTop)
break
}
}
}
/**
* `document.body.scrollTo`
* @param {Number} x
* @param {Number} y
*/
scrollTo (x, y) {
this.getWindow().document.body.scrollTo(x, y)
}
preventImageDroppedHandler (e) {
e.preventDefault()
e.stopPropagation()
@@ -837,24 +1068,36 @@ export default class MarkdownPreview extends React.Component {
return new window.Notification(title, options)
}
handlelinkClick (e) {
handleLinkClick (e) {
e.preventDefault()
e.stopPropagation()
const href = e.target.href
const linkHash = href.split('/').pop()
const rawHref = e.target.getAttribute('href')
if (!rawHref) return // not checked href because parser will create file://... string for [empty link]()
const regexNoteInternalLink = /main.html#(.+)/
if (regexNoteInternalLink.test(linkHash)) {
const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1])
const targetElement = this.refs.root.contentWindow.document.getElementById(
targetId
)
const parser = document.createElement('a')
parser.href = rawHref
const isStartWithHash = rawHref[0] === '#'
const { href, hash } = parser
if (targetElement != null) {
this.getWindow().scrollTo(0, targetElement.offsetTop)
const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10
const extractIdRegex = /file:\/\/.*main.?\w*.html#/ // file://path/to/main(.development.)html
const regexNoteInternalLink = new RegExp(`${extractIdRegex.source}(.+)`)
if (isStartWithHash || regexNoteInternalLink.test(rawHref)) {
const posOfHash = linkHash.indexOf('#')
if (posOfHash > -1) {
const extractedId = linkHash.slice(posOfHash + 1)
const targetId = mdurl.encode(extractedId)
const targetElement = this.getWindow().document.getElementById(
targetId
)
if (targetElement != null) {
this.scrollTo(0, targetElement.offsetTop)
}
return
}
return
}
// this will match the new uuid v4 hash and the old hash

View File

@@ -24,9 +24,9 @@ class MarkdownSplitEditor extends React.Component {
this.refs.code.setValue(value)
}
handleOnChange () {
handleOnChange (e) {
this.value = this.refs.code.value
this.props.onChange()
this.props.onChange(e)
}
handleScroll (e) {
@@ -78,22 +78,25 @@ class MarkdownSplitEditor extends React.Component {
e.preventDefault()
e.stopPropagation()
const idMatch = /checkbox-([0-9]+)/
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
const checkedMatch = /^(\s*>?)*\s*[+\-*] \[x]/i
const uncheckedMatch = /^(\s*>?)*\s*[+\-*] \[ ]/
const checkReplace = /\[x]/i
const uncheckReplace = /\[ ]/
if (idMatch.test(e.target.getAttribute('id'))) {
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
const lines = this.refs.code.value
.split('\n')
const targetLine = lines[lineIndex]
let newLine = targetLine
if (targetLine.match(checkedMatch)) {
lines[lineIndex] = targetLine.replace(checkedMatch, '- [ ]')
newLine = targetLine.replace(checkReplace, '[ ]')
}
if (targetLine.match(uncheckedMatch)) {
lines[lineIndex] = targetLine.replace(uncheckedMatch, '- [x]')
newLine = targetLine.replace(uncheckReplace, '[x]')
}
this.refs.code.setValue(lines.join('\n'))
this.refs.code.setLineContent(lineIndex, newLine)
}
}
@@ -134,7 +137,7 @@ class MarkdownSplitEditor extends React.Component {
}
render () {
const {config, value, storageKey, noteKey} = this.props
const {config, value, storageKey, noteKey, linesHighlighted} = this.props
const storage = findStorage(storageKey)
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
@@ -148,7 +151,6 @@ class MarkdownSplitEditor extends React.Component {
onMouseMove={e => this.handleMouseMove(e)}
onMouseUp={e => this.handleMouseUp(e)}>
<CodeEditor
styleName='codeEditor'
ref='code'
width={this.state.codeEditorWidthInPercent + '%'}
mode='Boost Flavored Markdown'
@@ -158,6 +160,10 @@ class MarkdownSplitEditor extends React.Component {
fontFamily={config.editor.fontFamily}
fontSize={editorFontSize}
displayLineNumbers={config.editor.displayLineNumbers}
lineWrapping
matchingPairs={config.editor.matchingPairs}
matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
enableRulers={config.editor.enableRulers}
@@ -167,15 +173,22 @@ class MarkdownSplitEditor extends React.Component {
enableTableEditor={config.editor.enableTableEditor}
storageKey={storageKey}
noteKey={noteKey}
onChange={this.handleOnChange.bind(this)}
linesHighlighted={linesHighlighted}
onChange={(e) => this.handleOnChange(e)}
onScroll={this.handleScroll.bind(this)}
spellCheck={config.editor.spellcheck}
enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey}
switchPreview={config.editor.switchPreview}
enableMarkdownLint={config.editor.enableMarkdownLint}
customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
/>
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
<div styleName='slider-hitbox' />
</div>
<MarkdownPreview
style={previewStyle}
styleName='preview'
theme={config.ui.theme}
keyMap={config.editor.keyMap}
fontSize={config.preview.fontSize}
@@ -188,6 +201,7 @@ class MarkdownSplitEditor extends React.Component {
smartArrows={config.preview.smartArrows}
breaks={config.preview.breaks}
sanitize={config.preview.sanitize}
mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
ref='preview'
tabInde='0'
value={value}

View File

@@ -8,9 +8,30 @@
top -2px
width 0
z-index 0
border-left 1px solid $ui-borderColor
.slider-hitbox
absolute top bottom left right
width 7px
left -3px
z-index 10
cursor col-resize
body[data-theme="dark"]
.root
.slider
border-left 1px solid $ui-dark-borderColor
body[data-theme="solarized-dark"]
.root
.slider
border-left 1px solid $ui-solarized-dark-borderColor
body[data-theme="monokai"]
.root
.slider
border-left 1px solid $ui-monokai-borderColor
body[data-theme="dracula"]
.root
.slider
border-left 1px solid $ui-dracula-borderColor

View File

@@ -8,7 +8,7 @@ const ModalEscButton = ({
}) => (
<button styleName='escButton' onClick={handleEscButtonClick}>
<div styleName='esc-mark'>×</div>
<div styleName='esc-text'>esc</div>
<div>esc</div>
</button>
)

View File

@@ -16,8 +16,8 @@ const NavToggleButton = ({isFolded, handleToggleButtonClick}) => (
onClick={(e) => handleToggleButtonClick(e)}
>
{isFolded
? <i className='fa fa-angle-double-right' />
: <i className='fa fa-angle-double-left' />
? <i className='fa fa-angle-double-right fa-2x' />
: <i className='fa fa-angle-double-left fa-2x' />
}
</button>
)

View File

@@ -7,7 +7,7 @@
border-radius 16.5px
height 34px
width 34px
line-height 32px
line-height 100%
padding 0
&:hover
border: 1px solid #1EC38B;

View File

@@ -3,7 +3,9 @@
*/
import PropTypes from 'prop-types'
import React from 'react'
import { isArray } from 'lodash'
import { isArray, sortBy } from 'lodash'
import invertColor from 'invert-color'
import Emoji from 'react-emoji-render'
import CSSModules from 'browser/lib/CSSModules'
import { getTodoStatus } from 'browser/lib/getTodoStatus'
import styles from './NoteItem.styl'
@@ -13,29 +15,38 @@ import i18n from 'browser/lib/i18n'
/**
* @description Tag element component.
* @param {string} tagName
* @param {string} color
* @return {React.Component}
*/
const TagElement = ({ tagName }) => (
<span styleName='item-bottom-tagList-item' key={tagName}>
#{tagName}
</span>
)
const TagElement = ({ tagName, color }) => {
const style = {}
if (color) {
style.backgroundColor = color
style.color = invertColor(color, { black: '#222', white: '#f1f1f1', threshold: 0.3 })
}
return (
<span styleName='item-bottom-tagList-item' key={tagName} style={style}>
#{tagName}
</span>
)
}
/**
* @description Tag element list component.
* @param {Array|null} tags
* @param {boolean} showTagsAlphabetically
* @param {Object} coloredTags
* @return {React.Component}
*/
const TagElementList = (tags, showTagsAlphabetically) => {
const TagElementList = (tags, showTagsAlphabetically, coloredTags) => {
if (!isArray(tags)) {
return []
}
if (showTagsAlphabetically) {
return _.sortBy(tags).map(tag => TagElement({ tagName: tag }))
return sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
} else {
return tags.map(tag => TagElement({ tagName: tag }))
return tags.map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
}
}
@@ -46,6 +57,7 @@ const TagElementList = (tags, showTagsAlphabetically) => {
* @param {Function} handleNoteClick
* @param {Function} handleNoteContextMenu
* @param {Function} handleDragStart
* @param {Object} coloredTags
* @param {string} dateDisplay
*/
const NoteItem = ({
@@ -59,7 +71,8 @@ const NoteItem = ({
storageName,
folderName,
viewType,
showTagsAlphabetically
showTagsAlphabetically,
coloredTags
}) => (
<div
styleName={isActive ? 'item--active' : 'item'}
@@ -75,7 +88,7 @@ const NoteItem = ({
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />}
<div styleName='item-title'>
{note.title.trim().length > 0
? note.title
? <Emoji text={note.title} />
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>}
</div>
<div styleName='item-middle'>
@@ -97,7 +110,7 @@ const NoteItem = ({
<div styleName='item-bottom'>
<div styleName='item-bottom-tagList'>
{note.tags.length > 0
? TagElementList(note.tags, showTagsAlphabetically)
? TagElementList(note.tags, showTagsAlphabetically, coloredTags)
: <span
style={{ fontStyle: 'italic', opacity: 0.5 }}
styleName='item-bottom-tagList-empty'
@@ -127,6 +140,7 @@ const NoteItem = ({
NoteItem.propTypes = {
isActive: PropTypes.bool.isRequired,
dateDisplay: PropTypes.string.isRequired,
coloredTags: PropTypes.object,
note: PropTypes.shape({
storage: PropTypes.string.isRequired,
key: PropTypes.string.isRequired,
@@ -135,15 +149,14 @@ NoteItem.propTypes = {
tags: PropTypes.array,
isStarred: PropTypes.bool.isRequired,
isTrashed: PropTypes.bool.isRequired,
blog: {
blog: PropTypes.shape({
blogLink: PropTypes.string,
blogId: PropTypes.number
}
})
}),
handleNoteClick: PropTypes.func.isRequired,
handleNoteContextMenu: PropTypes.func.isRequired,
handleDragStart: PropTypes.func.isRequired,
handleDragEnd: PropTypes.func.isRequired
handleDragStart: PropTypes.func.isRequired
}
export default CSSModules(NoteItem, styles)

View File

@@ -74,7 +74,7 @@ SideNavFilter.propTypes = {
isStarredActive: PropTypes.bool.isRequired,
isTrashedActive: PropTypes.bool.isRequired,
handleStarredButtonClick: PropTypes.func.isRequired,
handleTrashdButtonClick: PropTypes.func.isRequired
handleTrashedButtonClick: PropTypes.func.isRequired
}
export default CSSModules(SideNavFilter, styles)

View File

@@ -114,7 +114,7 @@ class SnippetTab extends React.Component {
>
{snippet.name.trim().length > 0
? snippet.name
: <span styleName='button-unnamed'>
: <span>
{i18n.__('Unnamed')}
</span>
}

View File

@@ -3,19 +3,30 @@
flex 1
min-width 70px
overflow hidden
border-left 1px solid $ui-borderColor
border-top 1px solid $ui-borderColor
&:hover
background-color alpha($ui-button--active-backgroundColor, 20%)
.deleteButton
color $ui-inactive-text-color
&:hover
background-color darken($ui-backgroundColor, 15%)
&:active
color white
background-color $ui-active-color
color: $ui-text-color
visibility visible
transition 0.15s
.button
color: $ui-text-color
transition 0.15s
.root--active
@extend .root
min-width 100px
border-bottom $ui-border
background-color alpha($ui-button--active-backgroundColor, 60%)
.deleteButton
visibility visible
color: $ui-text-color
transition 0.15s
.button
font-weight bold
color: $ui-text-color
transition 0.15s
.button
width 100%
@@ -27,8 +38,7 @@
background-color transparent
transition 0.15s
border-left 4px solid transparent
&:hover
background-color $ui-button--hover-backgroundColor
color $ui-inactive-text-color
.deleteButton
position absolute
@@ -42,6 +52,7 @@
color $ui-inactive-text-color
background-color transparent
border-radius 2px
visibility hidden
.input
height 29px
@@ -50,76 +61,66 @@
width 100%
outline none
body[data-theme="default"], body[data-theme="white"]
.root--active
&:hover
background-color alpha($ui-button--active-backgroundColor, 60%)
body[data-theme="dark"]
.root
color $ui-dark-text-color
border-color $ui-dark-borderColor
border-top 1px solid $ui-dark-borderColor
&:hover
background-color $ui-dark-button--hover-backgroundColor
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
transition 0.15s
.button
color $ui-dark-text-color
transition 0.15s
.deleteButton
color $ui-dark-inactive-text-color
&:hover
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
&:active
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor
color $ui-dark-text-color
transition 0.15s
.root--active
color $ui-dark-text-color
border-color $ui-dark-borderColor
&:hover
background-color $ui-dark-button--hover-backgroundColor
.deleteButton
color $ui-dark-inactive-text-color
&:hover
background-color darken($ui-dark-button--hover-backgroundColor, 15%)
&:active
color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor
background-color $ui-dark-button--active-backgroundColor
border-left 1px solid $ui-dark-borderColor
border-top 1px solid $ui-dark-borderColor
.button
color $ui-dark-text-color
.deleteButton
color $ui-dark-text-color
.button
border none
color $ui-dark-text-color
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
&:hover
color $ui-dark-text-color
background-color $ui-dark-button--hover-backgroundColor
.input
background-color $ui-dark-button--hover-backgroundColor
background-color $ui-dark-button--active-backgroundColor
color $ui-dark-text-color
.deleteButton
color alpha($ui-dark-text-color, 30%)
transition 0.15s
body[data-theme="solarized-dark"]
.root
color $ui-solarized-dark-text-color
border-color $ui-dark-borderColor
&:hover
background-color $ui-solarized-dark-noteDetail-backgroundColor
.deleteButton
color $ui-solarized-dark-text-color
&:hover
background-color darken($ui-solarized-dark-noteDetail-backgroundColor, 15%)
&:active
color $ui-solarized-dark-text-color
background-color $ui-dark-button--active-backgroundColor
.root--active
color $ui-solarized-dark-text-color
border-color $ui-solarized-dark-borderColor
&:hover
background-color $ui-solarized-dark-noteDetail-backgroundColor
transition 0.15s
.deleteButton
color $ui-solarized-dark-text-color
&:hover
background-color darken($ui-solarized-dark-noteDetail-backgroundColor, 15%)
&:active
color $ui-solarized-dark-text-color
background-color $ui-dark-button--active-backgroundColor
color $ui-solarized-dark-button--active-color
transition 0.15s
.button
color $ui-solarized-dark-button--active-color
transition 0.15s
.root--active
color $ui-solarized-dark-button--active-color
background-color $ui-solarized-dark-button-backgroundColor
border-color $ui-solarized-dark-borderColor
.deleteButton
color $ui-solarized-dark-button--active-color
.button
color $ui-solarized-dark-button--active-color
.button
border none
@@ -127,101 +128,75 @@ body[data-theme="solarized-dark"]
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
&:hover
color $ui-solarized-dark-text-color
background-color $ui-solarized-dark-noteDetail-backgroundColor
.input
background-color $ui-solarized-dark-noteDetail-backgroundColor
color $ui-solarized-dark-text-color
.deleteButton
color alpha($ui-solarized-dark-text-color, 30%)
color $ui-solarized-dark-button--active-color
transition 0.15s
body[data-theme="monokai"]
.root
color $ui-monokai-text-color
border-color $ui-dark-borderColor
&:hover
background-color $ui-monokai-noteDetail-backgroundColor
.deleteButton
color $ui-monokai-text-color
&:hover
background-color darken($ui-monokai-noteDetail-backgroundColor, 15%)
&:active
color $ui-monokai-text-color
background-color $ui-dark-button--active-backgroundColor
.root--active
color $ui-monokai-text-color
border-color $ui-monokai-borderColor
&:hover
background-color $ui-monokai-noteDetail-backgroundColor
transition 0.15s
.deleteButton
color $ui-monokai-text-color
&:hover
background-color darken($ui-monokai-noteDetail-backgroundColor, 15%)
&:active
color $ui-monokai-text-color
background-color $ui-dark-button--active-backgroundColor
transition 0.15s
.button
color $ui-monokai-text-color
transition 0.15s
.root--active
color $ui-monokai-active-color
background-color $ui-monokai-button-backgroundColor
border-color $ui-monokai-borderColor
.deleteButton
color $ui-monokai-text-color
.button
color $ui-monokai-active-color
.button
border none
color $ui-monokai-text-color
color $ui-inactive-text-color
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
&:hover
color $ui-monokai-text-color
background-color $ui-monokai-noteDetail-backgroundColor
.input
background-color $ui-monokai-noteDetail-backgroundColor
color $ui-monokai-text-color
.deleteButton
color alpha($ui-monokai-text-color, 30%)
transition 0.15s
body[data-theme="dracula"]
.root
color $ui-dracula-text-color
border-color $ui-dark-borderColor
&:hover
background-color $ui-dracula-noteDetail-backgroundColor
.deleteButton
color $ui-dracula-text-color
&:hover
background-color darken($ui-dracula-noteDetail-backgroundColor, 15%)
&:active
color $ui-dracula-text-color
background-color $ui-dark-button--active-backgroundColor
.root--active
color $ui-dracula-text-color
border-color $ui-dracula-borderColor
&:hover
background-color $ui-dracula-noteDetail-backgroundColor
transition 0.15s
.deleteButton
color $ui-dracula-text-color
&:hover
background-color darken($ui-dracula-noteDetail-backgroundColor, 15%)
&:active
color $ui-dracula-text-color
background-color $ui-dark-button--active-backgroundColor
transition 0.15s
.button
color $ui-dracula-text-color
transition 0.15s
.root--active
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
border-color $ui-dracula-borderColor
.deleteButton
color $ui-dracula-text-color
.button
color $ui-dracula-active-color
.button
border none
color $ui-dracula-text-color
color $ui-inactive-text-color
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
&:hover
color $ui-dracula-text-color
background-color $ui-dracula-noteDetail-backgroundColor
.input
background-color $ui-dracula-noteDetail-backgroundColor
color $ui-dracula-text-color
.deleteButton
color alpha($ui-dracula-text-color, 30%)
color $ui-dracula-text-color

View File

@@ -10,11 +10,12 @@ import CSSModules from 'browser/lib/CSSModules'
* @param {string} name
* @param {Function} handleClickTagListItem
* @param {Function} handleClickNarrowToTag
* @param {bool} isActive
* @param {bool} isRelated
* @param {boolean} isActive
* @param {boolean} isRelated
* @param {string} bgColor tab backgroundColor
*/
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count}) => (
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count, color}) => (
<div styleName='tagList-itemContainer' onContextMenu={e => handleContextMenu(e, name)}>
{isRelated
? <button styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} onClick={() => handleClickNarrowToTag(name)}>
@@ -23,6 +24,7 @@ const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, hand
: <div styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} />
}
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
<span styleName='tagList-item-color' style={{backgroundColor: color || 'transparent'}} />
<span styleName='tagList-item-name'>
{`# ${name}`}
<span styleName='tagList-item-count'>{count !== 0 ? count : ''}</span>
@@ -33,7 +35,8 @@ const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, hand
TagListItem.propTypes = {
name: PropTypes.string.isRequired,
handleClickTagListItem: PropTypes.func.isRequired
handleClickTagListItem: PropTypes.func.isRequired,
color: PropTypes.string
}
export default CSSModules(TagListItem, styles)

View File

@@ -71,6 +71,11 @@
padding-right 15px
font-size 13px
.tagList-item-color
height 26px
width 3px
display inline-block
body[data-theme="white"]
.tagList-item
color $ui-inactive-text-color

View File

@@ -25,10 +25,10 @@ const TodoProcess = ({
)
TodoProcess.propTypes = {
todoStatus: {
todoStatus: PropTypes.exact({
total: PropTypes.number.isRequired,
completed: PropTypes.number.isRequired
}
})
}
export default CSSModules(TodoProcess, styles)

View File

@@ -55,11 +55,14 @@ body
line-height 1.6
overflow-x hidden
background-color $ui-noteDetail-backgroundColor
// do not allow display line breaks
.katex-display > .katex
white-space nowrap
// allow inline line breaks
.katex
font 400 1.2em 'KaTeX_Main'
line-height 1.2em
white-space initial
text-indent 0
.katex .katex-html
display inline-flex
.katex .mfrac>.vlist>span:nth-child(2)
top 0 !important
.katex-error
@@ -162,6 +165,7 @@ p
white-space pre-line
word-wrap break-word
img
cursor zoom-in
max-width 100%
strong, b
font-weight bold
@@ -183,6 +187,10 @@ ul
display list-item
&.taskListItem
list-style none
&>input
margin-left -1.6em
&>p
margin-left -1.8em
p
margin 0
&>li>ul, &>li>ol
@@ -355,7 +363,10 @@ admonition_types = {
danger: {color: #c2185b, icon: "block"},
caution: {color: #ffa726, icon: "warning"},
error: {color: #d32f2f, icon: "error_outline"},
attention: {color: #455a64, icon: "priority_high"}
question: {color: #64dd17, icon: "help_outline"},
quote: {color: #9e9e9e, icon: "format_quote"},
abstract: {color: #00b0ff, icon: "subject"},
attention: {color: #455a64, icon: "priority_high"},
}
for name, val in admonition_types
@@ -416,6 +427,67 @@ pre.fence
canvas, svg
max-width 100% !important
svg[ratio]
width 100%
.gallery
width 100%
height 50vh
.carousel
.carousel-main img
min-width auto
max-width 100%
min-height auto
max-height 100%
.carousel-footer::-webkit-scrollbar-corner
background-color transparent
.carousel-main, .carousel-footer
background-color $ui-noteDetail-backgroundColor
.prev, .next
color $ui-text-color
background-color $ui-tag-backgroundColor
.markdownIt-TOC-wrapper
list-style none
position fixed
right 0
top 0
margin-left 15px
z-index 1000
transition transform .2s ease-in-out
transform translateX(100%)
.markdownIt-TOC
display block
max-height 90vh
overflow-y auto
padding 25px
padding-left 38px
&,
&:before
background-color $ui-dark-backgroundColor
color: $ui-dark-text-color
&:hover
transform translateX(-15px)
&:before
content 'TOC'
position absolute
width 60px
height 30px
top 60px
left -29px
display flex
align-items center
justify-content center
transform-origin top left
transform rotate(-90deg)
themeDarkBackground = darken(#21252B, 10%)
themeDarkText = #f9f9f9
themeDarkBorder = lighten(themeDarkBackground, 20%)
@@ -475,6 +547,22 @@ body[data-theme="dark"]
border-color themeDarkBorder
background-color themeDarkPreview
pre.fence
.gallery
.carousel-main, .carousel-footer
background-color $ui-dark-noteDetail-backgroundColor
.prev, .next
color $ui-dark-text-color
background-color $ui-dark-tag-backgroundColor
.markdownIt-TOC-wrapper
&,
&:before
background-color darken(themeDarkBackground, 5%)
color themeDarkText
themeSolarizedDarkBackground = $ui-solarized-dark-noteDetail-backgroundColor
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
themeSolarizedDarkTableHead = themeSolarizedDarkTableEven
@@ -483,7 +571,7 @@ themeSolarizedDarkTableBorder = themeDarkBorder
body[data-theme="solarized-dark"]
color $ui-solarized-dark-text-color
border-color themeDarkBorder
background-color $ui-solarized-dark-noteDetail-backgroundColor
background-color themeSolarizedDarkBackground
table
thead
tr
@@ -510,6 +598,21 @@ body[data-theme="solarized-dark"]
border-color themeDarkBorder
background-color $ui-solarized-dark-noteDetail-backgroundColor
pre.fence
.gallery
.carousel-main, .carousel-footer
background-color $ui-solarized-dark-noteDetail-backgroundColor
.prev, .next
color $ui-solarized-dark-button--active-color
background-color $ui-solarized-dark-button-backgroundColor
.markdownIt-TOC-wrapper
&,
&:before
background-color darken(themeSolarizedDarkBackground, 15%)
color themeDarkText
themeMonokaiBackground = $ui-monokai-noteDetail-backgroundColor
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
themeMonokaiTableHead = themeMonokaiTableEven
@@ -518,7 +621,7 @@ themeMonokaiTableBorder = themeDarkBorder
body[data-theme="monokai"]
color $ui-monokai-text-color
border-color themeDarkBorder
background-color $ui-monokai-noteDetail-backgroundColor
background-color themeMonokaiBackground
table
thead
tr
@@ -538,6 +641,7 @@ body[data-theme="monokai"]
border-right solid 1px themeMonokaiTableBorder
kbd
background-color themeDarkBackground
dl
border-color themeDarkBorder
background-color themeMonokaiTableHead
@@ -547,6 +651,21 @@ body[data-theme="monokai"]
border-color themeDarkBorder
background-color $ui-monokai-noteDetail-backgroundColor
pre.fence
.gallery
.carousel-main, .carousel-footer
background-color $ui-monokai-noteDetail-backgroundColor
.prev, .next
color $ui-monokai-button--active-color
background-color $ui-monokai-button-backgroundColor
.markdownIt-TOC-wrapper
&,
&:before
background-color darken(themeMonokaiBackground, 15%)
color themeDarkText
themeDraculaBackground = $ui-dracula-noteDetail-backgroundColor
themeDraculaTableOdd = $ui-dracula-noteDetail-backgroundColor
themeDraculaTableEven = darken($ui-dracula-noteDetail-backgroundColor, 10%)
themeDraculaTableHead = themeDraculaTableEven
@@ -555,7 +674,7 @@ themeDraculaTableBorder = themeDarkBorder
body[data-theme="dracula"]
color $ui-dracula-text-color
border-color themeDarkBorder
background-color $ui-dracula-noteDetail-backgroundColor
background-color themeDraculaBackground
table
thead
tr
@@ -575,6 +694,7 @@ body[data-theme="dracula"]
border-right solid 1px themeDraculaTableBorder
kbd
background-color themeDarkBackground
dl
border-color themeDarkBorder
background-color themeDraculaTableHead
@@ -583,3 +703,17 @@ body[data-theme="dracula"]
dd
border-color themeDarkBorder
background-color $ui-dracula-noteDetail-backgroundColor
pre.fence
.gallery
.carousel-main, .carousel-footer
background-color $ui-dracula-noteDetail-backgroundColor
.prev, .next
color $ui-dracula-button--active-color
background-color $ui-dracula-button-backgroundColor
.markdownIt-TOC-wrapper
&,
&:before
background-color darken(themeDraculaBackground, 15%)
color themeDarkText

View File

@@ -19,20 +19,43 @@ function getId () {
return id
}
function render (element, content, theme) {
function render (element, content, theme, enableHTMLLabel) {
try {
const height = element.attributes.getNamedItem('data-height')
if (height && height.value !== 'undefined') {
const isPredefined = height && height.value !== 'undefined'
if (isPredefined) {
element.style.height = height.value + 'vh'
}
let isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula'
const isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula'
mermaidAPI.initialize({
theme: isDarkTheme ? 'dark' : 'default',
themeCSS: isDarkTheme ? darkThemeStyling : '',
useMaxWidth: false
flowchart: {
htmlLabels: enableHTMLLabel
},
gantt: {
useWidth: element.clientWidth
}
})
mermaidAPI.render(getId(), content, (svgGraph) => {
element.innerHTML = svgGraph
if (!isPredefined) {
const el = element.firstChild
const viewBox = el.getAttribute('viewBox').split(' ')
let ratio = viewBox[2] / viewBox[3]
if (el.style.maxWidth) {
const maxWidth = parseFloat(el.style.maxWidth)
ratio *= el.parentNode.clientWidth / maxWidth
}
el.setAttribute('ratio', ratio)
el.setAttribute('height', el.parentNode.clientWidth / ratio)
console.log(el)
}
})
} catch (e) {
element.className = 'mermaid-error'

View File

@@ -0,0 +1,78 @@
export const languageMaps = {
brainfuck: 'Brainfuck',
cpp: 'C++',
cs: 'C#',
clojure: 'Clojure',
'clojure-repl': 'ClojureScript',
cmake: 'CMake',
coffeescript: 'CoffeeScript',
crystal: 'Crystal',
css: 'CSS',
d: 'D',
dart: 'Dart',
delphi: 'Pascal',
diff: 'Diff',
django: 'Django',
dockerfile: 'Dockerfile',
ebnf: 'EBNF',
elm: 'Elm',
erlang: 'Erlang',
'erlang-repl': 'Erlang',
fortran: 'Fortran',
fsharp: 'F#',
gherkin: 'Gherkin',
go: 'Go',
groovy: 'Groovy',
haml: 'HAML',
haskell: 'Haskell',
haxe: 'Haxe',
http: 'HTTP',
ini: 'toml',
java: 'Java',
javascript: 'JavaScript',
json: 'JSON',
julia: 'Julia',
kotlin: 'Kotlin',
less: 'LESS',
livescript: 'LiveScript',
lua: 'Lua',
markdown: 'Markdown',
mathematica: 'Mathematica',
nginx: 'Nginx',
nsis: 'NSIS',
objectivec: 'Objective-C',
ocaml: 'Ocaml',
perl: 'Perl',
php: 'PHP',
powershell: 'PowerShell',
properties: 'Properties files',
protobuf: 'ProtoBuf',
python: 'Python',
puppet: 'Puppet',
q: 'Q',
r: 'R',
ruby: 'Ruby',
rust: 'Rust',
sas: 'SAS',
scala: 'Scala',
scheme: 'Scheme',
scss: 'SCSS',
shell: 'Shell',
smalltalk: 'Smalltalk',
sml: 'SML',
sql: 'SQL',
stylus: 'Stylus',
swift: 'Swift',
tcl: 'Tcl',
tex: 'LaTex',
typescript: 'TypeScript',
twig: 'Twig',
vbnet: 'VB.NET',
vbscript: 'VBScript',
verilog: 'Verilog',
vhdl: 'VHDL',
xml: 'HTML',
xquery: 'XQuery',
yaml: 'YAML',
elixir: 'Elixir'
}

View File

@@ -1,5 +1,5 @@
import CSSModules from 'react-css-modules'
export default function (component, styles) {
return CSSModules(component, styles, {errorWhenNotFound: false})
return CSSModules(component, styles, {handleNotFoundStyleName: 'log'})
}

View File

@@ -11,6 +11,10 @@ const languages = [
name: 'Chinese (zh-TW)',
locale: 'zh-TW'
},
{
name: 'Czech',
locale: 'cs'
},
{
name: 'Danish',
locale: 'da'
@@ -62,10 +66,12 @@ const languages = [
{
name: 'Spanish',
locale: 'es-ES'
}, {
},
{
name: 'Turkish',
locale: 'tr'
}, {
},
{
name: 'Thai',
locale: 'th'
}
@@ -82,4 +88,3 @@ module.exports = {
return languages
}
}

View File

@@ -0,0 +1,91 @@
import crypto from 'crypto'
import fs from 'fs'
import consts from './consts'
class SnippetManager {
constructor () {
this.defaultSnippet = [
{
id: crypto.randomBytes(16).toString('hex'),
name: 'Dummy text',
prefix: ['lorem', 'ipsum'],
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
}
]
this.snippets = []
this.expandSnippet = this.expandSnippet.bind(this)
this.init = this.init.bind(this)
this.assignSnippets = this.assignSnippets.bind(this)
}
init () {
if (fs.existsSync(consts.SNIPPET_FILE)) {
try {
this.snippets = JSON.parse(
fs.readFileSync(consts.SNIPPET_FILE, { encoding: 'UTF-8' })
)
} catch (error) {
console.log('Error while parsing snippet file')
}
return
}
fs.writeFileSync(
consts.SNIPPET_FILE,
JSON.stringify(this.defaultSnippet, null, 4),
'utf8'
)
this.snippets = this.defaultSnippet
}
assignSnippets (snippets) {
this.snippets = snippets
}
expandSnippet (wordBeforeCursor, cursor, cm) {
const templateCursorString = ':{}'
for (let i = 0; i < this.snippets.length; i++) {
if (this.snippets[i].prefix.indexOf(wordBeforeCursor.text) === -1) {
continue
}
if (this.snippets[i].content.indexOf(templateCursorString) !== -1) {
const snippetLines = this.snippets[i].content.split('\n')
let cursorLineNumber = 0
let cursorLinePosition = 0
let cursorIndex
for (let j = 0; j < snippetLines.length; j++) {
cursorIndex = snippetLines[j].indexOf(templateCursorString)
if (cursorIndex !== -1) {
cursorLineNumber = j
cursorLinePosition = cursorIndex
break
}
}
cm.replaceRange(
this.snippets[i].content.replace(templateCursorString, ''),
wordBeforeCursor.range.from,
wordBeforeCursor.range.to
)
cm.setCursor({
line: cursor.line + cursorLineNumber,
ch: cursorLinePosition + cursor.ch - wordBeforeCursor.text.length
})
} else {
cm.replaceRange(
this.snippets[i].content,
wordBeforeCursor.range.from,
wordBeforeCursor.range.to
)
}
return true
}
return false
}
}
const manager = new SnippetManager()
export default manager

View File

@@ -6,7 +6,7 @@ const { dialog } = remote
export function confirmDeleteNote (confirmDeletion, permanent) {
if (confirmDeletion || permanent) {
const alertConfig = {
ype: 'warning',
type: 'warning',
message: i18n.__('Confirm note deletion'),
detail: i18n.__('This will permanently remove this note.'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]

View File

@@ -3,14 +3,43 @@ const fs = require('sander')
const { remote } = require('electron')
const { app } = remote
const themePath = process.env.NODE_ENV === 'production'
? path.join(app.getAppPath(), './node_modules/codemirror/theme')
: require('path').resolve('./node_modules/codemirror/theme')
const themes = fs.readdirSync(themePath)
.map((themePath) => {
return themePath.substring(0, themePath.lastIndexOf('.'))
})
themes.splice(themes.indexOf('solarized'), 1, 'solarized dark', 'solarized light')
const CODEMIRROR_THEME_PATH = 'node_modules/codemirror/theme'
const CODEMIRROR_EXTRA_THEME_PATH = 'extra_scripts/codemirror/theme'
const isProduction = process.env.NODE_ENV === 'production'
const paths = [
isProduction ? path.join(app.getAppPath(), CODEMIRROR_THEME_PATH) : path.resolve(CODEMIRROR_THEME_PATH),
isProduction ? path.join(app.getAppPath(), CODEMIRROR_EXTRA_THEME_PATH) : path.resolve(CODEMIRROR_EXTRA_THEME_PATH)
]
const themes = paths
.map(directory => fs.readdirSync(directory).map(file => {
const name = file.substring(0, file.lastIndexOf('.'))
return {
name,
path: path.join(directory, file),
className: `cm-s-${name}`
}
}))
.reduce((accumulator, value) => accumulator.concat(value), [])
.sort((a, b) => a.name.localeCompare(b.name))
themes.splice(themes.findIndex(({ name }) => name === 'solarized'), 1, {
name: 'solarized dark',
path: path.join(paths[0], 'solarized.css'),
className: `cm-s-solarized cm-s-dark`
}, {
name: 'solarized light',
path: path.join(paths[0], 'solarized.css'),
className: `cm-s-solarized cm-s-light`
})
themes.splice(0, 0, {
name: 'default',
path: path.join(paths[0], 'elegant.css'),
className: `cm-s-default`
})
const snippetFile = process.env.NODE_ENV !== 'test'
? path.join(app.getPath('userData'), 'snippets.json')
@@ -35,7 +64,7 @@ const consts = {
'Dodger Blue',
'Violet Eggplant'
],
THEMES: ['default'].concat(themes),
THEMES: themes,
SNIPPET_FILE: snippetFile,
DEFAULT_EDITOR_FONT_FAMILY: [
'Monaco',

View File

@@ -0,0 +1,124 @@
import i18n from 'browser/lib/i18n'
import fs from 'fs'
const {remote} = require('electron')
const {Menu} = remote.require('electron')
const {clipboard} = remote.require('electron')
const {shell} = remote.require('electron')
const spellcheck = require('./spellcheck')
const uri2path = require('file-uri-to-path')
/**
* Creates the context menu that is shown when there is a right click in the editor of a (not-snippet) note.
* If the word is does not contains a spelling error (determined by the 'error style'), no suggestions for corrections are requested
* => they are not visible in the context menu
* @param editor CodeMirror editor
* @param {MouseEvent} event that has triggered the creation of the context menu
* @returns {Electron.Menu} The created electron context menu
*/
const buildEditorContextMenu = function (editor, event) {
if (editor == null || event == null || event.pageX == null || event.pageY == null) {
return null
}
const cursor = editor.coordsChar({left: event.pageX, top: event.pageY})
const wordRange = editor.findWordAt(cursor)
const word = editor.getRange(wordRange.anchor, wordRange.head)
const existingMarks = editor.findMarks(wordRange.anchor, wordRange.head) || []
let isMisspelled = false
for (const mark of existingMarks) {
if (mark.className === spellcheck.getCSSClassName()) {
isMisspelled = true
break
}
}
let suggestion = []
if (isMisspelled) {
suggestion = spellcheck.getSpellingSuggestion(word)
}
const selection = {
isMisspelled: isMisspelled,
spellingSuggestions: suggestion
}
const template = [{
role: 'cut'
}, {
role: 'copy'
}, {
role: 'paste'
}, {
role: 'selectall'
}]
if (selection.isMisspelled) {
const suggestions = selection.spellingSuggestions
template.unshift.apply(template, suggestions.map(function (suggestion) {
return {
label: suggestion,
click: function (suggestion) {
if (editor != null) {
editor.replaceRange(suggestion.label, wordRange.anchor, wordRange.head)
}
}
}
}).concat({
type: 'separator'
}))
}
return Menu.buildFromTemplate(template)
}
/**
* Creates the context menu that is shown when there is a right click Markdown preview of a (not-snippet) note.
* @param {MarkdownPreview} markdownPreview
* @param {MouseEvent} event that has triggered the creation of the context menu
* @returns {Electron.Menu} The created electron context menu
*/
const buildMarkdownPreviewContextMenu = function (markdownPreview, event) {
if (markdownPreview == null || event == null || event.pageX == null || event.pageY == null) {
return null
}
// Default context menu inclusions
const template = [{
role: 'copy'
}, {
role: 'selectall'
}]
if (event.target.tagName.toLowerCase() === 'a' && event.target.getAttribute('href')) {
// Link opener for files on the local system pointed to by href
const href = event.target.href
const isLocalFile = href.startsWith('file:')
if (isLocalFile) {
const absPath = uri2path(href)
try {
if (fs.lstatSync(absPath).isFile()) {
template.push(
{
label: i18n.__('Show in explorer'),
click: (e) => shell.showItemInFolder(absPath)
}
)
}
} catch (e) {
console.log('Error while evaluating if the file is locally available', e)
}
}
// Add option to context menu to copy url
template.push(
{
label: i18n.__('Copy Url'),
click: (e) => clipboard.writeText(href)
}
)
}
return Menu.buildFromTemplate(template)
}
module.exports =
{
buildEditorContextMenu: buildEditorContextMenu,
buildMarkdownPreviewContextMenu: buildMarkdownPreviewContextMenu
}

View File

@@ -1,5 +1,10 @@
import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir'
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})
const stylusCodeInfo = CodeMirror.modeInfo.find(info => info.name === 'Stylus')
if (stylusCodeInfo == null) {
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})
} else {
stylusCodeInfo.alias = ['styl']
}
CodeMirror.modeInfo.push({name: 'Elixir', mime: 'text/x-elixir', mode: 'elixir', ext: ['ex']})

View File

@@ -4,11 +4,11 @@ export function getTodoStatus (content) {
let numberOfCompletedTodo = 0
splitted.forEach((line) => {
const trimmedLine = line.trim()
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./i)) {
const trimmedLine = line.trim().replace(/^(>\s*)*/, '')
if (trimmedLine.match(/^[+\-*] \[(\s|x)] ./i)) {
numberOfTodo++
}
if (trimmedLine.match(/^[\+\-\*] \[x\] ./i)) {
if (trimmedLine.match(/^[+\-*] \[x] ./i)) {
numberOfCompletedTodo++
}
})

View File

@@ -1,5 +1,4 @@
const crypto = require('crypto')
const _ = require('lodash')
const uuidv4 = require('uuid/v4')
module.exports = function (uuid) {

View File

@@ -3,7 +3,7 @@
module.exports = function (md, renderers, defaultRenderer) {
const paramsRE = /^[ \t]*([\w+#-]+)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/
function fence (state, startLine, endLine) {
function fence (state, startLine, endLine, silent) {
let pos = state.bMarks[startLine] + state.tShift[startLine]
let max = state.eMarks[startLine]
@@ -12,7 +12,7 @@ module.exports = function (md, renderers, defaultRenderer) {
}
const marker = state.src.charCodeAt(pos)
if (!(marker === 96 || marker === 126)) {
if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) {
return false
}
@@ -27,6 +27,10 @@ module.exports = function (md, renderers, defaultRenderer) {
const markup = state.src.slice(mem, pos)
const params = state.src.slice(pos, max)
if (silent) {
return true
}
let nextLine = startLine
let haveEndMarker = false

View File

@@ -15,7 +15,7 @@ module.exports = function sanitizePlugin (md, options) {
options
)
}
if (state.tokens[tokenIdx].type === '_fence') {
if (state.tokens[tokenIdx].type.match(/.*_fence$/)) {
// escapeHtmlCharacters has better performance
state.tokens[tokenIdx].content = escapeHtmlCharacters(
state.tokens[tokenIdx].content,
@@ -96,6 +96,10 @@ function sanitizeInline (html, options) {
function naughtyHRef (href, options) {
// href = href.replace(/[\x00-\x20]+/g, '')
if (!href) {
// No href
return false
}
href = href.replace(/<\!\-\-.*?\-\-\>/g, '')
const matches = href.match(/^([a-zA-Z]+)\:/)

View File

@@ -2,49 +2,34 @@
* @fileoverview Markdown table of contents generator
*/
import { EOL } from 'os'
import toc from 'markdown-toc'
import diacritics from 'diacritics-map'
import stripColor from 'strip-color'
import mdlink from 'markdown-link'
import slugify from './slugify'
const EOL = require('os').EOL
const hasProp = Object.prototype.hasOwnProperty
/**
* @caseSensitiveSlugify Custom slugify function
* Same implementation that the original used by markdown-toc (node_modules/markdown-toc/lib/utils.js),
* but keeps original case to properly handle https://github.com/BoostIO/Boostnote/issues/2067
* From @enyaxu/markdown-it-anchor
*/
function caseSensitiveSlugify (str) {
function replaceDiacritics (str) {
return str.replace(/[À-ž]/g, function (ch) {
return diacritics[ch] || ch
})
}
function uniqueSlug (slug, slugs, opts) {
let uniq = slug
let i = opts.uniqueSlugStartIndex
while (hasProp.call(slugs, uniq)) uniq = `${slug}-${i++}`
slugs[uniq] = true
return uniq
}
function getTitle (str) {
if (/^\[[^\]]+\]\(/.test(str)) {
var m = /^\[([^\]]+)\]/.exec(str)
if (m) return m[1]
}
return str
}
str = getTitle(str)
str = stripColor(str)
// str = str.toLowerCase() //let's be case sensitive
// `.split()` is often (but not always) faster than `.replace()`
str = str.split(' ').join('-')
str = str.split(/\t/).join('--')
str = str.split(/<\/?[^>]+>/).join('')
str = str.split(/[|$&`~=\\\/@+*!?({[\]})<>=.,;:'"^]/).join('')
str = str.split(/[。?!,、;:“”【】()〔〕[]﹃﹄“ ”‘’﹁﹂—…-~《》〈〉「」]/).join('')
str = replaceDiacritics(str)
return str
function linkify (token) {
token.content = mdlink(token.content, `#${decodeURI(token.slug)}`)
return token
}
const TOC_MARKER_START = '<!-- toc -->'
const TOC_MARKER_END = '<!-- tocstop -->'
const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`)
/**
* Takes care of proper updating given editor with TOC.
* If TOC doesn't exit in the editor, it's inserted at current caret position.
@@ -52,12 +37,6 @@ const TOC_MARKER_END = '<!-- tocstop -->'
* @param editor CodeMirror editor to be updated with TOC
*/
export function generateInEditor (editor) {
const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`)
function tocExistsInEditor () {
return tocRegex.test(editor.getValue())
}
function updateExistingToc () {
const toc = generate(editor.getValue())
const search = editor.getSearchCursor(tocRegex)
@@ -71,21 +50,40 @@ export function generateInEditor (editor) {
editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor())
}
if (tocExistsInEditor()) {
if (tocExistsInEditor(editor)) {
updateExistingToc()
} else {
addTocAtCursorPosition()
}
}
export function tocExistsInEditor (editor) {
return tocRegex.test(editor.getValue())
}
/**
* Generates MD TOC based on MD document passed as string.
* @param markdownText MD document
* @returns generatedTOC String containing generated TOC
*/
export function generate (markdownText) {
const generatedToc = toc(markdownText, {slugify: caseSensitiveSlugify})
return TOC_MARKER_START + EOL + EOL + generatedToc.content + EOL + EOL + TOC_MARKER_END
const slugs = {}
const opts = {
uniqueSlugStartIndex: 1
}
const result = toc(markdownText, {
slugify: title => {
return uniqueSlug(slugify(title), slugs, opts)
},
linkify: false
})
const md = toc.bullets(result.json.map(linkify), {
highest: result.highest
})
return TOC_MARKER_START + EOL + EOL + md + EOL + EOL + TOC_MARKER_END
}
function wrapTocWithEol (toc, editor) {
@@ -96,5 +94,6 @@ function wrapTocWithEol (toc, editor) {
export default {
generate,
generateInEditor
generateInEditor,
tocExistsInEditor
}

View File

@@ -2,12 +2,13 @@ import markdownit from 'markdown-it'
import sanitize from './markdown-it-sanitize-html'
import emoji from 'markdown-it-emoji'
import math from '@rokt33r/markdown-it-math'
import mdurl from 'mdurl'
import smartArrows from 'markdown-it-smartarrows'
import markdownItTocAndAnchor from '@hikerpig/markdown-it-toc-and-anchor'
import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager'
import katex from 'katex'
import { lastFindInArray } from './utils'
import ee from 'browser/main/lib/eventEmitter'
function createGutter (str, firstLineNumber) {
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
@@ -33,6 +34,7 @@ class Markdown {
const updatedOptions = Object.assign(defaultOptions, options)
this.md = markdownit(updatedOptions)
this.md.linkify.set({ fuzzyLink: false })
if (updatedOptions.sanitize !== 'NONE') {
const allowedTags = ['iframe', 'input', 'b',
@@ -118,19 +120,27 @@ class Markdown {
this.md.use(require('markdown-it-imsize'))
this.md.use(require('markdown-it-footnote'))
this.md.use(require('markdown-it-multimd-table'))
this.md.use(require('markdown-it-named-headers'), {
slugify: (header) => {
return encodeURI(header.trim()
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
.replace(/\s+/g, '-'))
.replace(/\-+$/, '')
}
this.md.use(require('@enyaxu/markdown-it-anchor'), {
slugify: require('./slugify')
})
this.md.use(require('markdown-it-kbd'))
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']})
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error', 'quote', 'abstract', 'question']})
this.md.use(require('markdown-it-abbr'))
this.md.use(require('markdown-it-sub'))
this.md.use(require('markdown-it-sup'))
this.md.use(md => {
markdownItTocAndAnchor(md, {
toc: true,
tocPattern: /\[TOC\]/i,
anchorLink: false,
appendIdToHeading: false
})
md.renderer.rules.toc_open = () => '<div class="markdownIt-TOC-wrapper">'
md.renderer.rules.toc_close = () => '</div>'
})
this.md.use(require('./markdown-it-deflist'))
this.md.use(require('./markdown-it-frontmatter'))
@@ -151,6 +161,21 @@ class Markdown {
<div class="flowchart" data-height="${token.parameters.height}">${token.content}</div>
</pre>`
},
gallery: token => {
const content = token.content.split('\n').slice(0, -1).map(line => {
const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line)
if (match) {
return mdurl.encode(match[1])
} else {
return mdurl.encode(line)
}
}).join('\n')
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
<div class="gallery" data-autoplay="${token.parameters.autoplay}" data-height="${token.parameters.height}">${content}</div>
</pre>`
},
mermaid: token => {
return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span>
@@ -172,32 +197,47 @@ class Markdown {
})
const deflate = require('markdown-it-plantuml/lib/deflate')
this.md.use(require('markdown-it-plantuml'), '', {
generateSource: function (umlCode) {
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/svg'
const s = unescape(encodeURIComponent(umlCode))
const zippedCode = deflate.encode64(
deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9)
)
return `${serverAddress}/${zippedCode}`
}
const plantuml = require('markdown-it-plantuml')
const plantUmlStripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
const plantUmlServerAddress = plantUmlStripTrailingSlash(config.preview.plantUMLServerAddress)
const parsePlantUml = function (umlCode, openMarker, closeMarker, type) {
const s = unescape(encodeURIComponent(umlCode))
const zippedCode = deflate.encode64(
deflate.zip_deflate(`${openMarker}\n${s}\n${closeMarker}`, 9)
)
return `${plantUmlServerAddress}/${type}/${zippedCode}`
}
this.md.use(plantuml, {
generateSource: (umlCode) => parsePlantUml(umlCode, '@startuml', '@enduml', 'svg')
})
// Ditaa support
this.md.use(require('markdown-it-plantuml'), {
// Ditaa support. PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment.
this.md.use(plantuml, {
openMarker: '@startditaa',
closeMarker: '@endditaa',
generateSource: function (umlCode) {
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
// Currently PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment.
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/png'
const s = unescape(encodeURIComponent(umlCode))
const zippedCode = deflate.encode64(
deflate.zip_deflate(`@startditaa\n${s}\n@endditaa`, 9)
)
return `${serverAddress}/${zippedCode}`
}
generateSource: (umlCode) => parsePlantUml(umlCode, '@startditaa', '@endditaa', 'png')
})
// Mindmap support
this.md.use(plantuml, {
openMarker: '@startmindmap',
closeMarker: '@endmindmap',
generateSource: (umlCode) => parsePlantUml(umlCode, '@startmindmap', '@endmindmap', 'svg')
})
// WBS support
this.md.use(plantuml, {
openMarker: '@startwbs',
closeMarker: '@endwbs',
generateSource: (umlCode) => parsePlantUml(umlCode, '@startwbs', '@endwbs', 'svg')
})
// Gantt support
this.md.use(plantuml, {
openMarker: '@startgantt',
closeMarker: '@endgantt',
generateSource: (umlCode) => parsePlantUml(umlCode, '@startgantt', '@endgantt', 'svg')
})
// Override task item
@@ -278,7 +318,9 @@ class Markdown {
case 'list_item_open':
case 'paragraph_open':
case 'table_open':
token.attrPush(['data-line', token.map[0]])
if (token.map) {
token.attrPush(['data-line', token.map[0]])
}
}
})
const result = originalRender.call(this.md.renderer, tokens, options, env)

View File

@@ -22,7 +22,7 @@ export function strip (input) {
.replace(/\[(.*?)\][\[\(].*?[\]\)]/g, '$1')
.replace(/>/g, '')
.replace(/^\s{1,2}\[(.*?)\]: (\S+)( ".*?")?\s*$/g, '')
.replace(/^#{1,6}\s*([^#]*)\s*(#{1,6})?/gm, '$1')
.replace(/^#{1,6}\s*/gm, '')
.replace(/(`{3,})(.*?)\1/gm, '$2')
.replace(/^-{3,}\s*$/g, '')
.replace(/`(.+?)`/g, '$1')

View File

@@ -1,17 +1,26 @@
import { hashHistory } from 'react-router'
import dataApi from 'browser/main/lib/dataApi'
import ee from 'browser/main/lib/eventEmitter'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import queryString from 'query-string'
import { push } from 'connected-react-router'
export function createMarkdownNote (storage, folder, dispatch, location) {
export function createMarkdownNote (storage, folder, dispatch, location, params, config) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
let tags = []
if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) {
tags = params.tagname.split(' ')
}
return dataApi
.createNote(storage, {
type: 'MARKDOWN_NOTE',
folder: folder,
title: '',
content: ''
tags,
content: '',
linesHighlighted: []
})
.then(note => {
const noteHash = note.key
@@ -20,29 +29,39 @@ export function createMarkdownNote (storage, folder, dispatch, location) {
note: note
})
hashHistory.push({
dispatch(push({
pathname: location.pathname,
query: { key: noteHash }
})
search: queryString.stringify({ key: noteHash })
}))
ee.emit('list:jump', noteHash)
ee.emit('detail:focus')
})
}
export function createSnippetNote (storage, folder, dispatch, location, config) {
export function createSnippetNote (storage, folder, dispatch, location, params, config) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
let tags = []
if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) {
tags = params.tagname.split(' ')
}
const defaultLanguage = config.editor.snippetDefaultLanguage === 'Auto Detect' ? null : config.editor.snippetDefaultLanguage
return dataApi
.createNote(storage, {
type: 'SNIPPET_NOTE',
folder: folder,
title: '',
tags,
description: '',
snippets: [
{
name: '',
mode: config.editor.snippetDefaultLanguage || 'text',
content: ''
mode: defaultLanguage,
content: '',
linesHighlighted: []
}
]
})
@@ -52,10 +71,10 @@ export function createSnippetNote (storage, folder, dispatch, location, config)
type: 'UPDATE_NOTE',
note: note
})
hashHistory.push({
dispatch(push({
pathname: location.pathname,
query: { key: noteHash }
})
search: queryString.stringify({ key: noteHash })
}))
ee.emit('list:jump', noteHash)
ee.emit('detail:focus')
})

11
browser/lib/slugify.js Normal file
View File

@@ -0,0 +1,11 @@
module.exports = function slugify (title) {
const slug = encodeURI(
title.trim()
.replace(/^\s+/, '')
.replace(/\s+$/, '')
.replace(/\s+/g, '-')
.replace(/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`]/g, '')
)
return slug
}

232
browser/lib/spellcheck.js Normal file
View File

@@ -0,0 +1,232 @@
import styles from '../components/CodeEditor.styl'
import i18n from 'browser/lib/i18n'
const Typo = require('typo-js')
const _ = require('lodash')
const CSS_ERROR_CLASS = 'codeEditor-typo'
const SPELLCHECK_DISABLED = 'NONE'
const DICTIONARY_PATH = '../dictionaries'
const MILLISECONDS_TILL_LIVECHECK = 500
let dictionary = null
let self
function getAvailableDictionaries () {
return [
{label: i18n.__('Spellcheck disabled'), value: SPELLCHECK_DISABLED},
{label: i18n.__('English'), value: 'en_GB'},
{label: i18n.__('German'), value: 'de_DE'},
{label: i18n.__('French'), value: 'fr_FR'}
]
}
/**
* Only to be used in the tests :)
*/
function setDictionaryForTestsOnly (newDictionary) {
dictionary = newDictionary
}
/**
* @description Initializes the spellcheck. It removes all existing marks of the current editor.
* If a language was given (i.e. lang !== this.SPELLCHECK_DISABLED) it will load the stated dictionary and use it to check the whole document.
* @param {Codemirror} editor CodeMirror-Editor
* @param {String} lang on of the values from getAvailableDictionaries()-Method
*/
function setLanguage (editor, lang) {
self = this
dictionary = null
if (editor == null) {
return
}
const existingMarks = editor.getAllMarks() || []
for (const mark of existingMarks) {
mark.clear()
}
if (lang !== SPELLCHECK_DISABLED) {
dictionary = new Typo(lang, false, false, {
dictionaryPath: DICTIONARY_PATH,
asyncLoad: true,
loadedCallback: () =>
checkWholeDocument(editor)
})
}
}
/**
* Checks the whole content of the editor for typos
* @param {Codemirror} editor CodeMirror-Editor
*/
function checkWholeDocument (editor) {
const lastLine = editor.lineCount() - 1
const textOfLastLine = editor.getLine(lastLine) || ''
const lastChar = textOfLastLine.length
const from = {line: 0, ch: 0}
const to = {line: lastLine, ch: lastChar}
checkMultiLineRange(editor, from, to)
}
/**
* Checks the given range for typos
* @param {Codemirror} editor CodeMirror-Editor
* @param {line, ch} from starting position of the spellcheck
* @param {line, ch} to end position of the spellcheck
*/
function checkMultiLineRange (editor, from, to) {
function sortRange (pos1, pos2) {
if (pos1.line > pos2.line || (pos1.line === pos2.line && pos1.ch > pos2.ch)) {
return {from: pos2, to: pos1}
}
return {from: pos1, to: pos2}
}
const {from: smallerPos, to: higherPos} = sortRange(from, to)
for (let l = smallerPos.line; l <= higherPos.line; l++) {
const line = editor.getLine(l) || ''
let w = 0
if (l === smallerPos.line) {
w = smallerPos.ch
}
let wEnd = line.length
if (l === higherPos.line) {
wEnd = higherPos.ch
}
while (w <= wEnd) {
const wordRange = editor.findWordAt({line: l, ch: w})
self.checkWord(editor, wordRange)
w += (wordRange.head.ch - wordRange.anchor.ch) + 1
}
}
}
/**
* @description Checks whether a certain range of characters in the editor (i.e. a word) contains a typo.
* If so the ranged will be marked with the class CSS_ERROR_CLASS.
* Note: Due to performance considerations, only words with more then 3 signs are checked.
* @param {Codemirror} editor CodeMirror-Editor
* @param wordRange Object specifying the range that should be checked.
* Having the following structure: <code>{anchor: {line: integer, ch: integer}, head: {line: integer, ch: integer}}</code>
*/
function checkWord (editor, wordRange) {
const word = editor.getRange(wordRange.anchor, wordRange.head)
if (word == null || word.length <= 3) {
return
}
if (!dictionary.check(word)) {
editor.markText(wordRange.anchor, wordRange.head, {className: styles[CSS_ERROR_CLASS]})
}
}
/**
* Checks the changes recently made (aka live check)
* @param {Codemirror} editor CodeMirror-Editor
* @param fromChangeObject codeMirror changeObject describing the start of the editing
* @param toChangeObject codeMirror changeObject describing the end of the editing
*/
function checkChangeRange (editor, fromChangeObject, toChangeObject) {
/**
* Calculate the smallest respectively largest position as a start, resp. end, position and return it
* @param start CodeMirror change object
* @param end CodeMirror change object
* @returns {{start: {line: *, ch: *}, end: {line: *, ch: *}}}
*/
function getStartAndEnd (start, end) {
const possiblePositions = [start.from, start.to, end.from, end.to]
let smallest = start.from
let biggest = end.to
for (const currentPos of possiblePositions) {
if (currentPos.line < smallest.line || (currentPos.line === smallest.line && currentPos.ch < smallest.ch)) {
smallest = currentPos
}
if (currentPos.line > biggest.line || (currentPos.line === biggest.line && currentPos.ch > biggest.ch)) {
biggest = currentPos
}
}
return {start: smallest, end: biggest}
}
if (dictionary === null || editor == null) { return }
try {
const {start, end} = getStartAndEnd(fromChangeObject, toChangeObject)
// Expand the range to include words after/before whitespaces
start.ch = Math.max(start.ch - 1, 0)
end.ch = end.ch + 1
// clean existing marks
const existingMarks = editor.findMarks(start, end) || []
for (const mark of existingMarks) {
mark.clear()
}
self.checkMultiLineRange(editor, start, end)
} catch (e) {
console.info('Error during the spell check. It might be due to problems figuring out the range of the new text..', e)
}
}
function saveLiveSpellCheckFrom (changeObject) {
liveSpellCheckFrom = changeObject
}
let liveSpellCheckFrom
const debouncedSpellCheckLeading = _.debounce(saveLiveSpellCheckFrom, MILLISECONDS_TILL_LIVECHECK, {
'leading': true,
'trailing': false
})
const debouncedSpellCheck = _.debounce(checkChangeRange, MILLISECONDS_TILL_LIVECHECK, {
'leading': false,
'trailing': true
})
/**
* Handles a keystroke. Buffers the input and performs a live spell check after a certain time. Uses _debounce from lodash to buffer the input
* @param {Codemirror} editor CodeMirror-Editor
* @param changeObject codeMirror changeObject
*/
function handleChange (editor, changeObject) {
if (dictionary === null) {
return
}
debouncedSpellCheckLeading(changeObject)
debouncedSpellCheck(editor, liveSpellCheckFrom, changeObject)
}
/**
* Returns an array of spelling suggestions for the given (wrong written) word.
* Returns an empty array if the dictionary is null (=> spellcheck is disabled) or the given word was null
* @param word word to be checked
* @returns {String[]} Array of suggestions
*/
function getSpellingSuggestion (word) {
if (dictionary == null || word == null) {
return []
}
return dictionary.suggest(word)
}
/**
* Returns the name of the CSS class used for errors
*/
function getCSSClassName () {
return styles[CSS_ERROR_CLASS]
}
module.exports = {
DICTIONARY_PATH,
CSS_ERROR_CLASS,
SPELLCHECK_DISABLED,
getAvailableDictionaries,
setLanguage,
checkChangeRange,
handleChange,
getSpellingSuggestion,
checkWord,
checkMultiLineRange,
checkWholeDocument,
setDictionaryForTestsOnly,
getCSSClassName
}

9
browser/lib/turndown.js Normal file
View File

@@ -0,0 +1,9 @@
const TurndownService = require('turndown')
const { gfm } = require('turndown-plugin-gfm')
export const createTurndownService = function () {
const turndown = new TurndownService()
turndown.use(gfm)
turndown.remove('script')
return turndown
}

View File

@@ -132,8 +132,28 @@ export function isObjectEqual (a, b) {
return true
}
export function isMarkdownTitleURL (str) {
return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str)
}
export function humanFileSize (bytes) {
const threshold = 1000
if (Math.abs(bytes) < threshold) {
return bytes + ' B'
}
var units = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
var u = -1
do {
bytes /= threshold
++u
} while (Math.abs(bytes) >= threshold && u < units.length - 1)
return bytes.toFixed(1) + ' ' + units[u]
}
export default {
lastFindInArray,
escapeHtmlCharacters,
isObjectEqual
isObjectEqual,
isMarkdownTitleURL,
humanFileSize
}

View File

@@ -0,0 +1,69 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './FromUrlButton.styl'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'
class FromUrlButton extends React.Component {
constructor (props) {
super(props)
this.state = {
isActive: false
}
}
handleMouseDown (e) {
this.setState({
isActive: true
})
}
handleMouseUp (e) {
this.setState({
isActive: false
})
}
handleMouseLeave (e) {
this.setState({
isActive: false
})
}
render () {
const { className } = this.props
return (
<button className={_.isString(className)
? 'FromUrlButton ' + className
: 'FromUrlButton'
}
styleName={this.state.isActive || this.props.isActive
? 'root--active'
: 'root'
}
onMouseDown={(e) => this.handleMouseDown(e)}
onMouseUp={(e) => this.handleMouseUp(e)}
onMouseLeave={(e) => this.handleMouseLeave(e)}
onClick={this.props.onClick}>
<img styleName='icon'
src={this.state.isActive || this.props.isActive
? '../resources/icon/icon-external.svg'
: '../resources/icon/icon-external.svg'
}
/>
<span styleName='tooltip'>{i18n.__('Convert URL to Markdown')}</span>
</button>
)
}
}
FromUrlButton.propTypes = {
isActive: PropTypes.bool,
onClick: PropTypes.func,
className: PropTypes.string
}
export default CSSModules(FromUrlButton, styles)

View File

@@ -0,0 +1,41 @@
.root
top 45px
topBarButtonRight()
&:hover
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 50px
right 125px
width 90px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.root--active
@extend .root
transition 0.15s
color $ui-favorite-star-button-color
&:hover
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)
.icon
transition transform 0.15s
height 13px
body[data-theme="dark"]
.root
topBarButtonDark()
&:hover
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)

View File

@@ -5,15 +5,17 @@ import styles from './FullscreenButton.styl'
import i18n from 'browser/lib/i18n'
const OSX = global.process.platform === 'darwin'
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
const FullscreenButton = ({
onClick
}) => (
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
<span styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
</button>
)
}) => {
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
return (
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
<img src='../resources/icon/icon-full.svg' />
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
</button>
)
}
FullscreenButton.propTypes = {
onClick: PropTypes.func.isRequired

View File

@@ -17,6 +17,10 @@
opacity 0
transition 0.1s
.tooltip:lang(ja)
@extend .tooltip
right 35px
body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()

View File

@@ -14,7 +14,7 @@ class InfoPanel extends React.Component {
render () {
const {
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, wordCount, letterCount, type, print
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, exportAsPdf, wordCount, letterCount, type, print
} = this.props
return (
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}>
@@ -60,7 +60,7 @@ class InfoPanel extends React.Component {
</div>
<div>
<input styleName='infoPanel-noteLink' ref='noteLink' value={noteLink} onClick={(e) => { e.target.select() }} />
<input styleName='infoPanel-noteLink' ref='noteLink' defaultValue={noteLink} onClick={(e) => { e.target.select() }} />
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
<i className='fa fa-clipboard' />
</button>
@@ -70,22 +70,27 @@ class InfoPanel extends React.Component {
<hr />
<div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
<i className='fa fa-file-code-o' />
<p>{i18n.__('.md')}</p>
</button>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}>
<i className='fa fa-file-text-o' />
<p>{i18n.__('.txt')}</p>
</button>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
<i className='fa fa-html5' />
<p>{i18n.__('.html')}</p>
</button>
<button styleName='export--enable' onClick={(e) => print(e)}>
<button styleName='export--enable' onClick={(e) => exportAsPdf(e, 'export-pdf')}>
<i className='fa fa-file-pdf-o' />
<p>{i18n.__('.pdf')}</p>
</button>
<button styleName='export--enable' onClick={(e) => print(e, 'print')}>
<i className='fa fa-print' />
<p>{i18n.__('Print')}</p>
</button>
@@ -104,6 +109,7 @@ InfoPanel.propTypes = {
exportAsMd: PropTypes.func.isRequired,
exportAsTxt: PropTypes.func.isRequired,
exportAsHtml: PropTypes.func.isRequired,
exportAsPdf: PropTypes.func.isRequired,
wordCount: PropTypes.number,
letterCount: PropTypes.number,
type: PropTypes.string.isRequired,

View File

@@ -15,7 +15,7 @@
right 25px
position absolute
padding 20px 25px 0 25px
width 300px
// width 300px
overflow auto
background-color $ui-noteList-backgroundColor
box-shadow 2px 12px 15px 2px rgba(0, 0, 0, 0.1), 2px 1px 50px 2px rgba(0, 0, 0, 0.1)

View File

@@ -5,7 +5,7 @@ import styles from './InfoPanel.styl'
import i18n from 'browser/lib/i18n'
const InfoPanelTrashed = ({
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, exportAsPdf
}) => (
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}>
<div>
@@ -31,22 +31,22 @@ const InfoPanelTrashed = ({
</div>
<div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e)}>
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}>
<i className='fa fa-file-code-o' />
<p>.md</p>
</button>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e)}>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}>
<i className='fa fa-file-text-o' />
<p>.txt</p>
</button>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e)}>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}>
<i className='fa fa-html5' />
<p>.html</p>
</button>
<button styleName='export--unable'>
<button styleName='export--enable' onClick={(e) => exportAsPdf(e, 'export-pdf')}>
<i className='fa fa-file-pdf-o' />
<p>.pdf</p>
</button>
@@ -61,7 +61,8 @@ InfoPanelTrashed.propTypes = {
createdAt: PropTypes.string.isRequired,
exportAsMd: PropTypes.func.isRequired,
exportAsTxt: PropTypes.func.isRequired,
exportAsHtml: PropTypes.func.isRequired
exportAsHtml: PropTypes.func.isRequired,
exportAsPdf: PropTypes.func.isRequired
}
export default CSSModules(InfoPanelTrashed, styles)

View File

@@ -9,7 +9,6 @@ import StarButton from './StarButton'
import TagSelect from './TagSelect'
import FolderSelect from './FolderSelect'
import dataApi from 'browser/main/lib/dataApi'
import { hashHistory } from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
import markdown from 'browser/lib/markdownTextHelper'
import StatusBar from '../StatusBar'
@@ -30,6 +29,8 @@ import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
import striptags from 'striptags'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import markdownToc from 'browser/lib/markdown-toc-generator'
import queryString from 'query-string'
import { replace } from 'connected-react-router'
class MarkdownNoteDetail extends React.Component {
constructor (props) {
@@ -39,12 +40,15 @@ class MarkdownNoteDetail extends React.Component {
isMovingNote: false,
note: Object.assign({
title: '',
content: ''
content: '',
linesHighlighted: []
}, props.note),
isLockButtonShown: false,
isLockButtonShown: props.config.editor.type !== 'SPLIT',
isLocked: false,
editorType: props.config.editor.type
editorType: props.config.editor.type,
switchPreview: props.config.editor.switchPreview
}
this.dispatchTimer = null
this.toggleLockButton = this.handleToggleLockButton.bind(this)
@@ -71,12 +75,26 @@ class MarkdownNoteDetail extends React.Component {
if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
if (this.saveQueue != null) this.saveNow()
this.setState({
note: Object.assign({}, nextProps.note)
note: Object.assign({linesHighlighted: []}, nextProps.note)
}, () => {
this.refs.content.reload()
if (this.refs.tags) this.refs.tags.reset()
})
}
// Focus content if using blur or double click
// --> Moved here from componentDidMount so a re-render during search won't set focus to the editor
const {switchPreview} = nextProps.config.editor
if (this.state.switchPreview !== switchPreview) {
this.setState({
switchPreview
})
if (switchPreview === 'BLUR' || switchPreview === 'DBL_CLICK') {
console.log('setting focus', switchPreview)
this.focus()
}
}
}
componentWillUnmount () {
@@ -94,7 +112,12 @@ class MarkdownNoteDetail extends React.Component {
handleUpdateContent () {
const { note } = this.state
note.content = this.refs.content.value
note.title = markdown.strip(striptags(findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField)))
let title = findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField)
title = striptags(title)
title = markdown.strip(title)
note.title = title
this.updateNote(note)
}
@@ -148,12 +171,12 @@ class MarkdownNoteDetail extends React.Component {
originNote: note,
note: newNote
})
hashHistory.replace({
dispatch(replace({
pathname: location.pathname,
query: {
search: queryString.stringify({
key: newNote.key
}
})
})
}))
this.setState({
isMovingNote: false
})
@@ -190,6 +213,40 @@ class MarkdownNoteDetail extends React.Component {
ee.emit('export:save-html')
}
exportAsPdf () {
ee.emit('export:save-pdf')
}
handleKeyDown (e) {
switch (e.keyCode) {
// tab key
case 9:
if (e.ctrlKey && !e.shiftKey) {
e.preventDefault()
this.jumpNextTab()
} else if (e.ctrlKey && e.shiftKey) {
e.preventDefault()
this.jumpPrevTab()
} else if (!e.ctrlKey && !e.shiftKey && e.target === this.refs.description) {
e.preventDefault()
this.focusEditor()
}
break
// I key
case 73:
{
const isSuper = global.process.platform === 'darwin'
? e.metaKey
: e.ctrlKey
if (isSuper) {
e.preventDefault()
this.handleInfoButtonClick(e)
}
}
break
}
}
handleTrashButtonClick (e) {
const { note } = this.state
const { isTrashed } = note
@@ -253,7 +310,7 @@ class MarkdownNoteDetail extends React.Component {
}
getToggleLockButton () {
return this.state.isLocked ? '../resources/icon/icon-previewoff-on.svg' : '../resources/icon/icon-previewoff-off.svg'
return this.state.isLocked ? '../resources/icon/icon-lock.svg' : '../resources/icon/icon-unlock.svg'
}
handleDeleteKeyDown (e) {
@@ -262,7 +319,7 @@ class MarkdownNoteDetail extends React.Component {
handleToggleLockButton (event, noteStatus) {
// first argument event is not used
if (this.props.config.editor.switchPreview === 'BLUR' && noteStatus === 'CODE') {
if (noteStatus === 'CODE') {
this.setState({isLockButtonShown: true})
} else {
this.setState({isLockButtonShown: false})
@@ -288,7 +345,8 @@ class MarkdownNoteDetail extends React.Component {
}
handleSwitchMode (type) {
this.setState({ editorType: type }, () => {
// If in split mode, hide the lock button
this.setState({ editorType: type, isLockButtonShown: !(type === 'SPLIT') }, () => {
this.focus()
const newConfig = Object.assign({}, this.props.config)
newConfig.editor.type = type
@@ -331,7 +389,9 @@ class MarkdownNoteDetail extends React.Component {
value={note.content}
storageKey={note.storage}
noteKey={note.key}
linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)}
isLocked={this.state.isLocked}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
/>
} else {
@@ -341,6 +401,7 @@ class MarkdownNoteDetail extends React.Component {
value={note.content}
storageKey={note.storage}
noteKey={note.key}
linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)}
ignorePreviewPointerEvents={ignorePreviewPointerEvents}
/>
@@ -348,7 +409,7 @@ class MarkdownNoteDetail extends React.Component {
}
render () {
const { data, location, config } = this.props
const { data, dispatch, location, config } = this.props
const { note, editorType } = this.state
const storageKey = note.storage
const folderKey = note.folder
@@ -381,13 +442,14 @@ class MarkdownNoteDetail extends React.Component {
exportAsHtml={this.exportAsHtml}
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
exportAsPdf={this.exportAsPdf}
/>
</div>
</div>
const detailTopBar = <div styleName='info'>
<div styleName='info-left'>
<div styleName='info-left-top'>
<div>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
@@ -402,12 +464,15 @@ class MarkdownNoteDetail extends React.Component {
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data}
dispatch={dispatch}
onChange={this.handleUpdateTag.bind(this)}
coloredTags={config.coloredTags}
/>
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
</div>
<div styleName='info-right'>
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
<StarButton
onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred}
@@ -420,7 +485,7 @@ class MarkdownNoteDetail extends React.Component {
onFocus={(e) => this.handleFocus(e)}
onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
>
<img styleName='iconInfo' src={imgSrc} />
<img src={imgSrc} />
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>}
</button>
@@ -440,13 +505,14 @@ class MarkdownNoteDetail extends React.Component {
<InfoPanel
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}
noteLink={`[${note.title}](:note:${location.query.key})`}
noteLink={`[${note.title}](:note:${queryString.parse(location.search).key})`}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt}
exportAsHtml={this.exportAsHtml}
wordCount={note.content.split(' ').length}
exportAsPdf={this.exportAsPdf}
wordCount={note.content.trim().split(/\s+/g).length}
letterCount={note.content.replace(/\r?\n/g, '').length}
type={note.type}
print={this.print}
@@ -458,6 +524,7 @@ class MarkdownNoteDetail extends React.Component {
<div className='NoteDetail'
style={this.props.style}
styleName='root'
onKeyDown={(e) => this.handleKeyDown(e)}
>
{location.pathname === '/trashed' ? trashTopBar : detailTopBar}

View File

@@ -80,4 +80,5 @@ body[data-theme="monokai"]
body[data-theme="dracula"]
.root
border-left 1px solid $ui-dracula-borderColor
background-color $ui-dracula-noteDetail-backgroundColor
background-color $ui-dracula-noteDetail-backgroundColor

View File

@@ -107,4 +107,12 @@ body[data-theme="monokai"]
body[data-theme="dracula"]
.info
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteDetail-backgroundColor
background-color $ui-dracula-noteDetail-backgroundColor
.info > div
> button
-webkit-user-drag none
user-select none
> img, span
-webkit-user-drag none
user-select none

View File

@@ -10,7 +10,7 @@ const PermanentDeleteButton = ({
<button styleName='control-trashButton--in-trash'
onClick={(e) => onClick(e)}
>
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
<img src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
</button>
)

View File

@@ -8,7 +8,6 @@ import StarButton from './StarButton'
import TagSelect from './TagSelect'
import FolderSelect from './FolderSelect'
import dataApi from 'browser/main/lib/dataApi'
import {hashHistory} from 'react-router'
import ee from 'browser/main/lib/eventEmitter'
import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir'
@@ -18,8 +17,8 @@ import context from 'browser/lib/context'
import ConfigManager from 'browser/main/lib/ConfigManager'
import _ from 'lodash'
import {findNoteTitle} from 'browser/lib/findNoteTitle'
import convertModeName from 'browser/lib/convertModeName'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import FullscreenButton from './FullscreenButton'
import TrashButton from './TrashButton'
import RestoreButton from './RestoreButton'
import PermanentDeleteButton from './PermanentDeleteButton'
@@ -30,6 +29,8 @@ import { formatDate } from 'browser/lib/date-formatter'
import i18n from 'browser/lib/i18n'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import markdownToc from 'browser/lib/markdown-toc-generator'
import queryString from 'query-string'
import { replace } from 'connected-react-router'
const electron = require('electron')
const { remote } = electron
@@ -48,7 +49,7 @@ class SnippetNoteDetail extends React.Component {
note: Object.assign({
description: ''
}, props.note, {
snippets: props.note.snippets.map((snippet) => Object.assign({}, snippet))
snippets: props.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
})
}
@@ -76,8 +77,9 @@ class SnippetNoteDetail extends React.Component {
const nextNote = Object.assign({
description: ''
}, nextProps.note, {
snippets: nextProps.note.snippets.map((snippet) => Object.assign({}, snippet))
snippets: nextProps.note.snippets.map((snippet) => Object.assign({linesHighlighted: []}, snippet))
})
this.setState({
snippetIndex: 0,
note: nextNote
@@ -164,12 +166,12 @@ class SnippetNoteDetail extends React.Component {
originNote: note,
note: newNote
})
hashHistory.replace({
dispatch(replace({
pathname: location.pathname,
query: {
search: queryString.stringify({
key: newNote.key
}
})
})
}))
this.setState({
isMovingNote: false
})
@@ -410,6 +412,8 @@ class SnippetNoteDetail extends React.Component {
return (e) => {
const snippets = this.state.note.snippets.slice()
snippets[index].content = this.refs['code-' + index].value
snippets[index].linesHighlighted = e.options.linesHighlighted
this.setState(state => ({note: Object.assign(state.note, {snippets: snippets})}))
this.setState(state => ({
note: state.note
@@ -434,6 +438,18 @@ class SnippetNoteDetail extends React.Component {
this.focusEditor()
}
break
// I key
case 73:
{
const isSuper = global.process.platform === 'darwin'
? e.metaKey
: e.ctrlKey
if (isSuper) {
e.preventDefault()
this.handleInfoButtonClick(e)
}
}
break
// L key
case 76:
{
@@ -502,6 +518,19 @@ class SnippetNoteDetail extends React.Component {
])
}
handleWrapLineButtonClick (e) {
context.popup([
{
label: 'on',
click: (e) => this.handleWrapLineItemClick(e, true)
},
{
label: 'off',
click: (e) => this.handleWrapLineItemClick(e, false)
}
])
}
handleIndentSizeItemClick (e, indentSize) {
const { config, dispatch } = this.props
const editor = Object.assign({}, config.editor, {
@@ -534,6 +563,22 @@ class SnippetNoteDetail extends React.Component {
})
}
handleWrapLineItemClick (e, lineWrapping) {
const { config, dispatch } = this.props
const editor = Object.assign({}, config.editor, {
lineWrapping
})
ConfigManager.set({
editor
})
dispatch({
type: 'SET_CONFIG',
config: {
editor
}
})
}
focus () {
this.refs.description.focus()
}
@@ -584,13 +629,16 @@ class SnippetNoteDetail extends React.Component {
}
addSnippet () {
const { config } = this.props
const { config: { editor: { snippetDefaultLanguage } } } = this.props
const { note } = this.state
const defaultLanguage = snippetDefaultLanguage === 'Auto Detect' ? null : snippetDefaultLanguage
note.snippets = note.snippets.concat([{
name: '',
mode: config.editor.snippetDefaultLanguage || 'text',
content: ''
mode: defaultLanguage,
content: '',
linesHighlighted: []
}])
const snippetIndex = note.snippets.length - 1
@@ -633,22 +681,32 @@ class SnippetNoteDetail extends React.Component {
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none'
}
showWarning () {
showWarning (e, msg) {
const warningMessage = (msg) => ({
'export-txt': 'Text export',
'export-md': 'Markdown export',
'export-html': 'HTML export',
'export-pdf': 'PDF export',
'print': 'Print'
})[msg]
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Sorry!'),
detail: i18n.__('md/text import is available only a markdown note.'),
detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
buttons: [i18n.__('OK')]
})
}
render () {
const { data, config, location } = this.props
const { data, dispatch, config, location } = this.props
const { note } = this.state
const storageKey = note.storage
const folderKey = note.folder
const autoDetect = config.editor.snippetDefaultLanguage === 'Auto Detect'
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
let editorIndentSize = parseInt(config.editor.indentSize, 10)
@@ -673,10 +731,6 @@ class SnippetNoteDetail extends React.Component {
const viewList = note.snippets.map((snippet, index) => {
const isActive = this.state.snippetIndex === index
let syntax = CodeMirror.findModeByName(convertModeName(snippet.mode))
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
return <div styleName='tabView'
key={index}
style={{zIndex: isActive ? 5 : 4}}
@@ -685,26 +739,35 @@ class SnippetNoteDetail extends React.Component {
? <MarkdownEditor styleName='tabView-content'
value={snippet.content}
config={config}
linesHighlighted={snippet.linesHighlighted}
onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index}
ignorePreviewPointerEvents={this.props.ignorePreviewPointerEvents}
storageKey={storageKey}
/>
: <CodeEditor styleName='tabView-content'
mode={snippet.mode}
mode={snippet.mode || (autoDetect ? null : config.editor.snippetDefaultLanguage)}
value={snippet.content}
linesHighlighted={snippet.linesHighlighted}
lineWrapping={config.editor.lineWrapping}
theme={config.editor.theme}
fontFamily={config.editor.fontFamily}
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
displayLineNumbers={config.editor.displayLineNumbers}
matchingPairs={config.editor.matchingPairs}
matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs}
keyMap={config.editor.keyMap}
scrollPastEnd={config.editor.scrollPastEnd}
fetchUrlTitle={config.editor.fetchUrlTitle}
enableTableEditor={config.editor.enableTableEditor}
onChange={(e) => this.handleCodeChange(index)(e)}
ref={'code-' + index}
enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey}
autoDetect={autoDetect}
/>
}
</div>
@@ -738,13 +801,14 @@ class SnippetNoteDetail extends React.Component {
exportAsMd={this.showWarning}
exportAsTxt={this.showWarning}
exportAsHtml={this.showWarning}
exportAsPdf={this.showWarning}
/>
</div>
</div>
const detailTopBar = <div styleName='info'>
<div styleName='info-left'>
<div styleName='info-left-top'>
<div>
<FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder'
@@ -759,7 +823,9 @@ class SnippetNoteDetail extends React.Component {
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data}
dispatch={dispatch}
onChange={(e) => this.handleChange(e)}
coloredTags={config.coloredTags}
/>
</div>
<div styleName='info-right'>
@@ -768,11 +834,7 @@ class SnippetNoteDetail extends React.Component {
isActive={note.isStarred}
/>
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')}
onMouseDown={(e) => this.handleFullScreenButton(e)}>
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' />
<span styleName='tooltip'>{i18n.__('Fullscreen')}</span>
</button>
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} />
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} />
@@ -783,12 +845,15 @@ class SnippetNoteDetail extends React.Component {
<InfoPanel
storageName={currentOption.storage.name}
folderName={currentOption.folder.name}
noteLink={`[${note.title}](:note:${location.query.key})`}
noteLink={`[${note.title}](:note:${queryString.parse(location.search).key})`}
updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)}
exportAsMd={this.showWarning}
exportAsTxt={this.showWarning}
exportAsHtml={this.showWarning}
exportAsPdf={this.showWarning}
type={note.type}
print={this.showWarning}
/>
</div>
</div>
@@ -865,6 +930,12 @@ class SnippetNoteDetail extends React.Component {
size: {config.editor.indentSize}&nbsp;
<i className='fa fa-caret-down' />
</button>
<button
onClick={(e) => this.handleWrapLineButtonClick(e)}
>
Wrap Line: {config.editor.lineWrapping ? 'on' : 'off'}&nbsp;
<i className='fa fa-caret-down' />
</button>
</div>
<StatusBar

View File

@@ -31,7 +31,7 @@
.tabList
absolute left right
top 55px
top 70px
height 30px
display flex
background-color $ui-noteDetail-backgroundColor
@@ -57,6 +57,9 @@
.tabList .tabButton
navWhiteButtonColor()
width 30px
border-left 1px solid $ui-borderColor
border-top 1px solid $ui-borderColor
border-right 1px solid $ui-borderColor
.tabView
absolute left right bottom
@@ -98,17 +101,34 @@
opacity 0
transition 0.1s
body[data-theme="white"]
body[data-theme="white"], body[data-theme="default"]
.root
box-shadow $note-detail-box-shadow
border none
.tabButton
&:hover
background-color alpha($ui-button--active-backgroundColor, 20%)
color $ui-text-color
transition 0.15s
body[data-theme="dark"]
.root
border-left 1px solid $ui-dark-borderColor
background-color $ui-dark-noteDetail-backgroundColor
box-shadow none
.tabList .tabButton
border-color $ui-dark-borderColor
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
.tabButton
&:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%)
color $ui-dark-text-color
transition 0.15s
.body
background-color $ui-dark-noteDetail-backgroundColor
@@ -118,7 +138,6 @@ body[data-theme="dark"]
border 1px solid $ui-dark-borderColor
.tabList
background-color $ui-button--active-backgroundColor
background-color $ui-dark-noteDetail-backgroundColor
.tabList .list
@@ -150,6 +169,15 @@ body[data-theme="solarized-dark"]
color $ui-solarized-dark-text-color
border 1px solid $ui-solarized-dark-borderColor
.tabList .tabButton
border-color $ui-solarized-dark-borderColor
.tabButton
&:hover
color $ui-solarized-dark-button--active-color
background-color $ui-solarized-dark-noteDetail-backgroundColor
transition 0.15s
.tabList
background-color $ui-solarized-dark-noteDetail-backgroundColor
color $ui-solarized-dark-text-color
@@ -167,6 +195,14 @@ body[data-theme="monokai"]
color $ui-monokai-text-color
border 1px solid $ui-monokai-borderColor
.tabList .tabButton
border-color $ui-monokai-borderColor
.tabButton
&:hover
color $ui-monokai-text-color
background-color $ui-monokai-noteDetail-backgroundColor
.tabList
background-color $ui-monokai-noteDetail-backgroundColor
color $ui-monokai-text-color
@@ -184,6 +220,14 @@ body[data-theme="dracula"]
color $ui-dracula-text-color
border 1px solid $ui-dracula-borderColor
.tabList .tabButton
border-color $ui-dracula-borderColor
.tabButton
&:hover
color $ui-dracula-text-color
background-color $ui-dracula-noteDetail-backgroundColor
.tabList
background-color $ui-dracula-noteDetail-backgroundColor
color $ui-dracula-text-color

View File

@@ -54,7 +54,7 @@ class StarButton extends React.Component {
: '../resources/icon/icon-star.svg'
}
/>
<span styleName='tooltip'>{i18n.__('Star')}</span>
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Star')}</span>
</button>
)
}

View File

@@ -21,6 +21,11 @@
opacity 0
transition 0.1s
.tooltip:lang(ja)
@extend .tooltip
right 103px
width 70px
.root--active
@extend .root
transition 0.15s
@@ -37,4 +42,4 @@ body[data-theme="dark"]
topBarButtonDark()
&:hover
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)
color alpha($ui-favorite-star-button-color, 0.6)

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'
import React from 'react'
import invertColor from 'invert-color'
import CSSModules from 'browser/lib/CSSModules'
import styles from './TagSelect.styl'
import _ from 'lodash'
@@ -7,6 +8,7 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import i18n from 'browser/lib/i18n'
import ee from 'browser/main/lib/eventEmitter'
import Autosuggest from 'react-autosuggest'
import { push } from 'connected-react-router'
class TagSelect extends React.Component {
constructor (props) {
@@ -45,8 +47,14 @@ class TagSelect extends React.Component {
value = _.isArray(value)
? value.slice()
: []
value.push(newTag)
value = _.uniq(value)
if (!_.includes(value, newTag)) {
value.push(newTag)
}
if (this.props.saveTagsAlphabetically) {
value = _.sortBy(value)
}
this.setState({
newTag: ''
@@ -89,8 +97,11 @@ class TagSelect extends React.Component {
}
handleTagLabelClick (tag) {
const { router } = this.context
router.push(`/tags/${tag}`)
const { dispatch } = this.props
// Note: `tag` requires encoding later.
// E.g. % in tag is a problem (see issue #3170) - encodeURIComponent(tag) is not working.
dispatch(push(`/tags/${tag}`))
}
handleTagRemoveButtonClick (tag) {
@@ -179,19 +190,34 @@ class TagSelect extends React.Component {
}
render () {
const { value, className, showTagsAlphabetically } = this.props
const { value, className, showTagsAlphabetically, coloredTags } = this.props
const tagList = _.isArray(value)
? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => {
const wrapperStyle = {}
const textStyle = {}
const BLACK = '#333333'
const WHITE = '#f1f1f1'
const color = coloredTags[tag]
const invertedColor = color && invertColor(color, { black: BLACK, white: WHITE })
let iconRemove = '../resources/icon/icon-x.svg'
if (color) {
wrapperStyle.backgroundColor = color
textStyle.color = invertedColor
}
if (invertedColor === WHITE) {
iconRemove = '../resources/icon/icon-x-light.svg'
}
return (
<span styleName='tag'
key={tag}
style={wrapperStyle}
>
<span styleName='tag-label' onClick={(e) => this.handleTagLabelClick(tag)}>#{tag}</span>
<span styleName='tag-label' style={textStyle} onClick={(e) => this.handleTagLabelClick(tag)}>#{tag}</span>
<button styleName='tag-removeButton'
onClick={(e) => this.handleTagRemoveButtonClick(tag)}
>
<img className='tag-removeButton-icon' src='../resources/icon/icon-x.svg' width='8px' />
<img className='tag-removeButton-icon' src={iconRemove} width='8px' />
</button>
</span>
)
@@ -233,14 +259,12 @@ class TagSelect extends React.Component {
}
}
TagSelect.contextTypes = {
router: PropTypes.shape({})
}
TagSelect.propTypes = {
dispatch: PropTypes.func,
className: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func
onChange: PropTypes.func,
coloredTags: PropTypes.object
}
export default CSSModules(TagSelect, styles)

View File

@@ -3,19 +3,18 @@
align-items center
user-select none
vertical-align middle
width 100%
overflow-x scroll
width 96%
overflow-x auto
white-space nowrap
margin-top 31px
top 50px
position absolute
.root::-webkit-scrollbar
display none
&::-webkit-scrollbar
height 8px
.tag
display flex
align-items center
margin 0px 2px
margin 0px 2px 2px
padding 2px 4px
background-color alpha($ui-tag-backgroundColor, 3%)
border-radius 4px

View File

@@ -8,19 +8,19 @@ const ToggleModeButton = ({
onClick, editorType
}) => (
<div styleName='control-toggleModeButton'>
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
<div styleName={editorType === 'SPLIT' ? 'active' : undefined} onClick={() => onClick('SPLIT')}>
<img src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
</div>
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : undefined} onClick={() => onClick('EDITOR_PREVIEW')}>
<img src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
</div>
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
</div>
)
ToggleModeButton.propTypes = {
onClick: PropTypes.func.isRequired,
editorType: PropTypes.string.Required
editorType: PropTypes.string.isRequired
}
export default CSSModules(ToggleModeButton, styles)

View File

@@ -40,6 +40,11 @@
opacity 0
transition 0.1s
.tooltip:lang(ja)
@extend .tooltip
left -8px
width 70px
body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()
@@ -70,3 +75,10 @@ body[data-theme="dracula"]
.active
background-color #bd93f9
box-shadow 2px 0px 7px #222222
.control-toggleModeButton
-webkit-user-drag none
user-select none
> div img
-webkit-user-drag none
user-select none

View File

@@ -10,8 +10,8 @@ const TrashButton = ({
<button styleName='control-trashButton'
onClick={(e) => onClick(e)}
>
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>{i18n.__('Trash')}</span>
<img src='../resources/icon/icon-trash.svg' />
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Trash')}</span>
</button>
)

View File

@@ -17,6 +17,10 @@
opacity 0
transition 0.1s
.tooltip:lang(ja)
@extend .tooltip
right 46px
.control-trashButton--in-trash
top 60px
topBarButtonRight()

View File

@@ -10,6 +10,7 @@ import StatusBar from '../StatusBar'
import i18n from 'browser/lib/i18n'
import debounceRender from 'react-debounce-render'
import searchFromNotes from 'browser/lib/search'
import queryString from 'query-string'
const OSX = global.process.platform === 'darwin'
@@ -36,11 +37,11 @@ class Detail extends React.Component {
}
render () {
const { location, data, params, config } = this.props
const { location, data, match: { params }, config } = this.props
const noteKey = location.search !== '' && queryString.parse(location.search).key
let note = null
if (location.query.key != null) {
const noteKey = location.query.key
if (location.search !== '') {
const allNotes = data.noteMap.map(note => note)
const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey))
let displayedNotes = allNotes
@@ -49,16 +50,14 @@ class Detail extends React.Component {
const searchStr = params.searchword
displayedNotes = searchStr === undefined || searchStr === '' ? allNotes
: searchFromNotes(allNotes, searchStr)
}
if (location.pathname.match(/\/tags/)) {
} else if (location.pathname.match(/^\/tags/)) {
const listOfTags = params.tagname.split(' ')
displayedNotes = data.noteMap.map(note => note).filter(note =>
listOfTags.every(tag => note.tags.includes(tag))
)
}
if (location.pathname.match(/\/trashed/)) {
if (location.pathname.match(/^\/trashed/)) {
displayedNotes = trashedNotes
} else {
displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key)

View File

@@ -0,0 +1,16 @@
import React from 'react'
import { createDevTools } from 'redux-devtools'
import LogMonitor from 'redux-devtools-log-monitor'
import DockMonitor from 'redux-devtools-dock-monitor'
const DevTools = createDevTools(
<DockMonitor
toggleVisibilityKey='ctrl-h'
changePositionKey='ctrl-q'
defaultIsVisible={false}
>
<LogMonitor theme='tomorrow' />
</DockMonitor>
)
export default DevTools

View File

@@ -0,0 +1,8 @@
/* eslint-disable no-undef */
if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line global-require
module.exports = require('./index.dev').default
} else {
// eslint-disable-next-line global-require
module.exports = require('./index.prod').default
}

View File

@@ -0,0 +1,6 @@
import React from 'react'
const DevTools = () => <div />
DevTools.instrument = () => {}
export default DevTools

View File

@@ -12,11 +12,11 @@ import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager'
import mobileAnalytics from 'browser/main/lib/AwsMobileAnalyticsConfig'
import eventEmitter from 'browser/main/lib/eventEmitter'
import { hashHistory } from 'react-router'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import i18n from 'browser/lib/i18n'
import { getLocales } from 'browser/lib/Languages'
import applyShortcuts from 'browser/main/lib/shortcutManager'
import { push } from 'connected-react-router'
const path = require('path')
const electron = require('electron')
const { remote } = electron
@@ -96,12 +96,14 @@ class Main extends React.Component {
{
name: 'example.html',
mode: 'html',
content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>"
content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>",
linesHighlighted: []
},
{
name: 'example.js',
mode: 'javascript',
content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)"
content: "var boostnote = document.getElementById('hello').innerHTML\n\nconsole.log(boostnote)",
linesHighlighted: []
}
]
})
@@ -130,7 +132,7 @@ class Main extends React.Component {
.then(() => data.storage)
})
.then(storage => {
hashHistory.push('/storages/' + storage.key)
store.dispatch(push('/storages/' + storage.key))
})
.catch(err => {
throw err
@@ -167,13 +169,25 @@ class Main extends React.Component {
}
})
// eslint-disable-next-line no-undef
delete CodeMirror.keyMap.emacs['Ctrl-V']
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
eventEmitter.on('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this))
}
componentWillUnmount () {
eventEmitter.off('editor:fullscreen', this.toggleFullScreen)
eventEmitter.off('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this))
}
toggleMenuBarVisible () {
const { config } = this.props
const { ui } = config
const newUI = Object.assign(ui, {showMenuBar: !ui.showMenuBar})
const newConfig = Object.assign(config, newUI)
ConfigManager.set(newConfig)
}
handleLeftSlideMouseDown (e) {
@@ -234,8 +248,8 @@ class Main extends React.Component {
if (this.state.isRightSliderFocused) {
const offset = this.refs.body.getBoundingClientRect().left
let newListWidth = e.pageX - offset
if (newListWidth < 10) {
newListWidth = 10
if (newListWidth < 180) {
newListWidth = 180
} else if (newListWidth > 600) {
newListWidth = 600
}
@@ -298,7 +312,7 @@ class Main extends React.Component {
onMouseUp={e => this.handleMouseUp(e)}
>
<SideNav
{..._.pick(this.props, ['dispatch', 'data', 'config', 'params', 'location'])}
{..._.pick(this.props, ['dispatch', 'data', 'config', 'match', 'location'])}
width={this.state.navWidth}
/>
{!config.isSideNavFolded &&
@@ -328,7 +342,7 @@ class Main extends React.Component {
'dispatch',
'config',
'data',
'params',
'match',
'location'
])}
/>
@@ -338,7 +352,7 @@ class Main extends React.Component {
'dispatch',
'data',
'config',
'params',
'match',
'location'
])}
/>
@@ -360,7 +374,7 @@ class Main extends React.Component {
'dispatch',
'data',
'config',
'params',
'match',
'location'
])}
ignorePreviewPointerEvents={this.state.isRightSliderFocused}

View File

@@ -21,42 +21,39 @@ class NewNoteButton extends React.Component {
this.state = {
}
this.newNoteHandler = () => {
this.handleNewNoteButtonClick()
}
this.handleNewNoteButtonClick = this.handleNewNoteButtonClick.bind(this)
}
componentDidMount () {
eventEmitter.on('top:new-note', this.newNoteHandler)
eventEmitter.on('top:new-note', this.handleNewNoteButtonClick)
}
componentWillUnmount () {
eventEmitter.off('top:new-note', this.newNoteHandler)
eventEmitter.off('top:new-note', this.handleNewNoteButtonClick)
}
handleNewNoteButtonClick (e) {
const { location, dispatch, config } = this.props
const { location, dispatch, match: { params }, config } = this.props
const { storage, folder } = this.resolveTargetFolder()
if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
createMarkdownNote(storage.key, folder.key, dispatch, location)
createMarkdownNote(storage.key, folder.key, dispatch, location, params, config)
} else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
createSnippetNote(storage.key, folder.key, dispatch, location, config)
createSnippetNote(storage.key, folder.key, dispatch, location, params, config)
} else {
modal.open(NewNoteModal, {
storage: storage.key,
folder: folder.key,
dispatch,
location,
params,
config
})
}
}
resolveTargetFolder () {
const { data, params } = this.props
const { data, match: { params } } = this.props
let storage = data.storageMap.get(params.storageKey)
// Find first storage
if (storage == null) {
for (const kv of data.storageMap) {
@@ -92,8 +89,8 @@ class NewNoteButton extends React.Component {
>
<div styleName='control'>
<button styleName='control-newNoteButton'
onClick={(e) => this.handleNewNoteButtonClick(e)}>
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' />
onClick={this.handleNewNoteButtonClick}>
<img src='../resources/icon/icon-newnote.svg' />
<span styleName='control-newNoteButton-tooltip'>
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
</span>

View File

@@ -2,7 +2,6 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import debounceRender from 'react-debounce-render'
import styles from './NoteList.styl'
import moment from 'moment'
import _ from 'lodash'
@@ -15,23 +14,42 @@ import NoteItemSimple from 'browser/components/NoteItemSimple'
import searchFromNotes from 'browser/lib/search'
import fs from 'fs'
import path from 'path'
import { hashHistory } from 'react-router'
import { push, replace } from 'connected-react-router'
import copy from 'copy-to-clipboard'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import Markdown from '../../lib/markdown'
import i18n from 'browser/lib/i18n'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import context from 'browser/lib/context'
import queryString from 'query-string'
const { remote } = require('electron')
const { dialog } = remote
const WP_POST_PATH = '/wp/v2/posts'
const regexMatchStartingTitleNumber = new RegExp('^([0-9]*\.?[0-9]+).*$')
function sortByCreatedAt (a, b) {
return new Date(b.createdAt) - new Date(a.createdAt)
}
function sortByAlphabetical (a, b) {
const matchA = regexMatchStartingTitleNumber.exec(a.title)
const matchB = regexMatchStartingTitleNumber.exec(b.title)
if (matchA && matchA.length === 2 && matchB && matchB.length === 2) {
// Both note titles are starting with a float. We will compare it now.
const floatA = parseFloat(matchA[1])
const floatB = parseFloat(matchB[1])
const diff = floatA - floatB
if (diff !== 0) {
return diff
}
// The float values are equal. We will compare the full title.
}
return a.title.localeCompare(b.title)
}
@@ -64,13 +82,15 @@ class NoteList extends React.Component {
this.focusHandler = () => {
this.refs.list.focus()
}
this.alertIfSnippetHandler = () => {
this.alertIfSnippet()
this.alertIfSnippetHandler = (event, msg) => {
this.alertIfSnippet(msg)
}
this.importFromFileHandler = this.importFromFile.bind(this)
this.jumpNoteByHash = this.jumpNoteByHashHandler.bind(this)
this.handleNoteListKeyUp = this.handleNoteListKeyUp.bind(this)
this.handleNoteListBlur = this.handleNoteListBlur.bind(this)
this.getNoteKeyFromTargetIndex = this.getNoteKeyFromTargetIndex.bind(this)
this.cloneNote = this.cloneNote.bind(this)
this.deleteNote = this.deleteNote.bind(this)
this.focusNote = this.focusNote.bind(this)
this.pinToTop = this.pinToTop.bind(this)
@@ -96,6 +116,7 @@ class NoteList extends React.Component {
this.refreshTimer = setInterval(() => this.forceUpdate(), 60 * 1000)
ee.on('list:next', this.selectNextNoteHandler)
ee.on('list:prior', this.selectPriorNoteHandler)
ee.on('list:clone', this.cloneNote)
ee.on('list:focus', this.focusHandler)
ee.on('list:isMarkdownNote', this.alertIfSnippetHandler)
ee.on('import:file', this.importFromFileHandler)
@@ -118,6 +139,7 @@ class NoteList extends React.Component {
ee.off('list:next', this.selectNextNoteHandler)
ee.off('list:prior', this.selectPriorNoteHandler)
ee.off('list:clone', this.cloneNote)
ee.off('list:focus', this.focusHandler)
ee.off('list:isMarkdownNote', this.alertIfSnippetHandler)
ee.off('import:file', this.importFromFileHandler)
@@ -125,15 +147,15 @@ class NoteList extends React.Component {
}
componentDidUpdate (prevProps) {
const { location } = this.props
const { dispatch, location } = this.props
const { selectedNoteKeys } = this.state
const visibleNoteKeys = this.notes.map(note => note.key)
const note = this.notes[0]
const prevKey = prevProps.location.query.key
const visibleNoteKeys = this.notes && this.notes.map(note => note.key)
const note = this.notes && this.notes[0]
const key = location.search && queryString.parse(location.search).key
const prevKey = prevProps.location.search && queryString.parse(prevProps.location.search).key
const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && note.key
if (note && location.query.key == null) {
const { router } = this.context
if (note && location.search === '') {
if (!location.pathname.match(/\/searched/)) this.contextNotes = this.getContextNotes()
// A visible note is an active note
@@ -143,17 +165,17 @@ class NoteList extends React.Component {
ee.emit('list:moved')
}
router.replace({
dispatch(replace({ // was passed with context - we can use connected router here
pathname: location.pathname,
query: {
search: queryString.stringify({
key: noteKey
}
})
})
}))
return
}
// Auto scroll
if (_.isString(location.query.key) && prevProps.location.query.key === location.query.key) {
if (_.isString(key) && prevKey === key) {
const targetIndex = this.getTargetIndex()
if (targetIndex > -1) {
const list = this.refs.list
@@ -173,20 +195,19 @@ class NoteList extends React.Component {
}
}
focusNote (selectedNoteKeys, noteKey) {
const { router } = this.context
const { location } = this.props
focusNote (selectedNoteKeys, noteKey, pathname) {
const { dispatch } = this.props
this.setState({
selectedNoteKeys
})
router.push({
pathname: location.pathname,
query: {
dispatch(push({
pathname,
search: queryString.stringify({
key: noteKey
}
})
})
}))
}
getNoteKeyFromTargetIndex (targetIndex) {
@@ -201,6 +222,7 @@ class NoteList extends React.Component {
}
let { selectedNoteKeys } = this.state
const { shiftKeyDown } = this.state
const { location } = this.props
let targetIndex = this.getTargetIndex()
@@ -217,7 +239,7 @@ class NoteList extends React.Component {
selectedNoteKeys.push(priorNoteKey)
}
this.focusNote(selectedNoteKeys, priorNoteKey)
this.focusNote(selectedNoteKeys, priorNoteKey, location.pathname)
ee.emit('list:moved')
}
@@ -228,6 +250,7 @@ class NoteList extends React.Component {
}
let { selectedNoteKeys } = this.state
const { shiftKeyDown } = this.state
const { location } = this.props
let targetIndex = this.getTargetIndex()
const isTargetLastNote = targetIndex === this.notes.length - 1
@@ -250,19 +273,28 @@ class NoteList extends React.Component {
selectedNoteKeys.push(nextNoteKey)
}
this.focusNote(selectedNoteKeys, nextNoteKey)
this.focusNote(selectedNoteKeys, nextNoteKey, location.pathname)
ee.emit('list:moved')
}
jumpNoteByHashHandler (event, noteHash) {
const { data } = this.props
// first argument event isn't used.
if (this.notes === null || this.notes.length === 0) {
return
}
const selectedNoteKeys = [noteHash]
this.focusNote(selectedNoteKeys, noteHash)
let locationToSelect = '/home'
const noteByHash = data.noteMap.map((note) => note).find(note => note.key === noteHash)
if (noteByHash !== undefined) {
locationToSelect = '/storages/' + noteByHash.storage + '/folders/' + noteByHash.folder
}
this.focusNote(selectedNoteKeys, noteHash, locationToSelect)
ee.emit('list:moved')
}
@@ -276,12 +308,6 @@ class NoteList extends React.Component {
ee.emit('top:new-note')
}
// D key
if (e.keyCode === 68) {
e.preventDefault()
this.deleteNote()
}
// E key
if (e.keyCode === 69) {
e.preventDefault()
@@ -323,9 +349,15 @@ class NoteList extends React.Component {
}
}
getNotes () {
const { data, params, location } = this.props
handleNoteListBlur () {
this.setState({
shiftKeyDown: false,
ctrlKeyDown: false
})
}
getNotes () {
const { data, match: { params }, location } = this.props
if (location.pathname.match(/\/home/) || location.pathname.match(/alltags/)) {
const allNotes = data.noteMap.map((note) => note)
this.contextNotes = allNotes
@@ -366,7 +398,7 @@ class NoteList extends React.Component {
// get notes in the current folder
getContextNotes () {
const { data, params } = this.props
const { data, match: { params } } = this.props
const storageKey = params.storageKey
const folderKey = params.folderKey
const storage = data.storageMap.get(storageKey)
@@ -406,8 +438,7 @@ class NoteList extends React.Component {
}
handleNoteClick (e, uniqueKey) {
const { router } = this.context
const { location } = this.props
const { dispatch, location } = this.props
let { selectedNoteKeys, prevShiftNoteIndex } = this.state
const { ctrlKeyDown, shiftKeyDown } = this.state
const hasSelectedNoteKey = selectedNoteKeys.length > 0
@@ -458,16 +489,16 @@ class NoteList extends React.Component {
prevShiftNoteIndex
})
router.push({
dispatch(push({
pathname: location.pathname,
query: {
search: queryString.stringify({
key: uniqueKey
}
})
})
}))
}
handleSortByChange (e) {
const { dispatch, params: { folderKey } } = this.props
const { dispatch, match: { params: { folderKey } } } = this.props
const config = {
[folderKey]: { sortBy: e.target.value }
@@ -494,14 +525,22 @@ class NoteList extends React.Component {
})
}
alertIfSnippet () {
alertIfSnippet (msg) {
const warningMessage = (msg) => ({
'export-txt': 'Text export',
'export-md': 'Markdown export',
'export-html': 'HTML export',
'export-pdf': 'PDF export',
'print': 'Print'
})[msg]
const targetIndex = this.getTargetIndex()
if (this.notes[targetIndex].type === 'SNIPPET_NOTE') {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
message: i18n.__('Sorry!'),
detail: i18n.__('md/text import is available only a markdown note.'),
buttons: [i18n.__('OK'), i18n.__('Cancel')]
detail: i18n.__(warningMessage(msg) + ' is available only in markdown notes.'),
buttons: [i18n.__('OK')]
})
}
}
@@ -652,14 +691,18 @@ class NoteList extends React.Component {
})
)
.then((data) => {
data.forEach((item) => {
dispatch({
type: 'DELETE_NOTE',
storageKey: item.storageKey,
noteKey: item.noteKey
const dispatchHandler = () => {
data.forEach((item) => {
dispatch({
type: 'DELETE_NOTE',
storageKey: item.storageKey,
noteKey: item.noteKey
})
})
})
}
ee.once('list:next', dispatchHandler)
})
.then(() => ee.emit('list:next'))
.catch((err) => {
console.error('Cannot Delete note: ' + err)
})
@@ -683,6 +726,7 @@ class NoteList extends React.Component {
})
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
})
.then(() => ee.emit('list:next'))
.catch((err) => {
console.error('Notes could not go to trash: ' + err)
})
@@ -706,7 +750,12 @@ class NoteList extends React.Component {
type: firstNote.type,
folder: folder.key,
title: firstNote.title + ' ' + i18n.__('copy'),
content: firstNote.content
content: firstNote.content,
linesHighlighted: firstNote.linesHighlighted,
description: firstNote.description,
snippets: firstNote.snippets,
tags: firstNote.tags,
isStarred: firstNote.isStarred
})
.then((note) => {
attachmentManagement.cloneAttachments(firstNote, note)
@@ -722,10 +771,10 @@ class NoteList extends React.Component {
selectedNoteKeys: [note.key]
})
hashHistory.push({
dispatch(push({
pathname: location.pathname,
query: {key: note.key}
})
search: queryString.stringify({key: note.key})
}))
})
}
@@ -735,13 +784,13 @@ class NoteList extends React.Component {
}
navigate (sender, pathname) {
const { router } = this.context
router.push({
const { dispatch } = this.props
dispatch(push({
pathname,
query: {
search: queryString.stringify({
// key: noteKey
}
})
})
}))
}
save (note) {
@@ -871,7 +920,7 @@ class NoteList extends React.Component {
if (!location.pathname.match(/\/trashed/)) this.addNotesFromFiles(filepaths)
}
// Add notes to the current folder
// Add notes to the current folder
addNotesFromFiles (filepaths) {
const { dispatch, location } = this.props
const { storage, folder } = this.resolveTargetFolder()
@@ -895,13 +944,20 @@ class NoteList extends React.Component {
}
dataApi.createNote(storage.key, newNote)
.then((note) => {
dispatch({
type: 'UPDATE_NOTE',
note: note
})
hashHistory.push({
pathname: location.pathname,
query: {key: getNoteKey(note)}
attachmentManagement.importAttachments(note.content, filepath, storage.key, note.key)
.then((newcontent) => {
note.content = newcontent
dataApi.updateNote(storage.key, note.key, note)
dispatch({
type: 'UPDATE_NOTE',
note: note
})
dispatch(push({
pathname: location.pathname,
search: queryString.stringify({key: getNoteKey(note)})
}))
})
})
})
@@ -911,14 +967,15 @@ class NoteList extends React.Component {
getTargetIndex () {
const { location } = this.props
const key = queryString.parse(location.search).key
const targetIndex = _.findIndex(this.notes, (note) => {
return getNoteKey(note) === location.query.key
return getNoteKey(note) === key
})
return targetIndex
}
resolveTargetFolder () {
const { data, params } = this.props
const { data, match: { params } } = this.props
let storage = data.storageMap.get(params.storageKey)
// Find first storage
@@ -966,7 +1023,7 @@ class NoteList extends React.Component {
}
render () {
const { location, config, params: { folderKey } } = this.props
const { location, config, match: { params: { folderKey } } } = this.props
let { notes } = this.props
const { selectedNoteKeys } = this.state
const sortBy = _.get(config, [folderKey, 'sortBy'], config.sortBy.default)
@@ -1042,6 +1099,7 @@ class NoteList extends React.Component {
storageName={this.getNoteStorage(note).name}
viewType={viewType}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
coloredTags={config.coloredTags}
/>
)
}
@@ -1088,7 +1146,7 @@ class NoteList extends React.Component {
}
onClick={(e) => this.handleListStyleButtonClick(e, 'DEFAULT')}
>
<img styleName='iconTag' src='../resources/icon/icon-column.svg' />
<img src='../resources/icon/icon-column.svg' />
</button>
<button title={i18n.__('Compressed View')} styleName={config.listStyle === 'SMALL'
? 'control-button--active'
@@ -1096,7 +1154,7 @@ class NoteList extends React.Component {
}
onClick={(e) => this.handleListStyleButtonClick(e, 'SMALL')}
>
<img styleName='iconTag' src='../resources/icon/icon-column-list.svg' />
<img src='../resources/icon/icon-column-list.svg' />
</button>
</div>
</div>
@@ -1105,6 +1163,7 @@ class NoteList extends React.Component {
tabIndex='-1'
onKeyDown={(e) => this.handleNoteListKeyDown(e)}
onKeyUp={this.handleNoteListKeyUp}
onBlur={this.handleNoteListBlur}
>
{noteList}
</div>
@@ -1124,4 +1183,4 @@ NoteList.propTypes = {
})
}
export default debounceRender(CSSModules(NoteList, styles))
export default CSSModules(NoteList, styles)

View File

@@ -8,7 +8,7 @@ const PreferenceButton = ({
onClick
}) => (
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' />
<img src='../resources/icon/icon-setting.svg' />
<span styleName='tooltip'>{i18n.__('Preferences')}</span>
</button>
)

View File

@@ -2,7 +2,6 @@ import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './StorageItem.styl'
import { hashHistory } from 'react-router'
import modal from 'browser/main/lib/modal'
import CreateFolderModal from 'browser/main/modals/CreateFolderModal'
import RenameFolderModal from 'browser/main/modals/RenameFolderModal'
@@ -12,6 +11,7 @@ import _ from 'lodash'
import { SortableElement } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
import { push } from 'connected-react-router'
const { remote } = require('electron')
const { dialog } = remote
@@ -25,7 +25,8 @@ class StorageItem extends React.Component {
const { storage } = this.props
this.state = {
isOpen: !!storage.isOpen
isOpen: !!storage.isOpen,
draggedOver: null
}
}
@@ -133,14 +134,14 @@ class StorageItem extends React.Component {
}
handleHeaderInfoClick (e) {
const { storage } = this.props
hashHistory.push('/storages/' + storage.key)
const { storage, dispatch } = this.props
dispatch(push('/storages/' + storage.key))
}
handleFolderButtonClick (folderKey) {
return (e) => {
const { storage } = this.props
hashHistory.push('/storages/' + storage.key + '/folders/' + folderKey)
const { storage, dispatch } = this.props
dispatch(push('/storages/' + storage.key + '/folders/' + folderKey))
}
}
@@ -204,6 +205,20 @@ class StorageItem extends React.Component {
folderKey: data.folderKey,
fileType: data.fileType
})
return data
})
.then(data => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: 'Exported to "' + data.exportDir + '"'
})
})
.catch(err => {
dialog.showErrorBox(
'Export error',
err ? err.message || err : 'Unexpected error during export'
)
throw err
})
}
})
@@ -231,14 +246,20 @@ class StorageItem extends React.Component {
}
}
handleDragEnter (e) {
e.dataTransfer.setData('defaultColor', e.target.style.backgroundColor)
e.target.style.backgroundColor = 'rgba(129, 130, 131, 0.08)'
handleDragEnter (e, key) {
e.preventDefault()
if (this.state.draggedOver === key) { return }
this.setState({
draggedOver: key
})
}
handleDragLeave (e) {
e.target.style.opacity = '1'
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
e.preventDefault()
if (this.state.draggedOver === null) { return }
this.setState({
draggedOver: null
})
}
dropNote (storage, folder, dispatch, location, noteData) {
@@ -263,8 +284,12 @@ class StorageItem extends React.Component {
}
handleDrop (e, storage, folder, dispatch, location) {
e.target.style.opacity = '1'
e.target.style.backgroundColor = e.dataTransfer.getData('defaultColor')
e.preventDefault()
if (this.state.draggedOver !== null) {
this.setState({
draggedOver: null
})
}
const noteData = JSON.parse(e.dataTransfer.getData('note'))
this.dropNote(storage, folder, dispatch, location, noteData)
}
@@ -274,7 +299,7 @@ class StorageItem extends React.Component {
const { folderNoteMap, trashedSet } = data
const SortableStorageItemChild = SortableElement(StorageItemChild)
const folderList = storage.folders.map((folder, index) => {
let folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
const folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
const isActive = !!(location.pathname.match(folderRegex))
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
@@ -291,16 +316,22 @@ class StorageItem extends React.Component {
<SortableStorageItemChild
key={folder.key}
index={index}
isActive={isActive}
isActive={isActive || folder.key === this.state.draggedOver}
handleButtonClick={(e) => this.handleFolderButtonClick(folder.key)(e)}
handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)}
folderName={folder.name}
folderColor={folder.color}
isFolded={isFolded}
noteCount={noteCount}
handleDrop={(e) => this.handleDrop(e, storage, folder, dispatch, location)}
handleDragEnter={this.handleDragEnter}
handleDragLeave={this.handleDragLeave}
handleDrop={(e) => {
this.handleDrop(e, storage, folder, dispatch, location)
}}
handleDragEnter={(e) => {
this.handleDragEnter(e, folder.key)
}}
handleDragLeave={(e) => {
this.handleDragLeave(e, folder)
}}
/>
)
})
@@ -331,14 +362,14 @@ class StorageItem extends React.Component {
<button styleName='header-addFolderButton'
onClick={(e) => this.handleAddFolderButtonClick(e)}
>
<img styleName='iconTag' src='../resources/icon/icon-plus.svg' />
<img src='../resources/icon/icon-plus.svg' />
</button>
}
<button styleName='header-info'
onClick={(e) => this.handleHeaderInfoClick(e)}
>
<span styleName='header-info-name'>
<span>
{isFolded ? _.truncate(storage.name, {length: 1, omission: ''}) : storage.name}
</span>
{isFolded &&
@@ -349,7 +380,7 @@ class StorageItem extends React.Component {
</button>
</div>
{this.state.isOpen &&
<div styleName='folderList' >
<div>
{folderList}
</div>
}

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'
import React from 'react'
import { push } from 'connected-react-router'
import CSSModules from 'browser/lib/CSSModules'
import dataApi from 'browser/main/lib/dataApi'
import styles from './SideNav.styl'
@@ -20,13 +21,31 @@ import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
import { remote } from 'electron'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import ColorPicker from 'browser/components/ColorPicker'
import { every, sortBy } from 'lodash'
function matchActiveTags (tags, activeTags) {
return _.every(activeTags, v => tags.indexOf(v) >= 0)
return every(activeTags, v => tags.indexOf(v) >= 0)
}
class SideNav extends React.Component {
// TODO: should not use electron stuff v0.7
constructor (props) {
super(props)
this.state = {
colorPicker: {
show: false,
color: null,
tagName: null,
targetRect: null
}
}
this.dismissColorPicker = this.dismissColorPicker.bind(this)
this.handleColorPickerConfirm = this.handleColorPickerConfirm.bind(this)
this.handleColorPickerReset = this.handleColorPickerReset.bind(this)
}
componentDidMount () {
EventEmitter.on('side:preferences', this.handleMenuButtonClick)
@@ -38,14 +57,14 @@ class SideNav extends React.Component {
deleteTag (tag) {
const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
ype: 'warning',
type: 'warning',
message: i18n.__('Confirm tag deletion'),
detail: i18n.__('This will permanently remove this tag.'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
})
if (selectedButton === 0) {
const { data, dispatch, location, params } = this.props
const { data, dispatch, location, match: { params } } = this.props
const notes = data.noteMap
.map(note => note)
@@ -75,7 +94,7 @@ class SideNav extends React.Component {
if (index !== -1) {
tags.splice(index, 1)
this.context.router.push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`)
dispatch(push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`))
}
}
})
@@ -87,13 +106,13 @@ class SideNav extends React.Component {
}
handleHomeButtonClick (e) {
const { router } = this.context
router.push('/home')
const { dispatch } = this.props
dispatch(push('/home'))
}
handleStarredButtonClick (e) {
const { router } = this.context
router.push('/starred')
const { dispatch } = this.props
dispatch(push('/starred'))
}
handleTagContextMenu (e, tag) {
@@ -104,9 +123,64 @@ class SideNav extends React.Component {
click: this.deleteTag.bind(this, tag)
})
menu.push({
label: i18n.__('Customize Color'),
click: this.displayColorPicker.bind(this, tag, e.target.getBoundingClientRect())
})
context.popup(menu)
}
dismissColorPicker () {
this.setState({
colorPicker: {
show: false
}
})
}
displayColorPicker (tagName, rect) {
const { config } = this.props
this.setState({
colorPicker: {
show: true,
color: config.coloredTags[tagName],
tagName,
targetRect: rect
}
})
}
handleColorPickerConfirm (color) {
const { dispatch, config: {coloredTags} } = this.props
const { colorPicker: { tagName } } = this.state
const newColoredTags = Object.assign({}, coloredTags, {[tagName]: color.hex})
const config = { coloredTags: newColoredTags }
ConfigManager.set(config)
dispatch({
type: 'SET_CONFIG',
config
})
this.dismissColorPicker()
}
handleColorPickerReset () {
const { dispatch, config: {coloredTags} } = this.props
const { colorPicker: { tagName } } = this.state
const newColoredTags = Object.assign({}, coloredTags)
delete newColoredTags[tagName]
const config = { coloredTags: newColoredTags }
ConfigManager.set(config)
dispatch({
type: 'SET_CONFIG',
config
})
this.dismissColorPicker()
}
handleToggleButtonClick (e) {
const { dispatch, config } = this.props
@@ -118,18 +192,18 @@ class SideNav extends React.Component {
}
handleTrashedButtonClick (e) {
const { router } = this.context
router.push('/trashed')
const { dispatch } = this.props
dispatch(push('/trashed'))
}
handleSwitchFoldersButtonClick () {
const { router } = this.context
router.push('/home')
const { dispatch } = this.props
dispatch(push('/home'))
}
handleSwitchTagsButtonClick () {
const { router } = this.context
router.push('/alltags')
const { dispatch } = this.props
dispatch(push('/alltags'))
}
onSortEnd (storage) {
@@ -198,6 +272,7 @@ class SideNav extends React.Component {
<div styleName='tagList'>
{this.tagListComponent(data)}
</div>
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
</div>
)
}
@@ -207,9 +282,10 @@ class SideNav extends React.Component {
tagListComponent () {
const { data, location, config } = this.props
const { colorPicker } = this.state
const activeTags = this.getActiveTags(location.pathname)
const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
let tagList = _.sortBy(data.tagNoteMap.map(
let tagList = sortBy(data.tagNoteMap.map(
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
).filter(
tag => tag.size > 0
@@ -222,7 +298,7 @@ class SideNav extends React.Component {
})
}
if (config.sortTagsBy === 'COUNTER') {
tagList = _.sortBy(tagList, item => (0 - item.size))
tagList = sortBy(tagList, item => (0 - item.size))
}
if (config.ui.showOnlyRelatedTags && (relatedTags.size > 0)) {
tagList = tagList.filter(
@@ -237,10 +313,11 @@ class SideNav extends React.Component {
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
handleContextMenu={this.handleTagContextMenu.bind(this)}
isActive={this.getTagActive(location.pathname, tag.name)}
isActive={this.getTagActive(location.pathname, tag.name) || (colorPicker.tagName === tag.name)}
isRelated={tag.related}
key={tag.name}
count={tag.size}
color={config.coloredTags[tag.name]}
/>
)
})
@@ -274,8 +351,8 @@ class SideNav extends React.Component {
}
handleClickTagListItem (name) {
const { router } = this.context
router.push(`/tags/${encodeURIComponent(name)}`)
const { dispatch } = this.props
dispatch(push(`/tags/${encodeURIComponent(name)}`))
}
handleSortTagsByChange (e) {
@@ -293,8 +370,7 @@ class SideNav extends React.Component {
}
handleClickNarrowToTag (tag) {
const { router } = this.context
const { location } = this.props
const { dispatch, location } = this.props
const listOfTags = this.getActiveTags(location.pathname)
const indexOfTag = listOfTags.indexOf(tag)
if (indexOfTag > -1) {
@@ -302,7 +378,7 @@ class SideNav extends React.Component {
} else {
listOfTags.push(tag)
}
router.push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`)
dispatch(push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`))
}
emptyTrash (entries) {
@@ -333,6 +409,7 @@ class SideNav extends React.Component {
render () {
const { data, location, config, dispatch } = this.props
const { colorPicker: colorPickerState } = this.state
const isFolded = config.isSideNavFolded
@@ -349,9 +426,23 @@ class SideNav extends React.Component {
useDragHandle
/>
})
let colorPicker
if (colorPickerState.show) {
colorPicker = (
<ColorPicker
color={colorPickerState.color}
targetRect={colorPickerState.targetRect}
onConfirm={this.handleColorPickerConfirm}
onCancel={this.dismissColorPicker}
onReset={this.handleColorPickerReset}
/>
)
}
const style = {}
if (!isFolded) style.width = this.props.width
const isTagActive = location.pathname.match(/tag/)
const isTagActive = /tag/.test(location.pathname)
return (
<div className='SideNav'
styleName={isFolded ? 'root--folded' : 'root'}
@@ -368,6 +459,7 @@ class SideNav extends React.Component {
</div>
</div>
{this.SideNavComponent(isFolded, storageList)}
{colorPicker}
</div>
)
}

View File

@@ -47,6 +47,14 @@
.update-icon
color $brand-color
body[data-theme="default"]
.zoom
color $ui-text-color
body[data-theme="white"]
.zoom
color $ui-text-color
body[data-theme="dark"]
.root
border-color $ui-dark-borderColor

View File

@@ -5,6 +5,7 @@ import styles from './StatusBar.styl'
import ZoomManager from 'browser/main/lib/ZoomManager'
import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
import EventEmitter from 'browser/main/lib/eventEmitter'
const electron = require('electron')
const { remote, ipcRenderer } = electron
@@ -13,6 +14,26 @@ const { dialog } = remote
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
class StatusBar extends React.Component {
constructor (props) {
super(props)
this.handleZoomInMenuItem = this.handleZoomInMenuItem.bind(this)
this.handleZoomOutMenuItem = this.handleZoomOutMenuItem.bind(this)
this.handleZoomResetMenuItem = this.handleZoomResetMenuItem.bind(this)
}
componentDidMount () {
EventEmitter.on('status:zoomin', this.handleZoomInMenuItem)
EventEmitter.on('status:zoomout', this.handleZoomOutMenuItem)
EventEmitter.on('status:zoomreset', this.handleZoomResetMenuItem)
}
componentWillUnmount () {
EventEmitter.off('status:zoomin', this.handleZoomInMenuItem)
EventEmitter.off('status:zoomout', this.handleZoomOutMenuItem)
EventEmitter.off('status:zoomreset', this.handleZoomResetMenuItem)
}
updateApp () {
const index = dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning',
@@ -48,6 +69,20 @@ class StatusBar extends React.Component {
})
}
handleZoomInMenuItem () {
const zoomFactor = ZoomManager.getZoom() + 0.1
this.handleZoomMenuItemClick(zoomFactor)
}
handleZoomOutMenuItem () {
const zoomFactor = ZoomManager.getZoom() - 0.1
this.handleZoomMenuItemClick(zoomFactor)
}
handleZoomResetMenuItem () {
this.handleZoomMenuItemClick(1.0)
}
render () {
const { config, status } = this.context

View File

@@ -6,6 +6,9 @@ import _ from 'lodash'
import ee from 'browser/main/lib/eventEmitter'
import NewNoteButton from 'browser/main/NewNoteButton'
import i18n from 'browser/lib/i18n'
import debounce from 'lodash/debounce'
import CInput from 'react-composition-input'
import { push } from 'connected-react-router'
class TopBar extends React.Component {
constructor (props) {
@@ -14,22 +17,36 @@ class TopBar extends React.Component {
this.state = {
search: '',
searchOptions: [],
isSearching: false,
isAlphabet: false,
isIME: false,
isConfirmTranslation: false
isSearching: false
}
const { dispatch } = this.props
this.focusSearchHandler = () => {
this.handleOnSearchFocus()
}
this.codeInitHandler = this.handleCodeInit.bind(this)
this.handleKeyDown = this.handleKeyDown.bind(this)
this.handleSearchFocus = this.handleSearchFocus.bind(this)
this.handleSearchBlur = this.handleSearchBlur.bind(this)
this.handleSearchChange = this.handleSearchChange.bind(this)
this.handleSearchClearButton = this.handleSearchClearButton.bind(this)
this.debouncedUpdateKeyword = debounce((keyword) => {
dispatch(push(`/searched/${encodeURIComponent(keyword)}`))
this.setState({
search: keyword
})
ee.emit('top:search', keyword)
}, 1000 / 60, {
maxWait: 1000 / 8
})
}
componentDidMount () {
const { params } = this.props
const searchWord = params.searchword
const { match: { params } } = this.props
const searchWord = params && params.searchword
if (searchWord !== undefined) {
this.setState({
search: searchWord,
@@ -46,22 +63,22 @@ class TopBar extends React.Component {
}
handleSearchClearButton (e) {
const { router } = this.context
const { dispatch } = this.props
this.setState({
search: '',
isSearching: false
})
this.refs.search.childNodes[0].blur
router.push('/searched')
dispatch(push('/searched'))
e.preventDefault()
this.debouncedUpdateKeyword('')
}
handleKeyDown (e) {
// reset states
this.setState({
isAlphabet: false,
isIME: false
})
// Re-apply search field on ENTER key
if (e.keyCode === 13) {
this.debouncedUpdateKeyword(e.target.value)
}
// Clear search on ESC
if (e.keyCode === 27) {
@@ -79,52 +96,11 @@ class TopBar extends React.Component {
ee.emit('list:prior')
e.preventDefault()
}
// When the key is an alphabet, del, enter or ctr
if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) {
this.setState({
isAlphabet: true
})
// When the key is an IME input (Japanese, Chinese)
} else if (e.keyCode === 229) {
this.setState({
isIME: true
})
}
}
handleKeyUp (e) {
const { router } = this.context
// reset states
this.setState({
isConfirmTranslation: false
})
// When the key is translation confirmation (Enter, Space)
if (this.state.isIME && (e.keyCode === 32 || e.keyCode === 13)) {
this.setState({
isConfirmTranslation: true
})
const keyword = this.refs.searchInput.value
router.push(`/searched/${encodeURIComponent(keyword)}`)
this.setState({
search: keyword
})
}
}
handleSearchChange (e) {
const { router } = this.context
const keyword = this.refs.searchInput.value
if (this.state.isAlphabet || this.state.isConfirmTranslation) {
router.push(`/searched/${encodeURIComponent(keyword)}`)
} else {
e.preventDefault()
}
this.setState({
search: keyword
})
ee.emit('top:search', keyword)
const keyword = e.target.value
this.debouncedUpdateKeyword(keyword)
}
handleSearchFocus (e) {
@@ -132,6 +108,7 @@ class TopBar extends React.Component {
isSearching: true
})
}
handleSearchBlur (e) {
e.stopPropagation()
@@ -161,7 +138,7 @@ class TopBar extends React.Component {
}
handleCodeInit () {
ee.emit('top:search', this.refs.searchInput.value)
ee.emit('top:search', this.refs.searchInput.value || '')
}
render () {
@@ -174,24 +151,23 @@ class TopBar extends React.Component {
<div styleName='control'>
<div styleName='control-search'>
<div styleName='control-search-input'
onFocus={(e) => this.handleSearchFocus(e)}
onBlur={(e) => this.handleSearchBlur(e)}
onFocus={this.handleSearchFocus}
onBlur={this.handleSearchBlur}
tabIndex='-1'
ref='search'
>
<input
<CInput
ref='searchInput'
value={this.state.search}
onChange={(e) => this.handleSearchChange(e)}
onKeyDown={(e) => this.handleKeyDown(e)}
onKeyUp={(e) => this.handleKeyUp(e)}
onInputChange={this.handleSearchChange}
onKeyDown={this.handleKeyDown}
placeholder={i18n.__('Search')}
type='text'
className='searchInput'
/>
{this.state.search !== '' &&
<button styleName='control-search-input-clear'
onClick={(e) => this.handleSearchClearButton(e)}
onClick={this.handleSearchClearButton}
>
<i className='fa fa-fw fa-times' />
<span styleName='control-search-input-clear-tooltip'>{i18n.__('Clear Search')}</span>
@@ -206,8 +182,8 @@ class TopBar extends React.Component {
'dispatch',
'data',
'config',
'params',
'location'
'location',
'match'
])}
/>}
</div>

View File

@@ -97,6 +97,7 @@ modalBackColor = white
body[data-theme="dark"]
background-color $ui-dark-backgroundColor
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
.ModalBase
@@ -148,6 +149,7 @@ body[data-theme="dark"]
z-index modalZIndex + 5
body[data-theme="solarized-dark"]
background-color $ui-solarized-dark-backgroundColor
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
.ModalBase
@@ -157,6 +159,7 @@ body[data-theme="solarized-dark"]
color: $ui-solarized-dark-text-color
body[data-theme="monokai"]
background-color $ui-monokai-backgroundColor
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
.ModalBase
@@ -166,6 +169,7 @@ body[data-theme="monokai"]
color: $ui-monokai-text-color
body[data-theme="dracula"]
background-color $ui-dracula-backgroundColor
::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
.ModalBase

View File

@@ -1,11 +1,13 @@
import { Provider } from 'react-redux'
import Main from './Main'
import store from './store'
import React from 'react'
import { store, history } from './store'
import React, { Fragment } from 'react'
import ReactDOM from 'react-dom'
require('!!style!css!stylus?sourceMap!./global.styl')
import { Router, Route, IndexRoute, IndexRedirect, hashHistory } from 'react-router'
import { syncHistoryWithStore } from 'react-router-redux'
import { Route, Switch, Redirect } from 'react-router-dom'
import { ConnectedRouter } from 'connected-react-router'
import DevTools from './DevTools'
require('./lib/ipcClient')
require('../lib/customMeta')
import i18n from 'browser/lib/i18n'
@@ -77,7 +79,6 @@ document.addEventListener('click', function (e) {
})
const el = document.getElementById('content')
const history = syncHistoryWithStore(hashHistory, store)
function notify (...args) {
return new window.Notification(...args)
@@ -98,29 +99,24 @@ function updateApp () {
ReactDOM.render((
<Provider store={store}>
<Router history={history}>
<Route path='/' component={Main}>
<IndexRedirect to='/home' />
<Route path='home' />
<Route path='starred' />
<Route path='searched'>
<Route path=':searchword' />
</Route>
<Route path='trashed' />
<Route path='alltags' />
<Route path='tags'>
<IndexRedirect to='/alltags' />
<Route path=':tagname' />
</Route>
<Route path='storages'>
<IndexRedirect to='/home' />
<Route path=':storageKey'>
<IndexRoute />
<Route path='folders/:folderKey' />
</Route>
</Route>
</Route>
</Router>
<ConnectedRouter history={history}>
<Fragment>
<Switch>
<Redirect path='/' to='/home' exact />
<Route path='/(home|alltags|starred|trashed)' component={Main} />
<Route path='/searched' component={Main} exact />
<Route path='/searched/:searchword' component={Main} />
<Redirect path='/tags' to='/alltags' exact />
<Route path='/tags/:tagname' component={Main} />
{/* storages */}
<Redirect path='/storages' to='/home' exact />
<Route path='/storages/:storageKey' component={Main} exact />
<Route path='/storages/:storageKey/folders/:folderKey' component={Main} />
</Switch>
<DevTools />
</Fragment>
</ConnectedRouter>
</Provider>
), el, function () {
const loadingCover = document.getElementById('loadingCover')

View File

@@ -8,9 +8,14 @@ const win = global.process.platform === 'win32'
const electron = require('electron')
const { ipcRenderer } = electron
const consts = require('browser/lib/consts')
const electronConfig = new (require('electron-config'))()
let isInitialized = false
const DEFAULT_MARKDOWN_LINT_CONFIG = `{
"default": true
}`
export const DEFAULT_CONFIG = {
zoom: 1,
isSideNavFolded: false,
@@ -22,28 +27,40 @@ export const DEFAULT_CONFIG = {
sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER'
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
amaEnabled: true,
autoUpdateEnabled: true,
hotkey: {
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace'
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace',
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V',
prettifyMarkdown: OSX ? 'Command + Shift + F' : 'Ctrl + Shift + F',
sortLines: OSX ? 'Command + Shift + S' : 'Ctrl + Shift + S',
insertDate: OSX ? 'Command + /' : 'Ctrl + /',
insertDateTime: OSX ? 'Command + Alt + /' : 'Ctrl + Shift + /',
toggleMenuBar: 'Alt'
},
ui: {
language: 'en',
theme: 'default',
showCopyNotification: true,
disableDirectWrite: false,
defaultNote: 'ALWAYS_ASK' // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
defaultNote: 'ALWAYS_ASK', // 'ALWAYS_ASK', 'SNIPPET_NOTE', 'MARKDOWN_NOTE'
showMenuBar: false
},
editor: {
theme: 'base16-light',
keyMap: 'sublime',
fontSize: '14',
fontFamily: win ? 'Segoe UI' : 'Monaco, Consolas',
fontFamily: win ? 'Consolas' : 'Monaco',
indentType: 'space',
indentSize: '2',
lineWrapping: true,
enableRulers: false,
rulers: [80, 120],
displayLineNumbers: true,
matchingPairs: '()[]{}\'\'""$$**``~~__',
matchingTriples: '```"""\'\'\'',
explodingPairs: '[]{}``$$',
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE'
scrollPastEnd: false,
@@ -51,7 +68,18 @@ export const DEFAULT_CONFIG = {
fetchUrlTitle: true,
enableTableEditor: false,
enableFrontMatterTitle: true,
frontMatterTitleField: 'title'
frontMatterTitleField: 'title',
spellcheck: false,
enableSmartPaste: false,
enableMarkdownLint: false,
customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG,
prettierConfig: ` {
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": true
}`,
deleteUnusedAttachments: true
},
preview: {
fontSize: '14',
@@ -69,8 +97,10 @@ export const DEFAULT_CONFIG = {
breaks: true,
smartArrows: false,
allowCustomCSS: false,
customCSS: '',
customCSS: '/* Drop Your Custom CSS Code Here */',
sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE'
mermaidHTMLLabel: false,
lineThroughCheckbox: true
},
blog: {
@@ -80,7 +110,8 @@ export const DEFAULT_CONFIG = {
token: '',
username: '',
password: ''
}
},
coloredTags: {}
}
function validate (config) {
@@ -93,7 +124,6 @@ function validate (config) {
}
function _save (config) {
console.log(config)
window.localStorage.setItem('config', JSON.stringify(config))
}
@@ -113,6 +143,8 @@ function get () {
_save(config)
}
config.autoUpdateEnabled = electronConfig.get('autoUpdateEnabled', config.autoUpdateEnabled)
if (!isInitialized) {
isInitialized = true
let editorTheme = document.getElementById('editorTheme')
@@ -123,16 +155,12 @@ function get () {
document.head.appendChild(editorTheme)
}
config.editor.theme = consts.THEMES.some((theme) => theme === config.editor.theme)
? config.editor.theme
: 'default'
const theme = consts.THEMES.find(theme => theme.name === config.editor.theme)
if (config.editor.theme !== 'default') {
if (config.editor.theme.startsWith('solarized')) {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/solarized.css')
} else {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + config.editor.theme + '.css')
}
if (theme) {
editorTheme.setAttribute('href', theme.path)
} else {
config.editor.theme = 'default'
}
}
@@ -141,7 +169,13 @@ function get () {
function set (updates) {
const currentConfig = get()
const newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, updates)
const arrangedUpdates = updates
if (updates.preview !== undefined && updates.preview.customCSS === '') {
arrangedUpdates.preview.customCSS = DEFAULT_CONFIG.preview.customCSS
}
const newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, arrangedUpdates)
if (!validate(newConfig)) throw new Error('INVALID CONFIG')
_save(newConfig)
@@ -168,18 +202,15 @@ function set (updates) {
editorTheme.setAttribute('rel', 'stylesheet')
document.head.appendChild(editorTheme)
}
const newTheme = consts.THEMES.some((theme) => theme === newConfig.editor.theme)
? newConfig.editor.theme
: 'default'
if (newTheme !== 'default') {
if (newTheme.startsWith('solarized')) {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/solarized.css')
} else {
editorTheme.setAttribute('href', '../node_modules/codemirror/theme/' + newTheme + '.css')
}
const newTheme = consts.THEMES.find(theme => theme.name === newConfig.editor.theme)
if (newTheme) {
editorTheme.setAttribute('href', newTheme.path)
}
electronConfig.set('autoUpdateEnabled', newConfig.autoUpdateEnabled)
ipcRenderer.send('config-renew', {
config: get()
})
@@ -202,7 +233,7 @@ function assignConfigValues (originalConfig, rcConfig) {
function rewriteHotkey (config) {
const keys = [...Object.keys(config.hotkey)]
keys.forEach(key => {
config.hotkey[key] = config.hotkey[key].replace(/Cmd/g, 'Command')
config.hotkey[key] = config.hotkey[key].replace(/Cmd\s/g, 'Command ')
config.hotkey[key] = config.hotkey[key].replace(/Opt\s/g, 'Option ')
})
return config

View File

@@ -6,7 +6,9 @@ const mdurl = require('mdurl')
const fse = require('fs-extra')
const escapeStringRegexp = require('escape-string-regexp')
const sander = require('sander')
const url = require('url')
import i18n from 'browser/lib/i18n'
import { isString } from 'lodash'
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
const DESTINATION_FOLDER = 'attachments'
@@ -18,15 +20,23 @@ const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(
* @returns {Promise<Image>} Image element created
*/
function getImage (file) {
return new Promise((resolve) => {
const reader = new FileReader()
const img = new Image()
img.onload = () => resolve(img)
reader.onload = e => {
img.src = e.target.result
}
reader.readAsDataURL(file)
})
if (isString(file)) {
return new Promise(resolve => {
const img = new Image()
img.onload = () => resolve(img)
img.src = file
})
} else {
return new Promise(resolve => {
const reader = new FileReader()
const img = new Image()
img.onload = () => resolve(img)
reader.onload = e => {
img.src = e.target.result
}
reader.readAsDataURL(file)
})
}
}
/**
@@ -76,7 +86,7 @@ function getOrientation (file) {
return view.getUint16(offset + (i * 12) + 8, little)
}
}
} else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker
} else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker.
break
} else {
offset += view.getUint16(offset, false)
@@ -151,23 +161,28 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr
try {
const isBase64 = typeof sourceFilePath === 'object' && sourceFilePath.type === 'base64'
if (!fs.existsSync(sourceFilePath) && !isBase64) {
if (!isBase64 && !fs.existsSync(sourceFilePath)) {
return reject('source file does not exist')
}
const targetStorage = findStorage.findStorage(storageKey)
const sourcePath = sourceFilePath.sourceFilePath || sourceFilePath
const sourceURL = url.parse(/^\w+:\/\//.test(sourcePath) ? sourcePath : 'file:///' + sourcePath)
let destinationName
if (useRandomName) {
destinationName = `${uniqueSlug()}${path.extname(sourceFilePath.sourceFilePath || sourceFilePath)}`
destinationName = `${uniqueSlug()}${path.extname(sourceURL.pathname) || '.png'}`
} else {
destinationName = path.basename(sourceFilePath.sourceFilePath || sourceFilePath)
destinationName = path.basename(sourceURL.pathname)
}
const targetStorage = findStorage.findStorage(storageKey)
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
createAttachmentDestinationFolder(targetStorage.path, noteKey)
const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName))
if (isBase64) {
const base64Data = sourceFilePath.data.replace(/^data:image\/\w+;base64,/, '')
const dataBuffer = new Buffer(base64Data, 'base64')
const dataBuffer = Buffer.from(base64Data, 'base64')
outputFile.write(dataBuffer, () => {
resolve(destinationName)
})
@@ -227,9 +242,20 @@ function migrateAttachments (markdownContent, storagePath, noteKey) {
* @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths.
*/
function fixLocalURLS (renderedHTML, storagePath) {
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?"', 'g'), function (match) {
var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g')
return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
const encodedWin32SeparatorRegex = /%5C/g
const storageRegex = new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g')
const storageUrl = 'file:///' + path.join(storagePath, DESTINATION_FOLDER).replace(/\\/g, '/')
/*
A :storage reference is like `:storage/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`.
- `STORAGE_FOLDER_PLACEHOLDER` will match `:storage`
- `(?:(?:\\\/|%5C)[-.\\w]+)+` will match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`
- `(?:\\\/|%5C)[-.\\w]+` will either match `/3b6f8bd6-4edd-4b15-96e0-eadc4475b564` or `/f939b2c3.jpg`
- `(?:\\\/|%5C)` match the path seperator. `\\\/` for posix systems and `%5C` for windows.
*/
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\\/|%5C)[-.\\w]+)+', 'g'), function (match) {
return match.replace(encodedWin32SeparatorRegex, '/').replace(storageRegex, storageUrl)
})
}
@@ -253,22 +279,87 @@ function generateAttachmentMarkdown (fileName, path, showPreview) {
* @param {Event} dropEvent DropEvent
*/
function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
const file = dropEvent.dataTransfer.files[0]
const filePath = file.path
const originalFileName = path.basename(filePath)
const fileType = file['type']
const isImage = fileType.startsWith('image')
let promise
if (isImage) {
promise = fixRotate(file).then(base64data => {
return copyAttachment({type: 'base64', data: base64data, sourceFilePath: filePath}, storageKey, noteKey)
})
if (dropEvent.dataTransfer.files.length > 0) {
promise = Promise.all(Array.from(dropEvent.dataTransfer.files).map(file => {
const filePath = file.path
const fileType = file.type // EX) 'image/gif' or 'text/html'
if (fileType.startsWith('image')) {
if (fileType === 'image/gif' || fileType === 'image/svg+xml') {
return copyAttachment(filePath, storageKey, noteKey).then(fileName => ({
fileName,
title: path.basename(filePath),
isImage: true
}))
} else {
return getOrientation(file)
.then((orientation) => {
if (orientation === -1) { // The image rotation is correct and does not need adjustment
return copyAttachment(filePath, storageKey, noteKey)
} else {
return fixRotate(file).then(data => copyAttachment({
type: 'base64',
data: data,
sourceFilePath: filePath
}, storageKey, noteKey))
}
})
.then(fileName =>
({
fileName,
title: path.basename(filePath),
isImage: true
})
)
}
} else {
return copyAttachment(filePath, storageKey, noteKey).then(fileName => ({
fileName,
title: path.basename(filePath),
isImage: false
}))
}
}))
} else {
promise = copyAttachment(filePath, storageKey, noteKey)
let imageURL = dropEvent.dataTransfer.getData('text/plain')
if (!imageURL) {
const match = /<img[^>]*[\s"']src="([^"]+)"/.exec(dropEvent.dataTransfer.getData('text/html'))
if (match) {
imageURL = match[1]
}
}
if (!imageURL) {
return
}
promise = Promise.all([getImage(imageURL)
.then(image => {
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
canvas.width = image.width
canvas.height = image.height
context.drawImage(image, 0, 0)
return copyAttachment({
type: 'base64',
data: canvas.toDataURL(),
sourceFilePath: imageURL
}, storageKey, noteKey)
})
.then(fileName => ({
fileName,
title: imageURL,
isImage: true
}))
])
}
promise.then((fileName) => {
const imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), isImage)
codeEditor.insertAttachmentMd(imageMd)
promise.then(files => {
const attachments = files.filter(file => !!file).map(file => generateAttachmentMarkdown(file.title, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, file.fileName), file.isImage))
codeEditor.insertAttachmentMd(attachments.join('\n'))
})
}
@@ -279,7 +370,7 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
* @param {String} noteKey Key of the current note
* @param {DataTransferItem} dataTransferItem Part of the past-event
*/
function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem) {
function handlePasteImageEvent (codeEditor, storageKey, noteKey, dataTransferItem) {
if (!codeEditor) {
throw new Error('codeEditor has to be given')
}
@@ -316,6 +407,44 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
reader.readAsDataURL(blob)
}
/**
* @description Creates a new file in the storage folder belonging to the current note and inserts the correct markdown code
* @param {CodeEditor} codeEditor Markdown editor. Its insertAttachmentMd() method will be called to include the markdown code
* @param {String} storageKey Key of the current storage
* @param {String} noteKey Key of the current note
* @param {NativeImage} image The native image
*/
function handlePasteNativeImage (codeEditor, storageKey, noteKey, image) {
if (!codeEditor) {
throw new Error('codeEditor has to be given')
}
if (!storageKey) {
throw new Error('storageKey has to be given')
}
if (!noteKey) {
throw new Error('noteKey has to be given')
}
if (!image) {
throw new Error('image has to be given')
}
const targetStorage = findStorage.findStorage(storageKey)
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
createAttachmentDestinationFolder(targetStorage.path, noteKey)
const imageName = `${uniqueSlug()}.png`
const imagePath = path.join(destinationDir, imageName)
const binaryData = image.toPNG()
fs.writeFileSync(imagePath, binaryData, 'binary')
const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName)
const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true)
codeEditor.insertAttachmentMd(imageMd)
}
/**
* @description Returns all attachment paths of the given markdown
* @param {String} markdownContent content in which the attachment paths should be found
@@ -342,6 +471,54 @@ function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) {
return result
}
/**
* @description Copies the attachments to the storage folder and returns the mardown content it should be replaced with
* @param {String} markDownContent content in which the attachment paths should be found
* @param {String} filepath The path of the file with attachments to import
* @param {String} storageKey Storage key of the destination storage
* @param {String} noteKey Key of the current note. Will be used as subfolder in :storage
*/
function importAttachments (markDownContent, filepath, storageKey, noteKey) {
return new Promise((resolve, reject) => {
const nameRegex = /(!\[.*?]\()(.+?\..+?)(\))/g
let attachPath = nameRegex.exec(markDownContent)
const promiseArray = []
const attachmentPaths = []
const groupIndex = 2
while (attachPath) {
let attachmentPath = attachPath[groupIndex]
attachmentPaths.push(attachmentPath)
attachmentPath = path.isAbsolute(attachmentPath) ? attachmentPath : path.join(path.dirname(filepath), attachmentPath)
promiseArray.push(this.copyAttachment(attachmentPath, storageKey, noteKey))
attachPath = nameRegex.exec(markDownContent)
}
let numResolvedPromises = 0
if (promiseArray.length === 0) {
resolve(markDownContent)
}
for (let j = 0; j < promiseArray.length; j++) {
promiseArray[j]
.then((fileName) => {
const newPath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName)
markDownContent = markDownContent.replace(attachmentPaths[j], newPath)
})
.catch((e) => {
console.error('File does not exist in path: ' + attachmentPaths[j])
})
.finally(() => {
numResolvedPromises++
if (numResolvedPromises === promiseArray.length) {
resolve(markDownContent)
}
})
}
})
}
/**
* @description Moves the attachments of the current note to the new location.
* Returns a modified version of the given content so that the links to the attachments point to the new note key.
@@ -383,7 +560,14 @@ function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) {
* @returns {String} Input without the references
*/
function removeStorageAndNoteReferences (input, noteKey) {
return input.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), DESTINATION_FOLDER)
return input.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?("|])', 'g'), function (match) {
const temp = match
.replace(new RegExp(mdurl.encode(path.win32.sep), 'g'), path.sep)
.replace(new RegExp(mdurl.encode(path.posix.sep), 'g'), path.sep)
.replace(new RegExp(escapeStringRegexp(path.win32.sep), 'g'), path.sep)
.replace(new RegExp(escapeStringRegexp(path.posix.sep), 'g'), path.sep)
return temp.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), DESTINATION_FOLDER)
})
}
/**
@@ -437,11 +621,79 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey
}
})
})
} else {
console.info('Attachment folder ("' + attachmentFolder + '") did not exist..')
}
}
/**
* @description Get all existing attachments related to a specific note
including their status (in use or not) and their path. Return null if there're no attachment related to note or specified parametters are invalid
* @param markdownContent markdownContent of the current note
* @param storageKey StorageKey of the current note
* @param noteKey NoteKey of the currentNote
* @return {Promise<Array<{path: String, isInUse: bool}>>} Promise returning the
list of attachments with their properties */
function getAttachmentsPathAndStatus (markdownContent, storageKey, noteKey) {
if (storageKey == null || noteKey == null || markdownContent == null) {
return null
}
const targetStorage = findStorage.findStorage(storageKey)
const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent)
const attachmentsInNoteOnlyFileNames = []
if (attachmentsInNote) {
for (let i = 0; i < attachmentsInNote.length; i++) {
attachmentsInNoteOnlyFileNames.push(attachmentsInNote[i].replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey + escapeStringRegexp(path.sep), 'g'), ''))
}
}
if (fs.existsSync(attachmentFolder)) {
return new Promise((resolve, reject) => {
fs.readdir(attachmentFolder, (err, files) => {
if (err) {
console.error('Error reading directory "' + attachmentFolder + '". Error:')
console.error(err)
reject(err)
return
}
const attachments = []
for (const file of files) {
const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file)
if (!attachmentsInNoteOnlyFileNames.includes(file)) {
attachments.push({ path: absolutePathOfFile, isInUse: false })
} else {
attachments.push({ path: absolutePathOfFile, isInUse: true })
}
}
resolve(attachments)
})
})
} else {
return null
}
}
/**
* @description Remove all specified attachment paths
* @param attachments attachment paths
* @return {Promise} Promise after all attachments are removed */
function removeAttachmentsByPaths (attachments) {
const promises = []
for (const attachment of attachments) {
const promise = new Promise((resolve, reject) => {
fs.unlink(attachment, (err) => {
if (err) {
console.error('Could not delete "%s"', attachment)
console.error(err)
reject(err)
return
}
resolve()
})
})
promises.push(promise)
}
return Promise.all(promises)
}
/**
* Clones the attachments of a given note.
* Copies the attachments to their new destination and updates the content of the new note so that the attachment-links again point to the correct destination.
@@ -538,12 +790,16 @@ module.exports = {
fixLocalURLS,
generateAttachmentMarkdown,
handleAttachmentDrop,
handlePastImageEvent,
handlePasteImageEvent,
handlePasteNativeImage,
getAttachmentsInMarkdownContent,
getAbsolutePathsOfAttachmentsInContent,
importAttachments,
removeStorageAndNoteReferences,
removeAttachmentsByPaths,
deleteAttachmentFolder,
deleteAttachmentsNotPresentInNote,
getAttachmentsPathAndStatus,
moveAttachments,
cloneAttachments,
isAttachmentLink,

View File

@@ -16,7 +16,7 @@ function copyFile (srcPath, dstPath) {
const dstFolder = path.dirname(dstPath)
if (!fs.existsSync(dstFolder)) fs.mkdirSync(dstFolder)
const input = fs.createReadStream(srcPath)
const input = fs.createReadStream(decodeURI(srcPath))
const output = fs.createWriteStream(dstPath)
output.on('error', reject)

View File

@@ -16,6 +16,7 @@ function validateInput (input) {
switch (input.type) {
case 'MARKDOWN_NOTE':
if (!_.isString(input.content)) input.content = ''
if (!_.isArray(input.linesHighlighted)) input.linesHighlighted = []
break
case 'SNIPPET_NOTE':
if (!_.isString(input.description)) input.description = ''
@@ -23,7 +24,8 @@ function validateInput (input) {
input.snippets = [{
name: '',
mode: 'text',
content: ''
content: '',
linesHighlighted: []
}]
}
break

View File

@@ -0,0 +1,86 @@
const http = require('http')
const https = require('https')
const { createTurndownService } = require('../../../lib/turndown')
const createNote = require('./createNote')
import { push } from 'connected-react-router'
import ee from 'browser/main/lib/eventEmitter'
function validateUrl (str) {
if (/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(str)) {
return true
} else {
return false
}
}
const ERROR_MESSAGES = {
ENOTFOUND: 'URL not found. Please check the URL, or your internet connection and try again.',
VALIDATION_ERROR: 'Please check if the URL follows this format: https://www.google.com',
UNEXPECTED: 'Unexpected error! Please check console for details!'
}
function createNoteFromUrl (url, storage, folder, dispatch = null, location = null) {
return new Promise((resolve, reject) => {
const td = createTurndownService()
if (!validateUrl(url)) {
reject({result: false, error: ERROR_MESSAGES.VALIDATION_ERROR})
}
const request = url.startsWith('https') ? https : http
const req = request.request(url, (res) => {
let data = ''
res.on('data', (chunk) => {
data += chunk
})
res.on('end', () => {
const markdownHTML = td.turndown(data)
if (dispatch !== null) {
createNote(storage, {
type: 'MARKDOWN_NOTE',
folder: folder,
title: '',
content: markdownHTML
})
.then((note) => {
const noteHash = note.key
dispatch({
type: 'UPDATE_NOTE',
note: note
})
dispatch(push({
pathname: location.pathname,
query: {key: noteHash}
}))
ee.emit('list:jump', noteHash)
ee.emit('detail:focus')
resolve({result: true, error: null})
})
} else {
createNote(storage, {
type: 'MARKDOWN_NOTE',
folder: folder,
title: '',
content: markdownHTML
}).then((note) => {
resolve({result: true, note, error: null})
})
}
})
})
req.on('error', (e) => {
console.error('error in parsing URL', e)
reject({result: false, error: ERROR_MESSAGES[e.code] || ERROR_MESSAGES.UNEXPECTED})
})
req.end()
})
}
module.exports = createNoteFromUrl

View File

@@ -9,7 +9,8 @@ function createSnippet (snippetFile) {
id: crypto.randomBytes(16).toString('hex'),
name: 'Unnamed snippet',
prefix: [],
content: ''
content: '',
linesHighlighted: []
}
fetchSnippet(null, snippetFile).then((snippets) => {
snippets.push(newSnippet)

View File

@@ -3,7 +3,6 @@ const path = require('path')
const resolveStorageData = require('./resolveStorageData')
const resolveStorageNotes = require('./resolveStorageNotes')
const CSON = require('@rokt33r/season')
const sander = require('sander')
const { findStorage } = require('browser/lib/findStorage')
const deleteSingleNote = require('./deleteNote')

View File

@@ -1,9 +1,9 @@
import { findStorage } from 'browser/lib/findStorage'
import resolveStorageData from './resolveStorageData'
import resolveStorageNotes from './resolveStorageNotes'
import exportNote from './exportNote'
import filenamify from 'filenamify'
import * as path from 'path'
import * as fs from 'fs'
/**
* @param {String} storageKey
@@ -43,19 +43,18 @@ function exportFolder (storageKey, folderKey, fileType, exportDir) {
.then(function exportNotes (data) {
const { storage, notes } = data
notes
return Promise.all(notes
.filter(note => note.folder === folderKey && note.isTrashed === false && note.type === 'MARKDOWN_NOTE')
.forEach(snippet => {
const notePath = path.join(exportDir, `${filenamify(snippet.title, {replacement: '_'})}.${fileType}`)
fs.writeFileSync(notePath, snippet.content)
.map(note => {
const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
return exportNote(note.key, storage.path, note.content, notePath, null)
})
return {
).then(() => ({
storage,
folderKey,
fileType,
exportDir
}
}))
})
}

View File

@@ -4,37 +4,56 @@ import { findStorage } from 'browser/lib/findStorage'
const fs = require('fs')
const path = require('path')
const attachmentManagement = require('./attachmentManagement')
/**
* Export note together with images
* Export note together with attachments
*
* If images is stored in the storage, creates 'images' subfolder in target directory
* and copies images to it. Changes links to images in the content of the note
* If attachments are stored in the storage, creates 'attachments' subfolder in target directory
* and copies attachments to it. Changes links to images in the content of the note
*
* @param {String} nodeKey key of the node that should be exported
* @param {String} storageKey or storage path
* @param {String} noteContent Content to export
* @param {String} targetPath Path to exported file
* @param {function} outputFormatter
* @return {Promise.<*[]>}
*/
function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatter) {
const storagePath = path.isAbsolute(storageKey) ? storageKey : findStorage(storageKey).path
const exportTasks = []
if (!storagePath) {
throw new Error('Storage path is not found')
}
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
storagePath
)
attachmentsAbsolutePaths.forEach(attachment => {
exportTasks.push({
src: attachment,
dst: attachmentManagement.DESTINATION_FOLDER
})
})
let exportedData = noteContent
let exportedData = attachmentManagement.removeStorageAndNoteReferences(
noteContent,
nodeKey
)
if (outputFormatter) {
exportedData = outputFormatter(exportedData, exportTasks)
exportedData = outputFormatter(exportedData, exportTasks, targetPath)
} else {
exportedData = Promise.resolve(exportedData)
}
const tasks = prepareTasks(exportTasks, storagePath, path.dirname(targetPath))
return Promise.all(tasks.map((task) => copyFile(task.src, task.dst)))
.then(() => {
return saveToFile(exportedData, targetPath)
.then(() => exportedData)
.then(data => {
return saveToFile(data, targetPath)
}).catch((err) => {
rollbackExport(tasks)
throw err

View File

@@ -11,6 +11,7 @@ const dataApi = {
exportFolder: require('./exportFolder'),
exportStorage: require('./exportStorage'),
createNote: require('./createNote'),
createNoteFromUrl: require('./createNoteFromUrl'),
updateNote: require('./updateNote'),
deleteNote: require('./deleteNote'),
moveNote: require('./moveNote'),

View File

@@ -4,6 +4,7 @@ const resolveStorageData = require('./resolveStorageData')
const resolveStorageNotes = require('./resolveStorageNotes')
const consts = require('browser/lib/consts')
const path = require('path')
const fs = require('fs')
const CSON = require('@rokt33r/season')
/**
* @return {Object} all storages and notes
@@ -19,11 +20,14 @@ const CSON = require('@rokt33r/season')
* 2. legacy
* 3. empty directory
*/
function init () {
const fetchStorages = function () {
let rawStorages
try {
rawStorages = JSON.parse(window.localStorage.getItem('storages'))
// Remove storages who's location is inaccesible.
rawStorages = rawStorages.filter(storage => fs.existsSync(storage.path))
if (!_.isArray(rawStorages)) throw new Error('Cached data is not valid.')
} catch (e) {
console.warn('Failed to parse cached data from localStorage', e)
@@ -36,6 +40,7 @@ function init () {
const fetchNotes = function (storages) {
const findNotesFromEachStorage = storages
.filter(storage => fs.existsSync(storage.path))
.map((storage) => {
return resolveStorageNotes(storage)
.then((notes) => {
@@ -51,7 +56,11 @@ function init () {
}
})
if (unknownCount > 0) {
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
try {
CSON.writeFileSync(path.join(storage.path, 'boostnote.json'), _.pick(storage, ['folders', 'version']))
} catch (e) {
console.log('Error writting boostnote.json: ' + e + ' from init.js')
}
}
return notes
})

View File

@@ -69,7 +69,8 @@ function importAll (storage, data) {
isStarred: false,
title: article.title,
content: '# ' + article.title + '\n\n' + article.content,
key: noteKey
key: noteKey,
linesHighlighted: article.linesHighlighted
}
notes.push(newNote)
} else {
@@ -87,7 +88,8 @@ function importAll (storage, data) {
snippets: [{
name: article.mode,
mode: article.mode,
content: article.content
content: article.content,
linesHighlighted: article.linesHighlighted
}]
}
notes.push(newNote)

View File

@@ -1,7 +1,6 @@
const resolveStorageData = require('./resolveStorageData')
const _ = require('lodash')
const path = require('path')
const fs = require('fs')
const CSON = require('@rokt33r/season')
const keygen = require('browser/lib/keygen')
const sander = require('sander')

Some files were not shown because too many files have changed in this diff Show More