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

Compare commits

..

421 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
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
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
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
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
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
900f20f164 handle all dropped images 2018-11-28 15:58:58 +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
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
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
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
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
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
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
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
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
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
43bd0b0dd5 Merge branch 'master' of https://github.com/BoostIO/Boostnote 2018-10-18 12:07:40 +07: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
167 changed files with 4819 additions and 1619 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

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

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

View File

@@ -1,10 +1,10 @@
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

View File

@@ -2,28 +2,39 @@ import PropTypes from 'prop-types'
import React from 'react'
import _ from 'lodash'
import CodeMirror from 'codemirror'
import hljs from 'highlight.js'
import 'codemirror-mode-elixir'
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
import convertModeName from 'browser/lib/convertModeName'
import { options, TableEditor, Alignment } from '@susisu/mte-kernel'
import {
options,
TableEditor,
Alignment
} from '@susisu/mte-kernel'
import TextEditorInterface from 'browser/lib/TextEditorInterface'
import eventEmitter from 'browser/main/lib/eventEmitter'
import iconv from 'iconv-lite'
import crypto from 'crypto'
import consts from 'browser/lib/consts'
import { isMarkdownTitleURL } from 'browser/lib/utils'
import styles from '../components/CodeEditor.styl'
import fs from 'fs'
const { ipcRenderer, remote, clipboard } = require('electron')
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
const spellcheck = require('browser/lib/spellcheck')
const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
const buildEditorContextMenu = require('browser/lib/contextMenuBuilder').buildEditorContextMenu
import TurndownService from 'turndown'
import { gfm } from 'turndown-plugin-gfm'
import {languageMaps} from '../lib/CMLanguageList'
import snippetManager from '../lib/SnippetManager'
import {generateInEditor, tocExistsInEditor} from 'browser/lib/markdown-toc-generator'
import markdownlint from 'markdownlint'
import Jsonlint from 'jsonlint-mod'
import { DEFAULT_CONFIG } from '../main/lib/ConfigManager'
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
const buildCMRulers = (rulers, enableRulers) =>
(enableRulers ? rulers.map(ruler => ({ column: ruler })) : [])
(enableRulers ? rulers.map(ruler => ({
column: ruler
})) : [])
function translateHotkey (hotkey) {
return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl')
@@ -38,9 +49,11 @@ export default class CodeEditor extends React.Component {
trailing: true
})
this.changeHandler = (editor, changeObject) => this.handleChange(editor, changeObject)
this.highlightHandler = (editor, changeObject) => this.handleHighlight(editor, changeObject)
this.focusHandler = () => {
ipcRenderer.send('editor:focused', true)
}
const debouncedDeletionOfAttachments = _.debounce(attachmentManagement.deleteAttachmentsNotPresentInNote, 30000)
this.blurHandler = (editor, e) => {
ipcRenderer.send('editor:focused', false)
if (e == null) return null
@@ -52,13 +65,11 @@ export default class CodeEditor extends React.Component {
el = el.parentNode
}
this.props.onBlur != null && this.props.onBlur(e)
const { storageKey, noteKey } = this.props
attachmentManagement.deleteAttachmentsNotPresentInNote(
this.editor.getValue(),
const {
storageKey,
noteKey
)
} = this.props
debouncedDeletionOfAttachments(this.editor.getValue(), storageKey, noteKey)
}
this.pasteHandler = (editor, e) => {
e.preventDefault()
@@ -71,6 +82,8 @@ export default class CodeEditor extends React.Component {
this.searchHandler = (e, msg) => this.handleSearch(msg)
this.searchState = null
this.scrollToLineHandeler = this.scrollToLine.bind(this)
this.getCodeEditorLintConfig = this.getCodeEditorLintConfig.bind(this)
this.validatorOfMarkdown = this.validatorOfMarkdown.bind(this)
this.formatTable = () => this.handleFormatTable()
@@ -123,7 +136,9 @@ export default class CodeEditor extends React.Component {
}
handleFormatTable () {
this.tableEditor.formatAll(options({textWidthOptions: {}}))
this.tableEditor.formatAll(options({
textWidthOptions: {}
}))
}
handleEditorActivity () {
@@ -134,7 +149,8 @@ export default class CodeEditor extends React.Component {
updateDefaultKeyMap () {
const { hotkey } = this.props
const expandSnippet = this.expandSnippet.bind(this)
const self = this
const expandSnippet = snippetManager.expandSnippet
this.defaultKeyMap = CodeMirror.normalizeKeyMap({
Tab: function (cm) {
@@ -154,14 +170,16 @@ export default class CodeEditor extends React.Component {
}
cm.execCommand('goLineEnd')
} else if (
!charBeforeCursor.match(/\t|\s|\r|\n/) &&
!charBeforeCursor.match(/\t|\s|\r|\n|\$/) &&
cursor.ch > 1
) {
// text expansion on tab key if the char before is alphabet
const snippets = JSON.parse(
fs.readFileSync(consts.SNIPPET_FILE, 'utf8')
const wordBeforeCursor = self.getWordBeforeCursor(
line,
cursor.line,
cursor.ch
)
if (expandSnippet(line, cursor, cm, snippets) === false) {
if (expandSnippet(wordBeforeCursor, cursor, cm) === false) {
if (tabs) {
cm.execCommand('insertTab')
} else {
@@ -177,9 +195,20 @@ export default class CodeEditor extends React.Component {
}
}
},
'Cmd-Left': function (cm) {
cm.execCommand('goLineLeft')
},
'Cmd-T': function (cm) {
// Do nothing
},
[translateHotkey(hotkey.insertDate)]: function (cm) {
const dateNow = new Date()
cm.replaceSelection(dateNow.toLocaleDateString())
},
[translateHotkey(hotkey.insertDateTime)]: function (cm) {
const dateNow = new Date()
cm.replaceSelection(dateNow.toLocaleString())
},
Enter: 'boostNewLineAndIndentContinueMarkdownList',
'Ctrl-C': cm => {
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
@@ -210,33 +239,19 @@ export default class CodeEditor extends React.Component {
}
componentDidMount () {
const { rulers, enableRulers } = this.props
const { rulers, enableRulers, enableMarkdownLint } = this.props
eventEmitter.on('line:jump', this.scrollToLineHandeler)
const 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.'
}
]
if (!fs.existsSync(consts.SNIPPET_FILE)) {
fs.writeFileSync(
consts.SNIPPET_FILE,
JSON.stringify(defaultSnippet, null, 4),
'utf8'
)
}
snippetManager.init()
this.updateDefaultKeyMap()
this.value = this.props.value
this.editor = CodeMirror(this.refs.root, {
rulers: buildCMRulers(rulers, enableRulers),
value: this.props.value,
linesHighlighted: this.props.linesHighlighted,
lineNumbers: this.props.displayLineNumbers,
lineWrapping: true,
lineWrapping: this.props.lineWrapping,
theme: this.props.theme,
indentUnit: this.props.indentSize,
tabSize: this.props.indentSize,
@@ -246,21 +261,29 @@ export default class CodeEditor extends React.Component {
inputStyle: 'textarea',
dragDrop: false,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
lint: enableMarkdownLint ? this.getCodeEditorLintConfig() : false,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
autoCloseBrackets: {
pairs: '()[]{}\'\'""$$**``',
triples: '```"""\'\'\'',
explode: '[]{}``$$',
pairs: this.props.matchingPairs,
triples: this.props.matchingTriples,
explode: this.props.explodingPairs,
override: true
},
extraKeys: this.defaultKeyMap
})
this.setMode(this.props.mode)
document.querySelector('.CodeMirror-lint-markers').style.display = enableMarkdownLint ? 'inline-block' : 'none'
if (!this.props.mode && this.props.value && this.props.autoDetect) {
this.autoDetectLanguage(this.props.value)
} else {
this.setMode(this.props.mode)
}
this.editor.on('focus', this.focusHandler)
this.editor.on('blur', this.blurHandler)
this.editor.on('change', this.changeHandler)
this.editor.on('gutterClick', this.highlightHandler)
this.editor.on('paste', this.pasteHandler)
if (this.props.switchPreview !== 'RIGHTCLICK') {
this.editor.on('contextmenu', this.contextMenuHandler)
@@ -292,43 +315,117 @@ export default class CodeEditor extends React.Component {
})
this.editorKeyMap = CodeMirror.normalizeKeyMap({
'Tab': () => { this.tableEditor.nextCell(this.tableEditorOptions) },
'Shift-Tab': () => { this.tableEditor.previousCell(this.tableEditorOptions) },
'Enter': () => { this.tableEditor.nextRow(this.tableEditorOptions) },
'Ctrl-Enter': () => { this.tableEditor.escape(this.tableEditorOptions) },
'Cmd-Enter': () => { this.tableEditor.escape(this.tableEditorOptions) },
'Shift-Ctrl-Left': () => { this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) },
'Shift-Cmd-Left': () => { this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) },
'Shift-Ctrl-Right': () => { this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) },
'Shift-Cmd-Right': () => { this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) },
'Shift-Ctrl-Up': () => { this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) },
'Shift-Cmd-Up': () => { this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) },
'Shift-Ctrl-Down': () => { this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) },
'Shift-Cmd-Down': () => { this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) },
'Ctrl-Left': () => { this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) },
'Cmd-Left': () => { this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) },
'Ctrl-Right': () => { this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) },
'Cmd-Right': () => { this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) },
'Ctrl-Up': () => { this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) },
'Cmd-Up': () => { this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) },
'Ctrl-Down': () => { this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) },
'Cmd-Down': () => { this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) },
'Ctrl-K Ctrl-I': () => { this.tableEditor.insertRow(this.tableEditorOptions) },
'Cmd-K Cmd-I': () => { this.tableEditor.insertRow(this.tableEditorOptions) },
'Ctrl-L Ctrl-I': () => { this.tableEditor.deleteRow(this.tableEditorOptions) },
'Cmd-L Cmd-I': () => { this.tableEditor.deleteRow(this.tableEditorOptions) },
'Ctrl-K Ctrl-J': () => { this.tableEditor.insertColumn(this.tableEditorOptions) },
'Cmd-K Cmd-J': () => { this.tableEditor.insertColumn(this.tableEditorOptions) },
'Ctrl-L Ctrl-J': () => { this.tableEditor.deleteColumn(this.tableEditorOptions) },
'Cmd-L Cmd-J': () => { this.tableEditor.deleteColumn(this.tableEditorOptions) },
'Alt-Shift-Ctrl-Left': () => { this.tableEditor.moveColumn(-1, this.tableEditorOptions) },
'Alt-Shift-Cmd-Left': () => { this.tableEditor.moveColumn(-1, this.tableEditorOptions) },
'Alt-Shift-Ctrl-Right': () => { this.tableEditor.moveColumn(1, this.tableEditorOptions) },
'Alt-Shift-Cmd-Right': () => { this.tableEditor.moveColumn(1, this.tableEditorOptions) },
'Alt-Shift-Ctrl-Up': () => { this.tableEditor.moveRow(-1, this.tableEditorOptions) },
'Alt-Shift-Cmd-Up': () => { this.tableEditor.moveRow(-1, this.tableEditorOptions) },
'Alt-Shift-Ctrl-Down': () => { this.tableEditor.moveRow(1, this.tableEditorOptions) },
'Alt-Shift-Cmd-Down': () => { this.tableEditor.moveRow(1, this.tableEditorOptions) }
'Tab': () => {
this.tableEditor.nextCell(this.tableEditorOptions)
},
'Shift-Tab': () => {
this.tableEditor.previousCell(this.tableEditorOptions)
},
'Enter': () => {
this.tableEditor.nextRow(this.tableEditorOptions)
},
'Ctrl-Enter': () => {
this.tableEditor.escape(this.tableEditorOptions)
},
'Cmd-Enter': () => {
this.tableEditor.escape(this.tableEditorOptions)
},
'Shift-Ctrl-Left': () => {
this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions)
},
'Shift-Cmd-Left': () => {
this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions)
},
'Shift-Ctrl-Right': () => {
this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions)
},
'Shift-Cmd-Right': () => {
this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions)
},
'Shift-Ctrl-Up': () => {
this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions)
},
'Shift-Cmd-Up': () => {
this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions)
},
'Shift-Ctrl-Down': () => {
this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions)
},
'Shift-Cmd-Down': () => {
this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions)
},
'Ctrl-Left': () => {
this.tableEditor.moveFocus(0, -1, this.tableEditorOptions)
},
'Cmd-Left': () => {
this.tableEditor.moveFocus(0, -1, this.tableEditorOptions)
},
'Ctrl-Right': () => {
this.tableEditor.moveFocus(0, 1, this.tableEditorOptions)
},
'Cmd-Right': () => {
this.tableEditor.moveFocus(0, 1, this.tableEditorOptions)
},
'Ctrl-Up': () => {
this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions)
},
'Cmd-Up': () => {
this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions)
},
'Ctrl-Down': () => {
this.tableEditor.moveFocus(1, 0, this.tableEditorOptions)
},
'Cmd-Down': () => {
this.tableEditor.moveFocus(1, 0, this.tableEditorOptions)
},
'Ctrl-K Ctrl-I': () => {
this.tableEditor.insertRow(this.tableEditorOptions)
},
'Cmd-K Cmd-I': () => {
this.tableEditor.insertRow(this.tableEditorOptions)
},
'Ctrl-L Ctrl-I': () => {
this.tableEditor.deleteRow(this.tableEditorOptions)
},
'Cmd-L Cmd-I': () => {
this.tableEditor.deleteRow(this.tableEditorOptions)
},
'Ctrl-K Ctrl-J': () => {
this.tableEditor.insertColumn(this.tableEditorOptions)
},
'Cmd-K Cmd-J': () => {
this.tableEditor.insertColumn(this.tableEditorOptions)
},
'Ctrl-L Ctrl-J': () => {
this.tableEditor.deleteColumn(this.tableEditorOptions)
},
'Cmd-L Cmd-J': () => {
this.tableEditor.deleteColumn(this.tableEditorOptions)
},
'Alt-Shift-Ctrl-Left': () => {
this.tableEditor.moveColumn(-1, this.tableEditorOptions)
},
'Alt-Shift-Cmd-Left': () => {
this.tableEditor.moveColumn(-1, this.tableEditorOptions)
},
'Alt-Shift-Ctrl-Right': () => {
this.tableEditor.moveColumn(1, this.tableEditorOptions)
},
'Alt-Shift-Cmd-Right': () => {
this.tableEditor.moveColumn(1, this.tableEditorOptions)
},
'Alt-Shift-Ctrl-Up': () => {
this.tableEditor.moveRow(-1, this.tableEditorOptions)
},
'Alt-Shift-Cmd-Up': () => {
this.tableEditor.moveRow(-1, this.tableEditorOptions)
},
'Alt-Shift-Ctrl-Down': () => {
this.tableEditor.moveRow(1, this.tableEditorOptions)
},
'Alt-Shift-Cmd-Down': () => {
this.tableEditor.moveRow(1, this.tableEditorOptions)
}
})
if (this.props.enableTableEditor) {
@@ -339,63 +436,16 @@ export default class CodeEditor extends React.Component {
this.setState({
clientWidth: this.refs.root.clientWidth
})
}
expandSnippet (line, cursor, cm, snippets) {
const wordBeforeCursor = this.getWordBeforeCursor(
line,
cursor.line,
cursor.ch
)
const templateCursorString = ':{}'
for (let i = 0; i < snippets.length; i++) {
if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) {
if (snippets[i].content.indexOf(templateCursorString) !== -1) {
const snippetLines = 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(
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(
snippets[i].content,
wordBeforeCursor.range.from,
wordBeforeCursor.range.to
)
}
return true
}
}
return false
this.initialHighlighting()
}
getWordBeforeCursor (line, lineNumber, cursorPosition) {
let wordBeforeCursor = ''
const originCursorPosition = cursorPosition
const emptyChars = /\t|\s|\r|\n/
const emptyChars = /\t|\s|\r|\n|\$/
// to prevent the word to expand is long that will crash the whole app
// to prevent the word is long that will crash the whole app
// the safeStop is there to stop user to expand words that longer than 20 chars
const safeStop = 20
@@ -405,7 +455,7 @@ export default class CodeEditor extends React.Component {
if (!emptyChars.test(currentChar)) {
wordBeforeCursor = currentChar + wordBeforeCursor
} else if (wordBeforeCursor.length >= safeStop) {
throw new Error('Your snippet trigger is too long !')
throw new Error('Stopped after 20 loops for safety reason !')
} else {
break
}
@@ -415,8 +465,14 @@ export default class CodeEditor extends React.Component {
return {
text: wordBeforeCursor,
range: {
from: { line: lineNumber, ch: originCursorPosition },
to: { line: lineNumber, ch: cursorPosition }
from: {
line: lineNumber,
ch: originCursorPosition
},
to: {
line: lineNumber,
ch: cursorPosition
}
}
}
}
@@ -442,7 +498,12 @@ export default class CodeEditor extends React.Component {
componentDidUpdate (prevProps, prevState) {
let needRefresh = false
const { rulers, enableRulers } = this.props
const {
rulers,
enableRulers,
enableMarkdownLint,
customMarkdownLintConfig
} = this.props
if (prevProps.mode !== this.props.mode) {
this.setMode(this.props.mode)
}
@@ -459,6 +520,16 @@ export default class CodeEditor extends React.Component {
if (prevProps.keyMap !== this.props.keyMap) {
needRefresh = true
}
if (prevProps.enableMarkdownLint !== enableMarkdownLint || prevProps.customMarkdownLintConfig !== customMarkdownLintConfig) {
if (!enableMarkdownLint) {
this.editor.setOption('lint', {default: false})
document.querySelector('.CodeMirror-lint-markers').style.display = 'none'
} else {
this.editor.setOption('lint', this.getCodeEditorLintConfig())
document.querySelector('.CodeMirror-lint-markers').style.display = 'inline-block'
}
needRefresh = true
}
if (
prevProps.enableRulers !== enableRulers ||
@@ -479,10 +550,26 @@ export default class CodeEditor extends React.Component {
this.editor.setOption('lineNumbers', this.props.displayLineNumbers)
}
if (prevProps.lineWrapping !== this.props.lineWrapping) {
this.editor.setOption('lineWrapping', this.props.lineWrapping)
}
if (prevProps.scrollPastEnd !== this.props.scrollPastEnd) {
this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd)
}
if (prevProps.matchingPairs !== this.props.matchingPairs ||
prevProps.matchingTriples !== this.props.matchingTriples ||
prevProps.explodingPairs !== this.props.explodingPairs) {
const bracketObject = {
pairs: this.props.matchingPairs,
triples: this.props.matchingTriples,
explode: this.props.explodingPairs,
override: true
}
this.editor.setOption('autoCloseBrackets', bracketObject)
}
if (prevProps.enableTableEditor !== this.props.enableTableEditor) {
if (this.props.enableTableEditor) {
this.editor.on('cursorActivity', this.editorActivityHandler)
@@ -515,7 +602,7 @@ export default class CodeEditor extends React.Component {
if (prevProps.spellCheck !== this.props.spellCheck) {
if (this.props.spellCheck === false) {
spellcheck.setLanguage(this.editor, spellcheck.SPELLCHECK_DISABLED)
let elem = document.getElementById('editor-bottom-panel')
const elem = document.getElementById('editor-bottom-panel')
elem.parentNode.removeChild(elem)
} else {
this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'})
@@ -527,8 +614,58 @@ export default class CodeEditor extends React.Component {
}
}
getCodeEditorLintConfig () {
const { mode } = this.props
const checkMarkdownNoteIsOpen = mode === 'Boost Flavored Markdown'
return checkMarkdownNoteIsOpen ? {
'getAnnotations': this.validatorOfMarkdown,
'async': true
} : false
}
validatorOfMarkdown (text, updateLinting) {
const { customMarkdownLintConfig } = this.props
let lintConfigJson
try {
Jsonlint.parse(customMarkdownLintConfig)
lintConfigJson = JSON.parse(customMarkdownLintConfig)
} catch (err) {
eventEmitter.emit('APP_SETTING_ERROR')
return
}
const lintOptions = {
'strings': {
'content': text
},
'config': lintConfigJson
}
return markdownlint(lintOptions, (err, result) => {
if (!err) {
const foundIssues = []
const splitText = text.split('\n')
result.content.map(item => {
let ruleNames = ''
item.ruleNames.map((ruleName, index) => {
ruleNames += ruleName
ruleNames += (index === item.ruleNames.length - 1) ? ': ' : '/'
})
const lineNumber = item.lineNumber - 1
foundIssues.push({
from: CodeMirror.Pos(lineNumber, 0),
to: CodeMirror.Pos(lineNumber, splitText[lineNumber].length),
message: ruleNames + item.ruleDescription,
severity: 'warning'
})
})
updateLinting(foundIssues)
}
})
}
setMode (mode) {
let syntax = CodeMirror.findModeByName(convertModeName(mode))
let syntax = CodeMirror.findModeByName(convertModeName(mode || 'text'))
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
this.editor.setOption('mode', syntax.mime)
@@ -537,12 +674,130 @@ export default class CodeEditor extends React.Component {
handleChange (editor, changeObject) {
spellcheck.handleChange(editor, changeObject)
// The current note contains an toc. We'll check for changes on headlines.
// origin is undefined when markdownTocGenerator replace the old tod
if (tocExistsInEditor(editor) && changeObject.origin !== undefined) {
let requireTocUpdate
// Check if one of the changed lines contains a headline
for (let line = 0; line < changeObject.text.length; line++) {
if (this.linePossibleContainsHeadline(editor.getLine(changeObject.from.line + line))) {
requireTocUpdate = true
break
}
}
if (!requireTocUpdate) {
// Check if one of the removed lines contains a headline
for (let line = 0; line < changeObject.removed.length; line++) {
if (this.linePossibleContainsHeadline(changeObject.removed[line])) {
requireTocUpdate = true
break
}
}
}
if (requireTocUpdate) {
generateInEditor(editor)
}
}
this.updateHighlight(editor, changeObject)
this.value = editor.getValue()
if (this.props.onChange) {
this.props.onChange(editor)
}
}
linePossibleContainsHeadline (currentLine) {
// We can't check if the line start with # because when some write text before
// the # we also need to update the toc
return currentLine.includes('# ')
}
incrementLines (start, linesAdded, linesRemoved, editor) {
const highlightedLines = editor.options.linesHighlighted
const totalHighlightedLines = highlightedLines.length
const offset = linesAdded - linesRemoved
// Store new items to be added as we're changing the lines
const newLines = []
let i = totalHighlightedLines
while (i--) {
const lineNumber = highlightedLines[i]
// Interval that will need to be updated
// Between start and (start + offset) remove highlight
if (lineNumber >= start) {
highlightedLines.splice(highlightedLines.indexOf(lineNumber), 1)
// Lines that need to be relocated
if (lineNumber >= (start + linesRemoved)) {
newLines.push(lineNumber + offset)
}
}
}
// Adding relocated lines
highlightedLines.push(...newLines)
if (this.props.onChange) {
this.props.onChange(editor)
}
}
handleHighlight (editor, changeObject) {
const lines = editor.options.linesHighlighted
if (!lines.includes(changeObject)) {
lines.push(changeObject)
editor.addLineClass(changeObject, 'text', 'CodeMirror-activeline-background')
} else {
lines.splice(lines.indexOf(changeObject), 1)
editor.removeLineClass(changeObject, 'text', 'CodeMirror-activeline-background')
}
if (this.props.onChange) {
this.props.onChange(editor)
}
}
updateHighlight (editor, changeObject) {
const linesAdded = changeObject.text.length - 1
const linesRemoved = changeObject.removed.length - 1
// If no lines added or removed return
if (linesAdded === 0 && linesRemoved === 0) {
return
}
let start = changeObject.from.line
switch (changeObject.origin) {
case '+insert", "undo':
start += 1
break
case 'paste':
case '+delete':
case '+input':
if (changeObject.to.ch !== 0 || changeObject.from.ch !== 0) {
start += 1
}
break
default:
return
}
this.incrementLines(start, linesAdded, linesRemoved, editor)
}
moveCursorTo (row, col) {}
scrollToLine (event, num) {
@@ -551,6 +806,9 @@ export default class CodeEditor extends React.Component {
ch: 1
}
this.editor.setCursor(cursor)
const top = this.editor.charCoords({line: num, ch: 0}, 'local').top
const middleHeight = this.editor.getScrollerElement().offsetHeight / 2
this.editor.scrollTo(null, top - middleHeight - 5)
}
focus () {
@@ -567,6 +825,7 @@ export default class CodeEditor extends React.Component {
this.value = this.props.value
this.editor.setValue(this.props.value)
this.editor.clearHistory()
this.restartHighlighting()
this.editor.on('change', this.changeHandler)
this.editor.refresh()
}
@@ -579,7 +838,10 @@ export default class CodeEditor extends React.Component {
handleDropImage (dropEvent) {
dropEvent.preventDefault()
const { storageKey, noteKey } = this.props
const {
storageKey,
noteKey
} = this.props
attachmentManagement.handleAttachmentDrop(
this,
storageKey,
@@ -592,25 +854,33 @@ export default class CodeEditor extends React.Component {
this.editor.replaceSelection(imageMd)
}
autoDetectLanguage (content) {
const res = hljs.highlightAuto(content, Object.keys(languageMaps))
this.setMode(languageMaps[res.language])
}
handlePaste (editor, forceSmartPaste) {
const { storageKey, noteKey, fetchUrlTitle, enableSmartPaste } = this.props
const isURL = str => {
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
return matcher.test(str)
}
const isURL = str => /(?:^\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str)
const isInLinkTag = editor => {
const startCursor = editor.getCursor('start')
const prevChar = editor.getRange(
{ line: startCursor.line, ch: startCursor.ch - 2 },
{ line: startCursor.line, ch: startCursor.ch }
)
const prevChar = editor.getRange({
line: startCursor.line,
ch: startCursor.ch - 2
}, {
line: startCursor.line,
ch: startCursor.ch
})
const endCursor = editor.getCursor('end')
const nextChar = editor.getRange(
{ line: endCursor.line, ch: endCursor.ch },
{ line: endCursor.line, ch: endCursor.ch + 1 }
)
const nextChar = editor.getRange({
line: endCursor.line,
ch: endCursor.ch
}, {
line: endCursor.line,
ch: endCursor.ch + 1
})
return prevChar === '](' && nextChar === ')'
}
@@ -654,25 +924,10 @@ export default class CodeEditor extends React.Component {
if (isInFencedCodeBlock(editor)) {
this.handlePasteText(editor, pastedTxt)
} else if (fetchUrlTitle && isMarkdownTitleURL(pastedTxt) && !isInLinkTag(editor)) {
this.handlePasteUrl(editor, pastedTxt)
} else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
this.handlePasteUrl(editor, pastedTxt)
} else if (enableSmartPaste || forceSmartPaste) {
const image = clipboard.readImage()
if (!image.isEmpty()) {
attachmentManagement.handlePastNativeImage(
this,
storageKey,
noteKey,
image
)
} else {
const pastedHtml = clipboard.readHTML()
if (pastedHtml.length > 0) {
this.handlePasteHtml(editor, pastedHtml)
} else {
this.handlePasteText(editor, pastedTxt)
}
}
} else if (attachmentManagement.isAttachmentLink(pastedTxt)) {
attachmentManagement
.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
@@ -680,7 +935,28 @@ export default class CodeEditor extends React.Component {
this.editor.replaceSelection(modifiedText)
})
} else {
this.handlePasteText(editor, pastedTxt)
const image = clipboard.readImage()
if (!image.isEmpty()) {
attachmentManagement.handlePasteNativeImage(
this,
storageKey,
noteKey,
image
)
} else if (enableSmartPaste || forceSmartPaste) {
const pastedHtml = clipboard.readHTML()
if (pastedHtml.length > 0) {
this.handlePasteHtml(editor, pastedHtml)
} else {
this.handlePasteText(editor, pastedTxt)
}
} else {
this.handlePasteText(editor, pastedTxt)
}
}
if (!this.props.mode && this.props.autoDetect) {
this.autoDetectLanguage(editor.doc.getValue())
}
}
@@ -691,7 +967,17 @@ export default class CodeEditor extends React.Component {
}
handlePasteUrl (editor, pastedTxt) {
const taggedUrl = `<${pastedTxt}>`
let taggedUrl = `<${pastedTxt}>`
let urlToFetch = pastedTxt
let titleMark = ''
if (isMarkdownTitleURL(pastedTxt)) {
const pastedTxtSplitted = pastedTxt.split(' ')
titleMark = `${pastedTxtSplitted[0]} `
urlToFetch = pastedTxtSplitted[1]
taggedUrl = `<${urlToFetch}>`
}
editor.replaceSelection(taggedUrl)
const isImageReponse = response => {
@@ -703,22 +989,23 @@ export default class CodeEditor extends React.Component {
const replaceTaggedUrl = replacement => {
const value = editor.getValue()
const cursor = editor.getCursor()
const newValue = value.replace(taggedUrl, replacement)
const newValue = value.replace(taggedUrl, titleMark + replacement)
const newCursor = Object.assign({}, cursor, {
ch: cursor.ch + newValue.length - value.length
ch: cursor.ch + newValue.length - (value.length - titleMark.length)
})
editor.setValue(newValue)
editor.setCursor(newCursor)
}
fetch(pastedTxt, {
fetch(urlToFetch, {
method: 'get'
})
.then(response => {
if (isImageReponse(response)) {
return this.mapImageResponse(response, pastedTxt)
return this.mapImageResponse(response, urlToFetch)
} else {
return this.mapNormalResponse(response, pastedTxt)
return this.mapNormalResponse(response, urlToFetch)
}
})
.then(replacement => {
@@ -758,6 +1045,29 @@ export default class CodeEditor extends React.Component {
})
}
initialHighlighting () {
if (this.editor.options.linesHighlighted == null) {
return
}
const totalHighlightedLines = this.editor.options.linesHighlighted.length
const totalAvailableLines = this.editor.lineCount()
for (let i = 0; i < totalHighlightedLines; i++) {
const lineNumber = this.editor.options.linesHighlighted[i]
if (lineNumber > totalAvailableLines) {
// make sure that we skip the invalid lines althrough this case should not be happened.
continue
}
this.editor.addLineClass(lineNumber, 'text', 'CodeMirror-activeline-background')
}
}
restartHighlighting () {
this.editor.options.linesHighlighted = this.props.linesHighlighted
this.initialHighlighting()
}
mapImageResponse (response, pastedTxt) {
return new Promise((resolve, reject) => {
try {
@@ -783,7 +1093,7 @@ export default class CodeEditor extends React.Component {
iconv.encodingExists(_charset)
? _charset
: 'utf-8'
resolve(iconv.decode(new Buffer(buff), charset).toString())
resolve(iconv.decode(Buffer.from(buff), charset).toString())
} catch (e) {
reject(e)
}
@@ -803,20 +1113,26 @@ export default class CodeEditor extends React.Component {
}
render () {
const {className, fontSize} = this.props
const {
className,
fontSize
} = this.props
const fontFamily = normalizeEditorFontFamily(this.props.fontFamily)
const width = this.props.width
return (
<div
className={className == null ? 'CodeEditor' : `CodeEditor ${className}`}
ref='root'
tabIndex='-1'
style={{
fontFamily,
fontSize: fontSize,
width: width
}}
onDrop={e => this.handleDropImage(e)}
return (<
div className={
className == null ? 'CodeEditor' : `CodeEditor ${className}`
}
ref='root'
tabIndex='-1'
style={{
fontFamily,
fontSize: fontSize,
width: width
}}
onDrop={
e => this.handleDropImage(e)
}
/>
)
}
@@ -850,7 +1166,10 @@ CodeEditor.propTypes = {
onBlur: PropTypes.func,
onChange: PropTypes.func,
readOnly: PropTypes.bool,
spellCheck: PropTypes.bool
autoDetect: PropTypes.bool,
spellCheck: PropTypes.bool,
enableMarkdownLint: PropTypes.bool,
customMarkdownLintConfig: PropTypes.string
}
CodeEditor.defaultProps = {
@@ -861,5 +1180,8 @@ CodeEditor.defaultProps = {
fontFamily: 'Monaco, Consolas',
indentSize: 4,
indentType: 'space',
spellCheck: false
autoDetect: false,
spellCheck: false,
enableMarkdownLint: DEFAULT_CONFIG.editor.enableMarkdownLint,
customMarkdownLintConfig: DEFAULT_CONFIG.editor.customMarkdownLintConfig
}

View File

@@ -3,4 +3,3 @@
.spellcheck-select
border: none
text-decoration underline wavy red

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'
@@ -147,10 +159,10 @@ class MarkdownEditor extends React.Component {
e.preventDefault()
e.stopPropagation()
const idMatch = /checkbox-([0-9]+)/
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
const checkReplace = /\[x\]/i
const uncheckReplace = /\[ \]/
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
@@ -221,6 +233,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)
@@ -232,7 +266,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
@@ -270,17 +304,24 @@ 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}
/>
<MarkdownPreview styleName={this.state.status === 'PREVIEW'
? 'preview'
@@ -314,6 +355,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,13 +18,13 @@ 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 i18n from 'browser/lib/i18n'
import fs from 'fs'
import { render } from 'react-dom'
import Carousel from 'react-image-carousel'
import ConfigManager from '../main/lib/ConfigManager'
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')
@@ -32,16 +32,16 @@ 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`
]
const win = global.process.platform === 'win32'
function buildStyle (
fontFamily,
@@ -189,6 +189,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)
@@ -205,9 +218,10 @@ 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.linkClickHandler = this.handlelinkClick.bind(this)
this.linkClickHandler = this.handleLinkClick.bind(this)
this.initMarkdown = this.initMarkdown.bind(this)
this.initMarkdown()
}
@@ -232,30 +246,9 @@ 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)) {
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)
}
}
const menu = buildMarkdownPreviewContextMenu(this, event)
if (menu != null) {
menu.popup(remote.getCurrentWindow())
}
}
@@ -265,17 +258,27 @@ export default class MarkdownPreview extends React.Component {
handleMouseDown (e) {
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.target != null) {
switch (e.target.tagName) {
case 'A':
case 'INPUT':
return null
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) {
@@ -294,58 +297,81 @@ export default class MarkdownPreview extends React.Component {
this.exportAsDocument('md')
}
handleSaveAsHtml () {
this.exportAsDocument('html', (noteContent, exportTasks) => {
const {
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
codeBlockTheme,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
} = this.getStyleParams()
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.markdown.render(noteContent)
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'
const inlineStyles = buildStyle(
fontFamily,
fontSize,
codeBlockFontFamily,
lineNumber,
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
)
let body = this.markdown.render(noteContent)
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, targetDir) => Promise.resolve(this.htmlContentFormatter(noteContent, exportTasks, targetDir)))
}
handleSaveAsPdf () {
this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => {
const printout = new remote.BrowserWindow({show: false, webPreferences: {webSecurity: 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()
})
})
})
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>`
})
}
@@ -395,6 +421,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
@@ -410,6 +461,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',
@@ -447,7 +500,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',
@@ -460,10 +513,13 @@ export default class MarkdownPreview extends React.Component {
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
@@ -482,7 +538,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',
@@ -495,6 +551,7 @@ export default class MarkdownPreview extends React.Component {
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)
}
@@ -593,14 +650,12 @@ export default class MarkdownPreview extends React.Component {
)
}
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
? (win ? theme.path : `${appPath}/${theme.path}`)
: `${appPath}/node_modules/codemirror/theme/elegant.css`
}
rewriteIframe () {
@@ -625,11 +680,16 @@ export default class MarkdownPreview extends React.Component {
indentSize,
showCopyNotification,
storagePath,
noteKey
noteKey,
sanitize
} = 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(
@@ -653,9 +713,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'),
@@ -668,6 +728,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!', {
@@ -676,14 +738,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
})
@@ -767,6 +826,133 @@ export default class MarkdownPreview extends React.Component {
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme)
}
)
_.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)
}
this.getWindow().scrollTo(0, 0)
}
focus () {
@@ -809,16 +995,22 @@ 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')
const parser = document.createElement('a')
parser.href = e.target.getAttribute('href')
const { href, hash } = parser
const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10
const regexNoteInternalLink = /main.html#(.+)/
if (!rawHref) return // not checked href because parser will create file://... string for [empty link]()
const extractId = /(main.html)?#/
const regexNoteInternalLink = new RegExp(`${extractId.source}(.+)`)
if (regexNoteInternalLink.test(linkHash)) {
const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1])
const targetId = mdurl.encode(linkHash.replace(extractId, ''))
const targetElement = this.refs.root.contentWindow.document.getElementById(
targetId
)

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,10 +78,10 @@ class MarkdownSplitEditor extends React.Component {
e.preventDefault()
e.stopPropagation()
const idMatch = /checkbox-([0-9]+)/
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
const checkReplace = /\[x\]/i
const uncheckReplace = /\[ \]/
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
@@ -136,7 +136,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
@@ -150,7 +150,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'
@@ -160,6 +159,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}
@@ -169,19 +172,21 @@ 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}
/>
<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}

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

@@ -4,6 +4,7 @@
import PropTypes from 'prop-types'
import React from 'react'
import { isArray } from 'lodash'
import invertColor from 'invert-color'
import CSSModules from 'browser/lib/CSSModules'
import { getTodoStatus } from 'browser/lib/getTodoStatus'
import styles from './NoteItem.styl'
@@ -13,29 +14,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 +56,7 @@ const TagElementList = (tags, showTagsAlphabetically) => {
* @param {Function} handleNoteClick
* @param {Function} handleNoteContextMenu
* @param {Function} handleDragStart
* @param {Object} coloredTags
* @param {string} dateDisplay
*/
const NoteItem = ({
@@ -59,7 +70,8 @@ const NoteItem = ({
storageName,
folderName,
viewType,
showTagsAlphabetically
showTagsAlphabetically,
coloredTags
}) => (
<div
styleName={isActive ? 'item--active' : 'item'}
@@ -97,7 +109,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 +139,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 +148,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
@@ -416,6 +424,26 @@ pre.fence
canvas, svg
max-width 100% !important
.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
themeDarkBackground = darken(#21252B, 10%)
themeDarkText = #f9f9f9
themeDarkBorder = lighten(themeDarkBackground, 20%)
@@ -475,6 +503,14 @@ 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
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
themeSolarizedDarkTableHead = themeSolarizedDarkTableEven
@@ -510,6 +546,14 @@ 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
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
themeMonokaiTableHead = themeMonokaiTableEven
@@ -538,6 +582,7 @@ body[data-theme="monokai"]
border-right solid 1px themeMonokaiTableBorder
kbd
background-color themeDarkBackground
dl
border-color themeDarkBorder
background-color themeMonokaiTableHead
@@ -547,6 +592,14 @@ 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
themeDraculaTableOdd = $ui-dracula-noteDetail-backgroundColor
themeDraculaTableEven = darken($ui-dracula-noteDetail-backgroundColor, 10%)
themeDraculaTableHead = themeDraculaTableEven
@@ -575,6 +628,7 @@ body[data-theme="dracula"]
border-right solid 1px themeDraculaTableBorder
kbd
background-color themeDarkBackground
dl
border-color themeDarkBorder
background-color themeDraculaTableHead
@@ -583,3 +637,11 @@ 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

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

@@ -62,10 +62,12 @@ const languages = [
{
name: 'Spanish',
locale: 'es-ES'
}, {
},
{
name: 'Turkish',
locale: 'tr'
}, {
},
{
name: 'Thai',
locale: 'th'
}
@@ -82,4 +84,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

@@ -3,14 +3,44 @@ 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.split(/\//g).slice(-3).join('/'), 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: `${CODEMIRROR_THEME_PATH}/solarized.css`,
className: `cm-s-solarized cm-s-dark`
}, {
name: 'solarized light',
path: `${CODEMIRROR_THEME_PATH}/solarized.css`,
className: `cm-s-solarized cm-s-light`
})
themes.splice(0, 0, {
name: 'default',
path: `${CODEMIRROR_THEME_PATH}/elegant.css`,
className: `cm-s-default`
})
const snippetFile = process.env.NODE_ENV !== 'test'
? path.join(app.getPath('userData'), 'snippets.json')
@@ -35,7 +65,7 @@ const consts = {
'Dodger Blue',
'Violet Eggplant'
],
THEMES: ['default'].concat(themes),
THEMES: themes,
SNIPPET_FILE: snippetFile,
DEFAULT_EDITOR_FONT_FAMILY: [
'Monaco',

View File

@@ -1,6 +1,12 @@
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.
@@ -62,4 +68,57 @@ const buildEditorContextMenu = function (editor, event) {
return Menu.buildFromTemplate(template)
}
module.exports = buildEditorContextMenu
/**
* 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

@@ -2,56 +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 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 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 linkify (tok, text, slug, opts) {
var uniqeID = opts.num === 0 ? '' : '-' + opts.num
tok.content = mdlink(text, '#' + slug + uniqeID)
return tok
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.
@@ -59,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)
@@ -78,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, linkify: linkify})
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) {
@@ -103,5 +94,6 @@ function wrapTocWithEol (toc, editor) {
export default {
generate,
generateInEditor
generateInEditor,
tocExistsInEditor
}

View File

@@ -2,12 +2,12 @@ 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 _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager'
import katex from 'katex'
import { lastFindInArray } from './utils'
import anchor from '@enyaxu/markdown-it-anchor'
function createGutter (str, firstLineNumber) {
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
@@ -33,6 +33,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,14 +119,8 @@ 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(anchor, {
slugify: (title) => {
var slug = encodeURI(title.trim()
.replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '')
.replace(/\s+/g, '-'))
.replace(/\-+$/, '')
return slug
}
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']})
@@ -152,6 +147,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>
@@ -173,7 +183,7 @@ class Markdown {
})
const deflate = require('markdown-it-plantuml/lib/deflate')
this.md.use(require('markdown-it-plantuml'), '', {
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'
@@ -279,7 +289,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,7 +1,8 @@
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, params, config) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
@@ -18,7 +19,8 @@ export function createMarkdownNote (storage, folder, dispatch, location, params,
folder: folder,
title: '',
tags,
content: ''
content: '',
linesHighlighted: []
})
.then(note => {
const noteHash = note.key
@@ -27,10 +29,10 @@ export function createMarkdownNote (storage, folder, dispatch, location, params,
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')
})
@@ -45,6 +47,8 @@ export function createSnippetNote (storage, folder, dispatch, location, params,
tags = params.tagname.split(' ')
}
const defaultLanguage = config.editor.snippetDefaultLanguage === 'Auto Detect' ? null : config.editor.snippetDefaultLanguage
return dataApi
.createNote(storage, {
type: 'SNIPPET_NOTE',
@@ -55,8 +59,9 @@ export function createSnippetNote (storage, folder, dispatch, location, params,
snippets: [
{
name: '',
mode: config.editor.snippetDefaultLanguage || 'text',
content: ''
mode: defaultLanguage,
content: '',
linesHighlighted: []
}
]
})
@@ -66,10 +71,10 @@ export function createSnippetNote (storage, folder, dispatch, location, params,
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
}

View File

@@ -14,7 +14,7 @@ let self
function getAvailableDictionaries () {
return [
{label: i18n.__('Disabled'), value: SPELLCHECK_DISABLED},
{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'}

View File

@@ -132,8 +132,13 @@ export function isObjectEqual (a, b) {
return true
}
export function isMarkdownTitleURL (str) {
return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str)
}
export default {
lastFindInArray,
escapeHtmlCharacters,
isObjectEqual
isObjectEqual,
isMarkdownTitleURL
}

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>
@@ -85,6 +85,11 @@ class InfoPanel extends React.Component {
<p>{i18n.__('.html')}</p>
</button>
<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>
@@ -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>
@@ -46,7 +46,7 @@ const InfoPanelTrashed = ({
<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)
}
@@ -129,6 +152,7 @@ class MarkdownNoteDetail extends React.Component {
}
handleFolderChange (e) {
const { dispatch } = this.props
const { note } = this.state
const value = this.refs.folder.value
const splitted = value.split('-')
@@ -148,12 +172,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 +214,10 @@ class MarkdownNoteDetail extends React.Component {
ee.emit('export:save-html')
}
exportAsPdf () {
ee.emit('export:save-pdf')
}
handleKeyDown (e) {
switch (e.keyCode) {
// tab key
@@ -283,7 +311,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) {
@@ -292,7 +320,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})
@@ -318,7 +346,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
@@ -361,7 +390,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 {
@@ -371,6 +402,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}
/>
@@ -411,13 +443,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'
@@ -433,6 +466,7 @@ class MarkdownNoteDetail extends React.Component {
showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data}
onChange={this.handleUpdateTag.bind(this)}
coloredTags={config.coloredTags}
/>
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
</div>
@@ -450,7 +484,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>
@@ -470,12 +504,13 @@ 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}
exportAsPdf={this.exportAsPdf}
wordCount={note.content.split(' ').length}
letterCount={note.content.replace(/\r?\n/g, '').length}
type={note.type}

View File

@@ -80,4 +80,12 @@ 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
div
> button, div
-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
@@ -514,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, {
@@ -546,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()
}
@@ -596,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
@@ -650,6 +686,7 @@ class SnippetNoteDetail extends React.Component {
'export-txt': 'Text export',
'export-md': 'Markdown export',
'export-html': 'HTML export',
'export-pdf': 'PDF export',
'print': 'Print'
})[msg]
@@ -668,6 +705,8 @@ class SnippetNoteDetail extends React.Component {
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)
@@ -692,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}}
@@ -704,20 +739,26 @@ 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}
@@ -726,6 +767,7 @@ class SnippetNoteDetail extends React.Component {
ref={'code-' + index}
enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey}
autoDetect={autoDetect}
/>
}
</div>
@@ -759,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'
@@ -781,6 +824,7 @@ class SnippetNoteDetail extends React.Component {
showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data}
onChange={(e) => this.handleChange(e)}
coloredTags={config.coloredTags}
/>
</div>
<div styleName='info-right'>
@@ -789,11 +833,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)} />
@@ -804,12 +844,13 @@ 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}
/>
@@ -888,6 +929,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

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'
@@ -45,8 +46,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: ''
@@ -179,19 +186,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>
)
@@ -240,7 +262,8 @@ TagSelect.contextTypes = {
TagSelect.propTypes = {
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()

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

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('enjoy').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
@@ -170,10 +172,21 @@ class Main extends React.Component {
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 +247,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 +311,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 +341,7 @@ class Main extends React.Component {
'dispatch',
'config',
'data',
'params',
'match',
'location'
])}
/>
@@ -338,7 +351,7 @@ class Main extends React.Component {
'dispatch',
'data',
'config',
'params',
'match',
'location'
])}
/>
@@ -360,7 +373,7 @@ class Main extends React.Component {
'dispatch',
'data',
'config',
'params',
'match',
'location'
])}
ignorePreviewPointerEvents={this.state.isRightSliderFocused}

View File

@@ -21,23 +21,20 @@ 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, params, 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, params, config)
} else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
@@ -55,9 +52,8 @@ class NewNoteButton extends React.Component {
}
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) {
@@ -93,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)
}
@@ -128,15 +146,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
@@ -146,17 +164,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
@@ -177,18 +195,18 @@ class NoteList extends React.Component {
}
focusNote (selectedNoteKeys, noteKey, pathname) {
const { router } = this.context
const { dispatch } = this.props
this.setState({
selectedNoteKeys
})
router.push({
dispatch(push({
pathname,
query: {
search: queryString.stringify({
key: noteKey
}
})
})
}))
}
getNoteKeyFromTargetIndex (targetIndex) {
@@ -260,13 +278,22 @@ class NoteList extends React.Component {
}
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, '/home')
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')
}
@@ -322,8 +349,7 @@ class NoteList extends React.Component {
}
getNotes () {
const { data, params, location } = this.props
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
@@ -364,7 +390,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)
@@ -404,8 +430,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
@@ -456,16 +481,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 }
@@ -497,6 +522,7 @@ class NoteList extends React.Component {
'export-txt': 'Text export',
'export-md': 'Markdown export',
'export-html': 'HTML export',
'export-pdf': 'PDF export',
'print': 'Print'
})[msg]
@@ -657,14 +683,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)
})
@@ -688,6 +718,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)
})
@@ -711,7 +742,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)
@@ -727,10 +763,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})
}))
})
}
@@ -740,13 +776,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) {
@@ -876,7 +912,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()
@@ -900,13 +936,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)})
}))
})
})
})
@@ -916,14 +959,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
@@ -971,7 +1015,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)
@@ -1047,6 +1091,7 @@ class NoteList extends React.Component {
storageName={this.getNoteStorage(note).name}
viewType={viewType}
showTagsAlphabetically={config.ui.showTagsAlphabetically}
coloredTags={config.coloredTags}
/>
)
}
@@ -1093,7 +1138,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'
@@ -1101,7 +1146,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>
@@ -1129,4 +1174,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
@@ -134,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))
}
}
@@ -362,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 &&
@@ -380,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,6 +21,7 @@ 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'
function matchActiveTags (tags, activeTags) {
return _.every(activeTags, v => tags.indexOf(v) >= 0)
@@ -27,6 +29,22 @@ function matchActiveTags (tags, activeTags) {
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)
@@ -45,7 +63,7 @@ class SideNav extends React.Component {
})
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 +93,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 +105,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 +122,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 +191,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) {
@@ -207,6 +280,7 @@ 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(
@@ -237,10 +311,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 +349,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 +368,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 +376,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 +407,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 +424,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 +457,7 @@ class SideNav extends React.Component {
</div>
</div>
{this.SideNavComponent(isFolded, storageList)}
{colorPicker}
</div>
)
}

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

@@ -11,6 +11,10 @@ const consts = require('browser/lib/consts')
let isInitialized = false
const DEFAULT_MARKDOWN_LINT_CONFIG = `{
"default": true
}`
export const DEFAULT_CONFIG = {
zoom: 1,
isSideNavFolded: false,
@@ -26,25 +30,33 @@ export const DEFAULT_CONFIG = {
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace',
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V'
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V',
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,
@@ -54,7 +66,9 @@ export const DEFAULT_CONFIG = {
enableFrontMatterTitle: true,
frontMatterTitleField: 'title',
spellcheck: false,
enableSmartPaste: false
enableSmartPaste: false,
enableMarkdownLint: false,
customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG
},
preview: {
fontSize: '14',
@@ -72,7 +86,7 @@ export const DEFAULT_CONFIG = {
breaks: true,
smartArrows: false,
allowCustomCSS: false,
customCSS: '',
customCSS: '/* Drop Your Custom CSS Code Here */',
sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE'
lineThroughCheckbox: true
},
@@ -83,7 +97,8 @@ export const DEFAULT_CONFIG = {
token: '',
username: '',
password: ''
}
},
coloredTags: {}
}
function validate (config) {
@@ -96,7 +111,6 @@ function validate (config) {
}
function _save (config) {
console.log(config)
window.localStorage.setItem('config', JSON.stringify(config))
}
@@ -126,16 +140,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', win ? theme.path : `../${theme.path}`)
} else {
config.editor.theme = 'default'
}
}
@@ -144,7 +154,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)
@@ -171,16 +187,11 @@ 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', win ? newTheme.path : `../${newTheme.path}`)
}
ipcRenderer.send('config-renew', {
@@ -205,7 +216,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,6 +6,7 @@ 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'
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
@@ -18,15 +19,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 +85,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 +160,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 +241,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 +278,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 +369,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')
}
@@ -323,7 +413,7 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
* @param {String} noteKey Key of the current note
* @param {NativeImage} image The native image
*/
function handlePastNativeImage (codeEditor, storageKey, noteKey, image) {
function handlePasteNativeImage (codeEditor, storageKey, noteKey, image) {
if (!codeEditor) {
throw new Error('codeEditor has to be given')
}
@@ -380,6 +470,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.
@@ -421,7 +559,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)
})
}
/**
@@ -475,8 +620,6 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey
}
})
})
} else {
console.info('Attachment folder ("' + attachmentFolder + '") did not exist..')
}
}
@@ -576,10 +719,11 @@ module.exports = {
fixLocalURLS,
generateAttachmentMarkdown,
handleAttachmentDrop,
handlePastImageEvent,
handlePastNativeImage,
handlePasteImageEvent,
handlePasteNativeImage,
getAttachmentsInMarkdownContent,
getAbsolutePathsOfAttachmentsInContent,
importAttachments,
removeStorageAndNoteReferences,
deleteAttachmentFolder,
deleteAttachmentsNotPresentInNote,

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

@@ -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

@@ -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(note => {
.map(note => {
const notePath = path.join(exportDir, `${filenamify(note.title, {replacement: '_'})}.${fileType}`)
exportNote(note.key, storage.path, note.content, notePath, null)
return exportNote(note.key, storage.path, note.content, notePath, null)
})
return {
).then(() => ({
storage,
folderKey,
fileType,
exportDir
}
}))
})
}

View File

@@ -43,14 +43,17 @@ function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatt
)
if (outputFormatter) {
exportedData = outputFormatter(exportedData, exportTasks)
exportedData = outputFormatter(exportedData, exportTasks, path.dirname(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

@@ -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

@@ -39,6 +39,9 @@ function validateInput (input) {
if (input.content != null) {
if (!_.isString(input.content)) validatedInput.content = ''
else validatedInput.content = input.content
if (!_.isArray(input.linesHighlighted)) validatedInput.linesHighlighted = []
else validatedInput.linesHighlighted = input.linesHighlighted
}
return validatedInput
case 'SNIPPET_NOTE':
@@ -51,7 +54,8 @@ function validateInput (input) {
validatedInput.snippets = [{
name: '',
mode: 'text',
content: ''
content: '',
linesHighlighted: []
}]
} else {
validatedInput.snippets = input.snippets
@@ -96,12 +100,14 @@ function updateNote (storageKey, noteKey, input) {
snippets: [{
name: '',
mode: 'text',
content: ''
content: '',
linesHighlighted: []
}]
}
: {
type: 'MARKDOWN_NOTE',
content: ''
content: '',
linesHighlighted: []
}
noteData.title = ''
if (storage.folders.length === 0) throw new Error('Failed to restore note: No folder exists.')

View File

@@ -12,7 +12,8 @@ function updateSnippet (snippet, snippetFile) {
if (
currentSnippet.name === snippet.name &&
currentSnippet.prefix === snippet.prefix &&
currentSnippet.content === snippet.content
currentSnippet.content === snippet.content &&
currentSnippet.linesHighlighted === snippet.linesHighlighted
) {
// if everything is the same then don't write to disk
resolve(snippets)
@@ -20,6 +21,7 @@ function updateSnippet (snippet, snippetFile) {
currentSnippet.name = snippet.name
currentSnippet.prefix = snippet.prefix
currentSnippet.content = snippet.content
currentSnippet.linesHighlighted = (snippet.linesHighlighted)
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
if (err) reject(err)
resolve(snippets)

View File

@@ -1,7 +1,7 @@
import React from 'react'
import { Provider } from 'react-redux'
import ReactDOM from 'react-dom'
import store from '../store'
import { store } from '../store'
class ModalBase extends React.Component {
constructor (props) {

View File

@@ -6,5 +6,8 @@ module.exports = {
},
'deleteNote': () => {
ee.emit('hotkey:deletenote')
},
'toggleMenuBar': () => {
ee.emit('menubar:togglemenubar')
}
}

View File

@@ -3,7 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './CreateFolderModal.styl'
import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import consts from 'browser/lib/consts'
import ModalEscButton from 'browser/components/ModalEscButton'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'

View File

@@ -4,11 +4,12 @@ import styles from './NewNoteModal.styl'
import ModalEscButton from 'browser/components/ModalEscButton'
import i18n from 'browser/lib/i18n'
import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote'
import queryString from 'query-string'
class NewNoteModal extends React.Component {
constructor (props) {
super(props)
this.lock = false
this.state = {}
}
@@ -21,10 +22,14 @@ class NewNoteModal extends React.Component {
}
handleMarkdownNoteButtonClick (e) {
const { storage, folder, dispatch, location, params, config } = this.props
createMarkdownNote(storage, folder, dispatch, location, params, config).then(() => {
setTimeout(this.props.close, 200)
})
const { storage, folder, dispatch, location, config } = this.props
const params = location.search !== '' && queryString.parse(location.search)
if (!this.lock) {
this.lock = true
createMarkdownNote(storage, folder, dispatch, location, params, config).then(() => {
setTimeout(this.props.close, 200)
})
}
}
handleMarkdownNoteButtonKeyDown (e) {
@@ -35,10 +40,14 @@ class NewNoteModal extends React.Component {
}
handleSnippetNoteButtonClick (e) {
const { storage, folder, dispatch, location, params, config } = this.props
createSnippetNote(storage, folder, dispatch, location, params, config).then(() => {
setTimeout(this.props.close, 200)
})
const { storage, folder, dispatch, location, config } = this.props
const params = location.search !== '' && queryString.parse(location.search)
if (!this.lock) {
this.lock = true
createSnippetNote(storage, folder, dispatch, location, params, config).then(() => {
setTimeout(this.props.close, 200)
})
}
}
handleSnippetNoteButtonKeyDown (e) {

View File

@@ -19,6 +19,7 @@
.control
padding 25px 0px
text-align center
display: flex
.control-button
width 240px

View File

@@ -2,7 +2,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ConfigTab.styl'
import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import PropTypes from 'prop-types'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'

View File

@@ -18,6 +18,14 @@
margin-bottom 15px
margin-top 30px
.group-header--sub
@extend .group-header
margin-bottom 10px
.group-header2--sub
@extend .group-header2
margin-bottom 10px
.group-section
margin-bottom 20px
display flex
@@ -148,10 +156,12 @@ body[data-theme="dark"]
color $ui-dark-text-color
.group-header
.group-header--sub
color $ui-dark-text-color
border-color $ui-dark-borderColor
.group-header2
.group-header2--sub
color $ui-dark-text-color
.group-section-control-input
@@ -176,10 +186,12 @@ body[data-theme="solarized-dark"]
color $ui-solarized-dark-text-color
.group-header
.group-header--sub
color $ui-solarized-dark-text-color
border-color $ui-solarized-dark-borderColor
.group-header2
.group-header2--sub
color $ui-solarized-dark-text-color
.group-section-control-input
@@ -203,10 +215,12 @@ body[data-theme="monokai"]
color $ui-monokai-text-color
.group-header
.group-header--sub
color $ui-monokai-text-color
border-color $ui-monokai-borderColor
.group-header2
.group-header2--sub
color $ui-monokai-text-color
.group-section-control-input
@@ -230,10 +244,12 @@ body[data-theme="dracula"]
color $ui-dracula-text-color
.group-header
.group-header--sub
color $ui-dracula-text-color
border-color $ui-dracula-borderColor
.group-header2
.group-header2--sub
color $ui-dracula-text-color
.group-section-control-input

View File

@@ -22,19 +22,17 @@ class Crowdfunding extends React.Component {
render () {
return (
<div styleName='root'>
<div styleName='header'>{i18n.__('Crowdfunding')}</div>
<div styleName='group-header'>{i18n.__('Crowdfunding')}</div>
<p>{i18n.__('Thank you for using Boostnote!')}</p>
<br />
<p>{i18n.__('We launched IssueHunt which is an issue-based crowdfunding / sourcing platform for open source projects.')}</p>
<p>{i18n.__('Anyone can put a bounty on not only a bug but also on OSS feature requests listed on IssueHunt. Collected funds will be distributed to project owners and contributors.')}</p>
<br />
<p>{i18n.__('### Sustainable Open Source Ecosystem')}</p>
<div styleName='group-header2--sub'>{i18n.__('Sustainable Open Source Ecosystem')}</div>
<p>{i18n.__('We discussed about open-source ecosystem and IssueHunt concept with the Boostnote team repeatedly. We actually also discussed with Matz who father of Ruby.')}</p>
<p>{i18n.__('The original reason why we made IssueHunt was to reward our contributors of Boostnote project. Weve got tons of Github stars and hundred of contributors in two years.')}</p>
<p>{i18n.__('We thought that it will be nice if we can pay reward for our contributors.')}</p>
<br />
<p>{i18n.__('### We believe Meritocracy')}</p>
<p>{i18n.__('We think developers who has skill and did great things must be rewarded properly.')}</p>
<div styleName='group-header2--sub'>{i18n.__('We believe Meritocracy')}</div>
<p>{i18n.__('We think developers who have skills and do great things must be rewarded properly.')}</p>
<p>{i18n.__('OSS projects are used in everywhere on the internet, but no matter how they great, most of owners of those projects need to have another job to sustain their living.')}</p>
<p>{i18n.__('It sometimes looks like exploitation.')}</p>
<p>{i18n.__('Weve realized IssueHunt could enhance sustainability of open-source ecosystem.')}</p>

View File

@@ -1,14 +1,8 @@
@import('./Tab')
@import('./ConfigTab')
.root
padding 15px
white-space pre
line-height 1.4
color alpha($ui-text-color, 90%)
width 100%
font-size 14px
p
font-size 16px
line-height 1.4
.cf-link
height 35px

View File

@@ -4,7 +4,7 @@ import CSSModules from 'browser/lib/CSSModules'
import ReactDOM from 'react-dom'
import styles from './FolderItem.styl'
import dataApi from 'browser/main/lib/dataApi'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import { SketchPicker } from 'react-color'
import { SortableElement, SortableHandle } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
@@ -225,7 +225,7 @@ class FolderItem extends React.Component {
<div styleName='folderItem-left'
style={{borderColor: folder.color}}
>
<span styleName='folderItem-left-name'>{folder.name}</span>
<span>{folder.name}</span>
<span styleName='folderItem-left-key'>({folder.key})</span>
</div>
<div styleName='folderItem-right'>
@@ -288,10 +288,10 @@ class Handle extends React.Component {
class SortableFolderItemComponent extends React.Component {
render () {
const StyledHandle = CSSModules(Handle, this.props.styles)
const StyledHandle = CSSModules(Handle, styles)
const DragHandle = SortableHandle(StyledHandle)
const StyledFolderItem = CSSModules(FolderItem, this.props.styles)
const StyledFolderItem = CSSModules(FolderItem, styles)
return (
<div>

View File

@@ -62,7 +62,7 @@
.folderItem-right-button
vertical-align middle
height 25px
margin-top 2.5px
margin-top 2px
colorDefaultButton()
border-radius 2px
border $ui-border

View File

@@ -3,7 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import dataApi from 'browser/main/lib/dataApi'
import styles from './FolderList.styl'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import FolderItem from './FolderItem'
import { SortableContainer } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
@@ -22,7 +22,7 @@ class FolderList extends React.Component {
})
return (
<div styleName='folderList'>
<div>
{folderList.length > 0
? folderList
: <div styleName='folderList-empty'>{i18n.__('No Folders')}</div>

View File

@@ -3,7 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ConfigTab.styl'
import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'
@@ -80,7 +80,8 @@ class HotkeyTab extends React.Component {
toggleMain: this.refs.toggleMain.value,
toggleMode: this.refs.toggleMode.value,
deleteNote: this.refs.deleteNote.value,
pasteSmartly: this.refs.pasteSmartly.value
pasteSmartly: this.refs.pasteSmartly.value,
toggleMenuBar: this.refs.toggleMenuBar.value
}
this.setState({
config
@@ -128,6 +129,17 @@ class HotkeyTab extends React.Component {
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Show/Hide Menu Bar')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)}
ref='toggleMenuBar'
value={config.hotkey.toggleMenuBar}
type='text'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Toggle Editor Mode')}</div>
<div styleName='group-section-control'>
@@ -151,7 +163,7 @@ class HotkeyTab extends React.Component {
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Paste Smartly')}</div>
<div styleName='group-section-label'>{i18n.__('Paste HTML')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)}
@@ -161,6 +173,26 @@ class HotkeyTab extends React.Component {
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Insert Current Date')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
value={config.hotkey.insertDate}
type='text'
disabled='true'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Insert Current Date and Time')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
value={config.hotkey.insertDateTime}
type='text'
disabled='true'
/>
</div>
</div>
<div styleName='group-control'>
<button styleName='group-control-leftButton'
onClick={(e) => this.handleHintToggleButtonClick(e)}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoTab.styl'
import ConfigManager from 'browser/main/lib/ConfigManager'
import store from 'browser/main/store'
import { store } from 'browser/main/store'
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'
@@ -69,10 +69,14 @@ class InfoTab extends React.Component {
render () {
return (
<div styleName='root'>
<div styleName='header--sub'>{i18n.__('Community')}</div>
<div styleName='group-header'>{i18n.__('Community')}</div>
<div styleName='top'>
<ul styleName='list'>
<li>
<a href='https://issuehunt.io/repos/53266139'
onClick={(e) => this.handleLinkClick(e)}
>{i18n.__('Bounty on IssueHunt')}</a>
</li>
<li>
<a href='https://boostnote.io/#subscribe'
onClick={(e) => this.handleLinkClick(e)}
@@ -103,7 +107,7 @@ class InfoTab extends React.Component {
<hr />
<div styleName='header--sub'>{i18n.__('About')}</div>
<div styleName='group-header--sub'>{i18n.__('About')}</div>
<div styleName='top'>
<div styleName='icon-space'>
@@ -129,7 +133,7 @@ class InfoTab extends React.Component {
>{i18n.__('Development')}</a>{i18n.__(' : Development configurations for Boostnote.')}
</li>
<li styleName='cc'>
{i18n.__('Copyright (C) 2017 - 2018 BoostIO')}
{i18n.__('Copyright (C) 2017 - 2019 BoostIO')}
</li>
<li styleName='cc'>
{i18n.__('License: GPL v3')}
@@ -138,7 +142,7 @@ class InfoTab extends React.Component {
<hr styleName='separate-line' />
<div styleName='policy'>{i18n.__('Analytics')}</div>
<div styleName='group-header2--sub'>{i18n.__('Analytics')}</div>
<div>{i18n.__('Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.')}</div>
<div>{i18n.__('You can see how it works on ')}<a href='https://github.com/BoostIO/Boostnote' onClick={(e) => this.handleLinkClick(e)}>GitHub</a>.</div>
<br />

View File

@@ -1,16 +1,4 @@
@import('./Tab')
.root
padding 15px
white-space pre
line-height 1.4
color alpha($ui-text-color, 90%)
width 100%
font-size 14px
.top
text-align left
margin-bottom 20px
@import('./ConfigTab.styl')
.icon-space
margin 20px 0
@@ -45,13 +33,21 @@
.separate-line
margin 40px 0
.policy
width 100%
font-size 20px
margin-bottom 10px
.policy-submit
margin-top 10px
height 35px
border-radius 2px
border none
background-color alpha(#1EC38B, 90%)
padding-left 20px
padding-right 20px
text-decoration none
color white
font-weight 600
font-size 16px
&:hover
background-color #1EC38B
transition 0.2s
.policy-confirm
margin-top 10px
@@ -60,11 +56,14 @@
body[data-theme="dark"]
.root
color alpha($tab--dark-text-color, 80%)
.appId
color $ui-dark-text-color
body[data-theme="solarized-dark"]
.root
color $ui-solarized-dark-text-color
.appId
color $ui-solarized-dark-text-color
.list
a
color $ui-solarized-dark-active-color
@@ -72,6 +71,8 @@ body[data-theme="solarized-dark"]
body[data-theme="monokai"]
.root
color $ui-monokai-text-color
.appId
color $ui-monokai-text-color
.list
a
color $ui-monokai-active-color
@@ -79,6 +80,8 @@ body[data-theme="monokai"]
body[data-theme="dracula"]
.root
color $ui-dracula-text-color
.appId
color $ui-dracula-text-color
.list
a
color $ui-dracula-active-color
color $ui-dracula-active-color

View File

@@ -4,6 +4,7 @@ import _ from 'lodash'
import styles from './SnippetTab.styl'
import CSSModules from 'browser/lib/CSSModules'
import dataApi from 'browser/main/lib/dataApi'
import snippetManager from '../../../lib/SnippetManager'
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
const buildCMRulers = (rulers, enableRulers) =>
@@ -28,9 +29,9 @@ class SnippetEditor extends React.Component {
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
autoCloseBrackets: {
pairs: '()[]{}\'\'""$$**``',
triples: '```"""\'\'\'',
explode: '[]{}``$$',
pairs: this.props.matchingPairs,
triples: this.props.matchingTriples,
explode: this.props.explodingPairs,
override: true
},
mode: 'null'
@@ -64,7 +65,9 @@ class SnippetEditor extends React.Component {
}
saveSnippet () {
dataApi.updateSnippet(this.snippet).catch((err) => { throw err })
dataApi.updateSnippet(this.snippet)
.then(snippets => snippetManager.assignSnippets(snippets))
.catch((err) => { throw err })
}
render () {

View File

@@ -91,7 +91,7 @@ class SnippetTab extends React.Component {
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
return (
<div styleName='root'>
<div styleName='header'>{i18n.__('Snippets')}</div>
<div styleName='group-header'>{i18n.__('Snippets')}</div>
<SnippetList
onSnippetSelect={this.handleSnippetSelect.bind(this)}
onSnippetDeleted={this.handleDeleteSnippet.bind(this)}
@@ -136,6 +136,9 @@ class SnippetTab extends React.Component {
enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers}
displayLineNumbers={config.editor.displayLineNumbers}
matchingPairs={config.editor.matchingPairs}
matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs}
scrollPastEnd={config.editor.scrollPastEnd}
onRef={ref => { this.snippetEditor = ref }} />
</div>

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