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

Compare commits

...

295 Commits

Author SHA1 Message Date
ehhc
7f3fdedb5d Update readme.md 2020-06-25 10:03:02 +09:00
ehhc
3a80706938 Update readme.md
Added link to a project developing a mobile version of boostnote
2020-06-25 10:03:02 +09:00
ZeroX-DG
2cf5f8e966 updated slack invite link 2020-06-08 18:48:55 +09:00
ZeroX-DG
4fbbb4651d updated Boostnote name to Boostnote Legacy 2020-05-11 03:44:21 +09:00
Junyoung Choi
137aa692bc Merge pull request #3547 from ZeroX-DG/migrate-to-jest
[WIP] Migrate to jest
2020-05-08 18:49:27 +09:00
ZeroX-DG
634fec39c0 migrate more tests to jest 2020-05-08 20:13:39 +12:00
ZeroX-DG
d269f1e8fd migrate more tests to jest 2020-05-06 14:57:35 +12:00
Junyoung Choi
4b67026bbf Merge pull request #2936 from callumbooth/fix-2903
fixes #2903 - Rearrange layout of columns
2020-04-24 01:04:23 +09:00
Alexander Wolf
8b13ec4f0e Issue 1706 tag rename - finishing #2989 (#3469)
* allow a tag to be renamed and update all notes that use that tag

• repurpose RenameFolderModal.styl to RenameModal so it is more generic

* call handleConfirmButtonClick directly instead of sending through a confirm method

* better name for method to confirm the rename

* use close prop instead of a new method

* use callback ref instead of legacy string refs

* bind the handleChange in the constructor to allow for direct function assignment

* update the tag in the URL upon change

* use the eventEmitter to update the tags in the SnippetNoteDetail header via the TagSelect component

* respect themes when modal is opened

* show error message when trying to rename to an existing tag

* lint fix, const over let

* add missing letter

* fix routing and add merge warning dialog

* fix space-before-parens lint error

* change theming

* add check if tag changed

Co-authored-by: Khaliq Gant <khaliqgant@gmail.com>
2020-04-21 20:55:56 +09:00
Junyoung Choi
7fef7660e4 Add roadmap 2020-04-21 20:45:05 +09:00
Junyoung Choi
01d021cc4c Add boosthub intro 2020-04-21 18:33:57 +09:00
Callum Booth
c355f81525 remove redundant icons 2020-04-20 08:43:01 +01:00
Callum Booth
d138a54dfd fix accidental deletion of rtl state 2020-04-20 08:42:45 +01:00
Junyoung Choi
514d4b9059 v0.15.3 2020-04-20 11:43:38 +09:00
Callum Booth
a7ead67c2d moves orientation button to view menu 2020-04-18 14:17:08 +01:00
Callum Booth
2f16784a20 Merge branch 'master' of https://github.com/BoostIO/Boostnote into fix-2903 2020-04-18 13:54:27 +01:00
ZeroX-DG
8ca3ba21ee Merge branch 'master' of https://github.com/BoostIO/Boostnote into migrate-to-jest 2020-04-17 21:49:38 +12:00
ZeroX-DG
58ae6419f0 fixed markdown test error 2020-04-17 21:43:05 +12:00
Junyoung Choi
b56e0b98e3 Use netral color 2020-04-16 00:55:18 +09:00
Arcturus
4def32ab13 add style rule for table 2020-04-16 00:55:18 +09:00
Arcturus
0de78d12ef add hard coded styling 2020-04-16 00:55:18 +09:00
Arcturus
e756534db4 refactoring 2020-04-16 00:55:18 +09:00
Arcturus
2194965dc4 remove hard coded styling 2020-04-16 00:55:18 +09:00
Arcturus
f9e54bcbfc add styling for code 2020-04-16 00:55:18 +09:00
Junyoung Choi
667fd3a601 Fix test 2020-04-16 00:25:11 +09:00
Junyoung Choi
461e24bf39 Fix regex 2020-04-16 00:25:11 +09:00
Junyoung Choi
ac2cfe5169 Fix themeManager 2020-04-15 23:59:12 +09:00
ZeroX-DG
3f320f4337 resolved conflict 2020-04-12 18:45:49 +12:00
xatier
433ee9ed45 Update zh-TW.json (#3537)
- Update zh-TW translation.
- Sync with [locales/en.json](e44381f295/locales/en.json).
2020-04-09 11:58:35 +09:00
hikerpig
6ee92588b1 When storage or folder is removed, Detail components should render without error (#3168)
* optimize: when storage or folder is removed, Detail components should render without error, fix #2876

* optimize: Handle some scenarios where storage is not found, should not break the renderer

* optimize: NoteList should work without error when storage is not found
2020-04-06 18:02:52 +09:00
Junyoung Choi
0d797ce8a8 Merge pull request #2658 from gregueiras/fixIssue2534
Fix issue2534
2020-04-06 17:51:45 +09:00
Gonçalo Santos
4915c545d9 Merge branch 'master' into fixIssue2534 2020-03-27 01:47:02 +00:00
Gonçalo Santos
e1c95fb1f2 Fix Saving Configuration Bug 2020-03-27 01:42:18 +00:00
Junyoung Choi
5f56d3e0de v0.15.2 2020-03-26 18:32:15 +09:00
Junyoung Choi
d6b86b902c Fix scroll sync (#3531)
* Discard empty file

* Fix scroll sync
2020-03-26 18:02:53 +09:00
dependabot[bot]
3abc0fec38 Bump lodash.mergewith from 4.6.1 to 4.6.2
Bumps [lodash.mergewith](https://github.com/lodash/lodash) from 4.6.1 to 4.6.2.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-26 14:56:36 +09:00
dependabot[bot]
c0619eb746 Bump lodash-es from 4.17.10 to 4.17.15
Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.10 to 4.17.15.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.10...4.17.15)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-26 14:55:57 +09:00
AWolf81
791ababe1e add gfm with modified regex & improve link text handling 2020-03-26 03:55:23 +09:00
AWolf81
d829216c8d Remove duplicated if 2020-03-26 03:55:23 +09:00
AWolf81
1cf6f3b1e2 WIP: Change space before parens. Tag link handling issue present. 2020-03-26 03:55:23 +09:00
AWolf81
4d5939aaf4 address requested changes - tag link & redundant line 2020-03-26 03:55:23 +09:00
AWolf81
2695f62f3e add special link handling 2020-03-26 03:55:23 +09:00
KZ
ccd0355d0b Merge pull request #3521 from BoostIO/update-readme
Update readme
2020-03-18 12:48:04 +09:00
KZ
6d6e3a51c0 Update readme 2020-03-18 12:45:43 +09:00
Gonçalo Santos
48c8164689 Fix scheduled theme change timing 2020-03-10 15:41:34 +00:00
Gonçalo Santos
38ed5b8541 Remove handleSlider 2020-03-05 16:34:04 +00:00
Gonçalo Santos
8a6df8bf95 Remove comment 2020-03-05 16:07:45 +00:00
Junyoung Choi
050b1563df v0.15.1 2020-03-04 05:51:13 +09:00
Junyoung Choi
dbbcf385b1 Discard unused ref 2020-03-04 05:50:23 +09:00
Junyoung Choi
d95a3af667 Fix propTypes warning 2020-03-04 05:50:04 +09:00
Junyoung Choi
87a737babc Make rtl optional 2020-03-04 05:43:34 +09:00
Junyoung Choi
a27ddd7490 Fix ref 2020-03-04 05:43:34 +09:00
dependabot[bot]
5693b6d0f5 Bump url-parse from 1.4.0 to 1.4.7
Bumps [url-parse](https://github.com/unshiftio/url-parse) from 1.4.0 to 1.4.7.
- [Release notes](https://github.com/unshiftio/url-parse/releases)
- [Commits](https://github.com/unshiftio/url-parse/compare/1.4.0...1.4.7)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-04 03:56:54 +09:00
dependabot[bot]
9debe8218d Bump lodash.template from 4.4.0 to 4.5.0
Bumps [lodash.template](https://github.com/lodash/lodash) from 4.4.0 to 4.5.0.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.4.0...4.5.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-04 03:56:42 +09:00
Aaron Bird
9549355ab7 Tooltip misplaced (#3499)
* fix bug: tooltip misplaced

* Transition only opacity attributes
2020-03-04 03:56:31 +09:00
Nguyen Viet Hung
88e8d2e009 updated PR template (#3498)
* updated PR template

* fixed grammar
2020-02-28 11:53:27 +09:00
Flexo013
a2ea5dd12e Update issue template (#3489)
* Update issue template

* Capitals

* Replace ! with .
2020-02-28 11:52:56 +09:00
Junyoung Choi
9f932a0911 Merge pull request #3002 from AWolf81/feature-tag-links
Add tag link handling with :tag:#tag syntax
2020-02-26 17:32:05 +09:00
Aaron-Bird
71f05b9886 fix: Folder sidebar cannot scroll 2020-02-26 09:57:07 +09:00
AWolf81
2b4e2638dc change tag link format to :tag:tag 2020-02-25 08:16:52 +01:00
Gonçalo Santos
9c3f34fe04 Fix Lint Errors 2020-02-25 03:34:34 +00:00
Gonçalo Santos
d4123eeccd Merge branch 'master' of https://github.com/BoostIO/Boostnote into fixIssue2534 2020-02-25 03:31:21 +00:00
Junyoung Choi
d727a6110a 0.15.0 2020-02-24 18:11:36 +09:00
Junyoung Choi
c42635579c Bump optional deps 2020-02-24 18:10:54 +09:00
Gonçalo Santos
fd54a7b85c Scroll Bars can now be hidden (#2713)
* Scroll Bars can now be hidden

* Fix lint problems

* fix lint errors

* Fix lint

* Fix lint

* Change scrollBar to showScrollBar
2020-02-24 17:14:35 +09:00
xatier
76de78a72e Update build.md
Sync with [build.md](docs/build.md).

- 997ffa620d
- d475146d80
2020-02-24 16:57:19 +09:00
Hahn
5edce1fe6a [WIP]h1 padding bottom value changed (#3215)
* h1 padding bottom value changed

* padding top added to <p>

* padding top to h1

* h2 padding top updated

* heading changed

* typo

* Updated margin for h1 ~ h6
2020-02-24 16:56:50 +09:00
Alex Garrity
afbe43965e Reverse notes after filtering 2020-02-24 16:56:08 +09:00
Alex Garrity
d706a5375c Added sorting direction buttons 2020-02-24 16:56:08 +09:00
Alexander Wolf
feb2a878a9 Dropdown colour (theme aware) (#3472)
* catchup (#7)

* added rtl toggle button

* added rtl toggle button

* keep code styling aligned to the left and ltr at all times

* added hotkey setting for direction toggle

* fixed requested changes

* fix undefined variable

* Copyright info update

updated Copyright (C) 2017 - 2019 BoostIO to Copyright (C) 2017 - 2020 BoostIO

* Refine Chinese translation

- Should not translate "space", it means space key in most context
- Should translate "keymap", "spellcheck disabled", "auto detect"
- Should translate "On Right Click" to "右键点击"
- Refine misc translation.

* Added Wiki Link

* Add Traditional Chinese option to build.md

* Bug fix (sets tabWith to 2 on prettier configuration and ConfigManager so checkboxes can be clikable)

Co-authored-by: ibraude <48394109+ibraude@users.noreply.github.com>
Co-authored-by: Junyoung Choi <rokt33r.choi@gmail.com>
Co-authored-by: Satyendra <33686367+developersatyendra@users.noreply.github.com>
Co-authored-by: Andrew <andysim3d@gmail.com>

* Menu Backgroun Colour

* add white to the apply-theme loop

Co-authored-by: Milo Todt <milo@milotodt.com>
Co-authored-by: ibraude <48394109+ibraude@users.noreply.github.com>
Co-authored-by: Junyoung Choi <rokt33r.choi@gmail.com>
Co-authored-by: Satyendra <33686367+developersatyendra@users.noreply.github.com>
Co-authored-by: Andrew <andysim3d@gmail.com>
2020-02-24 16:51:05 +09:00
AWolf81
e55f1e0308 fix linting 2020-02-19 00:29:54 +01:00
ZeroX-DG
8b2ed8585f fixed wrong handler for setting button 2020-02-18 22:52:21 +09:00
Alexander Wolf
1d570df129 Merge branch 'master' into feature-tag-links 2020-02-12 20:51:59 +01:00
AWolf81
d125bd07f7 Merge branch 'master' into feature-tag-links 2020-02-12 20:42:09 +01:00
jhdcruz
997ffa620d Required both package for builds | Yarn 2020-02-12 18:20:37 +09:00
jhdcruz
d475146d80 Add build dependency packages cmds 2020-02-12 18:20:37 +09:00
dependabot[bot]
1fe59caa19 Bump merge from 1.2.0 to 1.2.1
Bumps [merge](https://github.com/yeikos/js.merge) from 1.2.0 to 1.2.1.
- [Release notes](https://github.com/yeikos/js.merge/releases)
- [Commits](https://github.com/yeikos/js.merge/compare/v1.2.0...v1.2.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-02-10 21:09:59 +09:00
Baptiste Augrain
a496a84cb8 fix broken headers' color in ConfigTab 2020-02-10 21:09:37 +09:00
Baptiste Augrain
eae964f7e7 add Vulcan UI theme 2020-02-10 21:09:37 +09:00
Junyoung Choi
e44381f295 Merge pull request #2964 from amedora/filter-tags-and-folders
Filter tags and folders
2020-02-06 16:21:15 +09:00
amedora
2c0e0a6e39 Merge branch 'master' into filter-tags-and-folders
# Conflicts:
#	browser/main/SideNav/index.js
2020-02-05 11:13:28 +09:00
Arcturus
216f588aa4 extended language support for multiple languages 2020-02-05 10:00:17 +09:00
Nguyen Viet Hung
592aca1539 fixed eslint error & integrated with prettier as well as formatted the whole codebase (#3450) 2020-02-05 09:28:27 +09:00
amedora
725bf2a691 Merge branch 'master' into filter-tags-and-folders
# Conflicts:
#	browser/main/SideNav/PreferenceButton.styl
#	browser/main/SideNav/SideNav.styl
2020-02-05 09:07:13 +09:00
hiiwave
051ce9e208 Fix #3397 (#3398)
* WIP: Fix #3397

* fixup! WIP: Fix #3397

* fix: catch potential URIError threw from decodeURI
2020-02-03 20:33:38 +09:00
dependabot[bot]
f367e9f08c Bump extend from 3.0.1 to 3.0.2
Bumps [extend](https://github.com/justmoon/node-extend) from 3.0.1 to 3.0.2.
- [Release notes](https://github.com/justmoon/node-extend/releases)
- [Changelog](https://github.com/justmoon/node-extend/blob/master/CHANGELOG.md)
- [Commits](https://github.com/justmoon/node-extend/compare/v3.0.1...v3.0.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-02-03 20:05:22 +09:00
dependabot[bot]
f72fdfe33f Bump mixin-deep from 1.3.1 to 1.3.2
Bumps [mixin-deep](https://github.com/jonschlinkert/mixin-deep) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/jonschlinkert/mixin-deep/releases)
- [Commits](https://github.com/jonschlinkert/mixin-deep/compare/1.3.1...1.3.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-02-03 20:05:10 +09:00
dependabot[bot]
000a54f5ed Bump handlebars from 4.0.11 to 4.5.3
Bumps [handlebars](https://github.com/wycats/handlebars.js) from 4.0.11 to 4.5.3.
- [Release notes](https://github.com/wycats/handlebars.js/releases)
- [Changelog](https://github.com/wycats/handlebars.js/blob/master/release-notes.md)
- [Commits](https://github.com/wycats/handlebars.js/compare/v4.0.11...v4.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2020-02-03 20:04:48 +09:00
xatier
57a5de97f8 Update build.md
text change per @rayou's suggestion.
2020-02-03 20:04:31 +09:00
xatier
262d173c65 Update zh_TW translation of build.md
Complete translation.
Sync with English version.

[1] https://github.com/BoostIO/Boostnote/blob/master/docs/build.md.
2020-02-03 20:04:31 +09:00
Ray Ou
e558fae4b0 Update build.md with correct description 2020-02-03 20:04:02 +09:00
Bumhan Yu
a90d801d08 update korean docs in docs/ko/ (#3454)
* update korean docs in docs/ko/

* Fix typo

Co-authored-by: Junyoung Choi <rokt33r.choi@gmail.com>
2020-02-03 20:03:21 +09:00
Bumhan Yu
636996356f update Korean section of contributing.md 2020-02-03 20:01:44 +09:00
Junyoung Choi
8a87c06b97 Merge pull request #2879 from daiyam/theme-nord
add Nord UI theme
2020-02-03 19:49:09 +09:00
Baptiste Augrain
69831571a5 fix broken title's color in ConfigTab by moving its default color to corresponding themes 2020-01-31 23:46:27 +01:00
Baptiste Augrain
24a5c839a7 fix text color of FolderSelect component 2020-01-31 12:10:19 +01:00
Baptiste Augrain
93e09f11dd fix UI theme for SplitEditor and CreateFromURL modal 2020-01-30 19:31:31 +01:00
Baptiste Augrain
c570fc9873 Merge branch 'master' into theme-nord 2020-01-30 19:14:58 +01:00
amedora
f1d03acbad Merge branch 'master' into filter-tags-and-folders
# Conflicts:
#	locales/zh-CN.json
2020-01-30 10:13:16 +09:00
Jeny Mazo
31ffbd98b6 Bug fix (sets tabWith to 2 on prettier configuration and ConfigManager so checkboxes can be clikable) 2020-01-29 16:37:54 +09:00
Ray Ou
87b9766bc0 Add Traditional Chinese option to build.md 2020-01-29 16:37:24 +09:00
Milo Todt
49c9bcac9a Added Wiki Link 2020-01-29 16:33:57 +09:00
Andrew
5105babd14 Refine Chinese translation
- Should not translate "space", it means space key in most context
- Should translate "keymap", "spellcheck disabled", "auto detect"
- Should translate "On Right Click" to "右键点击"
- Refine misc translation.
2020-01-29 16:31:38 +09:00
Satyendra
0a361f5f41 Copyright info update
updated Copyright (C) 2017 - 2019 BoostIO to Copyright (C) 2017 - 2020 BoostIO
2020-01-29 16:30:54 +09:00
Junyoung Choi
8218d5eb5a Merge pull request #3282 from ibraude/master
Added rtl toggle button
2020-01-29 16:28:31 +09:00
Itai Braude
7fe6925615 fix undefined variable 2020-01-16 23:52:19 +02:00
cephonodes
1dd71fc923 Updated Japanese document about contribution 2020-01-16 07:44:53 +09:00
ibraude
301f03dadd Merge branch 'master' into master 2020-01-07 12:11:27 +02:00
KZ
53c48d86b7 Merge pull request #3415 from BoostIO/update-readme
Update readme
2020-01-06 13:15:05 +09:00
KZ
d760259ce6 Update readme 2020-01-06 13:12:49 +09:00
Junyoung Choi
1195c77f7a v0.14.0 2020-01-03 11:28:19 -05:00
Flexo013
c373c207c0 Added link to downloads 2019-12-29 07:21:13 +09:00
Junyoung Choi
65e83e7017 Merge pull request #2585 from daiyam/fix-mermaid-height
fix height of mermaid diagrams
2019-12-25 05:07:07 -05:00
Itai Braude
0722c2505a fixed requested changes 2019-12-25 10:04:31 +02:00
Abner Soares Alves Junior
3c12e0d119 Fix emoji render on notes list 2019-12-24 06:54:20 +09:00
Abner Soares Alves Junior
60d6c68e48 Add some translations 2019-12-24 06:53:26 +09:00
Abner Soares Alves Junior
d8605965a8 Add Ok button to export confirmation box 2019-12-24 06:53:26 +09:00
Yuki Furukawa
6d455fc286 remove redundant conditions of macOS in main-menu 2019-12-24 06:39:18 +09:00
Mayke
2882667e94 rebuilding test snapshots 2019-12-24 06:36:53 +09:00
Mayke
7fa578880e refactoring to use the new .markdownIt-TOC-wrapper div 2019-12-24 06:36:53 +09:00
Mayke
3f465df1cd configuring a div wrapper for TOC plugin to use overflow-y in <ul> and still use &:before on parent element 2019-12-24 06:36:53 +09:00
Mayke
c423784cc5 adding TOC UI 2019-12-24 06:36:53 +09:00
Nicholas Browning
ce853a7e3a Export: CSS: adjusted relative path 2019-12-24 06:23:51 +09:00
Nicholas Browning
099ebad06b Export: uses markdown preview dom. Supports diagrams 2019-12-24 06:23:51 +09:00
Junyoung Choi
75a1347ae1 Update readme and contributing 2019-12-23 10:02:41 +09:00
amedora
f5779558bb Merge branch 'master' into filter-tags-and-folders
# Conflicts:
#	browser/main/SideNav/index.js
#	locales/da.json
#	locales/de.json
#	locales/en.json
#	locales/es-ES.json
#	locales/fa.json
#	locales/fr.json
#	locales/hu.json
#	locales/it.json
#	locales/ja.json
#	locales/ko.json
#	locales/no.json
#	locales/pl.json
#	locales/pt-BR.json
#	locales/pt-PT.json
#	locales/ru.json
#	locales/sq.json
#	locales/th.json
#	locales/tr.json
#	locales/zh-CN.json
#	locales/zh-TW.json
2019-12-23 09:41:40 +09:00
Baptiste Augrain
b83d3e5c33 Merge branch 'master' into fix-mermaid-height 2019-12-22 23:46:47 +01:00
Junyoung Choi
1a7c719a4e Update new BoostNote info 2019-11-26 14:03:57 +09:00
Gonçalo Santos
b91a76b3b1 Merge branch 'master' into fixIssue2534 2019-11-22 00:19:56 +00:00
Jack Hsieh
d78f6b7aba Ability to stop auto update 2019-11-22 05:42:55 +09:00
dependabot[bot]
7d4d176bf4 Bump js-yaml from 3.12.0 to 3.13.1
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.12.0 to 3.13.1.
- [Release notes](https://github.com/nodeca/js-yaml/releases)
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.12.0...3.13.1)

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

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

* Update locales/de.json

Fixed german translation as suggested.

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

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

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

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

* fix mermaid xss

* Revert "fix mermaid xss"

This reverts commit 1ff179a1bd.

* configuable mermaid HTML label

* add locales for mermaid configuration
2019-07-27 12:39:12 +09:00
Junyoung Choi
329066719e 0.12.1 2019-07-27 11:51:40 +09:00
Junyoung Choi
93b8ef35f7 0.12.1-1 2019-07-26 13:50:44 +09:00
Junyoung Choi
484b003b34 Fix theme paths 2019-07-26 13:49:40 +09:00
hikerpig
c2a26a8547 improvement: refactor buildStyle to NamedParameters style, and add some jsdoc 2019-07-21 15:28:12 +08:00
hikerpig
addf9b920f tweak MarkdownPreview style to optimize overflow scrollbar display, fix #2902 2019-07-21 15:28:12 +08:00
AWolf81
aeb77e5a40 Remove package-lock file & use startsWith for https check 2019-07-08 00:05:26 +02:00
nathan-castlehow
1d59d89588 feat(prettierOnMarkdown): Forced prettier options to always have parser set to markdown when used. 2019-07-03 09:28:36 +08:00
nathan-castlehow
bde357f952 feat(prettierOnMarkdown): Changed Prettier require to use import 2019-07-03 09:03:24 +08:00
AWolf81
558c091205 fix linting 2019-06-30 00:18:52 +02:00
AWolf81
f67175e628 fix test 2019-06-30 00:03:54 +02:00
AWolf81
390f6d545f fix PropTypes 2019-06-30 00:03:25 +02:00
AWolf81
44efb0178c Merge branch 'master' into html-to-md
# Conflicts:
#	browser/main/modals/NewNoteModal.js
#	package-lock.json
#	package.json
#	yarn.lock
2019-06-29 23:25:52 +02:00
AWolf81
37eee26bdf fix linting & routing 2019-06-29 23:21:32 +02:00
nathan-castlehow
ed4a670f0a feat(prettierOnMarkdown): Changed default hotkey value 2019-06-23 13:54:17 +08:00
nathan-castlehow
fbb9afe34f feat(prettierOnMarkdown):Fixed incorrect options passed to code mirror instance 2019-06-23 13:42:14 +08:00
nathan-castlehow
020bc11bd7 feat(prettierOnMarkdown):Tweaked spacing on default Prettier Config Value 2019-06-23 13:41:52 +08:00
nathan-castlehow
ae0837e29b feat(prettierOnMarkdown):Added prettier config default to config manager 2019-06-23 13:41:34 +08:00
nathan-castlehow
f0380ef733 feat(prettierOnMarkdown): Added support for prettyifing markdown as well as added hot key option. Partial Implementation of Prettier config in configuration screen. TODO Fix defaulting of prettier configuration 2019-06-23 13:40:20 +08:00
nathan-castlehow
25bdaf9f00 feat(prettierOnMarkdown): Added Reference to prettier in Code Editor and created config file 2019-06-23 13:34:47 +08:00
nathan-castlehow
ef1809305c feat(prettierOnMarkdown): Added Reference To Prettier 2019-06-23 13:34:47 +08:00
Callum Booth
ba34458feb changes if state to be more readable 2019-06-10 20:27:55 +01:00
Callum Booth
a2fb50a71c adds destructed fontFamily from props 2019-06-10 19:21:33 +01:00
Callum booth
b15a4007ee merge from master 2019-06-10 19:13:58 +01:00
nathan-castlehow
090b5c32f0 feat: Added Context Menu for markdown preview mode and copy url when hyperlink 2019-06-09 13:28:53 +08:00
AWolf81
2e380ceb02 use Connected-React-Router to navigate 2019-05-29 08:29:53 +02:00
AWolf81
f26dea2420 Merge branch 'master' into feature-tag-links 2019-05-29 08:11:51 +02:00
amedora
c5484fbb88 Merge branch 'master' into filter-tags-and-folders 2019-05-21 09:50:39 +09:00
amedora
0d4b6252e8 add locales 'filter tags/folders...' 2019-05-21 09:45:52 +09:00
amedora
7529feb4a5 add placeholder to show 'filter tags/folders...' 2019-05-21 09:26:59 +09:00
AWolf81
5f96e314fd add tag link handling with :tag:#tag syntax 2019-05-11 09:30:10 +02:00
amedora
a7f802db7c Merge branch 'master' into filter-tags-and-folders 2019-05-09 09:23:17 +09:00
amedora
db78f1b91e fix search input visuality for Monokai 2019-04-03 16:14:52 +09:00
amedora
04e0523cac fix .extra-buttons vertical position 2019-04-03 14:50:54 +09:00
HarlanLuo
885b9d2c26 remove unused ref 2019-04-03 14:40:14 +09:00
HarlanLuo
51aff20d65 improve style of sidenav 2019-04-03 14:40:13 +09:00
HarlanLuo
8dc5214c9e new feature: filter tags and folder list 2019-04-03 14:40:12 +09:00
Callum booth
93f0d3c1cf fixes #2903 - Rearrange layout of columns 2019-03-19 20:39:34 +00:00
Baptiste Augrain
1cdac943ba adding Nord theme and streamlining UI theming 2019-02-11 00:45:34 +01:00
Baptiste Augrain
a58b6f1b49 Merge branch 'master' into fix-mermaid-height 2018-12-24 10:06:15 +01:00
gregueiras
a19ff6762e Unit tests added 2018-12-13 18:50:02 +00:00
Gonçalo Santos
7034e7b620 Delete Slider.styl
I submitted an extra file, it was not necessary
2018-12-12 11:37:59 +00:00
gregueiras
cd53a65c14 Code Style Improvements 2018-11-29 13:58:15 +00:00
gregueiras
8b54f5aa69 Removed colons and semi colons 2018-11-29 12:25:14 +00:00
gregueiras
ceed178061 Inverted thumb order 2018-11-29 12:12:14 +00:00
gregueiras
33662974bf Fixed default theme thumb background 2018-11-29 12:05:14 +00:00
gregueiras
36b97fc6a2 Interface Improved 2018-11-29 11:58:36 +00:00
Gonçalo Santos
0f8c627474 Fixed checkbox 2018-11-28 12:09:42 +00:00
gregueiras
f38fef23a0 ThemeManager Created 2018-11-28 12:04:33 +00:00
gregueiras
8be0ea64a5 Scheduled Theme default configuration 2018-11-27 16:41:02 +00:00
gregueiras
1419c71ef5 Border added 2018-11-27 10:40:42 +00:00
gregueiras
e13742445e Format 2018-11-26 22:01:43 +00:00
gregueiras
1d21bb1ea3 Values are now saved 2018-11-26 22:00:02 +00:00
gregueiras
aa38b1f859 Multi tab WIP 2018-11-26 20:11:11 +00:00
Baptiste Augrain
b6b29e02f3 fix lint error 2018-11-08 14:41:25 +01:00
Baptiste Augrain
2fc37d54f2 fix height of mermaid's diagrams 2018-11-08 13:19:46 +01:00
Storm Burpee
18aae8cf7b getting very close 2018-05-28 22:12:04 +09:30
Storm Burpee
4a9bc69ac2 starting to write a test 2018-05-28 19:45:09 +09:30
Storm Burpee
d97e62f864 Import note from url with markdown 2018-05-26 17:00:12 +09:30
300 changed files with 18923 additions and 12583 deletions

View File

@@ -1,6 +1,6 @@
{ {
"extends": ["standard", "standard-jsx", "plugin:react/recommended"], "extends": ["standard", "standard-jsx", "plugin:react/recommended", "prettier"],
"plugins": ["react"], "plugins": ["react", "prettier"],
"rules": { "rules": {
"no-useless-escape": 0, "no-useless-escape": 0,
"prefer-const": ["warn", { "prefer-const": ["warn", {
@@ -13,12 +13,15 @@
"react/no-string-refs": 0, "react/no-string-refs": 0,
"react/no-find-dom-node": "warn", "react/no-find-dom-node": "warn",
"react/no-render-return-value": "warn", "react/no-render-return-value": "warn",
"react/no-deprecated": "warn" "react/no-deprecated": "warn",
"prettier/prettier": ["error"]
}, },
"globals": { "globals": {
"FileReader": true, "FileReader": true,
"localStorage": true, "localStorage": true,
"fetch": true "fetch": true,
"Image": true,
"MutationObserver": true
}, },
"env": { "env": {
"jest": true "jest": true

2
.gitignore vendored
View File

@@ -10,3 +10,5 @@ node_modules/*
*.log *.log
.idea .idea
.vscode .vscode
package-lock.json
config.json

5
.prettierrc Normal file
View File

@@ -0,0 +1,5 @@
{
"singleQuote": true,
"semi": false,
"jsxSingleQuote": true
}

View File

@@ -3,7 +3,6 @@ node_js:
- 8 - 8
script: script:
- npm run lint && npm run test - npm run lint && npm run test
- yarn jest
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@6.4 && 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: after_success:
- openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv - openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv

View File

@@ -5,19 +5,19 @@ Let us know what is currently happening.
Please include some **screenshots** with the **developer tools** open (console tab) when you report a bug. Please include some **screenshots** with the **developer tools** open (console tab) when you report a bug.
If your issue is regarding Boostnote mobile, please open an issue in the Boostnote Mobile repo 👉 https://github.com/BoostIO/boostnote-mobile. If your issue is regarding the new Boost Note.next, please open an issue in the new repo 👉 https://github.com/BoostIO/BoostNote.next/issues.
--> -->
# Expected behavior # Expected behavior
<!-- <!--
Let us know what you think should happen! Let us know what you think should happen.
--> -->
# Steps to reproduce # Steps to reproduce
<!-- <!--
Please be thorough, issues we can reproduce are easier to fix! Please be thorough, issues we can reproduce are easier to fix.
--> -->
1. 1.
@@ -26,8 +26,8 @@ Please be thorough, issues we can reproduce are easier to fix!
# Environment # Environment
- Version : - Boostnote version: <!-- 0.x.x -->
- OS Version and name : - OS version and name: <!-- Windows 10 / Ubuntu 18.04 / etc -->
<!-- <!--
Love Boostnote? Please consider supporting us on IssueHunt: Love Boostnote? Please consider supporting us on IssueHunt:

View File

@@ -3,13 +3,16 @@ Before submitting this PR, please make sure that:
- You have read and understand the contributing.md - You have read and understand the contributing.md
- You have checked docs/code_style.md for information on code style - You have checked docs/code_style.md for information on code style
--> -->
## Description ## Description
<!-- <!--
Tell us what your PR does. Tell us what your PR does.
Please attach a screenshot/ video/gif image describing your PR if possible. Please attach a screenshot/ video/gif image describing your PR if possible.
--> -->
## Issue fixed ## Issue fixed
<!-- <!--
Please list out all issue fixed with this PR here. Please list out all issue fixed with this PR here.
--> -->
@@ -20,6 +23,7 @@ your PR will be reviewed faster if we know exactly what it does.
Change :white_circle: to :radio_button: in all the options that apply Change :white_circle: to :radio_button: in all the options that apply
--> -->
## Type of changes ## Type of changes
- :white_circle: Bug fix (Change that fixed an issue) - :white_circle: Bug fix (Change that fixed an issue)
@@ -34,3 +38,5 @@ Change :white_circle: to :radio_button: in all the options that apply
- :white_circle: I have written test for my code and it has been tested - :white_circle: I have written test for my code and it has been tested
- :white_circle: All existing tests have been passed - :white_circle: All existing tests have been passed
- :white_circle: I have attached a screenshot/video to visualize my change if possible - :white_circle: I have attached a screenshot/video to visualize my change if possible
- :white_circle: This PR will modify the UI or affects the UX
- :white_circle: This PR will add/update/delete a keybinding

View File

@@ -6,11 +6,7 @@ import hljs from 'highlight.js'
import 'codemirror-mode-elixir' import 'codemirror-mode-elixir'
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
import convertModeName from 'browser/lib/convertModeName' import convertModeName from 'browser/lib/convertModeName'
import { import { options, TableEditor, Alignment } from '@susisu/mte-kernel'
options,
TableEditor,
Alignment
} from '@susisu/mte-kernel'
import TextEditorInterface from 'browser/lib/TextEditorInterface' import TextEditorInterface from 'browser/lib/TextEditorInterface'
import eventEmitter from 'browser/main/lib/eventEmitter' import eventEmitter from 'browser/main/lib/eventEmitter'
import iconv from 'iconv-lite' import iconv from 'iconv-lite'
@@ -20,40 +16,55 @@ import styles from '../components/CodeEditor.styl'
const { ipcRenderer, remote, clipboard } = require('electron') const { ipcRenderer, remote, clipboard } = require('electron')
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
const spellcheck = require('browser/lib/spellcheck') const spellcheck = require('browser/lib/spellcheck')
const buildEditorContextMenu = require('browser/lib/contextMenuBuilder').buildEditorContextMenu const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
import TurndownService from 'turndown' .buildEditorContextMenu
import {languageMaps} from '../lib/CMLanguageList' import { createTurndownService } from '../lib/turndown'
import { languageMaps } from '../lib/CMLanguageList'
import snippetManager from '../lib/SnippetManager' import snippetManager from '../lib/SnippetManager'
import {generateInEditor, tocExistsInEditor} from 'browser/lib/markdown-toc-generator' import {
generateInEditor,
tocExistsInEditor
} from 'browser/lib/markdown-toc-generator'
import markdownlint from 'markdownlint' import markdownlint from 'markdownlint'
import Jsonlint from 'jsonlint-mod' import Jsonlint from 'jsonlint-mod'
import { DEFAULT_CONFIG } from '../main/lib/ConfigManager' import { DEFAULT_CONFIG } from '../main/lib/ConfigManager'
import prettier from 'prettier'
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
const buildCMRulers = (rulers, enableRulers) => const buildCMRulers = (rulers, enableRulers) =>
(enableRulers ? rulers.map(ruler => ({ enableRulers
? rulers.map(ruler => ({
column: ruler column: ruler
})) : []) }))
: []
function translateHotkey (hotkey) { function translateHotkey(hotkey) {
return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl') return hotkey
.replace(/\s*\+\s*/g, '-')
.replace(/Command/g, 'Cmd')
.replace(/Control/g, 'Ctrl')
} }
export default class CodeEditor extends React.Component { export default class CodeEditor extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props)
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, { this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {
leading: false, leading: false,
trailing: true trailing: true
}) })
this.changeHandler = (editor, changeObject) => this.handleChange(editor, changeObject) this.changeHandler = (editor, changeObject) =>
this.highlightHandler = (editor, changeObject) => this.handleHighlight(editor, changeObject) this.handleChange(editor, changeObject)
this.highlightHandler = (editor, changeObject) =>
this.handleHighlight(editor, changeObject)
this.focusHandler = () => { this.focusHandler = () => {
ipcRenderer.send('editor:focused', true) ipcRenderer.send('editor:focused', true)
} }
const debouncedDeletionOfAttachments = _.debounce(attachmentManagement.deleteAttachmentsNotPresentInNote, 30000) const debouncedDeletionOfAttachments = _.debounce(
attachmentManagement.deleteAttachmentsNotPresentInNote,
30000
)
this.blurHandler = (editor, e) => { this.blurHandler = (editor, e) => {
ipcRenderer.send('editor:focused', false) ipcRenderer.send('editor:focused', false)
if (e == null) return null if (e == null) return null
@@ -65,11 +76,14 @@ export default class CodeEditor extends React.Component {
el = el.parentNode el = el.parentNode
} }
this.props.onBlur != null && this.props.onBlur(e) this.props.onBlur != null && this.props.onBlur(e)
const { const { storageKey, noteKey } = this.props
if (this.props.deleteUnusedAttachments === true) {
debouncedDeletionOfAttachments(
this.editor.getValue(),
storageKey, storageKey,
noteKey noteKey
} = this.props )
debouncedDeletionOfAttachments(this.editor.getValue(), storageKey, noteKey) }
} }
this.pasteHandler = (editor, e) => { this.pasteHandler = (editor, e) => {
e.preventDefault() e.preventDefault()
@@ -88,7 +102,7 @@ export default class CodeEditor extends React.Component {
this.formatTable = () => this.handleFormatTable() this.formatTable = () => this.handleFormatTable()
if (props.switchPreview !== 'RIGHTCLICK') { if (props.switchPreview !== 'RIGHTCLICK') {
this.contextMenuHandler = function (editor, event) { this.contextMenuHandler = function(editor, event) {
const menu = buildEditorContextMenu(editor, event) const menu = buildEditorContextMenu(editor, event)
if (menu != null) { if (menu != null) {
setTimeout(() => menu.popup(remote.getCurrentWindow()), 30) setTimeout(() => menu.popup(remote.getCurrentWindow()), 30)
@@ -98,27 +112,27 @@ export default class CodeEditor extends React.Component {
this.editorActivityHandler = () => this.handleEditorActivity() this.editorActivityHandler = () => this.handleEditorActivity()
this.turndownService = new TurndownService() this.turndownService = createTurndownService()
} }
handleSearch (msg) { handleSearch(msg) {
const cm = this.editor const cm = this.editor
const component = this const component = this
if (component.searchState) cm.removeOverlay(component.searchState) if (component.searchState) cm.removeOverlay(component.searchState)
if (msg.length < 3) return if (msg.length < 1) return
cm.operation(function () { cm.operation(function() {
component.searchState = makeOverlay(msg, 'searching') component.searchState = makeOverlay(msg, 'searching')
cm.addOverlay(component.searchState) cm.addOverlay(component.searchState)
function makeOverlay (query, style) { function makeOverlay(query, style) {
query = new RegExp( query = new RegExp(
query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'),
'gi' 'gi'
) )
return { return {
token: function (stream) { token: function(stream) {
query.lastIndex = stream.pos query.lastIndex = stream.pos
var match = query.exec(stream.string) var match = query.exec(stream.string)
if (match && match.index === stream.pos) { if (match && match.index === stream.pos) {
@@ -135,25 +149,27 @@ export default class CodeEditor extends React.Component {
}) })
} }
handleFormatTable () { handleFormatTable() {
this.tableEditor.formatAll(options({ this.tableEditor.formatAll(
options({
textWidthOptions: {} textWidthOptions: {}
})) })
)
} }
handleEditorActivity () { handleEditorActivity() {
if (!this.textEditorInterface.transaction) { if (!this.textEditorInterface.transaction) {
this.updateTableEditorState() this.updateTableEditorState()
} }
} }
updateDefaultKeyMap () { updateDefaultKeyMap() {
const { hotkey } = this.props const { hotkey } = this.props
const self = this const self = this
const expandSnippet = snippetManager.expandSnippet const expandSnippet = snippetManager.expandSnippet
this.defaultKeyMap = CodeMirror.normalizeKeyMap({ this.defaultKeyMap = CodeMirror.normalizeKeyMap({
Tab: function (cm) { Tab: function(cm) {
const cursor = cm.getCursor() const cursor = cm.getCursor()
const line = cm.getLine(cursor.line) const line = cm.getLine(cursor.line)
const cursorPosition = cursor.ch const cursorPosition = cursor.ch
@@ -195,17 +211,17 @@ export default class CodeEditor extends React.Component {
} }
} }
}, },
'Cmd-Left': function (cm) { 'Cmd-Left': function(cm) {
cm.execCommand('goLineLeft') cm.execCommand('goLineLeft')
}, },
'Cmd-T': function (cm) { 'Cmd-T': function(cm) {
// Do nothing // Do nothing
}, },
[translateHotkey(hotkey.insertDate)]: function (cm) { [translateHotkey(hotkey.insertDate)]: function(cm) {
const dateNow = new Date() const dateNow = new Date()
cm.replaceSelection(dateNow.toLocaleDateString()) cm.replaceSelection(dateNow.toLocaleDateString())
}, },
[translateHotkey(hotkey.insertDateTime)]: function (cm) { [translateHotkey(hotkey.insertDateTime)]: function(cm) {
const dateNow = new Date() const dateNow = new Date()
cm.replaceSelection(dateNow.toLocaleString()) cm.replaceSelection(dateNow.toLocaleString())
}, },
@@ -216,13 +232,48 @@ export default class CodeEditor extends React.Component {
} }
return CodeMirror.Pass return CodeMirror.Pass
}, },
[translateHotkey(hotkey.prettifyMarkdown)]: cm => {
// Default / User configured prettier options
const currentConfig = JSON.parse(self.props.prettierConfig)
// Parser type will always need to be markdown so we override the option before use
currentConfig.parser = 'markdown'
// Get current cursor position
const cursorPos = cm.getCursor()
currentConfig.cursorOffset = cm.doc.indexFromPos(cursorPos)
// Prettify contents of editor
const formattedTextDetails = prettier.formatWithCursor(
cm.doc.getValue(),
currentConfig
)
const formattedText = formattedTextDetails.formatted
const formattedCursorPos = formattedTextDetails.cursorOffset
cm.doc.setValue(formattedText)
// Reset Cursor position to be at the same markdown as was before prettifying
const newCursorPos = cm.doc.posFromIndex(formattedCursorPos)
cm.doc.setCursor(newCursorPos)
},
[translateHotkey(hotkey.sortLines)]: cm => {
const selection = cm.doc.getSelection()
const appendLineBreak = /\n$/.test(selection)
const sorted = _.split(selection.trim(), '\n').sort()
const sortedString =
_.join(sorted, '\n') + (appendLineBreak ? '\n' : '')
cm.doc.replaceSelection(sortedString)
},
[translateHotkey(hotkey.pasteSmartly)]: cm => { [translateHotkey(hotkey.pasteSmartly)]: cm => {
this.handlePaste(cm, true) this.handlePaste(cm, true)
} }
}) })
} }
updateTableEditorState () { updateTableEditorState() {
const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions) const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions)
if (active) { if (active) {
if (this.extraKeysMode !== 'editor') { if (this.extraKeysMode !== 'editor') {
@@ -238,8 +289,8 @@ export default class CodeEditor extends React.Component {
} }
} }
componentDidMount () { componentDidMount() {
const { rulers, enableRulers, enableMarkdownLint } = this.props const { rulers, enableRulers, enableMarkdownLint, RTL } = this.props
eventEmitter.on('line:jump', this.scrollToLineHandeler) eventEmitter.on('line:jump', this.scrollToLineHandeler)
snippetManager.init() snippetManager.init()
@@ -260,19 +311,28 @@ export default class CodeEditor extends React.Component {
scrollPastEnd: this.props.scrollPastEnd, scrollPastEnd: this.props.scrollPastEnd,
inputStyle: 'textarea', inputStyle: 'textarea',
dragDrop: false, dragDrop: false,
direction: RTL ? 'rtl' : 'ltr',
rtlMoveVisually: RTL,
foldGutter: true, foldGutter: true,
lint: enableMarkdownLint ? this.getCodeEditorLintConfig() : false, lint: enableMarkdownLint ? this.getCodeEditorLintConfig() : false,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'], gutters: [
'CodeMirror-linenumbers',
'CodeMirror-foldgutter',
'CodeMirror-lint-markers'
],
autoCloseBrackets: { autoCloseBrackets: {
pairs: this.props.matchingPairs, pairs: this.props.matchingPairs,
triples: this.props.matchingTriples, triples: this.props.matchingTriples,
explode: this.props.explodingPairs, explode: this.props.explodingPairs,
override: true override: true
}, },
extraKeys: this.defaultKeyMap extraKeys: this.defaultKeyMap,
prettierConfig: this.props.prettierConfig
}) })
document.querySelector('.CodeMirror-lint-markers').style.display = enableMarkdownLint ? 'inline-block' : 'none' document.querySelector(
'.CodeMirror-lint-markers'
).style.display = enableMarkdownLint ? 'inline-block' : 'none'
if (!this.props.mode && this.props.value && this.props.autoDetect) { if (!this.props.mode && this.props.value && this.props.autoDetect) {
this.autoDetectLanguage(this.props.value) this.autoDetectLanguage(this.props.value)
@@ -305,7 +365,7 @@ export default class CodeEditor extends React.Component {
this.textEditorInterface = new TextEditorInterface(this.editor) this.textEditorInterface = new TextEditorInterface(this.editor)
this.tableEditor = new TableEditor(this.textEditorInterface) this.tableEditor = new TableEditor(this.textEditorInterface)
if (this.props.spellCheck) { if (this.props.spellCheck) {
this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'}) this.editor.addPanel(this.createSpellCheckPanel(), { position: 'bottom' })
} }
eventEmitter.on('code:format-table', this.formatTable) eventEmitter.on('code:format-table', this.formatTable)
@@ -315,13 +375,13 @@ export default class CodeEditor extends React.Component {
}) })
this.editorKeyMap = CodeMirror.normalizeKeyMap({ this.editorKeyMap = CodeMirror.normalizeKeyMap({
'Tab': () => { Tab: () => {
this.tableEditor.nextCell(this.tableEditorOptions) this.tableEditor.nextCell(this.tableEditorOptions)
}, },
'Shift-Tab': () => { 'Shift-Tab': () => {
this.tableEditor.previousCell(this.tableEditorOptions) this.tableEditor.previousCell(this.tableEditorOptions)
}, },
'Enter': () => { Enter: () => {
this.tableEditor.nextRow(this.tableEditorOptions) this.tableEditor.nextRow(this.tableEditorOptions)
}, },
'Ctrl-Enter': () => { 'Ctrl-Enter': () => {
@@ -440,7 +500,7 @@ export default class CodeEditor extends React.Component {
this.initialHighlighting() this.initialHighlighting()
} }
getWordBeforeCursor (line, lineNumber, cursorPosition) { getWordBeforeCursor(line, lineNumber, cursorPosition) {
let wordBeforeCursor = '' let wordBeforeCursor = ''
const originCursorPosition = cursorPosition const originCursorPosition = cursorPosition
const emptyChars = /\t|\s|\r|\n|\$/ const emptyChars = /\t|\s|\r|\n|\$/
@@ -477,11 +537,11 @@ export default class CodeEditor extends React.Component {
} }
} }
quitEditor () { quitEditor() {
document.querySelector('textarea').blur() document.querySelector('textarea').blur()
} }
componentWillUnmount () { componentWillUnmount() {
this.editor.off('focus', this.focusHandler) this.editor.off('focus', this.focusHandler)
this.editor.off('blur', this.blurHandler) this.editor.off('blur', this.blurHandler)
this.editor.off('change', this.changeHandler) this.editor.off('change', this.changeHandler)
@@ -496,7 +556,7 @@ export default class CodeEditor extends React.Component {
eventEmitter.off('code:format-table', this.formatTable) eventEmitter.off('code:format-table', this.formatTable)
} }
componentDidUpdate (prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
let needRefresh = false let needRefresh = false
const { const {
rulers, rulers,
@@ -520,13 +580,22 @@ export default class CodeEditor extends React.Component {
if (prevProps.keyMap !== this.props.keyMap) { if (prevProps.keyMap !== this.props.keyMap) {
needRefresh = true needRefresh = true
} }
if (prevProps.enableMarkdownLint !== enableMarkdownLint || prevProps.customMarkdownLintConfig !== customMarkdownLintConfig) { if (prevProps.RTL !== this.props.RTL) {
this.editor.setOption('direction', this.props.RTL ? 'rtl' : 'ltr')
this.editor.setOption('rtlMoveVisually', this.props.RTL)
}
if (
prevProps.enableMarkdownLint !== enableMarkdownLint ||
prevProps.customMarkdownLintConfig !== customMarkdownLintConfig
) {
if (!enableMarkdownLint) { if (!enableMarkdownLint) {
this.editor.setOption('lint', {default: false}) this.editor.setOption('lint', { default: false })
document.querySelector('.CodeMirror-lint-markers').style.display = 'none' document.querySelector('.CodeMirror-lint-markers').style.display =
'none'
} else { } else {
this.editor.setOption('lint', this.getCodeEditorLintConfig()) this.editor.setOption('lint', this.getCodeEditorLintConfig())
document.querySelector('.CodeMirror-lint-markers').style.display = 'inline-block' document.querySelector('.CodeMirror-lint-markers').style.display =
'inline-block'
} }
needRefresh = true needRefresh = true
} }
@@ -558,9 +627,11 @@ export default class CodeEditor extends React.Component {
this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd) this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd)
} }
if (prevProps.matchingPairs !== this.props.matchingPairs || if (
prevProps.matchingPairs !== this.props.matchingPairs ||
prevProps.matchingTriples !== this.props.matchingTriples || prevProps.matchingTriples !== this.props.matchingTriples ||
prevProps.explodingPairs !== this.props.explodingPairs) { prevProps.explodingPairs !== this.props.explodingPairs
) {
const bracketObject = { const bracketObject = {
pairs: this.props.matchingPairs, pairs: this.props.matchingPairs,
triples: this.props.matchingTriples, triples: this.props.matchingTriples,
@@ -605,26 +676,38 @@ export default class CodeEditor extends React.Component {
const elem = document.getElementById('editor-bottom-panel') const elem = document.getElementById('editor-bottom-panel')
elem.parentNode.removeChild(elem) elem.parentNode.removeChild(elem)
} else { } else {
this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'}) this.editor.addPanel(this.createSpellCheckPanel(), {
position: 'bottom'
})
} }
} }
if (
prevProps.deleteUnusedAttachments !== this.props.deleteUnusedAttachments
) {
this.editor.setOption(
'deleteUnusedAttachments',
this.props.deleteUnusedAttachments
)
}
if (needRefresh) { if (needRefresh) {
this.editor.refresh() this.editor.refresh()
} }
} }
getCodeEditorLintConfig () { getCodeEditorLintConfig() {
const { mode } = this.props const { mode } = this.props
const checkMarkdownNoteIsOpen = mode === 'Boost Flavored Markdown' const checkMarkdownNoteIsOpen = mode === 'Boost Flavored Markdown'
return checkMarkdownNoteIsOpen ? { return checkMarkdownNoteIsOpen
'getAnnotations': this.validatorOfMarkdown, ? {
'async': true getAnnotations: this.validatorOfMarkdown,
} : false async: true
}
: false
} }
validatorOfMarkdown (text, updateLinting) { validatorOfMarkdown(text, updateLinting) {
const { customMarkdownLintConfig } = this.props const { customMarkdownLintConfig } = this.props
let lintConfigJson let lintConfigJson
try { try {
@@ -635,10 +718,10 @@ export default class CodeEditor extends React.Component {
return return
} }
const lintOptions = { const lintOptions = {
'strings': { strings: {
'content': text content: text
}, },
'config': lintConfigJson config: lintConfigJson
} }
return markdownlint(lintOptions, (err, result) => { return markdownlint(lintOptions, (err, result) => {
@@ -649,7 +732,7 @@ export default class CodeEditor extends React.Component {
let ruleNames = '' let ruleNames = ''
item.ruleNames.map((ruleName, index) => { item.ruleNames.map((ruleName, index) => {
ruleNames += ruleName ruleNames += ruleName
ruleNames += (index === item.ruleNames.length - 1) ? ': ' : '/' ruleNames += index === item.ruleNames.length - 1 ? ': ' : '/'
}) })
const lineNumber = item.lineNumber - 1 const lineNumber = item.lineNumber - 1
foundIssues.push({ foundIssues.push({
@@ -664,7 +747,7 @@ export default class CodeEditor extends React.Component {
}) })
} }
setMode (mode) { setMode(mode) {
let syntax = CodeMirror.findModeByName(convertModeName(mode || 'text')) let syntax = CodeMirror.findModeByName(convertModeName(mode || 'text'))
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
@@ -672,7 +755,7 @@ export default class CodeEditor extends React.Component {
CodeMirror.autoLoadMode(this.editor, syntax.mode) CodeMirror.autoLoadMode(this.editor, syntax.mode)
} }
handleChange (editor, changeObject) { handleChange(editor, changeObject) {
spellcheck.handleChange(editor, changeObject) spellcheck.handleChange(editor, changeObject)
// The current note contains an toc. We'll check for changes on headlines. // The current note contains an toc. We'll check for changes on headlines.
@@ -682,7 +765,11 @@ export default class CodeEditor extends React.Component {
// Check if one of the changed lines contains a headline // Check if one of the changed lines contains a headline
for (let line = 0; line < changeObject.text.length; line++) { for (let line = 0; line < changeObject.text.length; line++) {
if (this.linePossibleContainsHeadline(editor.getLine(changeObject.from.line + line))) { if (
this.linePossibleContainsHeadline(
editor.getLine(changeObject.from.line + line)
)
) {
requireTocUpdate = true requireTocUpdate = true
break break
} }
@@ -711,13 +798,13 @@ export default class CodeEditor extends React.Component {
} }
} }
linePossibleContainsHeadline (currentLine) { linePossibleContainsHeadline(currentLine) {
// We can't check if the line start with # because when some write text before // We can't check if the line start with # because when some write text before
// the # we also need to update the toc // the # we also need to update the toc
return currentLine.includes('# ') return currentLine.includes('# ')
} }
incrementLines (start, linesAdded, linesRemoved, editor) { incrementLines(start, linesAdded, linesRemoved, editor) {
const highlightedLines = editor.options.linesHighlighted const highlightedLines = editor.options.linesHighlighted
const totalHighlightedLines = highlightedLines.length const totalHighlightedLines = highlightedLines.length
@@ -738,7 +825,7 @@ export default class CodeEditor extends React.Component {
highlightedLines.splice(highlightedLines.indexOf(lineNumber), 1) highlightedLines.splice(highlightedLines.indexOf(lineNumber), 1)
// Lines that need to be relocated // Lines that need to be relocated
if (lineNumber >= (start + linesRemoved)) { if (lineNumber >= start + linesRemoved) {
newLines.push(lineNumber + offset) newLines.push(lineNumber + offset)
} }
} }
@@ -752,22 +839,30 @@ export default class CodeEditor extends React.Component {
} }
} }
handleHighlight (editor, changeObject) { handleHighlight(editor, changeObject) {
const lines = editor.options.linesHighlighted const lines = editor.options.linesHighlighted
if (!lines.includes(changeObject)) { if (!lines.includes(changeObject)) {
lines.push(changeObject) lines.push(changeObject)
editor.addLineClass(changeObject, 'text', 'CodeMirror-activeline-background') editor.addLineClass(
changeObject,
'text',
'CodeMirror-activeline-background'
)
} else { } else {
lines.splice(lines.indexOf(changeObject), 1) lines.splice(lines.indexOf(changeObject), 1)
editor.removeLineClass(changeObject, 'text', 'CodeMirror-activeline-background') editor.removeLineClass(
changeObject,
'text',
'CodeMirror-activeline-background'
)
} }
if (this.props.onChange) { if (this.props.onChange) {
this.props.onChange(editor) this.props.onChange(editor)
} }
} }
updateHighlight (editor, changeObject) { updateHighlight(editor, changeObject) {
const linesAdded = changeObject.text.length - 1 const linesAdded = changeObject.text.length - 1
const linesRemoved = changeObject.removed.length - 1 const linesRemoved = changeObject.removed.length - 1
@@ -798,28 +893,28 @@ export default class CodeEditor extends React.Component {
this.incrementLines(start, linesAdded, linesRemoved, editor) this.incrementLines(start, linesAdded, linesRemoved, editor)
} }
moveCursorTo (row, col) {} moveCursorTo(row, col) {}
scrollToLine (event, num) { scrollToLine(event, num) {
const cursor = { const cursor = {
line: num, line: num,
ch: 1 ch: 1
} }
this.editor.setCursor(cursor) this.editor.setCursor(cursor)
const top = this.editor.charCoords({line: num, ch: 0}, 'local').top const top = this.editor.charCoords({ line: num, ch: 0 }, 'local').top
const middleHeight = this.editor.getScrollerElement().offsetHeight / 2 const middleHeight = this.editor.getScrollerElement().offsetHeight / 2
this.editor.scrollTo(null, top - middleHeight - 5) this.editor.scrollTo(null, top - middleHeight - 5)
} }
focus () { focus() {
this.editor.focus() this.editor.focus()
} }
blur () { blur() {
this.editor.blur() this.editor.blur()
} }
reload () { reload() {
// Change event shouldn't be fired when switch note // Change event shouldn't be fired when switch note
this.editor.off('change', this.changeHandler) this.editor.off('change', this.changeHandler)
this.value = this.props.value this.value = this.props.value
@@ -830,18 +925,30 @@ export default class CodeEditor extends React.Component {
this.editor.refresh() this.editor.refresh()
} }
setValue (value) { setValue(value) {
const cursor = this.editor.getCursor() const cursor = this.editor.getCursor()
this.editor.setValue(value) this.editor.setValue(value)
this.editor.setCursor(cursor) this.editor.setCursor(cursor)
} }
handleDropImage (dropEvent) { /**
* Update content of one line
* @param {Number} lineNumber
* @param {String} content
*/
setLineContent(lineNumber, content) {
const prevContent = this.editor.getLine(lineNumber)
const prevContentLength = prevContent ? prevContent.length : 0
this.editor.replaceRange(
content,
{ line: lineNumber, ch: 0 },
{ line: lineNumber, ch: prevContentLength }
)
}
handleDropImage(dropEvent) {
dropEvent.preventDefault() dropEvent.preventDefault()
const { const { storageKey, noteKey } = this.props
storageKey,
noteKey
} = this.props
attachmentManagement.handleAttachmentDrop( attachmentManagement.handleAttachmentDrop(
this, this,
storageKey, storageKey,
@@ -850,37 +957,44 @@ export default class CodeEditor extends React.Component {
) )
} }
insertAttachmentMd (imageMd) { insertAttachmentMd(imageMd) {
this.editor.replaceSelection(imageMd) this.editor.replaceSelection(imageMd)
} }
autoDetectLanguage (content) { autoDetectLanguage(content) {
const res = hljs.highlightAuto(content, Object.keys(languageMaps)) const res = hljs.highlightAuto(content, Object.keys(languageMaps))
this.setMode(languageMaps[res.language]) this.setMode(languageMaps[res.language])
} }
handlePaste (editor, forceSmartPaste) { handlePaste(editor, forceSmartPaste) {
const { storageKey, noteKey, fetchUrlTitle, enableSmartPaste } = this.props const { storageKey, noteKey, fetchUrlTitle, enableSmartPaste } = this.props
const isURL = str => /(?:^\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str) const isURL = str =>
/(?:^\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str)
const isInLinkTag = editor => { const isInLinkTag = editor => {
const startCursor = editor.getCursor('start') const startCursor = editor.getCursor('start')
const prevChar = editor.getRange({ const prevChar = editor.getRange(
{
line: startCursor.line, line: startCursor.line,
ch: startCursor.ch - 2 ch: startCursor.ch - 2
}, { },
{
line: startCursor.line, line: startCursor.line,
ch: startCursor.ch ch: startCursor.ch
}) }
)
const endCursor = editor.getCursor('end') const endCursor = editor.getCursor('end')
const nextChar = editor.getRange({ const nextChar = editor.getRange(
{
line: endCursor.line, line: endCursor.line,
ch: endCursor.ch ch: endCursor.ch
}, { },
{
line: endCursor.line, line: endCursor.line,
ch: endCursor.ch + 1 ch: endCursor.ch + 1
}) }
)
return prevChar === '](' && nextChar === ')' return prevChar === '](' && nextChar === ')'
} }
@@ -892,7 +1006,7 @@ export default class CodeEditor extends React.Component {
return true return true
} }
let line = line = cursor.line - 1 let line = (line = cursor.line - 1)
while (line >= 0) { while (line >= 0) {
token = editor.getTokenAt({ token = editor.getTokenAt({
ch: 3, ch: 3,
@@ -924,7 +1038,11 @@ export default class CodeEditor extends React.Component {
if (isInFencedCodeBlock(editor)) { if (isInFencedCodeBlock(editor)) {
this.handlePasteText(editor, pastedTxt) this.handlePasteText(editor, pastedTxt)
} else if (fetchUrlTitle && isMarkdownTitleURL(pastedTxt) && !isInLinkTag(editor)) { } else if (
fetchUrlTitle &&
isMarkdownTitleURL(pastedTxt) &&
!isInLinkTag(editor)
) {
this.handlePasteUrl(editor, pastedTxt) this.handlePasteUrl(editor, pastedTxt)
} else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) { } else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
this.handlePasteUrl(editor, pastedTxt) this.handlePasteUrl(editor, pastedTxt)
@@ -960,13 +1078,13 @@ export default class CodeEditor extends React.Component {
} }
} }
handleScroll (e) { handleScroll(e) {
if (this.props.onScroll) { if (this.props.onScroll) {
this.props.onScroll(e) this.props.onScroll(e)
} }
} }
handlePasteUrl (editor, pastedTxt) { handlePasteUrl(editor, pastedTxt) {
let taggedUrl = `<${pastedTxt}>` let taggedUrl = `<${pastedTxt}>`
let urlToFetch = pastedTxt let urlToFetch = pastedTxt
let titleMark = '' let titleMark = ''
@@ -1016,16 +1134,16 @@ export default class CodeEditor extends React.Component {
}) })
} }
handlePasteHtml (editor, pastedHtml) { handlePasteHtml(editor, pastedHtml) {
const markdown = this.turndownService.turndown(pastedHtml) const markdown = this.turndownService.turndown(pastedHtml)
editor.replaceSelection(markdown) editor.replaceSelection(markdown)
} }
handlePasteText (editor, pastedTxt) { handlePasteText(editor, pastedTxt) {
editor.replaceSelection(pastedTxt) editor.replaceSelection(pastedTxt)
} }
mapNormalResponse (response, pastedTxt) { mapNormalResponse(response, pastedTxt) {
return this.decodeResponse(response).then(body => { return this.decodeResponse(response).then(body => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
@@ -1033,10 +1151,12 @@ export default class CodeEditor extends React.Component {
body, body,
'text/html' 'text/html'
) )
const escapePipe = (str) => { const escapePipe = str => {
return str.replace('|', '\\|') return str.replace('|', '\\|')
} }
const linkWithTitle = `[${escapePipe(parsedBody.title)}](${pastedTxt})` const linkWithTitle = `[${escapePipe(
parsedBody.title
)}](${pastedTxt})`
resolve(linkWithTitle) resolve(linkWithTitle)
} catch (e) { } catch (e) {
reject(e) reject(e)
@@ -1045,7 +1165,7 @@ export default class CodeEditor extends React.Component {
}) })
} }
initialHighlighting () { initialHighlighting() {
if (this.editor.options.linesHighlighted == null) { if (this.editor.options.linesHighlighted == null) {
return return
} }
@@ -1059,16 +1179,20 @@ export default class CodeEditor extends React.Component {
// make sure that we skip the invalid lines althrough this case should not be happened. // make sure that we skip the invalid lines althrough this case should not be happened.
continue continue
} }
this.editor.addLineClass(lineNumber, 'text', 'CodeMirror-activeline-background') this.editor.addLineClass(
lineNumber,
'text',
'CodeMirror-activeline-background'
)
} }
} }
restartHighlighting () { restartHighlighting() {
this.editor.options.linesHighlighted = this.props.linesHighlighted this.editor.options.linesHighlighted = this.props.linesHighlighted
this.initialHighlighting() this.initialHighlighting()
} }
mapImageResponse (response, pastedTxt) { mapImageResponse(response, pastedTxt) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const url = response.url const url = response.url
@@ -1081,7 +1205,7 @@ export default class CodeEditor extends React.Component {
}) })
} }
decodeResponse (response) { decodeResponse(response) {
const headers = response.headers const headers = response.headers
const _charset = headers.has('content-type') const _charset = headers.has('content-type')
? this.extractContentTypeCharset(headers.get('content-type')) ? this.extractContentTypeCharset(headers.get('content-type'))
@@ -1089,8 +1213,8 @@ export default class CodeEditor extends React.Component {
return response.arrayBuffer().then(buff => { return response.arrayBuffer().then(buff => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
const charset = _charset !== undefined && const charset =
iconv.encodingExists(_charset) _charset !== undefined && iconv.encodingExists(_charset)
? _charset ? _charset
: 'utf-8' : 'utf-8'
resolve(iconv.decode(Buffer.from(buff), charset).toString()) resolve(iconv.decode(Buffer.from(buff), charset).toString())
@@ -1101,50 +1225,50 @@ export default class CodeEditor extends React.Component {
}) })
} }
extractContentTypeCharset (contentType) { extractContentTypeCharset(contentType) {
return contentType return contentType
.split(';') .split(';')
.filter(str => { .filter(str => {
return str.trim().toLowerCase().startsWith('charset') return str
.trim()
.toLowerCase()
.startsWith('charset')
}) })
.map(str => { .map(str => {
return str.replace(/['"]/g, '').split('=')[1] return str.replace(/['"]/g, '').split('=')[1]
})[0] })[0]
} }
render () { render() {
const { const { className, fontSize, fontFamily, width, height } = this.props
className, const normalisedFontFamily = normalizeEditorFontFamily(fontFamily)
fontSize
} = this.props return (
const fontFamily = normalizeEditorFontFamily(this.props.fontFamily) <div
const width = this.props.width className={className == null ? 'CodeEditor' : `CodeEditor ${className}`}
return (<
div className={
className == null ? 'CodeEditor' : `CodeEditor ${className}`
}
ref='root' ref='root'
tabIndex='-1' tabIndex='-1'
style={{ style={{
fontFamily, fontFamily: normalisedFontFamily,
fontSize: fontSize, fontSize,
width: width width,
height
}} }}
onDrop={ onDrop={e => this.handleDropImage(e)}
e => this.handleDropImage(e)
}
/> />
) )
} }
createSpellCheckPanel () { createSpellCheckPanel() {
const panel = document.createElement('div') const panel = document.createElement('div')
panel.className = 'panel bottom' panel.className = 'panel bottom'
panel.id = 'editor-bottom-panel' panel.id = 'editor-bottom-panel'
const dropdown = document.createElement('select') const dropdown = document.createElement('select')
dropdown.title = 'Spellcheck' dropdown.title = 'Spellcheck'
dropdown.className = styles['spellcheck-select'] dropdown.className = styles['spellcheck-select']
dropdown.addEventListener('change', (e) => spellcheck.setLanguage(this.editor, dropdown.value)) dropdown.addEventListener('change', e =>
spellcheck.setLanguage(this.editor, dropdown.value)
)
const options = spellcheck.getAvailableDictionaries() const options = spellcheck.getAvailableDictionaries()
for (const op of options) { for (const op of options) {
const option = document.createElement('option') const option = document.createElement('option')
@@ -1169,7 +1293,9 @@ CodeEditor.propTypes = {
autoDetect: PropTypes.bool, autoDetect: PropTypes.bool,
spellCheck: PropTypes.bool, spellCheck: PropTypes.bool,
enableMarkdownLint: PropTypes.bool, enableMarkdownLint: PropTypes.bool,
customMarkdownLintConfig: PropTypes.string customMarkdownLintConfig: PropTypes.string,
deleteUnusedAttachments: PropTypes.bool,
RTL: PropTypes.bool
} }
CodeEditor.defaultProps = { CodeEditor.defaultProps = {
@@ -1183,5 +1309,8 @@ CodeEditor.defaultProps = {
autoDetect: false, autoDetect: false,
spellCheck: false, spellCheck: false,
enableMarkdownLint: DEFAULT_CONFIG.editor.enableMarkdownLint, enableMarkdownLint: DEFAULT_CONFIG.editor.enableMarkdownLint,
customMarkdownLintConfig: DEFAULT_CONFIG.editor.customMarkdownLintConfig customMarkdownLintConfig: DEFAULT_CONFIG.editor.customMarkdownLintConfig,
prettierConfig: DEFAULT_CONFIG.editor.prettierConfig,
deleteUnusedAttachments: DEFAULT_CONFIG.editor.deleteUnusedAttachments,
RTL: false
} }

View File

@@ -7,7 +7,7 @@ import styles from './ColorPicker.styl'
const componentHeight = 330 const componentHeight = 330
class ColorPicker extends React.Component { class ColorPicker extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
@@ -18,21 +18,21 @@ class ColorPicker extends React.Component {
this.handleConfirm = this.handleConfirm.bind(this) this.handleConfirm = this.handleConfirm.bind(this)
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps(nextProps) {
this.onColorChange(nextProps.color) this.onColorChange(nextProps.color)
} }
onColorChange (color) { onColorChange(color) {
this.setState({ this.setState({
color color
}) })
} }
handleConfirm () { handleConfirm() {
this.props.onConfirm(this.state.color) this.props.onConfirm(this.state.color)
} }
render () { render() {
const { onReset, onCancel, targetRect } = this.props const { onReset, onCancel, targetRect } = this.props
const { color } = this.state const { color } = this.state
@@ -44,13 +44,22 @@ class ColorPicker extends React.Component {
} }
return ( return (
<div styleName='colorPicker' style={{top: `${alignY}px`, left: `${alignX}px`}}> <div
styleName='colorPicker'
style={{ top: `${alignY}px`, left: `${alignX}px` }}
>
<div styleName='cover' onClick={onCancel} /> <div styleName='cover' onClick={onCancel} />
<SketchPicker color={color} onChange={this.onColorChange} /> <SketchPicker color={color} onChange={this.onColorChange} />
<div styleName='footer'> <div styleName='footer'>
<button styleName='btn-reset' onClick={onReset}>Reset</button> <button styleName='btn-reset' onClick={onReset}>
<button styleName='btn-cancel' onClick={onCancel}>Cancel</button> Reset
<button styleName='btn-confirm' onClick={this.handleConfirm}>Confirm</button> </button>
<button styleName='btn-cancel' onClick={onCancel}>
Cancel
</button>
<button styleName='btn-confirm' onClick={this.handleConfirm}>
Confirm
</button>
</div> </div>
</div> </div>
) )

View File

@@ -1,3 +1,4 @@
/* eslint-disable camelcase */
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
@@ -10,7 +11,7 @@ import ConfigManager from 'browser/main/lib/ConfigManager'
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
class MarkdownEditor extends React.Component { class MarkdownEditor extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props)
// char codes for ctrl + w // char codes for ctrl + w
@@ -20,142 +21,171 @@ class MarkdownEditor extends React.Component {
this.supportMdSelectionBold = [16, 17, 186] this.supportMdSelectionBold = [16, 17, 186]
this.state = { this.state = {
status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'CODE', status:
props.config.editor.switchPreview === 'RIGHTCLICK'
? props.config.editor.delfaultStatus
: 'CODE',
renderValue: props.value, renderValue: props.value,
keyPressed: new Set(), keyPressed: new Set(),
isLocked: props.isLocked isLocked: props.isLocked
} }
this.lockEditorCode = () => this.handleLockEditor() this.lockEditorCode = this.handleLockEditor.bind(this)
this.focusEditor = this.focusEditor.bind(this)
this.previewRef = React.createRef()
} }
componentDidMount () { componentDidMount() {
this.value = this.refs.code.value this.value = this.refs.code.value
eventEmitter.on('editor:lock', this.lockEditorCode) eventEmitter.on('editor:lock', this.lockEditorCode)
eventEmitter.on('editor:focus', this.focusEditor.bind(this)) eventEmitter.on('editor:focus', this.focusEditor)
} }
componentDidUpdate () { componentDidUpdate() {
this.value = this.refs.code.value this.value = this.refs.code.value
} }
componentWillReceiveProps (props) { UNSAFE_componentWillReceiveProps(props) {
if (props.value !== this.props.value) { if (props.value !== this.props.value) {
this.queueRendering(props.value) this.queueRendering(props.value)
} }
} }
componentWillUnmount () { componentWillUnmount() {
this.cancelQueue() this.cancelQueue()
eventEmitter.off('editor:lock', this.lockEditorCode) eventEmitter.off('editor:lock', this.lockEditorCode)
eventEmitter.off('editor:focus', this.focusEditor.bind(this)) eventEmitter.off('editor:focus', this.focusEditor)
} }
focusEditor () { focusEditor() {
this.setState({ this.setState(
{
status: 'CODE' status: 'CODE'
}, () => { },
() => {
if (this.refs.code == null) {
return
}
this.refs.code.focus() this.refs.code.focus()
}) }
)
} }
queueRendering (value) { queueRendering(value) {
clearTimeout(this.renderTimer) clearTimeout(this.renderTimer)
this.renderTimer = setTimeout(() => { this.renderTimer = setTimeout(() => {
this.renderPreview(value) this.renderPreview(value)
}, 500) }, 500)
} }
cancelQueue () { cancelQueue() {
clearTimeout(this.renderTimer) clearTimeout(this.renderTimer)
} }
renderPreview (value) { renderPreview(value) {
this.setState({ this.setState({
renderValue: value renderValue: value
}) })
} }
setValue (value) { setValue(value) {
this.refs.code.setValue(value) this.refs.code.setValue(value)
} }
handleChange (e) { handleChange(e) {
this.value = this.refs.code.value this.value = this.refs.code.value
this.props.onChange(e) this.props.onChange(e)
} }
handleContextMenu (e) { handleContextMenu(e) {
if (this.state.isLocked) return if (this.state.isLocked) return
const { config } = this.props const { config } = this.props
if (config.editor.switchPreview === 'RIGHTCLICK') { if (config.editor.switchPreview === 'RIGHTCLICK') {
const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW' const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW'
this.setState({ this.setState(
{
status: newStatus status: newStatus
}, () => { },
() => {
if (newStatus === 'CODE') { if (newStatus === 'CODE') {
this.refs.code.focus() this.refs.code.focus()
} else { } else {
this.refs.preview.focus() this.previewRef.current.focus()
} }
eventEmitter.emit('topbar:togglelockbutton', this.state.status) eventEmitter.emit('topbar:togglelockbutton', this.state.status)
const newConfig = Object.assign({}, config) const newConfig = Object.assign({}, config)
newConfig.editor.delfaultStatus = newStatus newConfig.editor.delfaultStatus = newStatus
ConfigManager.set(newConfig) ConfigManager.set(newConfig)
}) }
)
} }
} }
handleBlur (e) { handleBlur(e) {
if (this.state.isLocked) return if (this.state.isLocked) return
this.setState({ keyPressed: new Set() }) this.setState({ keyPressed: new Set() })
const { config } = this.props const { config } = this.props
if (config.editor.switchPreview === 'BLUR' || if (
(config.editor.switchPreview === 'DBL_CLICK' && this.state.status === 'CODE') config.editor.switchPreview === 'BLUR' ||
(config.editor.switchPreview === 'DBL_CLICK' &&
this.state.status === 'CODE')
) { ) {
const cursorPosition = this.refs.code.editor.getCursor() const cursorPosition = this.refs.code.editor.getCursor()
this.setState({ this.setState(
{
status: 'PREVIEW' status: 'PREVIEW'
}, () => { },
this.refs.preview.focus() () => {
this.refs.preview.scrollTo(cursorPosition.line) this.previewRef.current.focus()
}) this.previewRef.current.scrollToRow(cursorPosition.line)
}
)
eventEmitter.emit('topbar:togglelockbutton', this.state.status) eventEmitter.emit('topbar:togglelockbutton', this.state.status)
} }
} }
handleDoubleClick (e) { handleDoubleClick(e) {
if (this.state.isLocked) return if (this.state.isLocked) return
this.setState({keyPressed: new Set()}) this.setState({ keyPressed: new Set() })
const { config } = this.props const { config } = this.props
if (config.editor.switchPreview === 'DBL_CLICK') { if (config.editor.switchPreview === 'DBL_CLICK') {
this.setState({ this.setState(
{
status: 'CODE' status: 'CODE'
}, () => { },
() => {
this.refs.code.focus() this.refs.code.focus()
eventEmitter.emit('topbar:togglelockbutton', this.state.status) eventEmitter.emit('topbar:togglelockbutton', this.state.status)
}) }
)
} }
} }
handlePreviewMouseDown (e) { handlePreviewMouseDown(e) {
this.previewMouseDownedAt = new Date() this.previewMouseDownedAt = new Date()
} }
handlePreviewMouseUp (e) { handlePreviewMouseUp(e) {
const { config } = this.props const { config } = this.props
if (config.editor.switchPreview === 'BLUR' && new Date() - this.previewMouseDownedAt < 200) { if (
this.setState({ config.editor.switchPreview === 'BLUR' &&
new Date() - this.previewMouseDownedAt < 200
) {
this.setState(
{
status: 'CODE' status: 'CODE'
}, () => { },
() => {
this.refs.code.focus() this.refs.code.focus()
}) }
)
eventEmitter.emit('topbar:togglelockbutton', this.state.status) eventEmitter.emit('topbar:togglelockbutton', this.state.status)
} }
} }
handleCheckboxClick (e) { handleCheckboxClick(e) {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
const idMatch = /checkbox-([0-9]+)/ const idMatch = /checkbox-([0-9]+)/
@@ -164,61 +194,73 @@ class MarkdownEditor extends React.Component {
const checkReplace = /\[x]/i const checkReplace = /\[x]/i
const uncheckReplace = /\[ ]/ const uncheckReplace = /\[ ]/
if (idMatch.test(e.target.getAttribute('id'))) { if (idMatch.test(e.target.getAttribute('id'))) {
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1 const lineIndex =
const lines = this.refs.code.value parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
.split('\n') const lines = this.refs.code.value.split('\n')
const targetLine = lines[lineIndex] const targetLine = lines[lineIndex]
let newLine = targetLine
if (targetLine.match(checkedMatch)) { if (targetLine.match(checkedMatch)) {
lines[lineIndex] = targetLine.replace(checkReplace, '[ ]') newLine = targetLine.replace(checkReplace, '[ ]')
} }
if (targetLine.match(uncheckedMatch)) { if (targetLine.match(uncheckedMatch)) {
lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]') newLine = targetLine.replace(uncheckReplace, '[x]')
} }
this.refs.code.setValue(lines.join('\n')) this.refs.code.setLineContent(lineIndex, newLine)
} }
} }
focus () { focus() {
if (this.state.status === 'PREVIEW') { if (this.state.status === 'PREVIEW') {
this.setState({ this.setState(
{
status: 'CODE' status: 'CODE'
}, () => { },
() => {
this.refs.code.focus() this.refs.code.focus()
}) }
)
} else { } else {
this.refs.code.focus() this.refs.code.focus()
} }
eventEmitter.emit('topbar:togglelockbutton', this.state.status) eventEmitter.emit('topbar:togglelockbutton', this.state.status)
} }
reload () { reload() {
this.refs.code.reload() this.refs.code.reload()
this.cancelQueue() this.cancelQueue()
this.renderPreview(this.props.value) this.renderPreview(this.props.value)
} }
handleKeyDown (e) { handleKeyDown(e) {
const { config } = this.props const { config } = this.props
if (this.state.status !== 'CODE') return false if (this.state.status !== 'CODE') return false
const keyPressed = this.state.keyPressed const keyPressed = this.state.keyPressed
keyPressed.add(e.keyCode) keyPressed.add(e.keyCode)
this.setState({ keyPressed }) this.setState({ keyPressed })
const isNoteHandlerKey = (el) => { return keyPressed.has(el) } const isNoteHandlerKey = el => {
return keyPressed.has(el)
}
// These conditions are for ctrl-e and ctrl-w // These conditions are for ctrl-e and ctrl-w
if (keyPressed.size === this.escapeFromEditor.length && if (
!this.state.isLocked && this.state.status === 'CODE' && keyPressed.size === this.escapeFromEditor.length &&
this.escapeFromEditor.every(isNoteHandlerKey)) { !this.state.isLocked &&
this.state.status === 'CODE' &&
this.escapeFromEditor.every(isNoteHandlerKey)
) {
this.handleContextMenu() this.handleContextMenu()
if (config.editor.switchPreview === 'BLUR') document.activeElement.blur() if (config.editor.switchPreview === 'BLUR') document.activeElement.blur()
} }
if (keyPressed.size === this.supportMdSelectionBold.length && this.supportMdSelectionBold.every(isNoteHandlerKey)) { if (
keyPressed.size === this.supportMdSelectionBold.length &&
this.supportMdSelectionBold.every(isNoteHandlerKey)
) {
this.addMdAroundWord('**') this.addMdAroundWord('**')
} }
} }
addMdAroundWord (mdElement) { addMdAroundWord(mdElement) {
if (this.refs.code.editor.getSelection()) { if (this.refs.code.editor.getSelection()) {
return this.addMdAroundSelection(mdElement) return this.addMdAroundSelection(mdElement)
} }
@@ -226,20 +268,27 @@ class MarkdownEditor extends React.Component {
const word = this.refs.code.editor.findWordAt(currentCaret) const word = this.refs.code.editor.findWordAt(currentCaret)
const cmDoc = this.refs.code.editor.getDoc() const cmDoc = this.refs.code.editor.getDoc()
cmDoc.replaceRange(mdElement, word.anchor) cmDoc.replaceRange(mdElement, word.anchor)
cmDoc.replaceRange(mdElement, { line: word.head.line, ch: word.head.ch + mdElement.length }) cmDoc.replaceRange(mdElement, {
line: word.head.line,
ch: word.head.ch + mdElement.length
})
} }
addMdAroundSelection (mdElement) { addMdAroundSelection(mdElement) {
this.refs.code.editor.replaceSelection(`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`) this.refs.code.editor.replaceSelection(
`${mdElement}${this.refs.code.editor.getSelection()}${mdElement}`
)
} }
handleDropImage (dropEvent) { handleDropImage(dropEvent) {
dropEvent.preventDefault() dropEvent.preventDefault()
const { storageKey, noteKey } = this.props const { storageKey, noteKey } = this.props
this.setState({ this.setState(
{
status: 'CODE' status: 'CODE'
}, () => { },
() => {
this.refs.code.focus() this.refs.code.focus()
this.refs.code.editor.execCommand('goDocEnd') this.refs.code.editor.execCommand('goDocEnd')
@@ -252,21 +301,30 @@ class MarkdownEditor extends React.Component {
noteKey, noteKey,
dropEvent dropEvent
) )
}) }
)
} }
handleKeyUp (e) { handleKeyUp(e) {
const keyPressed = this.state.keyPressed const keyPressed = this.state.keyPressed
keyPressed.delete(e.keyCode) keyPressed.delete(e.keyCode)
this.setState({ keyPressed }) this.setState({ keyPressed })
} }
handleLockEditor () { handleLockEditor() {
this.setState({ isLocked: !this.state.isLocked }) this.setState({ isLocked: !this.state.isLocked })
} }
render () { render() {
const {className, value, config, storageKey, noteKey, linesHighlighted} = this.props const {
className,
value,
config,
storageKey,
noteKey,
linesHighlighted,
RTL
} = this.props
let editorFontSize = parseInt(config.editor.fontSize, 10) let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
@@ -274,23 +332,24 @@ class MarkdownEditor extends React.Component {
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4 if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
const previewStyle = {} const previewStyle = {}
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none' if (this.props.ignorePreviewPointerEvents)
previewStyle.pointerEvents = 'none'
const storage = findStorage(storageKey) const storage = findStorage(storageKey)
return ( return (
<div className={className == null <div
? 'MarkdownEditor' className={
: `MarkdownEditor ${className}` className == null ? 'MarkdownEditor' : `MarkdownEditor ${className}`
} }
onContextMenu={(e) => this.handleContextMenu(e)} onContextMenu={e => this.handleContextMenu(e)}
tabIndex='-1' tabIndex='-1'
onKeyDown={(e) => this.handleKeyDown(e)} onKeyDown={e => this.handleKeyDown(e)}
onKeyUp={(e) => this.handleKeyUp(e)} onKeyUp={e => this.handleKeyUp(e)}
> >
<CodeEditor styleName={this.state.status === 'CODE' <CodeEditor
? 'codeEditor' styleName={
: 'codeEditor--hide' this.state.status === 'CODE' ? 'codeEditor' : 'codeEditor--hide'
} }
ref='code' ref='code'
mode='Boost Flavored Markdown' mode='Boost Flavored Markdown'
@@ -314,18 +373,22 @@ class MarkdownEditor extends React.Component {
fetchUrlTitle={config.editor.fetchUrlTitle} fetchUrlTitle={config.editor.fetchUrlTitle}
enableTableEditor={config.editor.enableTableEditor} enableTableEditor={config.editor.enableTableEditor}
linesHighlighted={linesHighlighted} linesHighlighted={linesHighlighted}
onChange={(e) => this.handleChange(e)} onChange={e => this.handleChange(e)}
onBlur={(e) => this.handleBlur(e)} onBlur={e => this.handleBlur(e)}
spellCheck={config.editor.spellcheck} spellCheck={config.editor.spellcheck}
enableSmartPaste={config.editor.enableSmartPaste} enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey} hotkey={config.hotkey}
switchPreview={config.editor.switchPreview} switchPreview={config.editor.switchPreview}
enableMarkdownLint={config.editor.enableMarkdownLint} enableMarkdownLint={config.editor.enableMarkdownLint}
customMarkdownLintConfig={config.editor.customMarkdownLintConfig} customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
prettierConfig={config.editor.prettierConfig}
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
RTL={RTL}
/> />
<MarkdownPreview styleName={this.state.status === 'PREVIEW' <MarkdownPreview
? 'preview' ref={this.previewRef}
: 'preview--hide' styleName={
this.state.status === 'PREVIEW' ? 'preview' : 'preview--hide'
} }
style={previewStyle} style={previewStyle}
theme={config.ui.theme} theme={config.ui.theme}
@@ -341,21 +404,22 @@ class MarkdownEditor extends React.Component {
smartArrows={config.preview.smartArrows} smartArrows={config.preview.smartArrows}
breaks={config.preview.breaks} breaks={config.preview.breaks}
sanitize={config.preview.sanitize} sanitize={config.preview.sanitize}
ref='preview' mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
onContextMenu={(e) => this.handleContextMenu(e)} onContextMenu={e => this.handleContextMenu(e)}
onDoubleClick={(e) => this.handleDoubleClick(e)} onDoubleClick={e => this.handleDoubleClick(e)}
tabIndex='0' tabIndex='0'
value={this.state.renderValue} value={this.state.renderValue}
onMouseUp={(e) => this.handlePreviewMouseUp(e)} onMouseUp={e => this.handlePreviewMouseUp(e)}
onMouseDown={(e) => this.handlePreviewMouseDown(e)} onMouseDown={e => this.handlePreviewMouseDown(e)}
onCheckboxClick={(e) => this.handleCheckboxClick(e)} onCheckboxClick={e => this.handleCheckboxClick(e)}
showCopyNotification={config.ui.showCopyNotification} showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path} storagePath={storage.path}
noteKey={noteKey} noteKey={noteKey}
customCSS={config.preview.customCSS} customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS} allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox} lineThroughCheckbox={config.preview.lineThroughCheckbox}
onDrop={(e) => this.handleDropImage(e)} onDrop={e => this.handleDropImage(e)}
RTL={RTL}
/> />
</div> </div>
) )

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import { connect } from 'react-redux'
import Markdown from 'browser/lib/markdown' import Markdown from 'browser/lib/markdown'
import _ from 'lodash' import _ from 'lodash'
import CodeMirror from 'codemirror' import CodeMirror from 'codemirror'
@@ -11,6 +12,7 @@ import mermaidRender from './render/MermaidRender'
import SequenceDiagram from '@rokt33r/js-sequence-diagrams' import SequenceDiagram from '@rokt33r/js-sequence-diagrams'
import Chart from 'chart.js' import Chart from 'chart.js'
import eventEmitter from 'browser/main/lib/eventEmitter' import eventEmitter from 'browser/main/lib/eventEmitter'
import config from 'browser/main/lib/ConfigManager'
import htmlTextHelper from 'browser/lib/htmlTextHelper' import htmlTextHelper from 'browser/lib/htmlTextHelper'
import convertModeName from 'browser/lib/convertModeName' import convertModeName from 'browser/lib/convertModeName'
import copy from 'copy-to-clipboard' import copy from 'copy-to-clipboard'
@@ -20,11 +22,15 @@ import { escapeHtmlCharacters } from 'browser/lib/utils'
import yaml from 'js-yaml' import yaml from 'js-yaml'
import { render } from 'react-dom' import { render } from 'react-dom'
import Carousel from 'react-image-carousel' import Carousel from 'react-image-carousel'
import { push } from 'connected-react-router'
import ConfigManager from '../main/lib/ConfigManager' import ConfigManager from '../main/lib/ConfigManager'
import uiThemes from 'browser/lib/ui-themes'
import i18n from 'browser/lib/i18n'
const { remote, shell } = require('electron') const { remote, shell } = require('electron')
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement') const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
const buildMarkdownPreviewContextMenu = require('browser/lib/contextMenuBuilder').buildMarkdownPreviewContextMenu const buildMarkdownPreviewContextMenu = require('browser/lib/contextMenuBuilder')
.buildMarkdownPreviewContextMenu
const { app } = remote const { app } = remote
const path = require('path') const path = require('path')
@@ -41,9 +47,21 @@ const CSS_FILES = [
`${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` `${appPath}/node_modules/react-image-carousel/lib/css/main.min.css`
] ]
const win = global.process.platform === 'win32'
function buildStyle ( /**
* @param {Object} opts
* @param {String} opts.fontFamily
* @param {Numberl} opts.fontSize
* @param {String} opts.codeBlockFontFamily
* @param {String} opts.theme
* @param {Boolean} [opts.lineNumber] Should show line number
* @param {Boolean} [opts.scrollPastEnd]
* @param {Boolean} [opts.allowCustomCSS] Should add custom css
* @param {String} [opts.customCSS] Will be added to bottom, only if `opts.allowCustomCSS` is truthy
* @returns {String}
*/
function buildStyle(opts) {
const {
fontFamily, fontFamily,
fontSize, fontSize,
codeBlockFontFamily, codeBlockFontFamily,
@@ -51,8 +69,9 @@ function buildStyle (
scrollPastEnd, scrollPastEnd,
theme, theme,
allowCustomCSS, allowCustomCSS,
customCSS customCSS,
) { RTL
} = opts
return ` return `
@font-face { @font-face {
font-family: 'Lato'; font-family: 'Lato';
@@ -82,12 +101,23 @@ function buildStyle (
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'), url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype'); url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
} }
${markdownStyle} ${markdownStyle}
body { body {
font-family: '${fontFamily.join("','")}'; font-family: '${fontFamily.join("','")}';
font-size: ${fontSize}px; font-size: ${fontSize}px;
${scrollPastEnd && 'padding-bottom: 90vh;'}
${
scrollPastEnd
? `
padding-bottom: 90vh;
box-sizing: border-box;
`
: ''
}
${RTL ? 'direction: rtl;' : ''}
${RTL ? 'text-align: right;' : ''}
} }
@media print { @media print {
body { body {
@@ -97,7 +127,84 @@ body {
code { code {
font-family: '${codeBlockFontFamily.join("','")}'; font-family: '${codeBlockFontFamily.join("','")}';
background-color: rgba(0,0,0,0.04); background-color: rgba(0,0,0,0.04);
text-align: left;
direction: ltr;
} }
p code,
li code,
td code
{
padding: 2px;
border-width: 1px;
border-style: solid;
border-radius: 5px;
}
[data-theme="default"] p code,
[data-theme="default"] li code,
[data-theme="default"] td code
{
background-color: #F4F4F4;
border-color: #d9d9d9;
color: inherit;
}
[data-theme="white"] p code,
[data-theme="white"] li code,
[data-theme="white"] td code
{
background-color: #F4F4F4;
border-color: #d9d9d9;
color: inherit;
}
[data-theme="dark"] p code,
[data-theme="dark"] li code,
[data-theme="dark"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="dracula"] p code,
[data-theme="dracula"] li code,
[data-theme="dracula"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="monokai"] p code,
[data-theme="monokai"] li code,
[data-theme="monokai"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="nord"] p code,
[data-theme="nord"] li code,
[data-theme="nord"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="solarized-dark"] p code,
[data-theme="solarized-dark"] li code,
[data-theme="solarized-dark"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
[data-theme="vulcan"] p code,
[data-theme="vulcan"] li code,
[data-theme="vulcan"] td code
{
background-color: #444444;
border-color: #555;
color: #FFFFFF;
}
.lineNumber { .lineNumber {
${lineNumber && 'display: block !important;'} ${lineNumber && 'display: block !important;'}
font-family: '${codeBlockFontFamily.join("','")}'; font-family: '${codeBlockFontFamily.join("','")}';
@@ -127,14 +234,22 @@ h1, h2 {
border: none; border: none;
} }
h3 {
margin: 1em 0 0.8em;
}
h4, h5, h6 {
margin: 1.1em 0 0.5em;
}
h1 { h1 {
padding-bottom: 4px; padding: 0.2em 0 0.2em;
margin: 1em 0 8px; margin: 1em 0 8px;
} }
h2 { h2 {
padding-bottom: 0.2em; padding: 0.2em 0 0.2em;
margin: 1em 0 0.37em; margin: 1em 0 0.7em;
} }
body p { body p {
@@ -157,21 +272,33 @@ ${allowCustomCSS ? customCSS : ''}
const scrollBarStyle = ` const scrollBarStyle = `
::-webkit-scrollbar { ::-webkit-scrollbar {
${config.get().ui.showScrollBar ? '' : 'display: none;'}
width: 12px; width: 12px;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
${config.get().ui.showScrollBar ? '' : 'display: none;'}
background-color: rgba(0, 0, 0, 0.15); background-color: rgba(0, 0, 0, 0.15);
} }
::-webkit-scrollbar-track-piece {
background-color: inherit;
}
` `
const scrollBarDarkStyle = ` const scrollBarDarkStyle = `
::-webkit-scrollbar { ::-webkit-scrollbar {
${config.get().ui.showScrollBar ? '' : 'display: none;'}
width: 12px; width: 12px;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
${config.get().ui.showScrollBar ? '' : 'display: none;'}
background-color: rgba(0, 0, 0, 0.3); background-color: rgba(0, 0, 0, 0.3);
} }
::-webkit-scrollbar-track-piece {
background-color: inherit;
}
` `
const OSX = global.process.platform === 'darwin' const OSX = global.process.platform === 'darwin'
@@ -192,7 +319,7 @@ const defaultCodeBlockFontFamily = [
// return the line number of the line that used to generate the specified element // return the line number of the line that used to generate the specified element
// return -1 if the line is not found // return -1 if the line is not found
function getSourceLineNumberByElement (element) { function getSourceLineNumberByElement(element) {
let isHasLineNumber = element.dataset.line !== undefined let isHasLineNumber = element.dataset.line !== undefined
let parent = element let parent = element
while (!isHasLineNumber && parent.parentElement !== null) { while (!isHasLineNumber && parent.parentElement !== null) {
@@ -202,8 +329,8 @@ function getSourceLineNumberByElement (element) {
return parent.dataset.line !== undefined ? parseInt(parent.dataset.line) : -1 return parent.dataset.line !== undefined ? parseInt(parent.dataset.line) : -1
} }
export default class MarkdownPreview extends React.Component { class MarkdownPreview extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props)
this.contextMenuHandler = e => this.handleContextMenu(e) this.contextMenuHandler = e => this.handleContextMenu(e)
@@ -220,13 +347,14 @@ export default class MarkdownPreview extends React.Component {
this.saveAsHtmlHandler = () => this.handleSaveAsHtml() this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
this.saveAsPdfHandler = () => this.handleSaveAsPdf() this.saveAsPdfHandler = () => this.handleSaveAsPdf()
this.printHandler = () => this.handlePrint() this.printHandler = () => this.handlePrint()
this.resizeHandler = _.throttle(this.handleResize.bind(this), 100)
this.linkClickHandler = this.handleLinkClick.bind(this) this.linkClickHandler = this.handleLinkClick.bind(this)
this.initMarkdown = this.initMarkdown.bind(this) this.initMarkdown = this.initMarkdown.bind(this)
this.initMarkdown() this.initMarkdown()
} }
initMarkdown () { initMarkdown() {
const { smartQuotes, sanitize, breaks } = this.props const { smartQuotes, sanitize, breaks } = this.props
this.markdown = new Markdown({ this.markdown = new Markdown({
typographer: smartQuotes, typographer: smartQuotes,
@@ -235,34 +363,41 @@ export default class MarkdownPreview extends React.Component {
}) })
} }
handleCheckboxClick (e) { handleCheckboxClick(e) {
this.props.onCheckboxClick(e) this.props.onCheckboxClick(e)
} }
handleScroll (e) { handleScroll(e) {
if (this.props.onScroll) { if (this.props.onScroll) {
this.props.onScroll(e) this.props.onScroll(e)
} }
} }
handleContextMenu (event) { handleContextMenu(event) {
const menu = buildMarkdownPreviewContextMenu(this, event) const menu = buildMarkdownPreviewContextMenu(this, event)
if (menu != null) { const switchPreview = ConfigManager.get().editor.switchPreview
if (menu != null && switchPreview !== 'RIGHTCLICK') {
menu.popup(remote.getCurrentWindow()) menu.popup(remote.getCurrentWindow())
} else if (_.isFunction(this.props.onContextMenu)) {
this.props.onContextMenu(event)
} }
} }
handleDoubleClick (e) { handleDoubleClick(e) {
if (this.props.onDoubleClick != null) this.props.onDoubleClick(e) if (this.props.onDoubleClick != null) this.props.onDoubleClick(e)
} }
handleMouseDown (e) { handleMouseDown(e) {
const config = ConfigManager.get() const config = ConfigManager.get()
const clickElement = e.target const clickElement = e.target
const targetTag = clickElement.tagName // The direct parent HTML of where was clicked ie "BODY" or "DIV" 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. const lineNumber = getSourceLineNumberByElement(clickElement) // Line location of element clicked.
if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') { if (
config.editor.switchPreview === 'RIGHTCLICK' &&
e.buttons === 2 &&
config.editor.type === 'SPLIT'
) {
eventEmitter.emit('topbar:togglemodebutton', 'CODE') eventEmitter.emit('topbar:togglemodebutton', 'CODE')
} }
if (e.ctrlKey) { if (e.ctrlKey) {
@@ -278,10 +413,11 @@ export default class MarkdownPreview extends React.Component {
} }
} }
if (this.props.onMouseDown != null && targetTag === 'BODY') this.props.onMouseDown(e) if (this.props.onMouseDown != null && targetTag === 'BODY')
this.props.onMouseDown(e)
} }
handleMouseUp (e) { handleMouseUp(e) {
if (!this.props.onMouseUp) return if (!this.props.onMouseUp) return
if (e.target != null && e.target.tagName === 'A') { if (e.target != null && e.target.tagName === 'A') {
return null return null
@@ -289,15 +425,15 @@ export default class MarkdownPreview extends React.Component {
if (this.props.onMouseUp != null) this.props.onMouseUp(e) if (this.props.onMouseUp != null) this.props.onMouseUp(e)
} }
handleSaveAsText () { handleSaveAsText() {
this.exportAsDocument('txt') this.exportAsDocument('txt')
} }
handleSaveAsMd () { handleSaveAsMd() {
this.exportAsDocument('md') this.exportAsDocument('md')
} }
htmlContentFormatter (noteContent, exportTasks, targetDir) { htmlContentFormatter(noteContent, exportTasks, targetDir) {
const { const {
fontFamily, fontFamily,
fontSize, fontSize,
@@ -307,10 +443,11 @@ export default class MarkdownPreview extends React.Component {
scrollPastEnd, scrollPastEnd,
theme, theme,
allowCustomCSS, allowCustomCSS,
customCSS customCSS,
RTL
} = this.getStyleParams() } = this.getStyleParams()
const inlineStyles = buildStyle( const inlineStyles = buildStyle({
fontFamily, fontFamily,
fontSize, fontSize,
codeBlockFontFamily, codeBlockFontFamily,
@@ -318,14 +455,12 @@ export default class MarkdownPreview extends React.Component {
scrollPastEnd, scrollPastEnd,
theme, theme,
allowCustomCSS, allowCustomCSS,
customCSS customCSS,
) RTL
let body = this.markdown.render(noteContent) })
body = attachmentManagement.fixLocalURLS( let body = this.refs.root.contentWindow.document.body.innerHTML
body, body = attachmentManagement.fixLocalURLS(body, this.props.storagePath)
this.props.storagePath const files = [this.getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
)
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
files.forEach(file => { files.forEach(file => {
if (global.process.platform === 'win32') { if (global.process.platform === 'win32') {
file = file.replace('file:///', '') file = file.replace('file:///', '')
@@ -340,7 +475,7 @@ export default class MarkdownPreview extends React.Component {
let styles = '' let styles = ''
files.forEach(file => { files.forEach(file => {
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">` styles += `<link rel="stylesheet" href="../css/${path.basename(file)}">`
}) })
return `<html> return `<html>
@@ -355,14 +490,24 @@ export default class MarkdownPreview extends React.Component {
</html>` </html>`
} }
handleSaveAsHtml () { handleSaveAsHtml() {
this.exportAsDocument('html', (noteContent, exportTasks, targetDir) => Promise.resolve(this.htmlContentFormatter(noteContent, exportTasks, targetDir))) this.exportAsDocument('html', (noteContent, exportTasks, targetDir) =>
Promise.resolve(
this.htmlContentFormatter(noteContent, exportTasks, targetDir)
)
)
} }
handleSaveAsPdf () { handleSaveAsPdf() {
this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => { this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => {
const printout = new remote.BrowserWindow({show: false, webPreferences: {webSecurity: false}}) const printout = new remote.BrowserWindow({
printout.loadURL('data:text/html;charset=UTF-8,' + this.htmlContentFormatter(noteContent, exportTasks, targetDir)) show: false,
webPreferences: { webSecurity: false, javascript: false }
})
printout.loadURL(
'data:text/html;charset=UTF-8,' +
this.htmlContentFormatter(noteContent, exportTasks, targetDir)
)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
printout.webContents.on('did-finish-load', () => { printout.webContents.on('did-finish-load', () => {
printout.webContents.printToPDF({}, (err, data) => { printout.webContents.printToPDF({}, (err, data) => {
@@ -375,11 +520,11 @@ export default class MarkdownPreview extends React.Component {
}) })
} }
handlePrint () { handlePrint() {
this.refs.root.contentWindow.print() this.refs.root.contentWindow.print()
} }
exportAsDocument (fileType, contentFormatter) { exportAsDocument(fileType, contentFormatter) {
const options = { const options = {
filters: [{ name: 'Documents', extensions: [fileType] }], filters: [{ name: 'Documents', extensions: [fileType] }],
properties: ['openFile', 'createDirectory'] properties: ['openFile', 'createDirectory']
@@ -395,7 +540,8 @@ export default class MarkdownPreview extends React.Component {
.then(res => { .then(res => {
dialog.showMessageBox(remote.getCurrentWindow(), { dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info', type: 'info',
message: `Exported to ${filename}` message: `Exported to ${filename}`,
buttons: [i18n.__('Ok')]
}) })
}) })
.catch(err => { .catch(err => {
@@ -409,7 +555,7 @@ export default class MarkdownPreview extends React.Component {
}) })
} }
fixDecodedURI (node) { fixDecodedURI(node) {
if ( if (
node && node &&
node.children.length === 1 && node.children.length === 1 &&
@@ -426,17 +572,18 @@ export default class MarkdownPreview extends React.Component {
* @param {string[]} splitWithCodeTag Array of HTML strings separated by three ``` * @param {string[]} splitWithCodeTag Array of HTML strings separated by three ```
* @returns {string} HTML in which special characters between three ``` have been converted * @returns {string} HTML in which special characters between three ``` have been converted
*/ */
escapeHtmlCharactersInCodeTag (splitWithCodeTag) { escapeHtmlCharactersInCodeTag(splitWithCodeTag) {
for (let index = 0; index < splitWithCodeTag.length; index++) { for (let index = 0; index < splitWithCodeTag.length; index++) {
const codeTagRequired = (splitWithCodeTag[index] !== '\`\`\`' && index < splitWithCodeTag.length - 1) const codeTagRequired =
splitWithCodeTag[index] !== '```' && index < splitWithCodeTag.length - 1
if (codeTagRequired) { if (codeTagRequired) {
splitWithCodeTag.splice((index + 1), 0, '\`\`\`') splitWithCodeTag.splice(index + 1, 0, '```')
} }
} }
let inCodeTag = false let inCodeTag = false
let result = '' let result = ''
for (let content of splitWithCodeTag) { for (let content of splitWithCodeTag) {
if (content === '\`\`\`') { if (content === '```') {
inCodeTag = !inCodeTag inCodeTag = !inCodeTag
} else if (inCodeTag) { } else if (inCodeTag) {
content = escapeHtmlCharacters(content) content = escapeHtmlCharacters(content)
@@ -446,21 +593,15 @@ export default class MarkdownPreview extends React.Component {
return result return result
} }
getScrollBarStyle () { getScrollBarStyle() {
const { theme } = this.props const { theme } = this.props
switch (theme) { return uiThemes.some(item => item.name === theme && item.isDark)
case 'dark': ? scrollBarDarkStyle
case 'solarized-dark': : scrollBarStyle
case 'monokai':
case 'dracula':
return scrollBarDarkStyle
default:
return scrollBarStyle
}
} }
componentDidMount () { componentDidMount() {
const { onDrop } = this.props const { onDrop } = this.props
this.refs.root.setAttribute('sandbox', 'allow-scripts') this.refs.root.setAttribute('sandbox', 'allow-scripts')
@@ -510,6 +651,7 @@ export default class MarkdownPreview extends React.Component {
'scroll', 'scroll',
this.scrollHandler this.scrollHandler
) )
this.refs.root.contentWindow.addEventListener('resize', this.resizeHandler)
eventEmitter.on('export:save-text', this.saveAsTextHandler) eventEmitter.on('export:save-text', this.saveAsTextHandler)
eventEmitter.on('export:save-md', this.saveAsMdHandler) eventEmitter.on('export:save-md', this.saveAsMdHandler)
eventEmitter.on('export:save-html', this.saveAsHtmlHandler) eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
@@ -517,7 +659,7 @@ export default class MarkdownPreview extends React.Component {
eventEmitter.on('print', this.printHandler) eventEmitter.on('print', this.printHandler)
} }
componentWillUnmount () { componentWillUnmount() {
const { onDrop } = this.props const { onDrop } = this.props
this.refs.root.contentWindow.document.body.removeEventListener( this.refs.root.contentWindow.document.body.removeEventListener(
@@ -548,6 +690,10 @@ export default class MarkdownPreview extends React.Component {
'scroll', 'scroll',
this.scrollHandler this.scrollHandler
) )
this.refs.root.contentWindow.removeEventListener(
'resize',
this.resizeHandler
)
eventEmitter.off('export:save-text', this.saveAsTextHandler) eventEmitter.off('export:save-text', this.saveAsTextHandler)
eventEmitter.off('export:save-md', this.saveAsMdHandler) eventEmitter.off('export:save-md', this.saveAsMdHandler)
eventEmitter.off('export:save-html', this.saveAsHtmlHandler) eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
@@ -555,17 +701,20 @@ export default class MarkdownPreview extends React.Component {
eventEmitter.off('print', this.printHandler) eventEmitter.off('print', this.printHandler)
} }
componentDidUpdate (prevProps) { componentDidUpdate(prevProps) {
if (prevProps.value !== this.props.value) this.rewriteIframe() // actual rewriteIframe function should be called only once
let needsRewriteIframe = false
if (prevProps.value !== this.props.value) needsRewriteIframe = true
if ( if (
prevProps.smartQuotes !== this.props.smartQuotes || prevProps.smartQuotes !== this.props.smartQuotes ||
prevProps.sanitize !== this.props.sanitize || prevProps.sanitize !== this.props.sanitize ||
prevProps.mermaidHTMLLabel !== this.props.mermaidHTMLLabel ||
prevProps.smartArrows !== this.props.smartArrows || prevProps.smartArrows !== this.props.smartArrows ||
prevProps.breaks !== this.props.breaks || prevProps.breaks !== this.props.breaks ||
prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox
) { ) {
this.initMarkdown() this.initMarkdown()
this.rewriteIframe() needsRewriteIframe = true
} }
if ( if (
prevProps.fontFamily !== this.props.fontFamily || prevProps.fontFamily !== this.props.fontFamily ||
@@ -577,14 +726,24 @@ export default class MarkdownPreview extends React.Component {
prevProps.theme !== this.props.theme || prevProps.theme !== this.props.theme ||
prevProps.scrollPastEnd !== this.props.scrollPastEnd || prevProps.scrollPastEnd !== this.props.scrollPastEnd ||
prevProps.allowCustomCSS !== this.props.allowCustomCSS || prevProps.allowCustomCSS !== this.props.allowCustomCSS ||
prevProps.customCSS !== this.props.customCSS prevProps.customCSS !== this.props.customCSS ||
prevProps.RTL !== this.props.RTL
) { ) {
this.applyStyle() this.applyStyle()
needsRewriteIframe = true
}
if (needsRewriteIframe) {
this.rewriteIframe() this.rewriteIframe()
} }
// Should scroll to top after selecting another note
if (prevProps.noteKey !== this.props.noteKey) {
this.scrollTo(0, 0)
}
} }
getStyleParams () { getStyleParams() {
const { const {
fontSize, fontSize,
lineNumber, lineNumber,
@@ -592,17 +751,19 @@ export default class MarkdownPreview extends React.Component {
scrollPastEnd, scrollPastEnd,
theme, theme,
allowCustomCSS, allowCustomCSS,
customCSS customCSS,
RTL
} = this.props } = this.props
let { fontFamily, codeBlockFontFamily } = this.props let { fontFamily, codeBlockFontFamily } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0 fontFamily =
_.isString(fontFamily) && fontFamily.trim().length > 0
? fontFamily ? fontFamily
.split(',') .split(',')
.map(fontName => fontName.trim()) .map(fontName => fontName.trim())
.concat(defaultFontFamily) .concat(defaultFontFamily)
: defaultFontFamily : defaultFontFamily
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily =
codeBlockFontFamily.trim().length > 0 _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
? codeBlockFontFamily ? codeBlockFontFamily
.split(',') .split(',')
.map(fontName => fontName.trim()) .map(fontName => fontName.trim())
@@ -618,11 +779,12 @@ export default class MarkdownPreview extends React.Component {
scrollPastEnd, scrollPastEnd,
theme, theme,
allowCustomCSS, allowCustomCSS,
customCSS customCSS,
RTL
} }
} }
applyStyle () { applyStyle() {
const { const {
fontFamily, fontFamily,
fontSize, fontSize,
@@ -632,13 +794,14 @@ export default class MarkdownPreview extends React.Component {
scrollPastEnd, scrollPastEnd,
theme, theme,
allowCustomCSS, allowCustomCSS,
customCSS customCSS,
RTL
} = this.getStyleParams() } = this.getStyleParams()
this.getWindow().document.getElementById( this.getWindow().document.getElementById(
'codeTheme' 'codeTheme'
).href = this.GetCodeThemeLink(codeBlockTheme) ).href = this.getCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle( this.getWindow().document.getElementById('style').innerHTML = buildStyle({
fontFamily, fontFamily,
fontSize, fontSize,
codeBlockFontFamily, codeBlockFontFamily,
@@ -646,19 +809,20 @@ export default class MarkdownPreview extends React.Component {
scrollPastEnd, scrollPastEnd,
theme, theme,
allowCustomCSS, allowCustomCSS,
customCSS customCSS,
) RTL
})
} }
GetCodeThemeLink (name) { getCodeThemeLink(name) {
const theme = consts.THEMES.find(theme => theme.name === name) const theme = consts.THEMES.find(theme => theme.name === name)
return theme return theme != null
? (win ? theme.path : `${appPath}/${theme.path}`) ? theme.path
: `${appPath}/node_modules/codemirror/theme/elegant.css` : `${appPath}/node_modules/codemirror/theme/elegant.css`
} }
rewriteIframe () { rewriteIframe() {
_.forEach( _.forEach(
this.refs.root.contentWindow.document.querySelectorAll( this.refs.root.contentWindow.document.querySelectorAll(
'input[type="checkbox"]' 'input[type="checkbox"]'
@@ -681,7 +845,8 @@ export default class MarkdownPreview extends React.Component {
showCopyNotification, showCopyNotification,
storagePath, storagePath,
noteKey, noteKey,
sanitize sanitize,
mermaidHTMLLabel
} = this.props } = this.props
let { value, codeBlockTheme } = this.props let { value, codeBlockTheme } = this.props
@@ -715,7 +880,9 @@ export default class MarkdownPreview extends React.Component {
codeBlockTheme = consts.THEMES.find(theme => theme.name === codeBlockTheme) codeBlockTheme = consts.THEMES.find(theme => theme.name === codeBlockTheme)
const codeBlockThemeClassName = codeBlockTheme ? codeBlockTheme.className : 'cm-s-default' const codeBlockThemeClassName = codeBlockTheme
? codeBlockTheme.className
: 'cm-s-default'
_.forEach( _.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.code code'), this.refs.root.contentWindow.document.querySelectorAll('.code code'),
@@ -801,7 +968,10 @@ export default class MarkdownPreview extends React.Component {
el => { el => {
try { try {
const format = el.attributes.getNamedItem('data-format').value const format = el.attributes.getNamedItem('data-format').value
const chartConfig = format === 'yaml' ? yaml.load(el.innerHTML) : JSON.parse(el.innerHTML) const chartConfig =
format === 'yaml'
? yaml.load(el.innerHTML)
: JSON.parse(el.innerHTML)
el.innerHTML = '' el.innerHTML = ''
const canvas = document.createElement('canvas') const canvas = document.createElement('canvas')
@@ -813,6 +983,7 @@ export default class MarkdownPreview extends React.Component {
canvas.height = height.value + 'vh' canvas.height = height.value + 'vh'
} }
// eslint-disable-next-line no-unused-vars
const chart = new Chart(canvas, chartConfig) const chart = new Chart(canvas, chartConfig)
} catch (e) { } catch (e) {
el.className = 'chart-error' el.className = 'chart-error'
@@ -823,7 +994,12 @@ export default class MarkdownPreview extends React.Component {
_.forEach( _.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'), this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
el => { el => {
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme) mermaidRender(
el,
htmlTextHelper.decodeEntities(el.innerHTML),
theme,
mermaidHTMLLabel
)
} }
) )
@@ -845,20 +1021,14 @@ export default class MarkdownPreview extends React.Component {
autoplay = 0 autoplay = 0
} }
render( render(<Carousel images={images} autoplay={autoplay} />, el)
<Carousel
images={images}
autoplay={autoplay}
/>,
el
)
} }
) )
const markdownPreviewIframe = document.querySelector('.MarkdownPreview') const markdownPreviewIframe = document.querySelector('.MarkdownPreview')
const rect = markdownPreviewIframe.getBoundingClientRect() const rect = markdownPreviewIframe.getBoundingClientRect()
const config = { attributes: true, subtree: true } const config = { attributes: true, subtree: true }
const imgObserver = new MutationObserver((mutationList) => { const imgObserver = new MutationObserver(mutationList => {
for (const mu of mutationList) { for (const mu of mutationList) {
if (mu.target.className === 'carouselContent-enter-done') { if (mu.target.className === 'carouselContent-enter-done') {
this.setImgOnClickEventHelper(mu.target, rect) this.setImgOnClickEventHelper(mu.target, rect)
@@ -867,26 +1037,32 @@ export default class MarkdownPreview extends React.Component {
} }
}) })
const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('img') const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll(
'img'
)
for (const img of imgList) { for (const img of imgList) {
const parentEl = img.parentElement const parentEl = img.parentElement
this.setImgOnClickEventHelper(img, rect) this.setImgOnClickEventHelper(img, rect)
imgObserver.observe(parentEl, config) imgObserver.observe(parentEl, config)
} }
const aList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('a') const aList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll(
'a'
)
for (const a of aList) { for (const a of aList) {
a.removeEventListener('click', this.linkClickHandler) a.removeEventListener('click', this.linkClickHandler)
a.addEventListener('click', this.linkClickHandler) a.addEventListener('click', this.linkClickHandler)
} }
} }
setImgOnClickEventHelper (img, rect) { setImgOnClickEventHelper(img, rect) {
img.onclick = () => { img.onclick = () => {
const widthMagnification = document.body.clientWidth / img.width const widthMagnification = document.body.clientWidth / img.width
const heightMagnification = document.body.clientHeight / img.height const heightMagnification = document.body.clientHeight / img.height
const baseOnWidth = widthMagnification < heightMagnification const baseOnWidth = widthMagnification < heightMagnification
const magnification = baseOnWidth ? widthMagnification : heightMagnification const magnification = baseOnWidth
? widthMagnification
: heightMagnification
const zoomImgWidth = img.width * magnification const zoomImgWidth = img.width * magnification
const zoomImgHeight = img.height * magnification const zoomImgHeight = img.height * magnification
@@ -917,10 +1093,7 @@ export default class MarkdownPreview extends React.Component {
width: ${zoomImgWidth}; width: ${zoomImgWidth};
height: ${zoomImgHeight}px; height: ${zoomImgHeight}px;
` `
zoomImg.animate([ zoomImg.animate([originalImgRect, zoomInImgRect], animationSpeed)
originalImgRect,
zoomInImgRect
], animationSpeed)
const overlay = document.createElement('div') const overlay = document.createElement('div')
overlay.style = ` overlay.style = `
@@ -941,29 +1114,40 @@ export default class MarkdownPreview extends React.Component {
width: ${img.width}px; width: ${img.width}px;
height: ${img.height}px; height: ${img.height}px;
` `
const zoomOutImgAnimation = zoomImg.animate([ const zoomOutImgAnimation = zoomImg.animate(
zoomInImgRect, [zoomInImgRect, originalImgRect],
originalImgRect animationSpeed
], animationSpeed) )
zoomOutImgAnimation.onfinish = () => overlay.remove() zoomOutImgAnimation.onfinish = () => overlay.remove()
} }
overlay.appendChild(zoomImg) overlay.appendChild(zoomImg)
document.body.appendChild(overlay) document.body.appendChild(overlay)
} }
this.getWindow().scrollTo(0, 0)
} }
focus () { handleResize() {
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll('svg[ratio]'),
el => {
el.setAttribute('height', el.clientWidth / el.getAttribute('ratio'))
}
)
}
focus() {
this.refs.root.focus() this.refs.root.focus()
} }
getWindow () { getWindow() {
return this.refs.root.contentWindow return this.refs.root.contentWindow
} }
scrollTo (targetRow) { /**
* @public
* @param {Number} targetRow
*/
scrollToRow(targetRow) {
const blocks = this.getWindow().document.querySelectorAll( const blocks = this.getWindow().document.querySelectorAll(
'body>[data-line]' 'body>[data-line]'
) )
@@ -973,18 +1157,27 @@ export default class MarkdownPreview extends React.Component {
const row = parseInt(block.getAttribute('data-line')) const row = parseInt(block.getAttribute('data-line'))
if (row > targetRow || index === blocks.length - 1) { if (row > targetRow || index === blocks.length - 1) {
block = blocks[index - 1] block = blocks[index - 1]
block != null && this.getWindow().scrollTo(0, block.offsetTop) block != null && this.scrollTo(0, block.offsetTop)
break break
} }
} }
} }
preventImageDroppedHandler (e) { /**
* `document.body.scrollTo`
* @param {Number} x
* @param {Number} y
*/
scrollTo(x, y) {
this.getWindow().document.body.scrollTo(x, y)
}
preventImageDroppedHandler(e) {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
} }
notify (title, options) { notify(title, options) {
if (global.process.platform === 'win32') { if (global.process.platform === 'win32') {
options.icon = path.join( options.icon = path.join(
'file://', 'file://',
@@ -995,31 +1188,36 @@ export default class MarkdownPreview extends React.Component {
return new window.Notification(title, options) return new window.Notification(title, options)
} }
handleLinkClick (e) { handleLinkClick(e) {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
const rawHref = e.target.getAttribute('href') const rawHref = e.target.getAttribute('href')
const parser = document.createElement('a') const { dispatch } = this.props
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
if (!rawHref) return // not checked href because parser will create file://... string for [empty link]() if (!rawHref) return // not checked href because parser will create file://... string for [empty link]()
const extractId = /(main.html)?#/ const parser = document.createElement('a')
const regexNoteInternalLink = new RegExp(`${extractId.source}(.+)`) parser.href = rawHref
if (regexNoteInternalLink.test(linkHash)) { const isStartWithHash = rawHref[0] === '#'
const targetId = mdurl.encode(linkHash.replace(extractId, '')) const { href, hash } = parser
const targetElement = this.refs.root.contentWindow.document.getElementById(
targetId const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10
)
const extractIdRegex = /file:\/\/.*main.?\w*.html#/ // file://path/to/main(.development.)html
const regexNoteInternalLink = new RegExp(`${extractIdRegex.source}(.+)`)
if (isStartWithHash || regexNoteInternalLink.test(rawHref)) {
const posOfHash = linkHash.indexOf('#')
if (posOfHash > -1) {
const extractedId = linkHash.slice(posOfHash + 1)
const targetId = mdurl.encode(extractedId)
const targetElement = this.getWindow().document.getElementById(targetId)
if (targetElement != null) { if (targetElement != null) {
this.getWindow().scrollTo(0, targetElement.offsetTop) this.scrollTo(0, targetElement.offsetTop)
} }
return return
} }
}
// this will match the new uuid v4 hash and the old hash // this will match the new uuid v4 hash and the old hash
// e.g. // e.g.
@@ -1049,11 +1247,29 @@ export default class MarkdownPreview extends React.Component {
return return
} }
// other case const regexIsTagLink = /^:tag:([\w]+)$/
shell.openExternal(href) if (regexIsTagLink.test(rawHref)) {
const tag = rawHref.match(regexIsTagLink)[1]
dispatch(push(`/tags/${encodeURIComponent(tag)}`))
return
} }
render () { // other case
this.openExternal(href)
}
openExternal(href) {
try {
const success =
shell.openExternal(href) || shell.openExternal(decodeURI(href))
if (!success) console.error('failed to open url ' + href)
} catch (e) {
// URI Error threw from decodeURI
console.error(e)
}
}
render() {
const { className, style, tabIndex } = this.props const { className, style, tabIndex } = this.props
return ( return (
<iframe <iframe
@@ -1082,3 +1298,10 @@ MarkdownPreview.propTypes = {
smartArrows: PropTypes.bool, smartArrows: PropTypes.bool,
breaks: PropTypes.bool breaks: PropTypes.bool
} }
export default connect(
null,
null,
null,
{ forwardRef: true }
)(MarkdownPreview)

View File

@@ -8,7 +8,7 @@ import styles from './MarkdownSplitEditor.styl'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
class MarkdownSplitEditor extends React.Component { class MarkdownSplitEditor extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props)
this.value = props.value this.value = props.value
this.focus = () => this.refs.code.focus() this.focus = () => this.refs.code.focus()
@@ -16,23 +16,27 @@ class MarkdownSplitEditor extends React.Component {
this.userScroll = true this.userScroll = true
this.state = { this.state = {
isSliderFocused: false, isSliderFocused: false,
codeEditorWidthInPercent: 50 codeEditorWidthInPercent: 50,
codeEditorHeightInPercent: 50
} }
} }
setValue (value) { setValue(value) {
this.refs.code.setValue(value) this.refs.code.setValue(value)
} }
handleOnChange (e) { handleOnChange(e) {
this.value = this.refs.code.value this.value = this.refs.code.value
this.props.onChange(e) this.props.onChange(e)
} }
handleScroll (e) { handleScroll(e) {
if (!this.props.config.preview.scrollSync) return if (!this.props.config.preview.scrollSync) return
const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document') const previewDoc = _.get(
this,
'refs.preview.refs.root.contentWindow.document'
)
const codeDoc = _.get(this, 'refs.code.editor.doc') const codeDoc = _.get(this, 'refs.code.editor.doc')
let srcTop, srcHeight, targetTop, targetHeight let srcTop, srcHeight, targetTop, targetHeight
@@ -49,7 +53,7 @@ class MarkdownSplitEditor extends React.Component {
targetHeight = _.get(codeDoc, 'height') targetHeight = _.get(codeDoc, 'height')
} }
const distance = (targetHeight * srcTop / srcHeight) - targetTop const distance = (targetHeight * srcTop) / srcHeight - targetTop
const framerate = 1000 / 60 const framerate = 1000 / 60
const frames = 20 const frames = 20
const refractory = frames * framerate const refractory = frames * framerate
@@ -60,21 +64,29 @@ class MarkdownSplitEditor extends React.Component {
let scrollPos, time let scrollPos, time
const timer = setInterval(() => { const timer = setInterval(() => {
time = frame / frames time = frame / frames
scrollPos = time < 0.5 scrollPos =
time < 0.5
? 2 * time * time // ease in ? 2 * time * time // ease in
: -1 + (4 - 2 * time) * time // ease out : -1 + (4 - 2 * time) * time // ease out
if (e.doc) _.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance) if (e.doc)
else _.get(this, 'refs.code.editor').scrollTo(0, targetTop + scrollPos * distance) _.set(previewDoc, 'body.scrollTop', targetTop + scrollPos * distance)
else
_.get(this, 'refs.code.editor').scrollTo(
0,
targetTop + scrollPos * distance
)
if (frame >= frames) { if (frame >= frames) {
clearInterval(timer) clearInterval(timer)
setTimeout(() => { this.userScroll = true }, refractory) setTimeout(() => {
this.userScroll = true
}, refractory)
} }
frame++ frame++
}, framerate) }, framerate)
} }
} }
handleCheckboxClick (e) { handleCheckboxClick(e) {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
const idMatch = /checkbox-([0-9]+)/ const idMatch = /checkbox-([0-9]+)/
@@ -83,28 +95,48 @@ class MarkdownSplitEditor extends React.Component {
const checkReplace = /\[x]/i const checkReplace = /\[x]/i
const uncheckReplace = /\[ ]/ const uncheckReplace = /\[ ]/
if (idMatch.test(e.target.getAttribute('id'))) { if (idMatch.test(e.target.getAttribute('id'))) {
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1 const lineIndex =
const lines = this.refs.code.value parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
.split('\n') const lines = this.refs.code.value.split('\n')
const targetLine = lines[lineIndex] const targetLine = lines[lineIndex]
let newLine = targetLine
if (targetLine.match(checkedMatch)) { if (targetLine.match(checkedMatch)) {
lines[lineIndex] = targetLine.replace(checkReplace, '[ ]') newLine = targetLine.replace(checkReplace, '[ ]')
} }
if (targetLine.match(uncheckedMatch)) { if (targetLine.match(uncheckedMatch)) {
lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]') newLine = targetLine.replace(uncheckReplace, '[x]')
} }
this.refs.code.setValue(lines.join('\n')) this.refs.code.setLineContent(lineIndex, newLine)
} }
} }
handleMouseMove (e) { handleMouseMove(e) {
if (this.state.isSliderFocused) { if (this.state.isSliderFocused) {
const rootRect = this.refs.root.getBoundingClientRect() const rootRect = this.refs.root.getBoundingClientRect()
if (this.props.isStacking) {
const rootHeight = rootRect.height
const offset = rootRect.top
let newCodeEditorHeightInPercent =
((e.pageY - offset) / rootHeight) * 100
// limit minSize to 10%, maxSize to 90%
if (newCodeEditorHeightInPercent <= 10) {
newCodeEditorHeightInPercent = 10
}
if (newCodeEditorHeightInPercent >= 90) {
newCodeEditorHeightInPercent = 90
}
this.setState({
codeEditorHeightInPercent: newCodeEditorHeightInPercent
})
} else {
const rootWidth = rootRect.width const rootWidth = rootRect.width
const offset = rootRect.left const offset = rootRect.left
let newCodeEditorWidthInPercent = (e.pageX - offset) / rootWidth * 100 let newCodeEditorWidthInPercent = ((e.pageX - offset) / rootWidth) * 100
// limit minSize to 10%, maxSize to 90% // limit minSize to 10%, maxSize to 90%
if (newCodeEditorWidthInPercent <= 10) { if (newCodeEditorWidthInPercent <= 10) {
@@ -120,51 +152,118 @@ class MarkdownSplitEditor extends React.Component {
}) })
} }
} }
}
handleMouseUp (e) { handleMouseUp(e) {
e.preventDefault() e.preventDefault()
this.setState({ this.setState({
isSliderFocused: false isSliderFocused: false
}) })
} }
handleMouseDown (e) { handleMouseDown(e) {
e.preventDefault() e.preventDefault()
this.setState({ this.setState({
isSliderFocused: true isSliderFocused: true
}) })
} }
render () { render() {
const {config, value, storageKey, noteKey, linesHighlighted} = this.props const {
const storage = findStorage(storageKey) config,
value,
storageKey,
noteKey,
linesHighlighted,
isStacking,
RTL
} = this.props
let storage
try {
storage = findStorage(storageKey)
} catch (e) {
return <div />
}
let editorStyle = {}
let previewStyle = {}
let sliderStyle = {}
let editorFontSize = parseInt(config.editor.fontSize, 10) let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14 if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
editorStyle.fontSize = editorFontSize
let editorIndentSize = parseInt(config.editor.indentSize, 10) let editorIndentSize = parseInt(config.editor.indentSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4 if (!(editorStyle.fontSize > 0 && editorStyle.fontSize < 132))
const previewStyle = {} editorIndentSize = 4
previewStyle.width = (100 - this.state.codeEditorWidthInPercent) + '%' editorStyle.indentSize = editorIndentSize
if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused) previewStyle.pointerEvents = 'none'
editorStyle = Object.assign(
editorStyle,
isStacking
? {
width: '100%',
height: `${this.state.codeEditorHeightInPercent}%`
}
: {
width: `${this.state.codeEditorWidthInPercent}%`,
height: '100%'
}
)
previewStyle = Object.assign(
previewStyle,
isStacking
? {
width: '100%',
height: `${100 - this.state.codeEditorHeightInPercent}%`
}
: {
width: `${100 - this.state.codeEditorWidthInPercent}%`,
height: '100%'
}
)
sliderStyle = Object.assign(
sliderStyle,
isStacking
? {
left: 0,
top: `${this.state.codeEditorHeightInPercent}%`
}
: {
left: `${this.state.codeEditorWidthInPercent}%`,
top: 0
}
)
if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused)
previewStyle.pointerEvents = 'none'
return ( return (
<div styleName='root' ref='root' <div
styleName='root'
ref='root'
onMouseMove={e => this.handleMouseMove(e)} onMouseMove={e => this.handleMouseMove(e)}
onMouseUp={e => this.handleMouseUp(e)}> onMouseUp={e => this.handleMouseUp(e)}
>
<CodeEditor <CodeEditor
ref='code' ref='code'
width={this.state.codeEditorWidthInPercent + '%'} width={editorStyle.width}
height={editorStyle.height}
mode='Boost Flavored Markdown' mode='Boost Flavored Markdown'
value={value} value={value}
theme={config.editor.theme} theme={config.editor.theme}
keyMap={config.editor.keyMap} keyMap={config.editor.keyMap}
fontFamily={config.editor.fontFamily} fontFamily={config.editor.fontFamily}
fontSize={editorFontSize} fontSize={editorStyle.fontSize}
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
lineWrapping lineWrapping
matchingPairs={config.editor.matchingPairs} matchingPairs={config.editor.matchingPairs}
matchingTriples={config.editor.matchingTriples} matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs} explodingPairs={config.editor.explodingPairs}
indentType={config.editor.indentType} indentType={config.editor.indentType}
indentSize={editorIndentSize} indentSize={editorStyle.indentSize}
enableRulers={config.editor.enableRulers} enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers} rulers={config.editor.rulers}
scrollPastEnd={config.editor.scrollPastEnd} scrollPastEnd={config.editor.scrollPastEnd}
@@ -173,7 +272,7 @@ class MarkdownSplitEditor extends React.Component {
storageKey={storageKey} storageKey={storageKey}
noteKey={noteKey} noteKey={noteKey}
linesHighlighted={linesHighlighted} linesHighlighted={linesHighlighted}
onChange={(e) => this.handleOnChange(e)} onChange={e => this.handleOnChange(e)}
onScroll={this.handleScroll.bind(this)} onScroll={this.handleScroll.bind(this)}
spellCheck={config.editor.spellcheck} spellCheck={config.editor.spellcheck}
enableSmartPaste={config.editor.enableSmartPaste} enableSmartPaste={config.editor.enableSmartPaste}
@@ -181,11 +280,18 @@ class MarkdownSplitEditor extends React.Component {
switchPreview={config.editor.switchPreview} switchPreview={config.editor.switchPreview}
enableMarkdownLint={config.editor.enableMarkdownLint} enableMarkdownLint={config.editor.enableMarkdownLint}
customMarkdownLintConfig={config.editor.customMarkdownLintConfig} customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
RTL={RTL}
/> />
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} > <div
styleName={isStacking ? 'slider-hoz' : 'slider'}
style={{ left: sliderStyle.left, top: sliderStyle.top }}
onMouseDown={e => this.handleMouseDown(e)}
>
<div styleName='slider-hitbox' /> <div styleName='slider-hitbox' />
</div> </div>
<MarkdownPreview <MarkdownPreview
ref='preview'
style={previewStyle} style={previewStyle}
theme={config.ui.theme} theme={config.ui.theme}
keyMap={config.editor.keyMap} keyMap={config.editor.keyMap}
@@ -199,10 +305,10 @@ class MarkdownSplitEditor extends React.Component {
smartArrows={config.preview.smartArrows} smartArrows={config.preview.smartArrows}
breaks={config.preview.breaks} breaks={config.preview.breaks}
sanitize={config.preview.sanitize} sanitize={config.preview.sanitize}
ref='preview' mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
tabInde='0' tabInde='0'
value={value} value={value}
onCheckboxClick={(e) => this.handleCheckboxClick(e)} onCheckboxClick={e => this.handleCheckboxClick(e)}
onScroll={this.handleScroll.bind(this)} onScroll={this.handleScroll.bind(this)}
showCopyNotification={config.ui.showCopyNotification} showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path} storagePath={storage.path}
@@ -210,6 +316,7 @@ class MarkdownSplitEditor extends React.Component {
customCSS={config.preview.customCSS} customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS} allowCustomCSS={config.preview.allowCustomCSS}
lineThroughCheckbox={config.preview.lineThroughCheckbox} lineThroughCheckbox={config.preview.lineThroughCheckbox}
RTL={RTL}
/> />
</div> </div>
) )

View File

@@ -3,6 +3,7 @@
height 100% height 100%
font-size 30px font-size 30px
display flex display flex
flex-wrap wrap
.slider .slider
absolute top bottom absolute top bottom
top -2px top -2px
@@ -15,23 +16,23 @@
left -3px left -3px
z-index 10 z-index 10
cursor col-resize cursor col-resize
.slider-hoz
absolute left right
.slider-hitbox
absolute left right
width: 100%
height 7px
cursor row-resize
body[data-theme="dark"]
apply-theme(theme)
body[data-theme={theme}]
.root .root
.slider .slider
border-left 1px solid $ui-dark-borderColor border-left 1px solid get-theme-var(theme, 'borderColor')
body[data-theme="solarized-dark"] for theme in 'dark' 'dracula' 'solarized-dark'
.root apply-theme(theme)
.slider
border-left 1px solid $ui-solarized-dark-borderColor
body[data-theme="monokai"] for theme in $themes
.root apply-theme(theme)
.slider
border-left 1px solid $ui-monokai-borderColor
body[data-theme="dracula"]
.root
.slider
border-left 1px solid $ui-dracula-borderColor

View File

@@ -3,9 +3,7 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './ModalEscButton.styl' import styles from './ModalEscButton.styl'
const ModalEscButton = ({ const ModalEscButton = ({ handleEscButtonClick }) => (
handleEscButtonClick
}) => (
<button styleName='escButton' onClick={handleEscButtonClick}> <button styleName='escButton' onClick={handleEscButtonClick}>
<div styleName='esc-mark'>×</div> <div styleName='esc-mark'>×</div>
<div>esc</div> <div>esc</div>

View File

@@ -1,24 +1,23 @@
/** /**
* @fileoverview Micro component for toggle SideNav * @fileoverview Micro component for toggle SideNav
*/ */
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import styles from './NavToggleButton.styl' import styles from './NavToggleButton.styl'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
/** /**
* @param {boolean} isFolded * @param {boolean} isFolded
* @param {Function} handleToggleButtonClick * @param {Function} handleToggleButtonClick
*/ */
const NavToggleButton = ({isFolded, handleToggleButtonClick}) => ( const NavToggleButton = ({ isFolded, handleToggleButtonClick }) => (
<button styleName='navToggle' <button styleName='navToggle' onClick={e => handleToggleButtonClick(e)}>
onClick={(e) => handleToggleButtonClick(e)} {isFolded ? (
> <i className='fa fa-angle-double-right fa-2x' />
{isFolded ) : (
? <i className='fa fa-angle-double-right fa-2x' /> <i className='fa fa-angle-double-left fa-2x' />
: <i className='fa fa-angle-double-left fa-2x' /> )}
}
</button> </button>
) )

View File

@@ -17,10 +17,16 @@
body[data-theme="white"] body[data-theme="white"]
navWhiteButtonColor() navWhiteButtonColor()
body[data-theme="dark"] apply-theme(theme)
.navToggle body[data-theme={theme}]
&:hover .navToggle:hover
background-color alpha($ui-dark-button--active-backgroundColor, 20%) background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 20%)
border 1px solid get-theme-var(theme, 'button--active-backgroundColor')
transition 0.15s transition 0.15s
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
for theme in 'dark' 'dracula' 'solarized-dark'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -3,8 +3,9 @@
*/ */
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import { isArray } from 'lodash' import { isArray, sortBy } from 'lodash'
import invertColor from 'invert-color' import invertColor from 'invert-color'
import Emoji from 'react-emoji-render'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import { getTodoStatus } from 'browser/lib/getTodoStatus' import { getTodoStatus } from 'browser/lib/getTodoStatus'
import styles from './NoteItem.styl' import styles from './NoteItem.styl'
@@ -21,7 +22,11 @@ const TagElement = ({ tagName, color }) => {
const style = {} const style = {}
if (color) { if (color) {
style.backgroundColor = color style.backgroundColor = color
style.color = invertColor(color, { black: '#222', white: '#f1f1f1', threshold: 0.3 }) style.color = invertColor(color, {
black: '#222',
white: '#f1f1f1',
threshold: 0.3
})
} }
return ( return (
<span styleName='item-bottom-tagList-item' key={tagName} style={style}> <span styleName='item-bottom-tagList-item' key={tagName} style={style}>
@@ -43,9 +48,13 @@ const TagElementList = (tags, showTagsAlphabetically, coloredTags) => {
} }
if (showTagsAlphabetically) { if (showTagsAlphabetically) {
return _.sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] })) return sortBy(tags).map(tag =>
TagElement({ tagName: tag, color: coloredTags[tag] })
)
} else { } else {
return tags.map(tag => TagElement({ tagName: tag, color: coloredTags[tag] })) return tags.map(tag =>
TagElement({ tagName: tag, color: coloredTags[tag] })
)
} }
} }
@@ -82,13 +91,17 @@ const NoteItem = ({
draggable='true' draggable='true'
> >
<div styleName='item-wrapper'> <div styleName='item-wrapper'>
{note.type === 'SNIPPET_NOTE' {note.type === 'SNIPPET_NOTE' ? (
? <i styleName='item-title-icon' className='fa fa-fw fa-code' /> <i styleName='item-title-icon' className='fa fa-fw fa-code' />
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />} ) : (
<i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />
)}
<div styleName='item-title'> <div styleName='item-title'>
{note.title.trim().length > 0 {note.title.trim().length > 0 ? (
? note.title <Emoji text={note.title} />
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>} ) : (
<span styleName='item-title-empty'>{i18n.__('Empty note')}</span>
)}
</div> </div>
<div styleName='item-middle'> <div styleName='item-middle'>
<div styleName='item-middle-time'>{dateDisplay}</div> <div styleName='item-middle-time'>{dateDisplay}</div>
@@ -97,7 +110,9 @@ const NoteItem = ({
title={ title={
viewType === 'ALL' viewType === 'ALL'
? storageName ? storageName
: viewType === 'STORAGE' ? folderName : null : viewType === 'STORAGE'
? folderName
: null
} }
styleName='item-middle-app-meta-label' styleName='item-middle-app-meta-label'
> >
@@ -108,28 +123,36 @@ const NoteItem = ({
</div> </div>
<div styleName='item-bottom'> <div styleName='item-bottom'>
<div styleName='item-bottom-tagList'> <div styleName='item-bottom-tagList'>
{note.tags.length > 0 {note.tags.length > 0 ? (
? TagElementList(note.tags, showTagsAlphabetically, coloredTags) TagElementList(note.tags, showTagsAlphabetically, coloredTags)
: <span ) : (
<span
style={{ fontStyle: 'italic', opacity: 0.5 }} style={{ fontStyle: 'italic', opacity: 0.5 }}
styleName='item-bottom-tagList-empty' styleName='item-bottom-tagList-empty'
> >
{i18n.__('No tags')} {i18n.__('No tags')}
</span>} </span>
)}
</div> </div>
<div> <div>
{note.isStarred {note.isStarred ? (
? <img <img
styleName='item-star' styleName='item-star'
src='../resources/icon/icon-starred.svg' src='../resources/icon/icon-starred.svg'
/> />
: ''} ) : (
{note.isPinned && !pathname.match(/\/starred|\/trash/) ''
? <i styleName='item-pin' className='fa fa-thumb-tack' /> )}
: ''} {note.isPinned && !pathname.match(/\/starred|\/trash/) ? (
{note.type === 'MARKDOWN_NOTE' <i styleName='item-pin' className='fa fa-thumb-tack' />
? <TodoProcess todoStatus={getTodoStatus(note.content)} /> ) : (
: ''} ''
)}
{note.type === 'MARKDOWN_NOTE' ? (
<TodoProcess todoStatus={getTodoStatus(note.content)} />
) : (
''
)}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -194,7 +194,7 @@ body[data-theme="dark"]
color $ui-dark-text-color color $ui-dark-text-color
.item-bottom-tagList-item .item-bottom-tagList-item
transition 0.15s transition 0.15s
background-color alpha(#fff, 20%) background-color alpha($ui-dark-tagList-backgroundColor, 20%)
color $ui-dark-text-color color $ui-dark-text-color
&:active &:active
transition 0.15s transition 0.15s
@@ -207,7 +207,7 @@ body[data-theme="dark"]
color $ui-dark-text-color color $ui-dark-text-color
.item-bottom-tagList-item .item-bottom-tagList-item
transition 0.15s transition 0.15s
background-color alpha(white, 10%) background-color alpha($ui-dark-tagList-backgroundColor, 10%)
color $ui-dark-text-color color $ui-dark-text-color
.item-wrapper .item-wrapper
@@ -223,13 +223,13 @@ body[data-theme="dark"]
.item-bottom-time .item-bottom-time
color $ui-dark-text-color color $ui-dark-text-color
.item-bottom-tagList-item .item-bottom-tagList-item
background-color alpha(white, 10%) background-color alpha($ui-dark-tagList-backgroundColor, 10%)
color $ui-dark-text-color color $ui-dark-text-color
&:hover &:hover
background-color alpha($ui-dark-button--active-backgroundColor, 60%) background-color alpha($ui-dark-button--active-backgroundColor, 60%)
color #c0392b color $ui-dark-button--hover-color
.item-bottom-tagList-item .item-bottom-tagList-item
background-color alpha(#fff, 20%) background-color alpha($ui-dark-tagList-backgroundColor, 20%)
.item-title .item-title
color $ui-inactive-text-color color $ui-inactive-text-color
@@ -322,61 +322,62 @@ body[data-theme="solarized-dark"]
color $ui-inactive-text-color color $ui-inactive-text-color
vertical-align middle vertical-align middle
body[data-theme="monokai"] apply-theme(theme)
body[data-theme={theme}]
.root .root
border-color $ui-monokai-borderColor border-color get-theme-var(theme, 'borderColor')
background-color $ui-monokai-noteList-backgroundColor background-color get-theme-var(theme, 'noteList-backgroundColor')
.item .item
border-color $ui-monokai-borderColor border-color get-theme-var(theme, 'borderColor')
background-color $ui-monokai-noteList-backgroundColor background-color get-theme-var(theme, 'noteList-backgroundColor')
&:hover &:hover
transition 0.15s transition 0.15s
// background-color alpha($ui-monokai-noteList-backgroundColor, 20%) // background-color alpha(get-theme-var(theme, 'noteList-backgroundColor'), 20%)
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
.item-title .item-title
.item-title-icon .item-title-icon
.item-bottom-time .item-bottom-time
transition 0.15s transition 0.15s
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
.item-bottom-tagList-item .item-bottom-tagList-item
transition 0.15s transition 0.15s
background-color alpha($ui-monokai-noteList-backgroundColor, 20%) background-color alpha(get-theme-var(theme, 'noteList-backgroundColor'), 20%)
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
&:active &:active
transition 0.15s transition 0.15s
background-color $ui-monokai-noteList-backgroundColor background-color get-theme-var(theme, 'noteList-backgroundColor')
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
.item-title .item-title
.item-title-icon .item-title-icon
.item-bottom-time .item-bottom-time
transition 0.15s transition 0.15s
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
.item-bottom-tagList-item .item-bottom-tagList-item
transition 0.15s transition 0.15s
background-color alpha($ui-monokai-noteList-backgroundColor, 10%) background-color alpha(get-theme-var(theme, 'noteList-backgroundColor'), 10%)
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
.item-wrapper .item-wrapper
border-color alpha($ui-monokai-button-backgroundColor, 60%) border-color alpha(get-theme-var(theme, 'button-backgroundColor'), 60%)
.item--active .item--active
border-color $ui-monokai-borderColor border-color get-theme-var(theme, 'borderColor')
background-color $ui-monokai-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
.item-wrapper .item-wrapper
border-color transparent border-color transparent
.item-title .item-title
.item-title-icon .item-title-icon
.item-bottom-time .item-bottom-time
color $ui-monokai-active-color color get-theme-var(theme, 'active-color')
.item-bottom-tagList-item .item-bottom-tagList-item
background-color alpha(white, 10%) background-color alpha(get-theme-var(theme, 'tagList-backgroundColor'), 10%)
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
&:hover &:hover
// background-color alpha($ui-monokai-button--active-backgroundColor, 60%) // background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 60%)
color #f92672 color get-theme-var(theme, 'button--hover-color')
.item-bottom-tagList-item .item-bottom-tagList-item
background-color alpha(#fff, 20%) background-color alpha(get-theme-var(theme, 'tagList-backgroundColor'), 20%)
.item-title .item-title
color $ui-inactive-text-color color $ui-inactive-text-color
@@ -395,75 +396,8 @@ body[data-theme="monokai"]
color $ui-inactive-text-color color $ui-inactive-text-color
vertical-align middle vertical-align middle
body[data-theme="dracula"] for theme in 'dracula'
.root apply-theme(theme)
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteList-backgroundColor
.item for theme in $themes
border-color $ui-dracula-borderColor apply-theme(theme)
background-color $ui-dracula-noteList-backgroundColor
&:hover
transition 0.15s
// background-color alpha($ui-dracula-noteList-backgroundColor, 20%)
color $ui-dracula-text-color
.item-title
.item-title-icon
.item-bottom-time
transition 0.15s
color $ui-dracula-text-color
.item-bottom-tagList-item
transition 0.15s
background-color alpha($ui-dracula-noteList-backgroundColor, 20%)
color $ui-dracula-text-color
&:active
transition 0.15s
background-color $ui-dracula-noteList-backgroundColor
color $ui-dracula-text-color
.item-title
.item-title-icon
.item-bottom-time
transition 0.15s
color $ui-dracula-text-color
.item-bottom-tagList-item
transition 0.15s
background-color alpha($ui-dracula-noteList-backgroundColor, 10%)
color $ui-dracula-text-color
.item-wrapper
border-color alpha($ui-dracula-button-backgroundColor, 60%)
.item--active
border-color $ui-dracula-borderColor
background-color $ui-dracula-button-backgroundColor
.item-wrapper
border-color transparent
.item-title
.item-title-icon
.item-bottom-time
color $ui-dracula-active-color
.item-bottom-tagList-item
background-color alpha(#f8f8f2, 10%)
color $ui-dracula-text-color
&:hover
// background-color alpha($ui-dracula-button--active-backgroundColor, 60%)
color #ff79c6
.item-bottom-tagList-item
background-color alpha(#f8f8f2, 20%)
.item-title
color $ui-inactive-text-color
.item-title-icon
color $ui-inactive-text-color
.item-title-empty
color $ui-inactive-text-color
.item-bottom-tagList-item
background-color alpha($ui-dark-button--active-backgroundColor, 40%)
color $ui-inactive-text-color
.item-bottom-tagList-empty
color $ui-inactive-text-color
vertical-align middle

View File

@@ -25,10 +25,8 @@ const NoteItemSimple = ({
pathname, pathname,
storage storage
}) => ( }) => (
<div styleName={isActive <div
? 'item-simple--active' styleName={isActive ? 'item-simple--active' : 'item-simple'}
: 'item-simple'
}
key={note.key} key={note.key}
onClick={e => handleNoteClick(e, note.key)} onClick={e => handleNoteClick(e, note.key)}
onContextMenu={e => handleNoteContextMenu(e, note.key)} onContextMenu={e => handleNoteContextMenu(e, note.key)}
@@ -36,23 +34,29 @@ const NoteItemSimple = ({
draggable='true' draggable='true'
> >
<div styleName='item-simple-title'> <div styleName='item-simple-title'>
{note.type === 'SNIPPET_NOTE' {note.type === 'SNIPPET_NOTE' ? (
? <i styleName='item-simple-title-icon' className='fa fa-fw fa-code' /> <i styleName='item-simple-title-icon' className='fa fa-fw fa-code' />
: <i styleName='item-simple-title-icon' className='fa fa-fw fa-file-text-o' /> ) : (
} <i
{note.isPinned && !pathname.match(/\/starred|\/trash/) styleName='item-simple-title-icon'
? <i styleName='item-pin' className='fa fa-thumb-tack' /> className='fa fa-fw fa-file-text-o'
: '' />
} )}
{note.title.trim().length > 0 {note.isPinned && !pathname.match(/\/starred|\/trash/) ? (
? note.title <i styleName='item-pin' className='fa fa-thumb-tack' />
: <span styleName='item-simple-title-empty'>{i18n.__('Empty note')}</span> ) : (
} ''
{isAllNotesView && <div styleName='item-simple-right'> )}
<span styleName='item-simple-right-storageName'> {note.title.trim().length > 0 ? (
{storage.name} note.title
</span> ) : (
</div>} <span styleName='item-simple-title-empty'>{i18n.__('Empty note')}</span>
)}
{isAllNotesView && (
<div styleName='item-simple-right'>
<span styleName='item-simple-right-storageName'>{storage.name}</span>
</div>
)}
</div> </div>
</div> </div>
) )

View File

@@ -223,61 +223,62 @@ body[data-theme="solarized-dark"]
padding-left 4px padding-left 4px
opacity 0.4 opacity 0.4
body[data-theme="monokai"] apply-theme(theme)
body[data-theme={theme}]
.root .root
border-color $ui-monokai-borderColor border-color get-theme-var(theme, 'borderColor')
background-color $ui-monokai-noteList-backgroundColor background-color get-theme-var(theme, 'noteList-backgroundColor')
.item-simple .item-simple
border-color $ui-monokai-borderColor border-color get-theme-var(theme, 'borderColor')
background-color $ui-monokai-noteList-backgroundColor background-color get-theme-var(theme, 'noteList-backgroundColor')
&:hover &:hover
transition 0.15s transition 0.15s
background-color alpha($ui-monokai-button-backgroundColor, 60%) background-color alpha(get-theme-var(theme, 'button-backgroundColor'), 60%)
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
.item-simple-title .item-simple-title
.item-simple-title-empty .item-simple-title-empty
.item-simple-title-icon .item-simple-title-icon
.item-simple-bottom-time .item-simple-bottom-time
transition 0.15s transition 0.15s
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
.item-simple-bottom-tagList-item .item-simple-bottom-tagList-item
transition 0.15s transition 0.15s
background-color alpha(#fff, 20%) background-color alpha(get-theme-var(theme, 'tagList-backgroundColor'), 20%)
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
&:active &:active
transition 0.15s transition 0.15s
background-color $ui-monokai-button--active-backgroundColor background-color get-theme-var(theme, 'button--active-backgroundColor')
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
.item-simple-title .item-simple-title
.item-simple-title-empty .item-simple-title-empty
.item-simple-title-icon .item-simple-title-icon
.item-simple-bottom-time .item-simple-bottom-time
transition 0.15s transition 0.15s
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
.item-simple-bottom-tagList-item .item-simple-bottom-tagList-item
transition 0.15s transition 0.15s
background-color alpha(white, 10%) background-color alpha(get-theme-var(theme, 'tagList-backgroundColor'), 10%)
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
.item-simple--active .item-simple--active
border-color $ui-monokai-borderColor border-color get-theme-var(theme, 'borderColor')
background-color $ui-monokai-button--active-backgroundColor background-color get-theme-var(theme, 'button--active-backgroundColor')
.item-simple-wrapper .item-simple-wrapper
border-color transparent border-color transparent
.item-simple-title .item-simple-title
.item-simple-title-empty .item-simple-title-empty
.item-simple-title-icon .item-simple-title-icon
.item-simple-bottom-time .item-simple-bottom-time
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
.item-simple-bottom-tagList-item .item-simple-bottom-tagList-item
background-color alpha(white, 10%) background-color alpha(get-theme-var(theme, 'tagList-backgroundColor'), 10%)
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
&:hover &:hover
// background-color alpha($ui-dark-button--active-backgroundColor, 60%) // background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 60%)
color #c0392b color #c0392b
.item-simple-bottom-tagList-item .item-simple-bottom-tagList-item
background-color alpha(#fff, 20%) background-color alpha(get-theme-var(theme, 'tagList-backgroundColor'), 20%)
.item-simple-title .item-simple-title
color $ui-dark-text-color color $ui-dark-text-color
border-bottom $ui-dark-borderColor border-bottom $ui-dark-borderColor
@@ -287,66 +288,8 @@ body[data-theme="monokai"]
padding-left 4px padding-left 4px
opacity 0.4 opacity 0.4
body[data-theme="dracula"] for theme in 'dracula'
.root apply-theme(theme)
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteList-backgroundColor
.item-simple for theme in $themes
border-color $ui-dracula-borderColor apply-theme(theme)
background-color $ui-dracula-noteList-backgroundColor
&:hover
transition 0.15s
background-color alpha($ui-dracula-button-backgroundColor, 60%)
color $ui-dracula-text-color
.item-simple-title
.item-simple-title-empty
.item-simple-title-icon
.item-simple-bottom-time
transition 0.15s
color $ui-dracula-text-color
.item-simple-bottom-tagList-item
transition 0.15s
background-color alpha(#f8f8f2, 20%)
color $ui-dracula-text-color
&:active
transition 0.15s
background-color $ui-dracula-button--active-backgroundColor
color $ui-dracula-text-color
.item-simple-title
.item-simple-title-empty
.item-simple-title-icon
.item-simple-bottom-time
transition 0.15s
color $ui-dracula-text-color
.item-simple-bottom-tagList-item
transition 0.15s
background-color alpha(#f8f8f2, 10%)
color $ui-dracula-text-color
.item-simple--active
border-color $ui-dracula-borderColor
background-color $ui-dracula-button--active-backgroundColor
.item-simple-wrapper
border-color transparent
.item-simple-title
.item-simple-title-empty
.item-simple-title-icon
.item-simple-bottom-time
color $ui-dracula-text-color
.item-simple-bottom-tagList-item
background-color alpha(#f8f8f2, 10%)
color $ui-dracula-text-color
&:hover
// background-color alpha($ui-dark-button--active-backgroundColor, 60%)
color #c0392b
.item-simple-bottom-tagList-item
background-color alpha(#f8f8f2, 20%)
.item-simple-title
color $ui-dark-text-color
border-bottom $ui-dark-borderColor
.item-simple-right
float right
.item-simple-right-storageName
padding-left 4px
opacity 0.4

View File

@@ -6,7 +6,7 @@ const electron = require('electron')
const { shell } = electron const { shell } = electron
class RealtimeNotification extends React.Component { class RealtimeNotification extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
@@ -14,38 +14,46 @@ class RealtimeNotification extends React.Component {
} }
} }
componentDidMount () { componentDidMount() {
this.fetchNotifications() this.fetchNotifications()
} }
fetchNotifications () { fetchNotifications() {
const notificationsUrl = 'https://raw.githubusercontent.com/BoostIO/notification/master/notification.json' const notificationsUrl =
'https://raw.githubusercontent.com/BoostIO/notification/master/notification.json'
fetch(notificationsUrl) fetch(notificationsUrl)
.then(response => { .then(response => {
return response.json() return response.json()
}) })
.then(json => { .then(json => {
this.setState({notifications: json.notifications}) this.setState({ notifications: json.notifications })
}) })
} }
handleLinkClick (e) { handleLinkClick(e) {
shell.openExternal(e.currentTarget.href) shell.openExternal(e.currentTarget.href)
e.preventDefault() e.preventDefault()
} }
render () { render() {
const { notifications } = this.state const { notifications } = this.state
const link = notifications.length > 0 const link =
? <a styleName='notification-link' href={notifications[0].linkUrl} notifications.length > 0 ? (
onClick={(e) => this.handleLinkClick(e)} <a
styleName='notification-link'
href={notifications[0].linkUrl}
onClick={e => this.handleLinkClick(e)}
> >
Info: {notifications[0].text} Info: {notifications[0].text}
</a> </a>
: '' ) : (
''
)
return ( return (
<div styleName='notification-area' style={this.props.style}>{link}</div> <div styleName='notification-area' style={this.props.style}>
{link}
</div>
) )
} }
} }

View File

@@ -30,36 +30,20 @@ body[data-theme="dark"]
&:hover &:hover
color #5CB85C color #5CB85C
apply-theme(theme)
body[data-theme="solarized-dark"] body[data-theme={theme}]
.notification-area .notification-area
background-color none background-color none
.notification-link .notification-link
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
border none border none
background-color $ui-solarized-dark-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
&:hover &:hover
color #5CB85C color get-theme-var(theme, 'button--hover-color')
body[data-theme="monokai"] for theme in 'solarized-dark' 'dracula'
.notification-area apply-theme(theme)
background-color none
.notification-link for theme in $themes
color $ui-monokai-text-color apply-theme(theme)
border none
background-color $ui-monokai-button-backgroundColor
&:hover
color #5CB85C
body[data-theme="dracula"]
.notification-area
background-color none
.notification-link
color $ui-dracula-text-color
border none
background-color $ui-dracula-button-backgroundColor
&:hover
color #ff79c6

View File

@@ -16,17 +16,27 @@ import i18n from 'browser/lib/i18n'
* @return {React.Component} * @return {React.Component}
*/ */
const SideNavFilter = ({ const SideNavFilter = ({
isFolded, isHomeActive, handleAllNotesButtonClick, isFolded,
isStarredActive, handleStarredButtonClick, isTrashedActive, handleTrashedButtonClick, counterDelNote, isHomeActive,
counterTotalNote, counterStarredNote, handleFilterButtonContextMenu handleAllNotesButtonClick,
isStarredActive,
handleStarredButtonClick,
isTrashedActive,
handleTrashedButtonClick,
counterDelNote,
counterTotalNote,
counterStarredNote,
handleFilterButtonContextMenu
}) => ( }) => (
<div styleName={isFolded ? 'menu--folded' : 'menu'}> <div styleName={isFolded ? 'menu--folded' : 'menu'}>
<button
<button styleName={isHomeActive ? 'menu-button--active' : 'menu-button'} styleName={isHomeActive ? 'menu-button--active' : 'menu-button'}
onClick={handleAllNotesButtonClick} onClick={handleAllNotesButtonClick}
> >
<div styleName='iconWrap'> <div styleName='iconWrap'>
<img src={isHomeActive <img
src={
isHomeActive
? '../resources/icon/icon-all-active.svg' ? '../resources/icon/icon-all-active.svg'
: '../resources/icon/icon-all.svg' : '../resources/icon/icon-all.svg'
} }
@@ -36,11 +46,14 @@ const SideNavFilter = ({
<span styleName='counters'>{counterTotalNote}</span> <span styleName='counters'>{counterTotalNote}</span>
</button> </button>
<button styleName={isStarredActive ? 'menu-button-star--active' : 'menu-button'} <button
styleName={isStarredActive ? 'menu-button-star--active' : 'menu-button'}
onClick={handleStarredButtonClick} onClick={handleStarredButtonClick}
> >
<div styleName='iconWrap'> <div styleName='iconWrap'>
<img src={isStarredActive <img
src={
isStarredActive
? '../resources/icon/icon-star-active.svg' ? '../resources/icon/icon-star-active.svg'
: '../resources/icon/icon-star-sidenav.svg' : '../resources/icon/icon-star-sidenav.svg'
} }
@@ -50,11 +63,15 @@ const SideNavFilter = ({
<span styleName='counters'>{counterStarredNote}</span> <span styleName='counters'>{counterStarredNote}</span>
</button> </button>
<button styleName={isTrashedActive ? 'menu-button-trash--active' : 'menu-button'} <button
onClick={handleTrashedButtonClick} onContextMenu={handleFilterButtonContextMenu} styleName={isTrashedActive ? 'menu-button-trash--active' : 'menu-button'}
onClick={handleTrashedButtonClick}
onContextMenu={handleFilterButtonContextMenu}
> >
<div styleName='iconWrap'> <div styleName='iconWrap'>
<img src={isTrashedActive <img
src={
isTrashedActive
? '../resources/icon/icon-trash-active.svg' ? '../resources/icon/icon-trash-active.svg'
: '../resources/icon/icon-trash-sidenav.svg' : '../resources/icon/icon-trash-sidenav.svg'
} }
@@ -63,7 +80,6 @@ const SideNavFilter = ({
<span styleName='menu-button-label'>{i18n.__('Trash')}</span> <span styleName='menu-button-label'>{i18n.__('Trash')}</span>
<span styleName='counters'>{counterDelNote}</span> <span styleName='counters'>{counterDelNote}</span>
</button> </button>
</div> </div>
) )

View File

@@ -1,5 +1,5 @@
.menu .menu
margin-bottom 30px margin-bottom 20px
.menu-button .menu-button
navButtonColor() navButtonColor()
@@ -180,129 +180,51 @@ body[data-theme="dark"]
.menu-button-label .menu-button-label
color $ui-dark-text-color color $ui-dark-text-color
apply-theme(theme)
body[data-theme="solarized-dark"] body[data-theme={theme}]
.menu-button .menu-button
&:active &:active
background-color $ui-solarized-dark-noteList-backgroundColor background-color get-theme-var(theme, 'noteList-backgroundColor')
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
&:hover &:hover
background-color $ui-solarized-dark-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
.menu-button--active .menu-button--active
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
background-color $ui-solarized-dark-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
.menu-button-label .menu-button-label
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
&:hover &:hover
background-color $ui-solarized-dark-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
.menu-button-label .menu-button-label
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
.menu-button-star--active .menu-button-star--active
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
background-color $ui-solarized-dark-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
.menu-button-label .menu-button-label
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
&:hover &:hover
background-color $ui-solarized-dark-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
.menu-button-label .menu-button-label
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
.menu-button-trash--active .menu-button-trash--active
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
background-color $ui-solarized-dark-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
.menu-button-label .menu-button-label
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
&:hover &:hover
background-color $ui-solarized-dark-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
.menu-button-label .menu-button-label
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
body[data-theme="monokai"] for theme in 'solarized-dark' 'dracula'
.menu-button apply-theme(theme)
&:active
background-color $ui-monokai-noteList-backgroundColor
color $ui-monokai-text-color
&:hover
background-color $ui-monokai-button-backgroundColor
color $ui-monokai-text-color
.menu-button--active for theme in $themes
color $ui-monokai-text-color apply-theme(theme)
background-color $ui-monokai-button-backgroundColor
.menu-button-label
color $ui-monokai-text-color
&:hover
background-color $ui-monokai-button-backgroundColor
color $ui-monokai-text-color
.menu-button-label
color $ui-monokai-text-color
.menu-button-star--active
color $ui-monokai-text-color
background-color $ui-monokai-button-backgroundColor
.menu-button-label
color $ui-monokai-text-color
&:hover
background-color $ui-monokai-button-backgroundColor
color $ui-monokai-text-color
.menu-button-label
color $ui-monokai-text-color
.menu-button-trash--active
color $ui-monokai-text-color
background-color $ui-monokai-button-backgroundColor
.menu-button-label
color $ui-monokai-text-color
&:hover
background-color $ui-monokai-button-backgroundColor
color $ui-monokai-text-color
.menu-button-label
color $ui-monokai-text-color
body[data-theme="dracula"]
.menu-button
&:active
background-color $ui-dracula-noteList-backgroundColor
color $ui-dracula-text-color
&:hover
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
.menu-button--active
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
.menu-button-label
color $ui-dracula-text-color
&:hover
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
.menu-button-label
color $ui-dracula-text-color
.menu-button-star--active
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
.menu-button-label
color $ui-dracula-text-color
&:hover
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
.menu-button-label
color $ui-dracula-text-color
.menu-button-trash--active
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
.menu-button-label
color $ui-dracula-text-color
&:hover
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
.menu-button-label
color $ui-dracula-text-color

View File

@@ -5,7 +5,7 @@ import context from 'browser/lib/context'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
class SnippetTab extends React.Component { class SnippetTab extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
@@ -14,7 +14,7 @@ class SnippetTab extends React.Component {
} }
} }
componentWillUpdate (nextProps) { componentWillUpdate(nextProps) {
if (nextProps.snippet.name !== this.props.snippet.name) { if (nextProps.snippet.name !== this.props.snippet.name) {
this.setState({ this.setState({
name: nextProps.snippet.name name: nextProps.snippet.name
@@ -22,34 +22,34 @@ class SnippetTab extends React.Component {
} }
} }
handleClick (e) { handleClick(e) {
this.props.onClick(e) this.props.onClick(e)
} }
handleContextMenu (e) { handleContextMenu(e) {
context.popup([ context.popup([
{ {
label: i18n.__('Rename'), label: i18n.__('Rename'),
click: (e) => this.handleRenameClick(e) click: e => this.handleRenameClick(e)
} }
]) ])
} }
handleRenameClick (e) { handleRenameClick(e) {
this.startRenaming() this.startRenaming()
} }
handleNameInputBlur (e) { handleNameInputBlur(e) {
this.handleRename() this.handleRename()
} }
handleNameInputChange (e) { handleNameInputChange(e) {
this.setState({ this.setState({
name: e.target.value name: e.target.value
}) })
} }
handleNameInputKeyDown (e) { handleNameInputKeyDown(e) {
switch (e.keyCode) { switch (e.keyCode) {
case 13: case 13:
this.handleRename() this.handleRename()
@@ -63,84 +63,87 @@ class SnippetTab extends React.Component {
} }
} }
handleRename () { handleRename() {
this.setState({ this.setState(
{
isRenaming: false isRenaming: false
}, () => { },
() => {
if (this.props.snippet.name !== this.state.name) { if (this.props.snippet.name !== this.state.name) {
this.props.onRename(this.state.name) this.props.onRename(this.state.name)
} }
}) }
)
} }
handleDeleteButtonClick (e) { handleDeleteButtonClick(e) {
this.props.onDelete(e) this.props.onDelete(e)
} }
startRenaming () { startRenaming() {
this.setState({ this.setState(
{
isRenaming: true isRenaming: true
}, () => { },
() => {
this.refs.name.focus() this.refs.name.focus()
this.refs.name.select() this.refs.name.select()
}) }
)
} }
handleDragStart (e) { handleDragStart(e) {
e.dataTransfer.dropEffect = 'move' e.dataTransfer.dropEffect = 'move'
this.props.onDragStart(e) this.props.onDragStart(e)
} }
handleDrop (e) { handleDrop(e) {
this.props.onDrop(e) this.props.onDrop(e)
} }
render () { render() {
const { isActive, snippet, isDeletable } = this.props const { isActive, snippet, isDeletable } = this.props
return ( return (
<div styleName={isActive <div styleName={isActive ? 'root--active' : 'root'}>
? 'root--active' {!this.state.isRenaming ? (
: 'root' <button
} styleName='button'
> onClick={e => this.handleClick(e)}
{!this.state.isRenaming onDoubleClick={e => this.handleRenameClick(e)}
? <button styleName='button' onContextMenu={e => this.handleContextMenu(e)}
onClick={(e) => this.handleClick(e)} onDragStart={e => this.handleDragStart(e)}
onDoubleClick={(e) => this.handleRenameClick(e)} onDrop={e => this.handleDrop(e)}
onContextMenu={(e) => this.handleContextMenu(e)}
onDragStart={(e) => this.handleDragStart(e)}
onDrop={(e) => this.handleDrop(e)}
draggable='true' draggable='true'
> >
{snippet.name.trim().length > 0 {snippet.name.trim().length > 0 ? (
? snippet.name snippet.name
: <span> ) : (
{i18n.__('Unnamed')} <span>{i18n.__('Unnamed')}</span>
</span> )}
}
</button> </button>
: <input styleName='input' ) : (
<input
styleName='input'
ref='name' ref='name'
value={this.state.name} value={this.state.name}
onChange={(e) => this.handleNameInputChange(e)} onChange={e => this.handleNameInputChange(e)}
onBlur={(e) => this.handleNameInputBlur(e)} onBlur={e => this.handleNameInputBlur(e)}
onKeyDown={(e) => this.handleNameInputKeyDown(e)} onKeyDown={e => this.handleNameInputKeyDown(e)}
/> />
} )}
{isDeletable && {isDeletable && (
<button styleName='deleteButton' <button
onClick={(e) => this.handleDeleteButtonClick(e)} styleName='deleteButton'
onClick={e => this.handleDeleteButtonClick(e)}
> >
<i className='fa fa-times' /> <i className='fa fa-times' />
</button> </button>
} )}
</div> </div>
) )
} }
} }
SnippetTab.propTypes = { SnippetTab.propTypes = {}
}
export default CSSModules(SnippetTab, styles) export default CSSModules(SnippetTab, styles)

View File

@@ -100,61 +100,28 @@ body[data-theme="dark"]
color $ui-dark-text-color color $ui-dark-text-color
transition 0.15s transition 0.15s
body[data-theme="solarized-dark"] apply-theme(theme)
body[data-theme={theme}]
.root .root
border-color $ui-solarized-dark-borderColor border-color get-theme-var(theme, 'borderColor')
&:hover &:hover
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color get-theme-var(theme, 'noteDetail-backgroundColor')
transition 0.15s transition 0.15s
.deleteButton .deleteButton
color $ui-solarized-dark-button--active-color color get-theme-var(theme, 'text-color')
transition 0.15s transition 0.15s
.button .button
color $ui-solarized-dark-button--active-color color get-theme-var(theme, 'text-color')
transition 0.15s transition 0.15s
.root--active .root--active
color $ui-solarized-dark-button--active-color color get-theme-var(theme, 'active-color')
background-color $ui-solarized-dark-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
border-color $ui-solarized-dark-borderColor border-color get-theme-var(theme, 'borderColor')
.deleteButton .deleteButton
color $ui-solarized-dark-button--active-color color get-theme-var(theme, 'text-color')
.button .button
color $ui-solarized-dark-button--active-color color get-theme-var(theme, 'active-color')
.button
border none
color $ui-solarized-dark-text-color
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
.input
background-color $ui-solarized-dark-noteDetail-backgroundColor
color $ui-solarized-dark-button--active-color
transition 0.15s
body[data-theme="monokai"]
.root
border-color $ui-monokai-borderColor
&:hover
background-color $ui-monokai-noteDetail-backgroundColor
transition 0.15s
.deleteButton
color $ui-monokai-text-color
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 .button
border none border none
@@ -164,39 +131,12 @@ body[data-theme="monokai"]
border-left 4px solid transparent border-left 4px solid transparent
.input .input
background-color $ui-monokai-noteDetail-backgroundColor background-color get-theme-var(theme, 'noteDetail-backgroundColor')
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
transition 0.15s transition 0.15s
body[data-theme="dracula"] for theme in 'solarized-dark' 'dracula'
.root apply-theme(theme)
border-color $ui-dracula-borderColor
&:hover
background-color $ui-dracula-noteDetail-backgroundColor
transition 0.15s
.deleteButton
color $ui-dracula-text-color
transition 0.15s
.button
color $ui-dracula-text-color
transition 0.15s
.root--active for theme in $themes
color $ui-dracula-text-color apply-theme(theme)
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-inactive-text-color
background-color transparent
transition color background-color 0.15s
border-left 4px solid transparent
.input
background-color $ui-dracula-noteDetail-backgroundColor
color $ui-dracula-text-color

View File

@@ -21,7 +21,9 @@ const FolderIcon = ({ className, color, isActive }) => {
/** /**
* @param {boolean} isActive * @param {boolean} isActive
* @param {object} tooltipRef,
* @param {Function} handleButtonClick * @param {Function} handleButtonClick
* @param {Function} handleMouseEnter
* @param {Function} handleContextMenu * @param {Function} handleContextMenu
* @param {string} folderName * @param {string} folderName
* @param {string} folderColor * @param {string} folderColor
@@ -35,7 +37,9 @@ const FolderIcon = ({ className, color, isActive }) => {
const StorageItem = ({ const StorageItem = ({
styles, styles,
isActive, isActive,
tooltipRef,
handleButtonClick, handleButtonClick,
handleMouseEnter,
handleContextMenu, handleContextMenu,
folderName, folderName,
folderColor, folderColor,
@@ -49,13 +53,15 @@ const StorageItem = ({
<button <button
styleName={isActive ? 'folderList-item--active' : 'folderList-item'} styleName={isActive ? 'folderList-item--active' : 'folderList-item'}
onClick={handleButtonClick} onClick={handleButtonClick}
onMouseEnter={handleMouseEnter}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
onDrop={handleDrop} onDrop={handleDrop}
onDragEnter={handleDragEnter} onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
> >
{!isFolded && {!isFolded && (
<DraggableIcon className={styles['folderList-item-reorder']} />} <DraggableIcon className={styles['folderList-item-reorder']} />
)}
<span <span
styleName={ styleName={
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name' isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
@@ -70,18 +76,23 @@ const StorageItem = ({
? _.truncate(folderName, { length: 1, omission: '' }) ? _.truncate(folderName, { length: 1, omission: '' })
: folderName} : folderName}
</span> </span>
{!isFolded && {!isFolded && _.isNumber(noteCount) && (
_.isNumber(noteCount) && <span styleName='folderList-item-noteCount'>{noteCount}</span>
<span styleName='folderList-item-noteCount'>{noteCount}</span>} )}
{isFolded && {isFolded && (
<span styleName='folderList-item-tooltip'>{folderName}</span>} <span styleName='folderList-item-tooltip' ref={tooltipRef}>
{folderName}
</span>
)}
</button> </button>
) )
} }
StorageItem.propTypes = { StorageItem.propTypes = {
isActive: PropTypes.bool.isRequired, isActive: PropTypes.bool.isRequired,
tooltipRef: PropTypes.object,
handleButtonClick: PropTypes.func, handleButtonClick: PropTypes.func,
handleMouseEnter: PropTypes.func,
handleContextMenu: PropTypes.func, handleContextMenu: PropTypes.func,
folderName: PropTypes.string.isRequired, folderName: PropTypes.string.isRequired,
folderColor: PropTypes.string, folderColor: PropTypes.string,

View File

@@ -60,6 +60,7 @@
border-bottom-right-radius 2px border-bottom-right-radius 2px
height 34px height 34px
line-height 32px line-height 32px
transition-property opacity
.folderList-item:hover, .folderList-item--active:hover .folderList-item:hover, .folderList-item--active:hover
.folderList-item-tooltip .folderList-item-tooltip
@@ -120,59 +121,28 @@ body[data-theme="dark"]
color $ui-dark-text-color color $ui-dark-text-color
background-color alpha($ui-dark-button--active-backgroundColor, 50%) background-color alpha($ui-dark-button--active-backgroundColor, 50%)
body[data-theme="solarized-dark"] apply-theme(theme)
body[data-theme={theme}]
.folderList-item .folderList-item
&:hover &:hover
background-color $ui-solarized-dark-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
&:active &:active
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
background-color $ui-solarized-dark-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
.folderList-item--active .folderList-item--active
@extend .folderList-item @extend .folderList-item
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
background-color $ui-solarized-dark-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
&:active &:active
background-color $ui-solarized-dark-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
&:hover &:hover
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
background-color $ui-solarized-dark-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
body[data-theme="monokai"] for theme in 'solarized-dark' 'dracula'
.folderList-item apply-theme(theme)
&:hover
background-color $ui-monokai-button-backgroundColor
color $ui-monokai-text-color
&:active
color $ui-monokai-text-color
background-color $ui-monokai-button-backgroundColor
.folderList-item--active for theme in $themes
@extend .folderList-item apply-theme(theme)
color $ui-monokai-text-color
background-color $ui-monokai-button-backgroundColor
&:active
background-color $ui-monokai-button-backgroundColor
&:hover
color $ui-monokai-text-color
background-color $ui-monokai-button-backgroundColor
body[data-theme="dracula"]
.folderList-item
&:hover
background-color $ui-dracula-button-backgroundColor
color $ui-dracula-text-color
&:active
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
.folderList-item--active
@extend .folderList-item
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
&:active
background-color $ui-dracula-button-backgroundColor
&:hover
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor

View File

@@ -1,18 +1,20 @@
/** /**
* @fileoverview Micro component for showing StorageList * @fileoverview Micro component for showing StorageList
*/ */
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import styles from './StorageList.styl' import styles from './StorageList.styl'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
/** /**
* @param {Array} storageList * @param {Array} storageList
*/ */
const StorageList = ({storageList, isFolded}) => ( const StorageList = ({ storageList, isFolded }) => (
<div styleName={isFolded ? 'storageList-folded' : 'storageList'}> <div styleName={isFolded ? 'storageList-folded' : 'storageList'}>
{storageList.length > 0 ? storageList : ( {storageList.length > 0 ? (
storageList
) : (
<div styleName='storageList-empty'>No storage mount.</div> <div styleName='storageList-empty'>No storage mount.</div>
)} )}
</div> </div>

View File

@@ -1,30 +1,58 @@
/** /**
* @fileoverview Micro component for showing TagList. * @fileoverview Micro component for showing TagList.
*/ */
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import styles from './TagListItem.styl' import styles from './TagListItem.styl'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
/** /**
* @param {string} name * @param {string} name
* @param {Function} handleClickTagListItem * @param {Function} handleClickTagListItem
* @param {Function} handleClickNarrowToTag * @param {Function} handleClickNarrowToTag
* @param {boolean} isActive * @param {boolean} isActive
* @param {boolean} isRelated * @param {boolean} isRelated
* @param {string} bgColor tab backgroundColor * @param {string} bgColor tab backgroundColor
*/ */
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count, color}) => ( const TagListItem = ({
<div styleName='tagList-itemContainer' onContextMenu={e => handleContextMenu(e, name)}> name,
{isRelated handleClickTagListItem,
? <button styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} onClick={() => handleClickNarrowToTag(name)}> 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)}
>
<i className={isActive ? 'fa fa-minus-circle' : 'fa fa-plus-circle'} /> <i className={isActive ? 'fa fa-minus-circle' : 'fa fa-plus-circle'} />
</button> </button>
: <div styleName={isActive ? 'tagList-itemNarrow-active' : 'tagList-itemNarrow'} /> ) : (
<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'}} /> )}
<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'> <span styleName='tagList-item-name'>
{`# ${name}`} {`# ${name}`}
<span styleName='tagList-item-count'>{count !== 0 ? count : ''}</span> <span styleName='tagList-item-count'>{count !== 0 ? count : ''}</span>

View File

@@ -94,23 +94,30 @@ body[data-theme="white"]
.tagList-item-count .tagList-item-count
color $ui-text-color color $ui-text-color
body[data-theme="dark"] apply-theme(theme)
body[data-theme={theme}]
.tagList-item .tagList-item
color $ui-dark-inactive-text-color color get-theme-var(theme, 'inactive-text-color')
&:hover &:hover
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
background-color alpha($ui-dark-button--active-backgroundColor, 20%) background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 20%)
&:active &:active
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
background-color $ui-dark-button--active-backgroundColor background-color get-theme-var(theme, 'button--active-backgroundColor')
.tagList-item-active .tagList-item-active
background-color $ui-dark-button--active-backgroundColor background-color get-theme-var(theme, 'button--active-backgroundColor')
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
&:active &:active
background-color alpha($ui-dark-button--active-backgroundColor, 50%) background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 50%)
&:hover &:hover
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
background-color alpha($ui-dark-button--active-backgroundColor, 50%) background-color alpha(get-theme-var(theme, 'button--active-backgroundColor'), 50%)
.tagList-item-count .tagList-item-count
color $ui-dark-button--active-color color get-theme-var(theme, 'button--active-color')
for theme in 'dark'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -11,17 +11,20 @@ import styles from './TodoListPercentage.styl'
* @param {number} percentageOfTodo * @param {number} percentageOfTodo
*/ */
const TodoListPercentage = ({ const TodoListPercentage = ({ percentageOfTodo, onClearCheckboxClick }) => (
percentageOfTodo, onClearCheckboxClick <div
}) => ( styleName='percentageBar'
<div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}> style={{ display: isNaN(percentageOfTodo) ? 'none' : '' }}
<div styleName='progressBar' style={{width: `${percentageOfTodo}%`}}> >
<div styleName='progressBar' style={{ width: `${percentageOfTodo}%` }}>
<div styleName='progressBarInner'> <div styleName='progressBarInner'>
<p styleName='percentageText'>{percentageOfTodo}%</p> <p styleName='percentageText'>{percentageOfTodo}%</p>
</div> </div>
</div> </div>
<div styleName='todoClear'> <div styleName='todoClear'>
<p styleName='todoClearText' onClick={(e) => onClearCheckboxClick(e)}>clear</p> <p styleName='todoClearText' onClick={e => onClearCheckboxClick(e)}>
clear
</p>
</div> </div>
</div> </div>
) )

View File

@@ -71,25 +71,19 @@ body[data-theme="solarized-dark"]
.todoClearText .todoClearText
color #fdf6e3 color #fdf6e3
body[data-theme="monokai"] apply-theme(theme)
body[data-theme={theme}]
.percentageBar .percentageBar
background-color: $ui-monokai-borderColor background-color: get-theme-var(theme, 'borderColor')
.progressBar .progressBar
background-color $ui-monokai-active-color background-color get-theme-var(theme, 'active-color')
.percentageText .percentageText
color $ui-monokai-text-color color get-theme-var(theme, 'text-color')
body[data-theme="dracula"] for theme in 'dracula'
.percentageBar apply-theme(theme)
background-color $ui-dracula-borderColor
.progressBar for theme in $themes
background-color: $ui-dracula-active-color apply-theme(theme)
.percentageText
color $ui-dracula-text-color
.percentageText
color $ui-dracula-text-color

View File

@@ -8,18 +8,21 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './TodoProcess.styl' import styles from './TodoProcess.styl'
const TodoProcess = ({ const TodoProcess = ({
todoStatus: { todoStatus: { total: totalTodo, completed: completedTodo }
total: totalTodo,
completed: completedTodo
}
}) => ( }) => (
<div styleName='todo-process' style={{display: totalTodo > 0 ? '' : 'none'}}> <div
styleName='todo-process'
style={{ display: totalTodo > 0 ? '' : 'none' }}
>
<div styleName='todo-process-text'> <div styleName='todo-process-text'>
<i className='fa fa-fw fa-check-square-o' /> <i className='fa fa-fw fa-check-square-o' />
{completedTodo} of {totalTodo} {completedTodo} of {totalTodo}
</div> </div>
<div styleName='todo-process-bar'> <div styleName='todo-process-bar'>
<div styleName='todo-process-bar--inner' style={{width: parseInt(completedTodo / totalTodo * 100) + '%'}} /> <div
styleName='todo-process-bar--inner'
style={{ width: parseInt((completedTodo / totalTodo) * 100) + '%' }}
/>
</div> </div>
</div> </div>
) )

View File

@@ -124,40 +124,34 @@ hr
border-bottom solid 1px borderColor border-bottom solid 1px borderColor
margin 15px 0 margin 15px 0
h1, h2, h3, h4, h5, h6 h1, h2, h3, h4, h5, h6
margin 1em 0 1.5em
line-height 1.4
font-weight bold font-weight bold
word-wrap break-word word-wrap break-word
padding .2em 0 .2em
h1 h1
font-size 2.55em font-size 2.55em
padding-bottom 0.3em line-height 1.2
line-height 1.2em
border-bottom solid 1px borderColor border-bottom solid 1px borderColor
margin 1em 0 0.44em
&:first-child &:first-child
margin-top 0 margin-top 0
h2 h2
font-size 1.75em font-size 1.75em
padding-bottom 0.3em line-height 1.225
line-height 1.225em
border-bottom solid 1px borderColor border-bottom solid 1px borderColor
margin 1em 0 0.57em
&:first-child &:first-child
margin-top 0 margin-top 0
h3 h3
font-size 1.5em font-size 1.5em
line-height 1.43em line-height 1.43
margin 1em 0 0.66em
h4 h4
font-size 1.25em font-size 1.25em
line-height 1.4em line-height 1.4
margin 1em 0 0.8em
h5 h5
font-size 1em font-size 1em
line-height 1.4em line-height 1.1
margin 1em 0 1em
h6 h6
font-size 1em font-size 1em
line-height 1.4em
margin 1em 0 1em
color #777 color #777
p p
line-height 1.6em line-height 1.6em
@@ -363,7 +357,10 @@ admonition_types = {
danger: {color: #c2185b, icon: "block"}, danger: {color: #c2185b, icon: "block"},
caution: {color: #ffa726, icon: "warning"}, caution: {color: #ffa726, icon: "warning"},
error: {color: #d32f2f, icon: "error_outline"}, error: {color: #d32f2f, icon: "error_outline"},
attention: {color: #455a64, icon: "priority_high"} question: {color: #64dd17, icon: "help_outline"},
quote: {color: #9e9e9e, icon: "format_quote"},
abstract: {color: #00b0ff, icon: "subject"},
attention: {color: #455a64, icon: "priority_high"},
} }
for name, val in admonition_types for name, val in admonition_types
@@ -424,6 +421,9 @@ pre.fence
canvas, svg canvas, svg
max-width 100% !important max-width 100% !important
svg[ratio]
width 100%
.gallery .gallery
width 100% width 100%
height 50vh height 50vh
@@ -444,6 +444,44 @@ pre.fence
color $ui-text-color color $ui-text-color
background-color $ui-tag-backgroundColor background-color $ui-tag-backgroundColor
.markdownIt-TOC-wrapper
list-style none
position fixed
right 0
top 0
margin-left 15px
z-index 1000
transition transform .2s ease-in-out
transform translateX(100%)
.markdownIt-TOC
display block
max-height 90vh
overflow-y auto
padding 25px
padding-left 38px
&,
&:before
background-color $ui-dark-backgroundColor
color: $ui-dark-text-color
&:hover
transform translateX(-15px)
&:before
content 'TOC'
position absolute
width 60px
height 30px
top 60px
left -29px
display flex
align-items center
justify-content center
transform-origin top left
transform rotate(-90deg)
themeDarkBackground = darken(#21252B, 10%) themeDarkBackground = darken(#21252B, 10%)
themeDarkText = #f9f9f9 themeDarkText = #f9f9f9
themeDarkBorder = lighten(themeDarkBackground, 20%) themeDarkBorder = lighten(themeDarkBackground, 20%)
@@ -511,137 +549,63 @@ body[data-theme="dark"]
color $ui-dark-text-color color $ui-dark-text-color
background-color $ui-dark-tag-backgroundColor background-color $ui-dark-tag-backgroundColor
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor .markdownIt-TOC-wrapper
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%) &,
themeSolarizedDarkTableHead = themeSolarizedDarkTableEven &:before
themeSolarizedDarkTableBorder = themeDarkBorder background-color darken(themeDarkBackground, 5%)
color themeDarkText
body[data-theme="solarized-dark"] apply-theme(theme)
color $ui-solarized-dark-text-color body[data-theme={theme}]
color get-theme-var(theme, 'text-color')
border-color themeDarkBorder border-color themeDarkBorder
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color get-theme-var(theme, 'noteDetail-backgroundColor')
table table
thead thead
tr tr
background-color themeSolarizedDarkTableHead background-color get-theme-var(theme, 'table-head-backgroundColor')
th th
border-color themeSolarizedDarkTableBorder border-color get-theme-var(theme, 'table-borderColor')
&:last-child &:last-child
border-right solid 1px themeSolarizedDarkTableBorder border-right solid 1px get-theme-var(theme, 'table-borderColor')
tbody tbody
tr:nth-child(2n + 1) tr:nth-child(2n + 1)
background-color themeSolarizedDarkTableOdd background-color get-theme-var(theme, 'table-odd-backgroundColor')
tr:nth-child(2n) tr:nth-child(2n)
background-color themeSolarizedDarkTableEven background-color get-theme-var(theme, 'table-even-backgroundColor')
td td
border-color themeSolarizedDarkTableBorder border-color get-theme-var(theme, 'table-borderColor')
&:last-child &:last-child
border-right solid 1px themeSolarizedDarkTableBorder border-right solid 1px get-theme-var(theme, 'table-borderColor')
dl
border-color themeDarkBorder
background-color themeSolarizedDarkTableHead
dt
border-color themeDarkBorder
dd
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
themeMonokaiTableBorder = themeDarkBorder
body[data-theme="monokai"]
color $ui-monokai-text-color
border-color themeDarkBorder
background-color $ui-monokai-noteDetail-backgroundColor
table
thead
tr
background-color themeMonokaiTableHead
th
border-color themeMonokaiTableBorder
&:last-child
border-right solid 1px themeMonokaiTableBorder
tbody
tr:nth-child(2n + 1)
background-color themeMonokaiTableOdd
tr:nth-child(2n)
background-color themeMonokaiTableEven
td
border-color themeMonokaiTableBorder
&:last-child
border-right solid 1px themeMonokaiTableBorder
kbd kbd
background-color themeDarkBackground background-color get-theme-var(theme, 'kbd-backgroundColor')
color get-theme-var(theme, 'kbd-color')
dl dl
border-color themeDarkBorder border-color themeDarkBorder
background-color themeMonokaiTableHead background-color get-theme-var(theme, 'table-head-backgroundColor')
dt dt
border-color themeDarkBorder border-color themeDarkBorder
dd dd
border-color themeDarkBorder border-color themeDarkBorder
background-color $ui-monokai-noteDetail-backgroundColor background-color get-theme-var(theme, 'noteDetail-backgroundColor')
pre.fence pre.fence
.gallery .gallery
.carousel-main, .carousel-footer .carousel-main, .carousel-footer
background-color $ui-monokai-noteDetail-backgroundColor background-color get-theme-var(theme, 'noteDetail-backgroundColor')
.prev, .next .prev, .next
color $ui-monokai-button--active-color color get-theme-var(theme, 'button--active-color')
background-color $ui-monokai-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
themeDraculaTableOdd = $ui-dracula-noteDetail-backgroundColor .markdownIt-TOC-wrapper
themeDraculaTableEven = darken($ui-dracula-noteDetail-backgroundColor, 10%) &,
themeDraculaTableHead = themeDraculaTableEven &:before
themeDraculaTableBorder = themeDarkBorder background-color darken(get-theme-var(theme, 'noteDetail-backgroundColor'), 15%)
color themeDarkText
body[data-theme="dracula"] for theme in 'solarized-dark' 'dracula'
color $ui-dracula-text-color apply-theme(theme)
border-color themeDarkBorder
background-color $ui-dracula-noteDetail-backgroundColor
table
thead
tr
background-color themeDraculaTableHead
th
border-color themeDraculaTableBorder
&:last-child
border-right solid 1px themeDraculaTableBorder
tbody
tr:nth-child(2n + 1)
background-color themeDraculaTableOdd
tr:nth-child(2n)
background-color themeDraculaTableEven
td
border-color themeDraculaTableBorder
&:last-child
border-right solid 1px themeDraculaTableBorder
kbd
background-color themeDarkBackground
dl for theme in $themes
border-color themeDarkBorder apply-theme(theme)
background-color themeDraculaTableHead
dt
border-color themeDarkBorder
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

@@ -1,4 +1,5 @@
import mermaidAPI from 'mermaid' import mermaidAPI from 'mermaid'
import uiThemes from 'browser/lib/ui-themes'
// fixes bad styling in the mermaid dark theme // fixes bad styling in the mermaid dark theme
const darkThemeStyling = ` const darkThemeStyling = `
@@ -6,11 +7,11 @@ const darkThemeStyling = `
fill: white; fill: white;
}` }`
function getRandomInt (min, max) { function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min return Math.floor(Math.random() * (max - min)) + min
} }
function getId () { function getId() {
const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
let id = 'm-' let id = 'm-'
for (let i = 0; i < 7; i++) { for (let i = 0; i < 7; i++) {
@@ -19,20 +20,49 @@ function getId () {
return id return id
} }
function render (element, content, theme) { function render(element, content, theme, enableHTMLLabel) {
try { try {
const height = element.attributes.getNamedItem('data-height') const height = element.attributes.getNamedItem('data-height')
if (height && height.value !== 'undefined') { const isPredefined = height && height.value !== 'undefined'
if (isPredefined) {
element.style.height = height.value + 'vh' element.style.height = height.value + 'vh'
} }
const isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula'
const isDarkTheme = uiThemes.some(
item => item.name === theme && item.isDark
)
mermaidAPI.initialize({ mermaidAPI.initialize({
theme: isDarkTheme ? 'dark' : 'default', theme: isDarkTheme ? 'dark' : 'default',
themeCSS: isDarkTheme ? darkThemeStyling : '', themeCSS: isDarkTheme ? darkThemeStyling : '',
useMaxWidth: false flowchart: {
htmlLabels: enableHTMLLabel
},
gantt: {
useWidth: element.clientWidth
}
}) })
mermaidAPI.render(getId(), content, (svgGraph) => {
mermaidAPI.render(getId(), content, svgGraph => {
element.innerHTML = svgGraph element.innerHTML = svgGraph
if (!isPredefined) {
const el = element.firstChild
const viewBox = el.getAttribute('viewBox').split(' ')
let ratio = viewBox[2] / viewBox[3]
if (el.style.maxWidth) {
const maxWidth = parseFloat(el.style.maxWidth)
ratio *= el.parentNode.clientWidth / maxWidth
}
el.setAttribute('ratio', ratio)
el.setAttribute('height', el.parentNode.clientWidth / ratio)
console.log(el)
}
}) })
} catch (e) { } catch (e) {
element.className = 'mermaid-error' element.className = 'mermaid-error'

View File

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

View File

@@ -11,6 +11,10 @@ const languages = [
name: 'Chinese (zh-TW)', name: 'Chinese (zh-TW)',
locale: 'zh-TW' locale: 'zh-TW'
}, },
{
name: 'Czech',
locale: 'cs'
},
{ {
name: 'Danish', name: 'Danish',
locale: 'da' locale: 'da'
@@ -74,13 +78,13 @@ const languages = [
] ]
module.exports = { module.exports = {
getLocales () { getLocales() {
return languages.reduce(function (localeList, locale) { return languages.reduce(function(localeList, locale) {
localeList.push(locale.locale) localeList.push(locale.locale)
return localeList return localeList
}, []) }, [])
}, },
getLanguages () { getLanguages() {
return languages return languages
} }
} }

View File

@@ -1,43 +1,43 @@
class MutableMap { class MutableMap {
constructor (iterable) { constructor(iterable) {
this._map = new Map(iterable) this._map = new Map(iterable)
Object.defineProperty(this, 'size', { Object.defineProperty(this, 'size', {
get: () => this._map.size, get: () => this._map.size,
set: function (value) { set: function(value) {
this['size'] = value this['size'] = value
} }
}) })
} }
get (...args) { get(...args) {
return this._map.get(...args) return this._map.get(...args)
} }
set (...args) { set(...args) {
return this._map.set(...args) return this._map.set(...args)
} }
delete (...args) { delete(...args) {
return this._map.delete(...args) return this._map.delete(...args)
} }
has (...args) { has(...args) {
return this._map.has(...args) return this._map.has(...args)
} }
clear (...args) { clear(...args) {
return this._map.clear(...args) return this._map.clear(...args)
} }
forEach (...args) { forEach(...args) {
return this._map.forEach(...args) return this._map.forEach(...args)
} }
[Symbol.iterator] () { [Symbol.iterator]() {
return this._map[Symbol.iterator]() return this._map[Symbol.iterator]()
} }
map (cb) { map(cb) {
const result = [] const result = []
for (const [key, value] of this._map) { for (const [key, value] of this._map) {
result.push(cb(value, key)) result.push(cb(value, key))
@@ -45,7 +45,7 @@ class MutableMap {
return result return result
} }
toJS () { toJS() {
const result = {} const result = {}
for (let [key, value] of this._map) { for (let [key, value] of this._map) {
if (value instanceof MutableSet || value instanceof MutableMap) { if (value instanceof MutableSet || value instanceof MutableMap) {
@@ -58,42 +58,42 @@ class MutableMap {
} }
class MutableSet { class MutableSet {
constructor (iterable) { constructor(iterable) {
this._set = new Set(iterable) this._set = new Set(iterable)
Object.defineProperty(this, 'size', { Object.defineProperty(this, 'size', {
get: () => this._set.size, get: () => this._set.size,
set: function (value) { set: function(value) {
this['size'] = value this['size'] = value
} }
}) })
} }
add (...args) { add(...args) {
return this._set.add(...args) return this._set.add(...args)
} }
delete (...args) { delete(...args) {
return this._set.delete(...args) return this._set.delete(...args)
} }
forEach (...args) { forEach(...args) {
return this._set.forEach(...args) return this._set.forEach(...args)
} }
[Symbol.iterator] () { [Symbol.iterator]() {
return this._set[Symbol.iterator]() return this._set[Symbol.iterator]()
} }
map (cb) { map(cb) {
const result = [] const result = []
this._set.forEach(function (value, key) { this._set.forEach(function(value, key) {
result.push(cb(value, key)) result.push(cb(value, key))
}) })
return result return result
} }
toJS () { toJS() {
return Array.from(this._set) return Array.from(this._set)
} }
} }

View File

@@ -5,13 +5,13 @@ const BOOSTNOTERC = '.boostnoterc'
const homePath = global.process.env.HOME || global.process.env.USERPROFILE const homePath = global.process.env.HOME || global.process.env.USERPROFILE
const _boostnotercPath = path.join(homePath, BOOSTNOTERC) const _boostnotercPath = path.join(homePath, BOOSTNOTERC)
export function parse (boostnotercPath = _boostnotercPath) { export function parse(boostnotercPath = _boostnotercPath) {
if (!sander.existsSync(boostnotercPath)) return {} if (!sander.existsSync(boostnotercPath)) return {}
try { try {
return JSON.parse(sander.readFileSync(boostnotercPath).toString()) return JSON.parse(sander.readFileSync(boostnotercPath).toString())
} catch (e) { } catch (e) {
console.warn(e) console.warn(e)
console.warn('Your .boostnoterc is broken so it\'s not used.') console.warn("Your .boostnoterc is broken so it's not used.")
return {} return {}
} }
} }

View File

@@ -3,13 +3,14 @@ import fs from 'fs'
import consts from './consts' import consts from './consts'
class SnippetManager { class SnippetManager {
constructor () { constructor() {
this.defaultSnippet = [ this.defaultSnippet = [
{ {
id: crypto.randomBytes(16).toString('hex'), id: crypto.randomBytes(16).toString('hex'),
name: 'Dummy text', name: 'Dummy text',
prefix: ['lorem', 'ipsum'], 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.' 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.snippets = []
@@ -18,7 +19,7 @@ class SnippetManager {
this.assignSnippets = this.assignSnippets.bind(this) this.assignSnippets = this.assignSnippets.bind(this)
} }
init () { init() {
if (fs.existsSync(consts.SNIPPET_FILE)) { if (fs.existsSync(consts.SNIPPET_FILE)) {
try { try {
this.snippets = JSON.parse( this.snippets = JSON.parse(
@@ -37,11 +38,11 @@ class SnippetManager {
this.snippets = this.defaultSnippet this.snippets = this.defaultSnippet
} }
assignSnippets (snippets) { assignSnippets(snippets) {
this.snippets = snippets this.snippets = snippets
} }
expandSnippet (wordBeforeCursor, cursor, cm) { expandSnippet(wordBeforeCursor, cursor, cm) {
const templateCursorString = ':{}' const templateCursorString = ':{}'
for (let i = 0; i < this.snippets.length; i++) { for (let i = 0; i < this.snippets.length; i++) {
if (this.snippets[i].prefix.indexOf(wordBeforeCursor.text) === -1) { if (this.snippets[i].prefix.indexOf(wordBeforeCursor.text) === -1) {

View File

@@ -1,44 +1,44 @@
import { Point } from '@susisu/mte-kernel' import { Point } from '@susisu/mte-kernel'
export default class TextEditorInterface { export default class TextEditorInterface {
constructor (editor) { constructor(editor) {
this.editor = editor this.editor = editor
this.doc = editor.getDoc() this.doc = editor.getDoc()
this.transaction = false this.transaction = false
} }
getCursorPosition () { getCursorPosition() {
const { line, ch } = this.doc.getCursor() const { line, ch } = this.doc.getCursor()
return new Point(line, ch) return new Point(line, ch)
} }
setCursorPosition (pos) { setCursorPosition(pos) {
this.doc.setCursor({ this.doc.setCursor({
line: pos.row, line: pos.row,
ch: pos.column ch: pos.column
}) })
} }
setSelectionRange (range) { setSelectionRange(range) {
this.doc.setSelection( this.doc.setSelection(
{ line: range.start.row, ch: range.start.column }, { line: range.start.row, ch: range.start.column },
{ line: range.end.row, ch: range.end.column } { line: range.end.row, ch: range.end.column }
) )
} }
getLastRow () { getLastRow() {
return this.doc.lineCount() - 1 return this.doc.lineCount() - 1
} }
acceptsTableEdit () { acceptsTableEdit() {
return true return true
} }
getLine (row) { getLine(row) {
return this.doc.getLine(row) return this.doc.getLine(row)
} }
insertLine (row, line) { insertLine(row, line) {
const lastRow = this.getLastRow() const lastRow = this.getLastRow()
if (row > lastRow) { if (row > lastRow) {
const lastLine = this.getLine(lastRow) const lastLine = this.getLine(lastRow)
@@ -56,7 +56,7 @@ export default class TextEditorInterface {
} }
} }
deleteLine (row) { deleteLine(row) {
const lastRow = this.getLastRow() const lastRow = this.getLastRow()
if (row >= lastRow) { if (row >= lastRow) {
if (lastRow > 0) { if (lastRow > 0) {
@@ -76,15 +76,11 @@ export default class TextEditorInterface {
) )
} }
} else { } else {
this.doc.replaceRange( this.doc.replaceRange('', { line: row, ch: 0 }, { line: row + 1, ch: 0 })
'',
{ line: row, ch: 0 },
{ line: row + 1, ch: 0 }
)
} }
} }
replaceLines (startRow, endRow, lines) { replaceLines(startRow, endRow, lines) {
const lastRow = this.getLastRow() const lastRow = this.getLastRow()
if (endRow > lastRow) { if (endRow > lastRow) {
const lastLine = this.getLine(lastRow) const lastLine = this.getLine(lastRow)
@@ -102,7 +98,7 @@ export default class TextEditorInterface {
} }
} }
transact (func) { transact(func) {
this.transaction = true this.transaction = true
func() func()
this.transaction = false this.transaction = false

View File

@@ -3,17 +3,18 @@ import i18n from 'browser/lib/i18n'
const { remote } = electron const { remote } = electron
const { dialog } = remote const { dialog } = remote
export function confirmDeleteNote (confirmDeletion, permanent) { export function confirmDeleteNote(confirmDeletion, permanent) {
if (confirmDeletion || permanent) { if (confirmDeletion || permanent) {
const alertConfig = { const alertConfig = {
ype: 'warning', type: 'warning',
message: i18n.__('Confirm note deletion'), message: i18n.__('Confirm note deletion'),
detail: i18n.__('This will permanently remove this note.'), detail: i18n.__('This will permanently remove this note.'),
buttons: [i18n.__('Confirm'), i18n.__('Cancel')] buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
} }
const dialogButtonIndex = dialog.showMessageBox( const dialogButtonIndex = dialog.showMessageBox(
remote.getCurrentWindow(), alertConfig remote.getCurrentWindow(),
alertConfig
) )
return dialogButtonIndex === 0 return dialogButtonIndex === 0

View File

@@ -9,40 +9,51 @@ const CODEMIRROR_EXTRA_THEME_PATH = 'extra_scripts/codemirror/theme'
const isProduction = process.env.NODE_ENV === 'production' const isProduction = process.env.NODE_ENV === 'production'
const paths = [ const paths = [
isProduction ? path.join(app.getAppPath(), CODEMIRROR_THEME_PATH) : path.resolve(CODEMIRROR_THEME_PATH), isProduction
isProduction ? path.join(app.getAppPath(), CODEMIRROR_EXTRA_THEME_PATH) : path.resolve(CODEMIRROR_EXTRA_THEME_PATH) ? 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 const themes = paths
.map(directory => fs.readdirSync(directory).map(file => { .map(directory =>
fs.readdirSync(directory).map(file => {
const name = file.substring(0, file.lastIndexOf('.')) const name = file.substring(0, file.lastIndexOf('.'))
return { return {
name, name,
path: path.join(directory.split(/\//g).slice(-3).join('/'), file), path: path.join(directory, file),
className: `cm-s-${name}` className: `cm-s-${name}`
} }
})) })
)
.reduce((accumulator, value) => accumulator.concat(value), []) .reduce((accumulator, value) => accumulator.concat(value), [])
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
themes.splice(themes.findIndex(({ name }) => name === 'solarized'), 1, { themes.splice(
themes.findIndex(({ name }) => name === 'solarized'),
1,
{
name: 'solarized dark', name: 'solarized dark',
path: `${CODEMIRROR_THEME_PATH}/solarized.css`, path: path.join(paths[0], 'solarized.css'),
className: `cm-s-solarized cm-s-dark` className: `cm-s-solarized cm-s-dark`
}, { },
{
name: 'solarized light', name: 'solarized light',
path: `${CODEMIRROR_THEME_PATH}/solarized.css`, path: path.join(paths[0], 'solarized.css'),
className: `cm-s-solarized cm-s-light` className: `cm-s-solarized cm-s-light`
}) }
)
themes.splice(0, 0, { themes.splice(0, 0, {
name: 'default', name: 'default',
path: `${CODEMIRROR_THEME_PATH}/elegant.css`, path: path.join(paths[0], 'elegant.css'),
className: `cm-s-default` className: `cm-s-default`
}) })
const snippetFile = process.env.NODE_ENV !== 'test' const snippetFile =
process.env.NODE_ENV !== 'test'
? path.join(app.getPath('userData'), 'snippets.json') ? path.join(app.getPath('userData'), 'snippets.json')
: '' // return nothing as we specified different path to snippets.json in test : '' // return nothing as we specified different path to snippets.json in test

View File

@@ -1,9 +1,9 @@
const { remote } = require('electron') const { remote } = require('electron')
const { Menu, MenuItem } = remote const { Menu, MenuItem } = remote
function popup (templates) { function popup(templates) {
const menu = new Menu() const menu = new Menu()
templates.forEach((item) => { templates.forEach(item => {
menu.append(new MenuItem(item)) menu.append(new MenuItem(item))
}) })
menu.popup(remote.getCurrentWindow()) menu.popup(remote.getCurrentWindow())

View File

@@ -1,10 +1,10 @@
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import fs from 'fs' import fs from 'fs'
const {remote} = require('electron') const { remote } = require('electron')
const {Menu} = remote.require('electron') const { Menu } = remote.require('electron')
const {clipboard} = remote.require('electron') const { clipboard } = remote.require('electron')
const {shell} = remote.require('electron') const { shell } = remote.require('electron')
const spellcheck = require('./spellcheck') const spellcheck = require('./spellcheck')
const uri2path = require('file-uri-to-path') const uri2path = require('file-uri-to-path')
@@ -16,11 +16,16 @@ const uri2path = require('file-uri-to-path')
* @param {MouseEvent} event that has triggered the creation of the context menu * @param {MouseEvent} event that has triggered the creation of the context menu
* @returns {Electron.Menu} The created electron context menu * @returns {Electron.Menu} The created electron context menu
*/ */
const buildEditorContextMenu = function (editor, event) { const buildEditorContextMenu = function(editor, event) {
if (editor == null || event == null || event.pageX == null || event.pageY == null) { if (
editor == null ||
event == null ||
event.pageX == null ||
event.pageY == null
) {
return null return null
} }
const cursor = editor.coordsChar({left: event.pageX, top: event.pageY}) const cursor = editor.coordsChar({ left: event.pageX, top: event.pageY })
const wordRange = editor.findWordAt(cursor) const wordRange = editor.findWordAt(cursor)
const word = editor.getRange(wordRange.anchor, wordRange.head) const word = editor.getRange(wordRange.anchor, wordRange.head)
const existingMarks = editor.findMarks(wordRange.anchor, wordRange.head) || [] const existingMarks = editor.findMarks(wordRange.anchor, wordRange.head) || []
@@ -40,30 +45,44 @@ const buildEditorContextMenu = function (editor, event) {
isMisspelled: isMisspelled, isMisspelled: isMisspelled,
spellingSuggestions: suggestion spellingSuggestions: suggestion
} }
const template = [{ const template = [
{
role: 'cut' role: 'cut'
}, { },
{
role: 'copy' role: 'copy'
}, { },
{
role: 'paste' role: 'paste'
}, { },
{
role: 'selectall' role: 'selectall'
}] }
]
if (selection.isMisspelled) { if (selection.isMisspelled) {
const suggestions = selection.spellingSuggestions const suggestions = selection.spellingSuggestions
template.unshift.apply(template, suggestions.map(function (suggestion) { template.unshift.apply(
template,
suggestions
.map(function(suggestion) {
return { return {
label: suggestion, label: suggestion,
click: function (suggestion) { click: function(suggestion) {
if (editor != null) { if (editor != null) {
editor.replaceRange(suggestion.label, wordRange.anchor, wordRange.head) editor.replaceRange(
suggestion.label,
wordRange.anchor,
wordRange.head
)
} }
} }
} }
}).concat({ })
.concat({
type: 'separator' type: 'separator'
})) })
)
} }
return Menu.buildFromTemplate(template) return Menu.buildFromTemplate(template)
} }
@@ -74,19 +93,30 @@ const buildEditorContextMenu = function (editor, event) {
* @param {MouseEvent} event that has triggered the creation of the context menu * @param {MouseEvent} event that has triggered the creation of the context menu
* @returns {Electron.Menu} The created electron context menu * @returns {Electron.Menu} The created electron context menu
*/ */
const buildMarkdownPreviewContextMenu = function (markdownPreview, event) { const buildMarkdownPreviewContextMenu = function(markdownPreview, event) {
if (markdownPreview == null || event == null || event.pageX == null || event.pageY == null) { if (
markdownPreview == null ||
event == null ||
event.pageX == null ||
event.pageY == null
) {
return null return null
} }
// Default context menu inclusions // Default context menu inclusions
const template = [{ const template = [
{
role: 'copy' role: 'copy'
}, { },
{
role: 'selectall' role: 'selectall'
}] }
]
if (event.target.tagName.toLowerCase() === 'a' && event.target.getAttribute('href')) { if (
event.target.tagName.toLowerCase() === 'a' &&
event.target.getAttribute('href')
) {
// Link opener for files on the local system pointed to by href // Link opener for files on the local system pointed to by href
const href = event.target.href const href = event.target.href
const isLocalFile = href.startsWith('file:') const isLocalFile = href.startsWith('file:')
@@ -94,31 +124,29 @@ const buildMarkdownPreviewContextMenu = function (markdownPreview, event) {
const absPath = uri2path(href) const absPath = uri2path(href)
try { try {
if (fs.lstatSync(absPath).isFile()) { if (fs.lstatSync(absPath).isFile()) {
template.push( template.push({
{
label: i18n.__('Show in explorer'), label: i18n.__('Show in explorer'),
click: (e) => shell.showItemInFolder(absPath) click: e => shell.showItemInFolder(absPath)
} })
)
} }
} catch (e) { } catch (e) {
console.log('Error while evaluating if the file is locally available', e) console.log(
'Error while evaluating if the file is locally available',
e
)
} }
} }
// Add option to context menu to copy url // Add option to context menu to copy url
template.push( template.push({
{
label: i18n.__('Copy Url'), label: i18n.__('Copy Url'),
click: (e) => clipboard.writeText(href) click: e => clipboard.writeText(href)
} })
)
} }
return Menu.buildFromTemplate(template) return Menu.buildFromTemplate(template)
} }
module.exports = module.exports = {
{
buildEditorContextMenu: buildEditorContextMenu, buildEditorContextMenu: buildEditorContextMenu,
buildMarkdownPreviewContextMenu: buildMarkdownPreviewContextMenu buildMarkdownPreviewContextMenu: buildMarkdownPreviewContextMenu
} }

View File

@@ -1,4 +1,4 @@
export default function convertModeName (name) { export default function convertModeName(name) {
switch (name) { switch (name) {
case 'ejs': case 'ejs':
return 'Embedded Javascript' return 'Embedded Javascript'

View File

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

View File

@@ -8,7 +8,7 @@ import moment from 'moment'
* @param {mixed} * @param {mixed}
* @return {string} * @return {string}
*/ */
export function formatDate (date) { export function formatDate(date) {
const m = moment(date) const m = moment(date)
if (!m.isValid()) { if (!m.isValid()) {
throw Error('Invalid argument.') throw Error('Invalid argument.')

View File

@@ -1,4 +1,8 @@
export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleField = 'title') { export function findNoteTitle(
value,
enableFrontMatterTitle,
frontMatterTitleField = 'title'
) {
const splitted = value.split('\n') const splitted = value.split('\n')
let title = null let title = null
let isInsideCodeBlock = false let isInsideCodeBlock = false
@@ -6,8 +10,13 @@ export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleFi
if (splitted[0] === '---') { if (splitted[0] === '---') {
let line = 0 let line = 0
while (++line < splitted.length) { while (++line < splitted.length) {
if (enableFrontMatterTitle && splitted[line].startsWith(frontMatterTitleField + ':')) { if (
title = splitted[line].substring(frontMatterTitleField.length + 1).trim() enableFrontMatterTitle &&
splitted[line].startsWith(frontMatterTitleField + ':')
) {
title = splitted[line]
.substring(frontMatterTitleField.length + 1)
.trim()
break break
} }
@@ -22,11 +31,15 @@ export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleFi
if (title === null) { if (title === null) {
splitted.some((line, index) => { splitted.some((line, index) => {
const trimmedLine = line.trim() const trimmedLine = line.trim()
const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim() const trimmedNextLine =
splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
if (trimmedLine.match('```')) { if (trimmedLine.match('```')) {
isInsideCodeBlock = !isInsideCodeBlock isInsideCodeBlock = !isInsideCodeBlock
} }
if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) { if (
isInsideCodeBlock === false &&
(trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))
) {
title = trimmedLine title = trimmedLine
return true return true
} }
@@ -35,7 +48,7 @@ export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleFi
if (title === null) { if (title === null) {
title = '' title = ''
splitted.some((line) => { splitted.some(line => {
if (line.trim().length > 0) { if (line.trim().length > 0) {
title = line.trim() title = line.trim()
return true return true

View File

@@ -1,10 +1,11 @@
const _ = require('lodash') const _ = require('lodash')
export function findStorage (storageKey) { export function findStorage(storageKey) {
const cachedStorageList = JSON.parse(localStorage.getItem('storages')) const cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('Target storage doesn\'t exist.') if (!_.isArray(cachedStorageList))
const storage = _.find(cachedStorageList, {key: storageKey}) throw new Error("Target storage doesn't exist.")
if (storage === undefined) throw new Error('Target storage doesn\'t exist.') const storage = _.find(cachedStorageList, { key: storageKey })
if (storage === undefined) throw new Error("Target storage doesn't exist.")
return storage return storage
} }

View File

@@ -1,9 +1,9 @@
export function getTodoStatus (content) { export function getTodoStatus(content) {
const splitted = content.split('\n') const splitted = content.split('\n')
let numberOfTodo = 0 let numberOfTodo = 0
let numberOfCompletedTodo = 0 let numberOfCompletedTodo = 0
splitted.forEach((line) => { splitted.forEach(line => {
const trimmedLine = line.trim().replace(/^(>\s*)*/, '') const trimmedLine = line.trim().replace(/^(>\s*)*/, '')
if (trimmedLine.match(/^[+\-*] \[(\s|x)] ./i)) { if (trimmedLine.match(/^[+\-*] \[(\s|x)] ./i)) {
numberOfTodo++ numberOfTodo++
@@ -19,7 +19,7 @@ export function getTodoStatus (content) {
} }
} }
export function getTodoPercentageOfCompleted (content) { export function getTodoPercentageOfCompleted(content) {
const state = getTodoStatus(content) const state = getTodoStatus(content)
return Math.floor(state.completed / state.total * 100) return Math.floor((state.completed / state.total) * 100)
} }

View File

@@ -7,9 +7,9 @@
* @return {string} * @return {string}
*/ */
export function decodeEntities (text) { export function decodeEntities(text) {
var entities = [ var entities = [
['apos', '\''], ['apos', "'"],
['amp', '&'], ['amp', '&'],
['lt', '<'], ['lt', '<'],
['gt', '>'], ['gt', '>'],
@@ -24,16 +24,16 @@ export function decodeEntities (text) {
return text return text
} }
export function encodeEntities (text) { export function encodeEntities(text) {
const entities = [ const entities = [
['\'', 'apos'], ["'", 'apos'],
['<', 'lt'], ['<', 'lt'],
['>', 'gt'], ['>', 'gt'],
['\\?', '#63'], ['\\?', '#63'],
['\\$', '#36'] ['\\$', '#36']
] ]
entities.forEach((entity) => { entities.forEach(entity => {
text = text.replace(new RegExp(entity[0], 'g'), `&${entity[1]};`) text = text.replace(new RegExp(entity[0], 'g'), `&${entity[1]};`)
}) })
return text return text

View File

@@ -8,7 +8,8 @@ const i18n = new (require('i18n-2'))({
// setup some locales - other locales default to the first locale // setup some locales - other locales default to the first locale
locales: getLocales(), locales: getLocales(),
extension: '.json', extension: '.json',
directory: process.env.NODE_ENV === 'production' directory:
process.env.NODE_ENV === 'production'
? path.join(app.getAppPath(), './locales') ? path.join(app.getAppPath(), './locales')
: path.resolve('./locales'), : path.resolve('./locales'),
devMode: false devMode: false

View File

@@ -1,8 +1,7 @@
const crypto = require('crypto') const crypto = require('crypto')
const _ = require('lodash')
const uuidv4 = require('uuid/v4') const uuidv4 = require('uuid/v4')
module.exports = function (uuid) { module.exports = function(uuid) {
if (typeof uuid === typeof true && uuid) { if (typeof uuid === typeof true && uuid) {
return uuidv4() return uuidv4()
} }

View File

@@ -1,35 +1,44 @@
'use strict' 'use strict'
module.exports = function definitionListPlugin (md) { module.exports = function definitionListPlugin(md) {
var isSpace = md.utils.isSpace var isSpace = md.utils.isSpace
// Search `[:~][\n ]`, returns next pos after marker on success // Search `[:~][\n ]`, returns next pos after marker on success
// or -1 on fail. // or -1 on fail.
function skipMarker (state, line) { function skipMarker(state, line) {
let start = state.bMarks[line] + state.tShift[line] let start = state.bMarks[line] + state.tShift[line]
const max = state.eMarks[line] const max = state.eMarks[line]
if (start >= max) { return -1 } if (start >= max) {
return -1
}
// Check bullet // Check bullet
const marker = state.src.charCodeAt(start++) const marker = state.src.charCodeAt(start++)
if (marker !== 0x7E/* ~ */ && marker !== 0x3A/* : */) { return -1 } if (marker !== 0x7e /* ~ */ && marker !== 0x3a /* : */) {
return -1
}
const pos = state.skipSpaces(start) const pos = state.skipSpaces(start)
// require space after ":" // require space after ":"
if (start === pos) { return -1 } if (start === pos) {
return -1
}
return start return start
} }
function markTightParagraphs (state, idx) { function markTightParagraphs(state, idx) {
const level = state.level + 2 const level = state.level + 2
let i let i
let l let l
for (i = idx + 2, l = state.tokens.length - 2; i < l; i++) { for (i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') { if (
state.tokens[i].level === level &&
state.tokens[i].type === 'paragraph_open'
) {
state.tokens[i + 2].hidden = true state.tokens[i + 2].hidden = true
state.tokens[i].hidden = true state.tokens[i].hidden = true
i += 2 i += 2
@@ -37,7 +46,7 @@ module.exports = function definitionListPlugin (md) {
} }
} }
function deflist (state, startLine, endLine, silent) { function deflist(state, startLine, endLine, silent) {
var ch, var ch,
contentStart, contentStart,
ddLine, ddLine,
@@ -63,28 +72,38 @@ module.exports = function definitionListPlugin (md) {
if (silent) { if (silent) {
// quirk: validation mode validates a dd block only, not a whole deflist // quirk: validation mode validates a dd block only, not a whole deflist
if (state.ddIndent < 0) { return false } if (state.ddIndent < 0) {
return false
}
return skipMarker(state, startLine) >= 0 return skipMarker(state, startLine) >= 0
} }
nextLine = startLine + 1 nextLine = startLine + 1
if (nextLine >= endLine) { return false } if (nextLine >= endLine) {
return false
}
if (state.isEmpty(nextLine)) { if (state.isEmpty(nextLine)) {
nextLine++ nextLine++
if (nextLine >= endLine) { return false } if (nextLine >= endLine) {
return false
}
} }
if (state.sCount[nextLine] < state.blkIndent) { return false } if (state.sCount[nextLine] < state.blkIndent) {
return false
}
contentStart = skipMarker(state, nextLine) contentStart = skipMarker(state, nextLine)
if (contentStart < 0) { return false } if (contentStart < 0) {
return false
}
// Start list // Start list
listTokIdx = state.tokens.length listTokIdx = state.tokens.length
tight = true tight = true
token = state.push('dl_open', 'dl', 1) token = state.push('dl_open', 'dl', 1)
token.map = listLines = [ startLine, 0 ] token.map = listLines = [startLine, 0]
// //
// Iterate list items // Iterate list items
@@ -100,34 +119,38 @@ module.exports = function definitionListPlugin (md) {
// needed to break out of the second one // needed to break out of the second one
// //
/* eslint no-labels:0,block-scoped-var:0 */ /* eslint no-labels:0,block-scoped-var:0 */
OUTER: OUTER: for (;;) {
for (;;) {
prevEmptyEnd = false prevEmptyEnd = false
token = state.push('dt_open', 'dt', 1) token = state.push('dt_open', 'dt', 1)
token.map = [ dtLine, dtLine ] token.map = [dtLine, dtLine]
token = state.push('inline', '', 0) token = state.push('inline', '', 0)
token.map = [ dtLine, dtLine ] token.map = [dtLine, dtLine]
token.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim() token.content = state
.getLines(dtLine, dtLine + 1, state.blkIndent, false)
.trim()
token.children = [] token.children = []
token = state.push('dt_close', 'dt', -1) token = state.push('dt_close', 'dt', -1)
for (;;) { for (;;) {
token = state.push('dd_open', 'dd', 1) token = state.push('dd_open', 'dd', 1)
token.map = itemLines = [ ddLine, 0 ] token.map = itemLines = [ddLine, 0]
pos = contentStart pos = contentStart
max = state.eMarks[ddLine] max = state.eMarks[ddLine]
offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine]) offset =
state.sCount[ddLine] +
contentStart -
(state.bMarks[ddLine] + state.tShift[ddLine])
while (pos < max) { while (pos < max) {
ch = state.src.charCodeAt(pos) ch = state.src.charCodeAt(pos)
if (isSpace(ch)) { if (isSpace(ch)) {
if (ch === 0x09) { if (ch === 0x09) {
offset += 4 - offset % 4 offset += 4 - (offset % 4)
} else { } else {
offset++ offset++
} }
@@ -153,8 +176,11 @@ module.exports = function definitionListPlugin (md) {
state.parentType = 'deflist' state.parentType = 'deflist'
newEndLine = ddLine newEndLine = ddLine
while (++newEndLine < endLine && (state.sCount[newEndLine] >= state.sCount[ddLine] || state.isEmpty(newEndLine))) { while (
} ++newEndLine < endLine &&
(state.sCount[newEndLine] >= state.sCount[ddLine] ||
state.isEmpty(newEndLine))
) {}
oldLineMax = state.lineMax oldLineMax = state.lineMax
state.lineMax = newEndLine state.lineMax = newEndLine
@@ -169,7 +195,7 @@ module.exports = function definitionListPlugin (md) {
} }
// Item become loose if finish with empty line, // Item become loose if finish with empty line,
// but we should filter last element, because it means list finish // but we should filter last element, because it means list finish
prevEmptyEnd = (state.line - ddLine) > 1 && state.isEmpty(state.line - 1) prevEmptyEnd = state.line - ddLine > 1 && state.isEmpty(state.line - 1)
state.tShift[ddLine] = oldTShift state.tShift[ddLine] = oldTShift
state.sCount[ddLine] = oldSCount state.sCount[ddLine] = oldSCount
@@ -182,11 +208,17 @@ module.exports = function definitionListPlugin (md) {
itemLines[1] = nextLine = state.line itemLines[1] = nextLine = state.line
if (nextLine >= endLine) { break OUTER } if (nextLine >= endLine) {
break OUTER
}
if (state.sCount[nextLine] < state.blkIndent) { break OUTER } if (state.sCount[nextLine] < state.blkIndent) {
break OUTER
}
contentStart = skipMarker(state, nextLine) contentStart = skipMarker(state, nextLine)
if (contentStart < 0) { break } if (contentStart < 0) {
break
}
ddLine = nextLine ddLine = nextLine
@@ -194,20 +226,36 @@ module.exports = function definitionListPlugin (md) {
// insert DD tag and repeat checking // insert DD tag and repeat checking
} }
if (nextLine >= endLine) { break } if (nextLine >= endLine) {
break
}
dtLine = nextLine dtLine = nextLine
if (state.isEmpty(dtLine)) { break } if (state.isEmpty(dtLine)) {
if (state.sCount[dtLine] < state.blkIndent) { break } break
}
if (state.sCount[dtLine] < state.blkIndent) {
break
}
ddLine = dtLine + 1 ddLine = dtLine + 1
if (ddLine >= endLine) { break } if (ddLine >= endLine) {
if (state.isEmpty(ddLine)) { ddLine++ } break
if (ddLine >= endLine) { break } }
if (state.isEmpty(ddLine)) {
ddLine++
}
if (ddLine >= endLine) {
break
}
if (state.sCount[ddLine] < state.blkIndent) { break } if (state.sCount[ddLine] < state.blkIndent) {
break
}
contentStart = skipMarker(state, ddLine) contentStart = skipMarker(state, ddLine)
if (contentStart < 0) { break } if (contentStart < 0) {
break
}
// go to the next loop iteration: // go to the next loop iteration:
// insert DT and DD tags and repeat checking // insert DT and DD tags and repeat checking
@@ -228,5 +276,7 @@ module.exports = function definitionListPlugin (md) {
return true return true
} }
md.block.ruler.before('paragraph', 'deflist', deflist, { alt: [ 'paragraph', 'reference' ] }) md.block.ruler.before('paragraph', 'deflist', deflist, {
alt: ['paragraph', 'reference']
})
} }

View File

@@ -1,9 +1,9 @@
'use strict' 'use strict'
module.exports = function (md, renderers, defaultRenderer) { module.exports = function(md, renderers, defaultRenderer) {
const paramsRE = /^[ \t]*([\w+#-]+)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/ const paramsRE = /^[ \t]*([\w+#-]+)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/
function fence (state, startLine, endLine, silent) { function fence(state, startLine, endLine, silent) {
let pos = state.bMarks[startLine] + state.tShift[startLine] let pos = state.bMarks[startLine] + state.tShift[startLine]
let max = state.eMarks[startLine] let max = state.eMarks[startLine]
@@ -12,7 +12,7 @@ module.exports = function (md, renderers, defaultRenderer) {
} }
const marker = state.src.charCodeAt(pos) const marker = state.src.charCodeAt(pos)
if (marker !== 0x7E/* ~ */ && marker !== 0x60 /* ` */) { if (marker !== 0x7e /* ~ */ && marker !== 0x60 /* ` */) {
return false return false
} }
@@ -46,7 +46,10 @@ module.exports = function (md, renderers, defaultRenderer) {
if (pos < max && state.sCount[nextLine] < state.blkIndent) { if (pos < max && state.sCount[nextLine] < state.blkIndent) {
break break
} }
if (state.src.charCodeAt(pos) !== marker || state.sCount[nextLine] - state.blkIndent >= 4) { if (
state.src.charCodeAt(pos) !== marker ||
state.sCount[nextLine] - state.blkIndent >= 4
) {
continue continue
} }
@@ -127,10 +130,12 @@ module.exports = function (md, renderers, defaultRenderer) {
}) })
for (const name in renderers) { for (const name in renderers) {
md.renderer.rules[`${name}_fence`] = (tokens, index) => renderers[name](tokens[index]) md.renderer.rules[`${name}_fence`] = (tokens, index) =>
renderers[name](tokens[index])
} }
if (defaultRenderer) { if (defaultRenderer) {
md.renderer.rules['_fence'] = (tokens, index) => defaultRenderer(tokens[index]) md.renderer.rules['_fence'] = (tokens, index) =>
defaultRenderer(tokens[index])
} }
} }

View File

@@ -1,14 +1,19 @@
'use strict' 'use strict'
module.exports = function frontMatterPlugin (md) { module.exports = function frontMatterPlugin(md) {
function frontmatter (state, startLine, endLine, silent) { function frontmatter(state, startLine, endLine, silent) {
if (startLine !== 0 || state.src.substr(startLine, state.eMarks[0]) !== '---') { if (
startLine !== 0 ||
state.src.substr(startLine, state.eMarks[0]) !== '---'
) {
return false return false
} }
let line = 0 let line = 0
while (++line < state.lineMax) { while (++line < state.lineMax) {
if (state.src.substring(state.bMarks[line], state.eMarks[line]) === '---') { if (
state.src.substring(state.bMarks[line], state.eMarks[line]) === '---'
) {
state.line = line + 1 state.line = line + 1
return true return true
@@ -19,6 +24,6 @@ module.exports = function frontMatterPlugin (md) {
} }
md.block.ruler.before('table', 'frontmatter', frontmatter, { md.block.ruler.before('table', 'frontmatter', frontmatter, {
alt: [ 'paragraph', 'reference', 'blockquote', 'list' ] alt: ['paragraph', 'reference', 'blockquote', 'list']
}) })
} }

View File

@@ -4,7 +4,7 @@ import sanitizeHtml from 'sanitize-html'
import { escapeHtmlCharacters } from './utils' import { escapeHtmlCharacters } from './utils'
import url from 'url' import url from 'url'
module.exports = function sanitizePlugin (md, options) { module.exports = function sanitizePlugin(md, options) {
options = options || {} options = options || {}
md.core.ruler.after('linkify', 'sanitize_inline', state => { md.core.ruler.after('linkify', 'sanitize_inline', state => {
@@ -15,7 +15,7 @@ module.exports = function sanitizePlugin (md, options) {
options options
) )
} }
if (state.tokens[tokenIdx].type === '_fence') { if (state.tokens[tokenIdx].type.match(/.*_fence$/)) {
// escapeHtmlCharacters has better performance // escapeHtmlCharacters has better performance
state.tokens[tokenIdx].content = escapeHtmlCharacters( state.tokens[tokenIdx].content = escapeHtmlCharacters(
state.tokens[tokenIdx].content, state.tokens[tokenIdx].content,
@@ -38,15 +38,20 @@ module.exports = function sanitizePlugin (md, options) {
} }
const tagRegex = /<([A-Z][A-Z0-9]*)\s*((?:\s*[A-Z][A-Z0-9]*(?:=("|')(?:[^\3]+?)\3)?)*)\s*\/?>|<\/([A-Z][A-Z0-9]*)\s*>/i const tagRegex = /<([A-Z][A-Z0-9]*)\s*((?:\s*[A-Z][A-Z0-9]*(?:=("|')(?:[^\3]+?)\3)?)*)\s*\/?>|<\/([A-Z][A-Z0-9]*)\s*>/i
const attributesRegex = /([A-Z][A-Z0-9]*)(?:=("|')([^\2]+?)\2)?/ig const attributesRegex = /([A-Z][A-Z0-9]*)(?:=("|')([^\2]+?)\2)?/gi
function sanitizeInline (html, options) { function sanitizeInline(html, options) {
let match = tagRegex.exec(html) let match = tagRegex.exec(html)
if (!match) { if (!match) {
return '' return ''
} }
const { allowedTags, allowedAttributes, selfClosing, allowedSchemesAppliedToAttributes } = options const {
allowedTags,
allowedAttributes,
selfClosing,
allowedSchemesAppliedToAttributes
} = options
if (match[1] !== undefined) { if (match[1] !== undefined) {
// opening tag // opening tag
@@ -65,9 +70,17 @@ function sanitizeInline (html, options) {
name = match[1].toLowerCase() name = match[1].toLowerCase()
value = match[3] value = match[3]
if (allowedAttributes['*'].indexOf(name) !== -1 || (allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)) { if (
allowedAttributes['*'].indexOf(name) !== -1 ||
(allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)
) {
if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) { if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) {
if (naughtyHRef(value, options) || (tag === 'iframe' && name === 'src' && naughtyIFrame(value, options))) { if (
naughtyHRef(value, options) ||
(tag === 'iframe' &&
name === 'src' &&
naughtyIFrame(value, options))
) {
continue continue
} }
} }
@@ -94,8 +107,12 @@ function sanitizeInline (html, options) {
} }
} }
function naughtyHRef (href, options) { function naughtyHRef(href, options) {
// href = href.replace(/[\x00-\x20]+/g, '') // href = href.replace(/[\x00-\x20]+/g, '')
if (!href) {
// No href
return false
}
href = href.replace(/<\!\-\-.*?\-\-\>/g, '') href = href.replace(/<\!\-\-.*?\-\-\>/g, '')
const matches = href.match(/^([a-zA-Z]+)\:/) const matches = href.match(/^([a-zA-Z]+)\:/)
@@ -113,7 +130,7 @@ function naughtyHRef (href, options) {
return options.allowedSchemes.indexOf(scheme) === -1 return options.allowedSchemes.indexOf(scheme) === -1
} }
function naughtyIFrame (src, options) { function naughtyIFrame(src, options) {
try { try {
const parsed = url.parse(src, false, true) const parsed = url.parse(src, false, true)

View File

@@ -12,7 +12,7 @@ const hasProp = Object.prototype.hasOwnProperty
/** /**
* From @enyaxu/markdown-it-anchor * From @enyaxu/markdown-it-anchor
*/ */
function uniqueSlug (slug, slugs, opts) { function uniqueSlug(slug, slugs, opts) {
let uniq = slug let uniq = slug
let i = opts.uniqueSlugStartIndex let i = opts.uniqueSlugStartIndex
while (hasProp.call(slugs, uniq)) uniq = `${slug}-${i++}` while (hasProp.call(slugs, uniq)) uniq = `${slug}-${i++}`
@@ -20,7 +20,7 @@ function uniqueSlug (slug, slugs, opts) {
return uniq return uniq
} }
function linkify (token) { function linkify(token) {
token.content = mdlink(token.content, `#${decodeURI(token.slug)}`) token.content = mdlink(token.content, `#${decodeURI(token.slug)}`)
return token return token
} }
@@ -36,8 +36,8 @@ const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`)
* Otherwise,TOC is updated in place. * Otherwise,TOC is updated in place.
* @param editor CodeMirror editor to be updated with TOC * @param editor CodeMirror editor to be updated with TOC
*/ */
export function generateInEditor (editor) { export function generateInEditor(editor) {
function updateExistingToc () { function updateExistingToc() {
const toc = generate(editor.getValue()) const toc = generate(editor.getValue())
const search = editor.getSearchCursor(tocRegex) const search = editor.getSearchCursor(tocRegex)
while (search.findNext()) { while (search.findNext()) {
@@ -45,8 +45,10 @@ export function generateInEditor (editor) {
} }
} }
function addTocAtCursorPosition () { function addTocAtCursorPosition() {
const toc = generate(editor.getRange(editor.getCursor(), {line: Infinity})) const toc = generate(
editor.getRange(editor.getCursor(), { line: Infinity })
)
editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor()) editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor())
} }
@@ -57,7 +59,7 @@ export function generateInEditor (editor) {
} }
} }
export function tocExistsInEditor (editor) { export function tocExistsInEditor(editor) {
return tocRegex.test(editor.getValue()) return tocRegex.test(editor.getValue())
} }
@@ -66,7 +68,7 @@ export function tocExistsInEditor (editor) {
* @param markdownText MD document * @param markdownText MD document
* @returns generatedTOC String containing generated TOC * @returns generatedTOC String containing generated TOC
*/ */
export function generate (markdownText) { export function generate(markdownText) {
const slugs = {} const slugs = {}
const opts = { const opts = {
uniqueSlugStartIndex: 1 uniqueSlugStartIndex: 1
@@ -86,9 +88,12 @@ export function generate (markdownText) {
return TOC_MARKER_START + EOL + EOL + md + EOL + EOL + TOC_MARKER_END return TOC_MARKER_START + EOL + EOL + md + EOL + EOL + TOC_MARKER_END
} }
function wrapTocWithEol (toc, editor) { function wrapTocWithEol(toc, editor) {
const leftWrap = editor.getCursor().ch === 0 ? '' : EOL const leftWrap = editor.getCursor().ch === 0 ? '' : EOL
const rightWrap = editor.getLine(editor.getCursor().line).length === editor.getCursor().ch ? '' : EOL const rightWrap =
editor.getLine(editor.getCursor().line).length === editor.getCursor().ch
? ''
: EOL
return leftWrap + toc + rightWrap return leftWrap + toc + rightWrap
} }

View File

@@ -4,23 +4,26 @@ import emoji from 'markdown-it-emoji'
import math from '@rokt33r/markdown-it-math' import math from '@rokt33r/markdown-it-math'
import mdurl from 'mdurl' import mdurl from 'mdurl'
import smartArrows from 'markdown-it-smartarrows' import smartArrows from 'markdown-it-smartarrows'
import markdownItTocAndAnchor from '@hikerpig/markdown-it-toc-and-anchor'
import _ from 'lodash' import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
import katex from 'katex' import katex from 'katex'
import { lastFindInArray } from './utils' import { lastFindInArray } from './utils'
function createGutter (str, firstLineNumber) { function createGutter(str, firstLineNumber) {
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1 if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
const lastLineNumber = (str.match(/\n/g) || []).length + firstLineNumber - 1 const lastLineNumber = (str.match(/\n/g) || []).length + firstLineNumber - 1
const lines = [] const lines = []
for (let i = firstLineNumber; i <= lastLineNumber; i++) { for (let i = firstLineNumber; i <= lastLineNumber; i++) {
lines.push('<span class="CodeMirror-linenumber">' + i + '</span>') lines.push('<span class="CodeMirror-linenumber">' + i + '</span>')
} }
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>' return (
'<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
)
} }
class Markdown { class Markdown {
constructor (options = {}) { constructor(options = {}) {
const config = ConfigManager.get() const config = ConfigManager.get()
const defaultOptions = { const defaultOptions = {
typographer: config.preview.smartQuotes, typographer: config.preview.smartQuotes,
@@ -36,29 +39,129 @@ class Markdown {
this.md.linkify.set({ fuzzyLink: false }) this.md.linkify.set({ fuzzyLink: false })
if (updatedOptions.sanitize !== 'NONE') { if (updatedOptions.sanitize !== 'NONE') {
const allowedTags = ['iframe', 'input', 'b', const allowedTags = [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'h7', 'h8', 'br', 'b', 'i', 'strong', 'em', 'a', 'pre', 'code', 'img', 'tt', 'iframe',
'div', 'ins', 'del', 'sup', 'sub', 'p', 'ol', 'ul', 'table', 'thead', 'tbody', 'tfoot', 'blockquote', 'input',
'dl', 'dt', 'dd', 'kbd', 'q', 'samp', 'var', 'hr', 'ruby', 'rt', 'rp', 'li', 'tr', 'td', 'th', 's', 'strike', 'summary', 'details' 'b',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'h7',
'h8',
'br',
'b',
'i',
'strong',
'em',
'a',
'pre',
'code',
'img',
'tt',
'div',
'ins',
'del',
'sup',
'sub',
'p',
'ol',
'ul',
'table',
'thead',
'tbody',
'tfoot',
'blockquote',
'dl',
'dt',
'dd',
'kbd',
'q',
'samp',
'var',
'hr',
'ruby',
'rt',
'rp',
'li',
'tr',
'td',
'th',
's',
'strike',
'summary',
'details'
] ]
const allowedAttributes = [ const allowedAttributes = [
'abbr', 'accept', 'accept-charset', 'abbr',
'accesskey', 'action', 'align', 'alt', 'axis', 'accept',
'border', 'cellpadding', 'cellspacing', 'char', 'accept-charset',
'charoff', 'charset', 'checked', 'accesskey',
'clear', 'cols', 'colspan', 'color', 'action',
'compact', 'coords', 'datetime', 'dir', 'align',
'disabled', 'enctype', 'for', 'frame', 'alt',
'headers', 'height', 'hreflang', 'axis',
'hspace', 'ismap', 'label', 'lang', 'border',
'maxlength', 'media', 'method', 'cellpadding',
'multiple', 'name', 'nohref', 'noshade', 'cellspacing',
'nowrap', 'open', 'prompt', 'readonly', 'rel', 'rev', 'char',
'rows', 'rowspan', 'rules', 'scope', 'charoff',
'selected', 'shape', 'size', 'span', 'charset',
'start', 'summary', 'tabindex', 'target', 'checked',
'title', 'type', 'usemap', 'valign', 'value', 'clear',
'vspace', 'width', 'itemprop' 'cols',
'colspan',
'color',
'compact',
'coords',
'datetime',
'dir',
'disabled',
'enctype',
'for',
'frame',
'headers',
'height',
'hreflang',
'hspace',
'ismap',
'label',
'lang',
'maxlength',
'media',
'method',
'multiple',
'name',
'nohref',
'noshade',
'nowrap',
'open',
'prompt',
'readonly',
'rel',
'rev',
'rows',
'rowspan',
'rules',
'scope',
'selected',
'shape',
'size',
'span',
'start',
'summary',
'tabindex',
'target',
'title',
'type',
'usemap',
'valign',
'value',
'vspace',
'width',
'itemprop'
] ]
if (updatedOptions.sanitize === 'ALLOW_STYLES') { if (updatedOptions.sanitize === 'ALLOW_STYLES') {
@@ -71,20 +174,20 @@ class Markdown {
allowedTags, allowedTags,
allowedAttributes: { allowedAttributes: {
'*': allowedAttributes, '*': allowedAttributes,
'a': ['href'], a: ['href'],
'div': ['itemscope', 'itemtype'], div: ['itemscope', 'itemtype'],
'blockquote': ['cite'], blockquote: ['cite'],
'del': ['cite'], del: ['cite'],
'ins': ['cite'], ins: ['cite'],
'q': ['cite'], q: ['cite'],
'img': ['src', 'width', 'height'], img: ['src', 'width', 'height'],
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'], iframe: ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
'input': ['type', 'id', 'checked'] input: ['type', 'id', 'checked']
}, },
allowedIframeHostnames: ['www.youtube.com'], allowedIframeHostnames: ['www.youtube.com'],
selfClosing: [ 'img', 'br', 'hr', 'input' ], selfClosing: ['img', 'br', 'hr', 'input'],
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ], allowedSchemes: ['http', 'https', 'ftp', 'mailto'],
allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ], allowedSchemesAppliedToAttributes: ['href', 'src', 'cite'],
allowProtocolRelative: true allowProtocolRelative: true
}) })
} }
@@ -97,7 +200,7 @@ class Markdown {
inlineClose: config.preview.latexInlineClose, inlineClose: config.preview.latexInlineClose,
blockOpen: config.preview.latexBlockOpen, blockOpen: config.preview.latexBlockOpen,
blockClose: config.preview.latexBlockClose, blockClose: config.preview.latexBlockClose,
inlineRenderer: function (str) { inlineRenderer: function(str) {
let output = '' let output = ''
try { try {
output = katex.renderToString(str.trim()) output = katex.renderToString(str.trim())
@@ -106,7 +209,7 @@ class Markdown {
} }
return output return output
}, },
blockRenderer: function (str) { blockRenderer: function(str) {
let output = '' let output = ''
try { try {
output = katex.renderToString(str.trim(), { displayMode: true }) output = katex.renderToString(str.trim(), { displayMode: true })
@@ -123,14 +226,41 @@ class Markdown {
slugify: require('./slugify') slugify: require('./slugify')
}) })
this.md.use(require('markdown-it-kbd')) this.md.use(require('markdown-it-kbd'))
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']}) this.md.use(require('markdown-it-admonition'), {
types: [
'note',
'hint',
'attention',
'caution',
'danger',
'error',
'quote',
'abstract',
'question'
]
})
this.md.use(require('markdown-it-abbr')) this.md.use(require('markdown-it-abbr'))
this.md.use(require('markdown-it-sub')) this.md.use(require('markdown-it-sub'))
this.md.use(require('markdown-it-sup')) this.md.use(require('markdown-it-sup'))
this.md.use(md => {
markdownItTocAndAnchor(md, {
toc: true,
tocPattern: /\[TOC\]/i,
anchorLink: false,
appendIdToHeading: false
})
md.renderer.rules.toc_open = () => '<div class="markdownIt-TOC-wrapper">'
md.renderer.rules.toc_close = () => '</div>'
})
this.md.use(require('./markdown-it-deflist')) this.md.use(require('./markdown-it-deflist'))
this.md.use(require('./markdown-it-frontmatter')) this.md.use(require('./markdown-it-frontmatter'))
this.md.use(require('./markdown-it-fence'), { this.md.use(
require('./markdown-it-fence'),
{
chart: token => { chart: token => {
if (token.parameters.hasOwnProperty('yaml')) { if (token.parameters.hasOwnProperty('yaml')) {
token.parameters.format = 'yaml' token.parameters.format = 'yaml'
@@ -138,81 +268,125 @@ class Markdown {
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
<div class="chart" data-height="${token.parameters.height}" data-format="${token.parameters.format || 'json'}">${token.content}</div> <div class="chart" data-height="${
token.parameters.height
}" data-format="${token.parameters.format || 'json'}">${
token.content
}</div>
</pre>` </pre>`
}, },
flowchart: token => { flowchart: token => {
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
<div class="flowchart" data-height="${token.parameters.height}">${token.content}</div> <div class="flowchart" data-height="${token.parameters.height}">${
token.content
}</div>
</pre>` </pre>`
}, },
gallery: token => { gallery: token => {
const content = token.content.split('\n').slice(0, -1).map(line => { const content = token.content
.split('\n')
.slice(0, -1)
.map(line => {
const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line) const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line)
if (match) { if (match) {
return mdurl.encode(match[1]) return mdurl.encode(match[1])
} else { } else {
return mdurl.encode(line) return mdurl.encode(line)
} }
}).join('\n') })
.join('\n')
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
<div class="gallery" data-autoplay="${token.parameters.autoplay}" data-height="${token.parameters.height}">${content}</div> <div class="gallery" data-autoplay="${
token.parameters.autoplay
}" data-height="${token.parameters.height}">${content}</div>
</pre>` </pre>`
}, },
mermaid: token => { mermaid: token => {
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
<div class="mermaid" data-height="${token.parameters.height}">${token.content}</div> <div class="mermaid" data-height="${token.parameters.height}">${
token.content
}</div>
</pre>` </pre>`
}, },
sequence: token => { sequence: token => {
return `<pre class="fence" data-line="${token.map[0]}"> return `<pre class="fence" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
<div class="sequence" data-height="${token.parameters.height}">${token.content}</div> <div class="sequence" data-height="${token.parameters.height}">${
token.content
}</div>
</pre>` </pre>`
} }
}, token => { },
token => {
return `<pre class="code CodeMirror" data-line="${token.map[0]}"> return `<pre class="code CodeMirror" data-line="${token.map[0]}">
<span class="filename">${token.fileName}</span> <span class="filename">${token.fileName}</span>
${createGutter(token.content, token.firstLineNumber)} ${createGutter(token.content, token.firstLineNumber)}
<code class="${token.langType}">${token.content}</code> <code class="${token.langType}">${token.content}</code>
</pre>` </pre>`
}) }
)
const deflate = require('markdown-it-plantuml/lib/deflate') const deflate = require('markdown-it-plantuml/lib/deflate')
this.md.use(require('markdown-it-plantuml'), { const plantuml = require('markdown-it-plantuml')
generateSource: function (umlCode) { const plantUmlStripTrailingSlash = url =>
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url url.endsWith('/') ? url.slice(0, -1) : url
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/svg' const plantUmlServerAddress = plantUmlStripTrailingSlash(
config.preview.plantUMLServerAddress
)
const parsePlantUml = function(umlCode, openMarker, closeMarker, type) {
const s = unescape(encodeURIComponent(umlCode)) const s = unescape(encodeURIComponent(umlCode))
const zippedCode = deflate.encode64( const zippedCode = deflate.encode64(
deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9) deflate.zip_deflate(`${openMarker}\n${s}\n${closeMarker}`, 9)
) )
return `${serverAddress}/${zippedCode}` return `${plantUmlServerAddress}/${type}/${zippedCode}`
} }
this.md.use(plantuml, {
generateSource: umlCode =>
parsePlantUml(umlCode, '@startuml', '@enduml', 'svg')
}) })
// Ditaa support // Ditaa support. PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment.
this.md.use(require('markdown-it-plantuml'), { this.md.use(plantuml, {
openMarker: '@startditaa', openMarker: '@startditaa',
closeMarker: '@endditaa', closeMarker: '@endditaa',
generateSource: function (umlCode) { generateSource: umlCode =>
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url parsePlantUml(umlCode, '@startditaa', '@endditaa', 'png')
// Currently PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment. })
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/png'
const s = unescape(encodeURIComponent(umlCode)) // Mindmap support
const zippedCode = deflate.encode64( this.md.use(plantuml, {
deflate.zip_deflate(`@startditaa\n${s}\n@endditaa`, 9) openMarker: '@startmindmap',
) closeMarker: '@endmindmap',
return `${serverAddress}/${zippedCode}` generateSource: umlCode =>
} parsePlantUml(umlCode, '@startmindmap', '@endmindmap', 'svg')
})
// WBS support
this.md.use(plantuml, {
openMarker: '@startwbs',
closeMarker: '@endwbs',
generateSource: umlCode =>
parsePlantUml(umlCode, '@startwbs', '@endwbs', 'svg')
})
// Gantt support
this.md.use(plantuml, {
openMarker: '@startgantt',
closeMarker: '@endgantt',
generateSource: umlCode =>
parsePlantUml(umlCode, '@startgantt', '@endgantt', 'svg')
}) })
// Override task item // Override task item
this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) { this.md.block.ruler.at('paragraph', function(
state,
startLine /*, endLine */
) {
let content, terminate, i, l, token let content, terminate, i, l, token
let nextLine = startLine + 1 let nextLine = startLine + 1
const terminatorRules = state.md.block.ruler.getRules('paragraph') const terminatorRules = state.md.block.ruler.getRules('paragraph')
@@ -222,10 +396,14 @@ class Markdown {
for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) {
// this would be a code block normally, but after paragraph // this would be a code block normally, but after paragraph
// it's considered a lazy continuation regardless of what's there // it's considered a lazy continuation regardless of what's there
if (state.sCount[nextLine] - state.blkIndent > 3) { continue } if (state.sCount[nextLine] - state.blkIndent > 3) {
continue
}
// quirk for blockquotes, this line should already be checked by that rule // quirk for blockquotes, this line should already be checked by that rule
if (state.sCount[nextLine] < 0) { continue } if (state.sCount[nextLine] < 0) {
continue
}
// Some tags can terminate paragraph without empty line. // Some tags can terminate paragraph without empty line.
terminate = false terminate = false
@@ -235,10 +413,14 @@ class Markdown {
break break
} }
} }
if (terminate) { break } if (terminate) {
break
}
} }
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim() content = state
.getLines(startLine, nextLine, state.blkIndent, false)
.trim()
state.line = nextLine state.line = nextLine
@@ -248,18 +430,31 @@ class Markdown {
if (state.parentType === 'list') { if (state.parentType === 'list') {
const match = content.match(/^\[( |x)\] ?(.+)/i) const match = content.match(/^\[( |x)\] ?(.+)/i)
if (match) { if (match) {
const liToken = lastFindInArray(state.tokens, token => token.type === 'list_item_open') const liToken = lastFindInArray(
state.tokens,
token => token.type === 'list_item_open'
)
if (liToken) { if (liToken) {
if (!liToken.attrs) { if (!liToken.attrs) {
liToken.attrs = [] liToken.attrs = []
} }
if (config.preview.lineThroughCheckbox) { if (config.preview.lineThroughCheckbox) {
liToken.attrs.push(['class', `taskListItem${match[1] !== ' ' ? ' checked' : ''}`]) liToken.attrs.push([
'class',
`taskListItem${match[1] !== ' ' ? ' checked' : ''}`
])
} else { } else {
liToken.attrs.push(['class', 'taskListItem']) liToken.attrs.push(['class', 'taskListItem'])
} }
} }
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>` content = `<label class='taskListItem${
match[1] !== ' ' ? ' checked' : ''
}' for='checkbox-${startLine + 1}'><input type='checkbox'${
match[1] !== ' ' ? ' checked' : ''
} id='checkbox-${startLine + 1}'/> ${content.substring(
4,
content.length
)}</label>`
} }
} }
@@ -280,7 +475,7 @@ class Markdown {
// Add line number attribute for scrolling // Add line number attribute for scrolling
const originalRender = this.md.renderer.render const originalRender = this.md.renderer.render
this.md.renderer.render = (tokens, options, env) => { this.md.renderer.render = (tokens, options, env) => {
tokens.forEach((token) => { tokens.forEach(token => {
switch (token.type) { switch (token.type) {
case 'blockquote_open': case 'blockquote_open':
case 'dd_open': case 'dd_open':
@@ -301,7 +496,7 @@ class Markdown {
window.md = this.md window.md = this.md
} }
render (content) { render(content) {
if (!_.isString(content)) content = '' if (!_.isString(content)) content = ''
return this.md.render(content) return this.md.render(content)
} }

View File

@@ -6,7 +6,7 @@
* @param {string} input * @param {string} input
* @return {string} * @return {string}
*/ */
export function strip (input) { export function strip(input) {
let output = input let output = input
try { try {
output = output output = output

View File

@@ -4,12 +4,22 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import queryString from 'query-string' import queryString from 'query-string'
import { push } from 'connected-react-router' import { push } from 'connected-react-router'
export function createMarkdownNote (storage, folder, dispatch, location, params, config) { export function createMarkdownNote(
storage,
folder,
dispatch,
location,
params,
config
) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN') AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE') AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
let tags = [] let tags = []
if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) { if (
config.ui.tagNewNoteWithFilteringTags &&
location.pathname.match(/\/tags/)
) {
tags = params.tagname.split(' ') tags = params.tagname.split(' ')
} }
@@ -29,25 +39,40 @@ export function createMarkdownNote (storage, folder, dispatch, location, params,
note: note note: note
}) })
dispatch(push({ dispatch(
push({
pathname: location.pathname, pathname: location.pathname,
search: queryString.stringify({ key: noteHash }) search: queryString.stringify({ key: noteHash })
})) })
)
ee.emit('list:jump', noteHash) ee.emit('list:jump', noteHash)
ee.emit('detail:focus') ee.emit('detail:focus')
}) })
} }
export function createSnippetNote (storage, folder, dispatch, location, params, config) { export function createSnippetNote(
storage,
folder,
dispatch,
location,
params,
config
) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET') AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE') AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
let tags = [] let tags = []
if (config.ui.tagNewNoteWithFilteringTags && location.pathname.match(/\/tags/)) { if (
config.ui.tagNewNoteWithFilteringTags &&
location.pathname.match(/\/tags/)
) {
tags = params.tagname.split(' ') tags = params.tagname.split(' ')
} }
const defaultLanguage = config.editor.snippetDefaultLanguage === 'Auto Detect' ? null : config.editor.snippetDefaultLanguage const defaultLanguage =
config.editor.snippetDefaultLanguage === 'Auto Detect'
? null
: config.editor.snippetDefaultLanguage
return dataApi return dataApi
.createNote(storage, { .createNote(storage, {
@@ -71,10 +96,12 @@ export function createSnippetNote (storage, folder, dispatch, location, params,
type: 'UPDATE_NOTE', type: 'UPDATE_NOTE',
note: note note: note
}) })
dispatch(push({ dispatch(
push({
pathname: location.pathname, pathname: location.pathname,
search: queryString.stringify({ key: noteHash }) search: queryString.stringify({ key: noteHash })
})) })
)
ee.emit('list:jump', noteHash) ee.emit('list:jump', noteHash)
ee.emit('detail:focus') ee.emit('detail:focus')
}) })

View File

@@ -1,7 +1,7 @@
import consts from 'browser/lib/consts' import consts from 'browser/lib/consts'
import isString from 'lodash/isString' import isString from 'lodash/isString'
export default function normalizeEditorFontFamily (fontFamily) { export default function normalizeEditorFontFamily(fontFamily) {
const defaultEditorFontFamily = consts.DEFAULT_EDITOR_FONT_FAMILY const defaultEditorFontFamily = consts.DEFAULT_EDITOR_FONT_FAMILY
return isString(fontFamily) && fontFamily.length > 0 return isString(fontFamily) && fontFamily.length > 0
? [fontFamily].concat(defaultEditorFontFamily).join(', ') ? [fontFamily].concat(defaultEditorFontFamily).join(', ')

View File

@@ -1,31 +1,38 @@
import _ from 'lodash' import _ from 'lodash'
export default function searchFromNotes (notes, search) { export default function searchFromNotes(notes, search) {
if (search.trim().length === 0) return [] if (search.trim().length === 0) return []
const searchBlocks = search.split(' ').filter(block => { return block !== '' }) const searchBlocks = search.split(' ').filter(block => {
return block !== ''
})
let foundNotes = notes let foundNotes = notes
searchBlocks.forEach((block) => { searchBlocks.forEach(block => {
foundNotes = findByWordOrTag(foundNotes, block) foundNotes = findByWordOrTag(foundNotes, block)
}) })
return foundNotes return foundNotes
} }
function findByWordOrTag (notes, block) { function findByWordOrTag(notes, block) {
let tag = block let tag = block
if (tag.match(/^#.+/)) { if (tag.match(/^#.+/)) {
tag = tag.match(/#(.+)/)[1] tag = tag.match(/#(.+)/)[1]
} }
const tagRegExp = new RegExp(_.escapeRegExp(tag), 'i') const tagRegExp = new RegExp(_.escapeRegExp(tag), 'i')
const wordRegExp = new RegExp(_.escapeRegExp(block), 'i') const wordRegExp = new RegExp(_.escapeRegExp(block), 'i')
return notes.filter((note) => { return notes.filter(note => {
if (_.isArray(note.tags) && note.tags.some((_tag) => _tag.match(tagRegExp))) { if (_.isArray(note.tags) && note.tags.some(_tag => _tag.match(tagRegExp))) {
return true return true
} }
if (note.type === 'SNIPPET_NOTE') { if (note.type === 'SNIPPET_NOTE') {
return note.description.match(wordRegExp) || note.snippets.some((snippet) => { return (
return snippet.name.match(wordRegExp) || snippet.content.match(wordRegExp) note.description.match(wordRegExp) ||
note.snippets.some(snippet => {
return (
snippet.name.match(wordRegExp) || snippet.content.match(wordRegExp)
)
}) })
)
} else if (note.type === 'MARKDOWN_NOTE') { } else if (note.type === 'MARKDOWN_NOTE') {
return note.content.match(wordRegExp) return note.content.match(wordRegExp)
} }

View File

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

View File

@@ -12,19 +12,19 @@ const MILLISECONDS_TILL_LIVECHECK = 500
let dictionary = null let dictionary = null
let self let self
function getAvailableDictionaries () { function getAvailableDictionaries() {
return [ return [
{label: i18n.__('Spellcheck disabled'), value: SPELLCHECK_DISABLED}, { label: i18n.__('Spellcheck disabled'), value: SPELLCHECK_DISABLED },
{label: i18n.__('English'), value: 'en_GB'}, { label: i18n.__('English'), value: 'en_GB' },
{label: i18n.__('German'), value: 'de_DE'}, { label: i18n.__('German'), value: 'de_DE' },
{label: i18n.__('French'), value: 'fr_FR'} { label: i18n.__('French'), value: 'fr_FR' }
] ]
} }
/** /**
* Only to be used in the tests :) * Only to be used in the tests :)
*/ */
function setDictionaryForTestsOnly (newDictionary) { function setDictionaryForTestsOnly(newDictionary) {
dictionary = newDictionary dictionary = newDictionary
} }
@@ -34,7 +34,7 @@ function setDictionaryForTestsOnly (newDictionary) {
* @param {Codemirror} editor CodeMirror-Editor * @param {Codemirror} editor CodeMirror-Editor
* @param {String} lang on of the values from getAvailableDictionaries()-Method * @param {String} lang on of the values from getAvailableDictionaries()-Method
*/ */
function setLanguage (editor, lang) { function setLanguage(editor, lang) {
self = this self = this
dictionary = null dictionary = null
@@ -50,8 +50,7 @@ function setLanguage (editor, lang) {
dictionary = new Typo(lang, false, false, { dictionary = new Typo(lang, false, false, {
dictionaryPath: DICTIONARY_PATH, dictionaryPath: DICTIONARY_PATH,
asyncLoad: true, asyncLoad: true,
loadedCallback: () => loadedCallback: () => checkWholeDocument(editor)
checkWholeDocument(editor)
}) })
} }
} }
@@ -60,12 +59,12 @@ function setLanguage (editor, lang) {
* Checks the whole content of the editor for typos * Checks the whole content of the editor for typos
* @param {Codemirror} editor CodeMirror-Editor * @param {Codemirror} editor CodeMirror-Editor
*/ */
function checkWholeDocument (editor) { function checkWholeDocument(editor) {
const lastLine = editor.lineCount() - 1 const lastLine = editor.lineCount() - 1
const textOfLastLine = editor.getLine(lastLine) || '' const textOfLastLine = editor.getLine(lastLine) || ''
const lastChar = textOfLastLine.length const lastChar = textOfLastLine.length
const from = {line: 0, ch: 0} const from = { line: 0, ch: 0 }
const to = {line: lastLine, ch: lastChar} const to = { line: lastLine, ch: lastChar }
checkMultiLineRange(editor, from, to) checkMultiLineRange(editor, from, to)
} }
@@ -75,15 +74,18 @@ function checkWholeDocument (editor) {
* @param {line, ch} from starting position of the spellcheck * @param {line, ch} from starting position of the spellcheck
* @param {line, ch} to end position of the spellcheck * @param {line, ch} to end position of the spellcheck
*/ */
function checkMultiLineRange (editor, from, to) { function checkMultiLineRange(editor, from, to) {
function sortRange (pos1, pos2) { function sortRange(pos1, pos2) {
if (pos1.line > pos2.line || (pos1.line === pos2.line && pos1.ch > pos2.ch)) { if (
return {from: pos2, to: pos1} pos1.line > pos2.line ||
(pos1.line === pos2.line && pos1.ch > pos2.ch)
) {
return { from: pos2, to: pos1 }
} }
return {from: pos1, to: pos2} return { from: pos1, to: pos2 }
} }
const {from: smallerPos, to: higherPos} = sortRange(from, to) const { from: smallerPos, to: higherPos } = sortRange(from, to)
for (let l = smallerPos.line; l <= higherPos.line; l++) { for (let l = smallerPos.line; l <= higherPos.line; l++) {
const line = editor.getLine(l) || '' const line = editor.getLine(l) || ''
let w = 0 let w = 0
@@ -95,9 +97,9 @@ function checkMultiLineRange (editor, from, to) {
wEnd = higherPos.ch wEnd = higherPos.ch
} }
while (w <= wEnd) { while (w <= wEnd) {
const wordRange = editor.findWordAt({line: l, ch: w}) const wordRange = editor.findWordAt({ line: l, ch: w })
self.checkWord(editor, wordRange) self.checkWord(editor, wordRange)
w += (wordRange.head.ch - wordRange.anchor.ch) + 1 w += wordRange.head.ch - wordRange.anchor.ch + 1
} }
} }
} }
@@ -110,13 +112,15 @@ function checkMultiLineRange (editor, from, to) {
* @param wordRange Object specifying the range that should be checked. * @param wordRange Object specifying the range that should be checked.
* Having the following structure: <code>{anchor: {line: integer, ch: integer}, head: {line: integer, ch: integer}}</code> * Having the following structure: <code>{anchor: {line: integer, ch: integer}, head: {line: integer, ch: integer}}</code>
*/ */
function checkWord (editor, wordRange) { function checkWord(editor, wordRange) {
const word = editor.getRange(wordRange.anchor, wordRange.head) const word = editor.getRange(wordRange.anchor, wordRange.head)
if (word == null || word.length <= 3) { if (word == null || word.length <= 3) {
return return
} }
if (!dictionary.check(word)) { if (!dictionary.check(word)) {
editor.markText(wordRange.anchor, wordRange.head, {className: styles[CSS_ERROR_CLASS]}) editor.markText(wordRange.anchor, wordRange.head, {
className: styles[CSS_ERROR_CLASS]
})
} }
} }
@@ -126,32 +130,40 @@ function checkWord (editor, wordRange) {
* @param fromChangeObject codeMirror changeObject describing the start of the editing * @param fromChangeObject codeMirror changeObject describing the start of the editing
* @param toChangeObject codeMirror changeObject describing the end of the editing * @param toChangeObject codeMirror changeObject describing the end of the editing
*/ */
function checkChangeRange (editor, fromChangeObject, toChangeObject) { function checkChangeRange(editor, fromChangeObject, toChangeObject) {
/** /**
* Calculate the smallest respectively largest position as a start, resp. end, position and return it * Calculate the smallest respectively largest position as a start, resp. end, position and return it
* @param start CodeMirror change object * @param start CodeMirror change object
* @param end CodeMirror change object * @param end CodeMirror change object
* @returns {{start: {line: *, ch: *}, end: {line: *, ch: *}}} * @returns {{start: {line: *, ch: *}, end: {line: *, ch: *}}}
*/ */
function getStartAndEnd (start, end) { function getStartAndEnd(start, end) {
const possiblePositions = [start.from, start.to, end.from, end.to] const possiblePositions = [start.from, start.to, end.from, end.to]
let smallest = start.from let smallest = start.from
let biggest = end.to let biggest = end.to
for (const currentPos of possiblePositions) { for (const currentPos of possiblePositions) {
if (currentPos.line < smallest.line || (currentPos.line === smallest.line && currentPos.ch < smallest.ch)) { if (
currentPos.line < smallest.line ||
(currentPos.line === smallest.line && currentPos.ch < smallest.ch)
) {
smallest = currentPos smallest = currentPos
} }
if (currentPos.line > biggest.line || (currentPos.line === biggest.line && currentPos.ch > biggest.ch)) { if (
currentPos.line > biggest.line ||
(currentPos.line === biggest.line && currentPos.ch > biggest.ch)
) {
biggest = currentPos biggest = currentPos
} }
} }
return {start: smallest, end: biggest} return { start: smallest, end: biggest }
} }
if (dictionary === null || editor == null) { return } if (dictionary === null || editor == null) {
return
}
try { try {
const {start, end} = getStartAndEnd(fromChangeObject, toChangeObject) const { start, end } = getStartAndEnd(fromChangeObject, toChangeObject)
// Expand the range to include words after/before whitespaces // Expand the range to include words after/before whitespaces
start.ch = Math.max(start.ch - 1, 0) start.ch = Math.max(start.ch - 1, 0)
@@ -165,29 +177,40 @@ function checkChangeRange (editor, fromChangeObject, toChangeObject) {
self.checkMultiLineRange(editor, start, end) self.checkMultiLineRange(editor, start, end)
} catch (e) { } catch (e) {
console.info('Error during the spell check. It might be due to problems figuring out the range of the new text..', e) console.info(
'Error during the spell check. It might be due to problems figuring out the range of the new text..',
e
)
} }
} }
function saveLiveSpellCheckFrom (changeObject) { function saveLiveSpellCheckFrom(changeObject) {
liveSpellCheckFrom = changeObject liveSpellCheckFrom = changeObject
} }
let liveSpellCheckFrom let liveSpellCheckFrom
const debouncedSpellCheckLeading = _.debounce(saveLiveSpellCheckFrom, MILLISECONDS_TILL_LIVECHECK, { const debouncedSpellCheckLeading = _.debounce(
'leading': true, saveLiveSpellCheckFrom,
'trailing': false MILLISECONDS_TILL_LIVECHECK,
}) {
const debouncedSpellCheck = _.debounce(checkChangeRange, MILLISECONDS_TILL_LIVECHECK, { leading: true,
'leading': false, trailing: false
'trailing': true }
}) )
const debouncedSpellCheck = _.debounce(
checkChangeRange,
MILLISECONDS_TILL_LIVECHECK,
{
leading: false,
trailing: true
}
)
/** /**
* Handles a keystroke. Buffers the input and performs a live spell check after a certain time. Uses _debounce from lodash to buffer the input * Handles a keystroke. Buffers the input and performs a live spell check after a certain time. Uses _debounce from lodash to buffer the input
* @param {Codemirror} editor CodeMirror-Editor * @param {Codemirror} editor CodeMirror-Editor
* @param changeObject codeMirror changeObject * @param changeObject codeMirror changeObject
*/ */
function handleChange (editor, changeObject) { function handleChange(editor, changeObject) {
if (dictionary === null) { if (dictionary === null) {
return return
} }
@@ -201,7 +224,7 @@ function handleChange (editor, changeObject) {
* @param word word to be checked * @param word word to be checked
* @returns {String[]} Array of suggestions * @returns {String[]} Array of suggestions
*/ */
function getSpellingSuggestion (word) { function getSpellingSuggestion(word) {
if (dictionary == null || word == null) { if (dictionary == null || word == null) {
return [] return []
} }
@@ -211,7 +234,7 @@ function getSpellingSuggestion (word) {
/** /**
* Returns the name of the CSS class used for errors * Returns the name of the CSS class used for errors
*/ */
function getCSSClassName () { function getCSSClassName() {
return styles[CSS_ERROR_CLASS] return styles[CSS_ERROR_CLASS]
} }

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

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

44
browser/lib/ui-themes.js Normal file
View File

@@ -0,0 +1,44 @@
import i18n from 'browser/lib/i18n'
export default [
{
name: 'dark',
label: i18n.__('Dark'),
isDark: true
},
{
name: 'default',
label: i18n.__('Default'),
isDark: false
},
{
name: 'dracula',
label: i18n.__('Dracula'),
isDark: true
},
{
name: 'monokai',
label: i18n.__('Monokai'),
isDark: true
},
{
name: 'nord',
label: i18n.__('Nord'),
isDark: true
},
{
name: 'solarized-dark',
label: i18n.__('Solarized Dark'),
isDark: true
},
{
name: 'vulcan',
label: i18n.__('Vulcan'),
isDark: true
},
{
name: 'white',
label: i18n.__('White'),
isDark: false
}
]

View File

@@ -1,4 +1,4 @@
export function lastFindInArray (array, callback) { export function lastFindInArray(array, callback) {
for (let i = array.length - 1; i >= 0; --i) { for (let i = array.length - 1; i >= 0; --i) {
if (callback(array[i], i, array)) { if (callback(array[i], i, array)) {
return array[i] return array[i]
@@ -6,7 +6,7 @@ export function lastFindInArray (array, callback) {
} }
} }
export function escapeHtmlCharacters ( export function escapeHtmlCharacters(
html, html,
opt = { detectCodeBlock: false, skipSingleQuote: false } opt = { detectCodeBlock: false, skipSingleQuote: false }
) { ) {
@@ -115,7 +115,7 @@ export function escapeHtmlCharacters (
return html return html
} }
export function isObjectEqual (a, b) { export function isObjectEqual(a, b) {
const aProps = Object.getOwnPropertyNames(a) const aProps = Object.getOwnPropertyNames(a)
const bProps = Object.getOwnPropertyNames(b) const bProps = Object.getOwnPropertyNames(b)
@@ -132,13 +132,30 @@ export function isObjectEqual (a, b) {
return true return true
} }
export function isMarkdownTitleURL (str) { export function isMarkdownTitleURL(str) {
return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str) return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(
str
)
}
export function humanFileSize(bytes) {
const threshold = 1000
if (Math.abs(bytes) < threshold) {
return bytes + ' B'
}
var units = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
var u = -1
do {
bytes /= threshold
++u
} while (Math.abs(bytes) >= threshold && u < units.length - 1)
return bytes.toFixed(1) + ' ' + units[u]
} }
export default { export default {
lastFindInArray, lastFindInArray,
escapeHtmlCharacters, escapeHtmlCharacters,
isObjectEqual, isObjectEqual,
isMarkdownTitleURL isMarkdownTitleURL,
humanFileSize
} }

View File

@@ -24,23 +24,16 @@ body[data-theme="dark"]
.empty-message .empty-message
color $ui-dark-inactive-text-color color $ui-dark-inactive-text-color
body[data-theme="solarized-dark"] apply-theme(theme)
body[data-theme={theme}]
.root .root
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color get-theme-var(theme, 'noteDetail-backgroundColor')
border-left 1px solid $ui-solarized-dark-borderColor border-left 1px solid get-theme-var(theme, 'borderColor')
.empty-message .empty-message
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
body[data-theme="monokai"] for theme in 'solarized-dark' 'dracula'
.root apply-theme(theme)
background-color $ui-monokai-noteDetail-backgroundColor
border-left 1px solid $ui-monokai-borderColor
.empty-message
color $ui-monokai-text-color
body[data-theme="dracula"] for theme in $themes
.root apply-theme(theme)
background-color $ui-dracula-noteDetail-backgroundColor
border-left 1px solid $ui-dracula-borderColor
.empty-message
color $ui-dracula-text-color

View File

@@ -6,7 +6,7 @@ import _ from 'lodash'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
class FolderSelect extends React.Component { class FolderSelect extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
@@ -16,24 +16,27 @@ class FolderSelect extends React.Component {
} }
} }
componentDidMount () { componentDidMount() {
this.value = this.props.value this.value = this.props.value
} }
componentDidUpdate () { componentDidUpdate() {
this.value = this.props.value this.value = this.props.value
} }
handleClick (e) { handleClick(e) {
this.setState({ this.setState(
{
status: 'SEARCH', status: 'SEARCH',
optionIndex: -1 optionIndex: -1
}, () => { },
() => {
this.refs.search.focus() this.refs.search.focus()
}) }
)
} }
handleFocus (e) { handleFocus(e) {
if (this.state.status === 'IDLE') { if (this.state.status === 'IDLE') {
this.setState({ this.setState({
status: 'FOCUS' status: 'FOCUS'
@@ -41,7 +44,7 @@ class FolderSelect extends React.Component {
} }
} }
handleBlur (e) { handleBlur(e) {
if (this.state.status === 'FOCUS') { if (this.state.status === 'FOCUS') {
this.setState({ this.setState({
status: 'IDLE' status: 'IDLE'
@@ -49,40 +52,49 @@ class FolderSelect extends React.Component {
} }
} }
handleKeyDown (e) { handleKeyDown(e) {
switch (e.keyCode) { switch (e.keyCode) {
case 13: case 13:
if (this.state.status === 'FOCUS') { if (this.state.status === 'FOCUS') {
this.setState({ this.setState(
{
status: 'SEARCH', status: 'SEARCH',
optionIndex: -1 optionIndex: -1
}, () => { },
() => {
this.refs.search.focus() this.refs.search.focus()
}) }
)
} }
break break
case 40: case 40:
case 38: case 38:
if (this.state.status === 'FOCUS') { if (this.state.status === 'FOCUS') {
this.setState({ this.setState(
{
status: 'SEARCH', status: 'SEARCH',
optionIndex: 0 optionIndex: 0
}, () => { },
() => {
this.refs.search.focus() this.refs.search.focus()
}) }
)
} }
break break
case 9: case 9:
if (e.shiftKey) { if (e.shiftKey) {
e.preventDefault() e.preventDefault()
const tabbable = document.querySelectorAll('a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])') const tabbable = document.querySelectorAll(
const previousEl = tabbable[Array.prototype.indexOf.call(tabbable, this.refs.root) - 1] 'a:not([disabled]), button:not([disabled]), input[type=text]:not([disabled]), [tabindex]:not([disabled]):not([tabindex="-1"])'
)
const previousEl =
tabbable[Array.prototype.indexOf.call(tabbable, this.refs.root) - 1]
if (previousEl != null) previousEl.focus() if (previousEl != null) previousEl.focus()
} }
} }
} }
handleSearchInputBlur (e) { handleSearchInputBlur(e) {
if (e.relatedTarget !== this.refs.root) { if (e.relatedTarget !== this.refs.root) {
this.setState({ this.setState({
status: 'IDLE' status: 'IDLE'
@@ -90,12 +102,15 @@ class FolderSelect extends React.Component {
} }
} }
handleSearchInputChange (e) { handleSearchInputChange(e) {
const { folders } = this.props const { folders } = this.props
const search = this.refs.search.value const search = this.refs.search.value
const optionIndex = search.length > 0 const optionIndex =
? _.findIndex(folders, (folder) => { search.length > 0
return folder.name.match(new RegExp('^' + _.escapeRegExp(search), 'i')) ? _.findIndex(folders, folder => {
return folder.name.match(
new RegExp('^' + _.escapeRegExp(search), 'i')
)
}) })
: -1 : -1
@@ -105,7 +120,7 @@ class FolderSelect extends React.Component {
}) })
} }
handleSearchInputKeyDown (e) { handleSearchInputKeyDown(e) {
switch (e.keyCode) { switch (e.keyCode) {
case 40: case 40:
e.stopPropagation() e.stopPropagation()
@@ -121,15 +136,18 @@ class FolderSelect extends React.Component {
break break
case 27: case 27:
e.stopPropagation() e.stopPropagation()
this.setState({ this.setState(
{
status: 'FOCUS' status: 'FOCUS'
}, () => { },
() => {
this.refs.root.focus() this.refs.root.focus()
}) }
)
} }
} }
nextOption () { nextOption() {
let { optionIndex } = this.state let { optionIndex } = this.state
const { folders } = this.props const { folders } = this.props
@@ -141,7 +159,7 @@ class FolderSelect extends React.Component {
}) })
} }
previousOption () { previousOption() {
const { folders } = this.props const { folders } = this.props
let { optionIndex } = this.state let { optionIndex } = this.state
@@ -153,46 +171,52 @@ class FolderSelect extends React.Component {
}) })
} }
selectOption () { selectOption() {
const { folders } = this.props const { folders } = this.props
const optionIndex = this.state.optionIndex const optionIndex = this.state.optionIndex
const folder = folders[optionIndex] const folder = folders[optionIndex]
if (folder != null) { if (folder != null) {
this.setState({ this.setState(
{
status: 'FOCUS' status: 'FOCUS'
}, () => { },
() => {
this.setValue(folder.key) this.setValue(folder.key)
this.refs.root.focus() this.refs.root.focus()
}) }
)
} }
} }
handleOptionClick (storageKey, folderKey) { handleOptionClick(storageKey, folderKey) {
return (e) => { return e => {
e.stopPropagation() e.stopPropagation()
this.setState({ this.setState(
{
status: 'FOCUS' status: 'FOCUS'
}, () => { },
() => {
this.setValue(storageKey + '-' + folderKey) this.setValue(storageKey + '-' + folderKey)
this.refs.root.focus() this.refs.root.focus()
}) }
)
} }
} }
setValue (value) { setValue(value) {
this.value = value this.value = value
this.props.onChange() this.props.onChange()
} }
render () { render() {
const { className, data, value } = this.props const { className, data, value } = this.props
const splitted = value.split('-') const splitted = value.split('-')
const storageKey = splitted.shift() const storageKey = splitted.shift()
const folderKey = splitted.shift() const folderKey = splitted.shift()
let options = [] let options = []
data.storageMap.forEach((storage, index) => { data.storageMap.forEach((storage, index) => {
storage.folders.forEach((folder) => { storage.folders.forEach(folder => {
options.push({ options.push({
storage: storage, storage: storage,
folder: folder folder: folder
@@ -200,39 +224,49 @@ class FolderSelect extends React.Component {
}) })
}) })
const currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0] const currentOption = options.filter(
option =>
option.storage.key === storageKey && option.folder.key === folderKey
)[0]
if (this.state.search.trim().length > 0) { if (this.state.search.trim().length > 0) {
const filter = new RegExp('^' + _.escapeRegExp(this.state.search), 'i') const filter = new RegExp('^' + _.escapeRegExp(this.state.search), 'i')
options = options.filter((option) => filter.test(option.folder.name)) options = options.filter(option => filter.test(option.folder.name))
} }
const optionList = options const optionList = options.map((option, index) => {
.map((option, index) => {
return ( return (
<div styleName={index === this.state.optionIndex <div
styleName={
index === this.state.optionIndex
? 'search-optionList-item--active' ? 'search-optionList-item--active'
: 'search-optionList-item' : 'search-optionList-item'
} }
key={option.storage.key + '-' + option.folder.key} key={option.storage.key + '-' + option.folder.key}
onClick={(e) => this.handleOptionClick(option.storage.key, option.folder.key)(e)} onClick={e =>
this.handleOptionClick(option.storage.key, option.folder.key)(e)
}
> >
<span styleName='search-optionList-item-name' <span
style={{borderColor: option.folder.color}} styleName='search-optionList-item-name'
style={{ borderColor: option.folder.color }}
> >
{option.folder.name} {option.folder.name}
<span styleName='search-optionList-item-name-surfix'>in {option.storage.name}</span> <span styleName='search-optionList-item-name-surfix'>
in {option.storage.name}
</span>
</span> </span>
</div> </div>
) )
}) })
return ( return (
<div className={_.isString(className) <div
? 'FolderSelect ' + className className={
: 'FolderSelect' _.isString(className) ? 'FolderSelect ' + className : 'FolderSelect'
} }
styleName={this.state.status === 'SEARCH' styleName={
this.state.status === 'SEARCH'
? 'root--search' ? 'root--search'
: this.state.status === 'FOCUS' : this.state.status === 'FOCUS'
? 'root--focus' ? 'root--focus'
@@ -240,28 +274,28 @@ class FolderSelect extends React.Component {
} }
ref='root' ref='root'
tabIndex='0' tabIndex='0'
onClick={(e) => this.handleClick(e)} onClick={e => this.handleClick(e)}
onFocus={(e) => this.handleFocus(e)} onFocus={e => this.handleFocus(e)}
onBlur={(e) => this.handleBlur(e)} onBlur={e => this.handleBlur(e)}
onKeyDown={(e) => this.handleKeyDown(e)} onKeyDown={e => this.handleKeyDown(e)}
> >
{this.state.status === 'SEARCH' {this.state.status === 'SEARCH' ? (
? <div styleName='search'> <div styleName='search'>
<input styleName='search-input' <input
styleName='search-input'
ref='search' ref='search'
value={this.state.search} value={this.state.search}
placeholder={i18n.__('Folder...')} placeholder={i18n.__('Folder...')}
onChange={(e) => this.handleSearchInputChange(e)} onChange={e => this.handleSearchInputChange(e)}
onBlur={(e) => this.handleSearchInputBlur(e)} onBlur={e => this.handleSearchInputBlur(e)}
onKeyDown={(e) => this.handleSearchInputKeyDown(e)} onKeyDown={e => this.handleSearchInputKeyDown(e)}
/> />
<div styleName='search-optionList' <div styleName='search-optionList' ref='optionList'>
ref='optionList'
>
{optionList} {optionList}
</div> </div>
</div> </div>
: <div styleName='idle' style={{color: currentOption.folder.color}}> ) : currentOption ? (
<div styleName='idle' style={{ color: currentOption.folder.color }}>
<div styleName='idle-label'> <div styleName='idle-label'>
<i className='fa fa-folder' /> <i className='fa fa-folder' />
<span styleName='idle-label-name'> <span styleName='idle-label-name'>
@@ -269,8 +303,7 @@ class FolderSelect extends React.Component {
</span> </span>
</div> </div>
</div> </div>
} ) : null}
</div> </div>
) )
} }
@@ -280,11 +313,13 @@ FolderSelect.propTypes = {
className: PropTypes.string, className: PropTypes.string,
onChange: PropTypes.func, onChange: PropTypes.func,
value: PropTypes.string, value: PropTypes.string,
folders: PropTypes.arrayOf(PropTypes.shape({ folders: PropTypes.arrayOf(
PropTypes.shape({
key: PropTypes.string, key: PropTypes.string,
name: PropTypes.string, name: PropTypes.string,
color: PropTypes.string color: PropTypes.string
})) })
)
} }
export default CSSModules(FolderSelect, styles) export default CSSModules(FolderSelect, styles)

View File

@@ -134,54 +134,39 @@ body[data-theme="dark"]
.search-optionList-item-name-surfix .search-optionList-item-name-surfix
color $ui-dark-inactive-text-color color $ui-dark-inactive-text-color
body[data-theme="monokai"] apply-theme(theme)
body[data-theme={theme}]
.root .root
color $ui-dark-text-color
&:hover &:hover
color white background-color get-theme-var(theme, 'button--hover-backgroundColor')
background-color $ui-monokai-button--hover-backgroundColor border-color get-theme-var(theme, 'borderColor')
border-color $ui-monokai-borderColor
.search-input
color get-theme-var(theme, 'text-color')
background-color transparent
border-color get-theme-var(theme, 'borderColor')
.search-optionList .search-optionList
color white color get-theme-var(theme, 'text-color')
border-color $ui-monokai-borderColor border-color get-theme-var(theme, 'borderColor')
background-color $ui-monokai-button-backgroundColor background-color get-theme-var(theme, 'button-backgroundColor')
.search-optionList-item .search-optionList-item
&:hover &:hover
background-color lighten($ui-monokai-button--hover-backgroundColor, 15%) background-color lighten(get-theme-var(theme, 'button--hover-backgroundColor'), 15%)
.search-optionList-item--active .search-optionList-item--active
background-color $ui-monokai-button--active-backgroundColor background-color get-theme-var(theme, 'button--active-backgroundColor')
color $ui-monokai-button--active-color color get-theme-var(theme, 'button--active-color')
&:hover &:hover
background-color $ui-monokai-button--active-backgroundColor background-color get-theme-var(theme, 'button--active-backgroundColor')
color $ui-monokai-button--active-color color get-theme-var(theme, 'button--active-color')
.search-optionList-item-name-surfix .search-optionList-item-name-surfix
color $ui-monokai-inactive-text-color color get-theme-var(theme, 'inactive-text-color')
body[data-theme="dracula"] for theme in 'solarized-dark' 'dracula'
.root apply-theme(theme)
color $ui-dracula-text-color
&:hover
color #f8f8f2
background-color $ui-dark-button--hover-backgroundColor
border-color $ui-dracula-borderColor
.search-optionList for theme in $themes
color #f8f8f2 apply-theme(theme)
border-color $ui-dracula-borderColor
background-color $ui-dracula-button-backgroundColor
.search-optionList-item
&:hover
background-color lighten($ui-dracula-button--hover-backgroundColor, 15%)
.search-optionList-item--active
background-color $ui-dracula-button--active-backgroundColor
color $ui-dracula-button--active-color
&:hover
background-color $ui-dark-button--hover-backgroundColor
color $ui-dracula-button--active-color
.search-optionList-item-name-surfix
color $ui-dracula-inactive-text-color

View File

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

View File

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

View File

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

View File

@@ -4,12 +4,8 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './InfoButton.styl' import styles from './InfoButton.styl'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
const InfoButton = ({ const InfoButton = ({ onClick }) => (
onClick <button styleName='control-infoButton' onClick={e => onClick(e)}>
}) => (
<button styleName='control-infoButton'
onClick={(e) => onClick(e)}
>
<img className='infoButton' src='../resources/icon/icon-info.svg' /> <img className='infoButton' src='../resources/icon/icon-info.svg' />
<span styleName='tooltip'>{i18n.__('Info')}</span> <span styleName='tooltip'>{i18n.__('Info')}</span>
</button> </button>

View File

@@ -6,28 +6,47 @@ import copy from 'copy-to-clipboard'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
class InfoPanel extends React.Component { class InfoPanel extends React.Component {
copyNoteLink () { copyNoteLink() {
const {noteLink} = this.props const { noteLink } = this.props
this.refs.noteLink.select() this.refs.noteLink.select()
copy(noteLink) copy(noteLink)
} }
render () { render() {
const { const {
storageName, folderName, noteLink, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, exportAsPdf, wordCount, letterCount, type, print storageName,
folderName,
noteLink,
updatedAt,
createdAt,
exportAsMd,
exportAsTxt,
exportAsHtml,
exportAsPdf,
wordCount,
letterCount,
type,
print
} = this.props } = this.props
return ( return (
<div className='infoPanel' styleName='control-infoButton-panel' style={{display: 'none'}}> <div
className='infoPanel'
styleName='control-infoButton-panel'
style={{ display: 'none' }}
>
<div> <div>
<p styleName='modification-date'>{updatedAt}</p> <p styleName='modification-date'>{updatedAt}</p>
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p> <p styleName='modification-date-desc'>
{i18n.__('MODIFICATION DATE')}
</p>
</div> </div>
<hr /> <hr />
{type === 'SNIPPET_NOTE' {type === 'SNIPPET_NOTE' ? (
? '' ''
: <div styleName='count-wrap'> ) : (
<div styleName='count-wrap'>
<div styleName='count-number'> <div styleName='count-number'>
<p styleName='infoPanel-defaul-count'>{wordCount}</p> <p styleName='infoPanel-defaul-count'>{wordCount}</p>
<p styleName='infoPanel-sub-count'>{i18n.__('Words')}</p> <p styleName='infoPanel-sub-count'>{i18n.__('Words')}</p>
@@ -37,12 +56,9 @@ class InfoPanel extends React.Component {
<p styleName='infoPanel-sub-count'>{i18n.__('Letters')}</p> <p styleName='infoPanel-sub-count'>{i18n.__('Letters')}</p>
</div> </div>
</div> </div>
} )}
{type === 'SNIPPET_NOTE' {type === 'SNIPPET_NOTE' ? '' : <hr />}
? ''
: <hr />
}
<div> <div>
<p styleName='infoPanel-default'>{storageName}</p> <p styleName='infoPanel-default'>{storageName}</p>
@@ -60,8 +76,18 @@ class InfoPanel extends React.Component {
</div> </div>
<div> <div>
<input styleName='infoPanel-noteLink' ref='noteLink' defaultValue={noteLink} onClick={(e) => { e.target.select() }} /> <input
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'> styleName='infoPanel-noteLink'
ref='noteLink'
defaultValue={noteLink}
onClick={e => {
e.target.select()
}}
/>
<button
onClick={() => this.copyNoteLink()}
styleName='infoPanel-copyButton'
>
<i className='fa fa-clipboard' /> <i className='fa fa-clipboard' />
</button> </button>
<p styleName='infoPanel-sub'>{i18n.__('NOTE LINK')}</p> <p styleName='infoPanel-sub'>{i18n.__('NOTE LINK')}</p>
@@ -70,27 +96,39 @@ class InfoPanel extends React.Component {
<hr /> <hr />
<div id='export-wrap'> <div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}> <button
styleName='export--enable'
onClick={e => exportAsMd(e, 'export-md')}
>
<i className='fa fa-file-code-o' /> <i className='fa fa-file-code-o' />
<p>{i18n.__('.md')}</p> <p>{i18n.__('.md')}</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}> <button
styleName='export--enable'
onClick={e => exportAsTxt(e, 'export-txt')}
>
<i className='fa fa-file-text-o' /> <i className='fa fa-file-text-o' />
<p>{i18n.__('.txt')}</p> <p>{i18n.__('.txt')}</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}> <button
styleName='export--enable'
onClick={e => exportAsHtml(e, 'export-html')}
>
<i className='fa fa-html5' /> <i className='fa fa-html5' />
<p>{i18n.__('.html')}</p> <p>{i18n.__('.html')}</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsPdf(e, 'export-pdf')}> <button
styleName='export--enable'
onClick={e => exportAsPdf(e, 'export-pdf')}
>
<i className='fa fa-file-pdf-o' /> <i className='fa fa-file-pdf-o' />
<p>{i18n.__('.pdf')}</p> <p>{i18n.__('.pdf')}</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => print(e, 'print')}> <button styleName='export--enable' onClick={e => print(e, 'print')}>
<i className='fa fa-print' /> <i className='fa fa-print' />
<p>{i18n.__('Print')}</p> <p>{i18n.__('Print')}</p>
</button> </button>

View File

@@ -138,162 +138,49 @@
.export--unable .export--unable
cursor not-allowed cursor not-allowed
body[data-theme="dark"] apply-theme(theme)
body[data-theme={theme}]
.control-infoButton-panel .control-infoButton-panel
background-color $ui-dark-noteList-backgroundColor background-color get-theme-var(theme, 'noteList-backgroundColor')
.control-infoButton-panel-trash .control-infoButton-panel-trash
background-color $ui-dark-noteList-backgroundColor background-color get-theme-var(theme, 'noteList-backgroundColor')
.modification-date .modification-date
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
.modification-date-desc .modification-date-desc
color $ui-inactive-text-color color $ui-inactive-text-color
.infoPanel-defaul-count .infoPanel-defaul-count
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
.infoPanel-sub-count .infoPanel-sub-count
color $ui-inactive-text-color color $ui-inactive-text-color
.infoPanel-default .infoPanel-default
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
.infoPanel-sub .infoPanel-sub
color $ui-inactive-text-color color $ui-inactive-text-color
.infoPanel-noteLink .infoPanel-noteLink
background-color alpha($ui-dark-borderColor, 60%) background-color alpha(get-theme-var(theme, 'borderColor'), 20%)
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
[id=export-wrap] [id=export-wrap]
button button
color $ui-dark-inactive-text-color color $ui-dark-inactive-text-color
&:hover &:hover
background-color alpha($ui-dark-borderColor, 20%) background-color alpha(get-theme-var(theme, 'borderColor'), 20%)
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
p p
color $ui-dark-inactive-text-color color $ui-dark-inactive-text-color
&:hover &:hover
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
body[data-theme="solarized-dark"] for theme in 'dark' 'solarized-dark' 'dracula'
.control-infoButton-panel apply-theme(theme)
background-color $ui-solarized-dark-noteList-backgroundColor
.control-infoButton-panel-trash for theme in $themes
background-color $ui-solarized-ark-noteList-backgroundColor apply-theme(theme)
.modification-date
color $ui-solarized-ark-text-color
.modification-date-desc
color $ui-inactive-text-color
.infoPanel-defaul-count
color $ui-solarized-dark-text-color
.infoPanel-sub-count
color $ui-inactive-text-color
.infoPanel-default
color $ui-solarized-ark-text-color
.infoPanel-sub
color $ui-inactive-text-color
.infoPanel-noteLink
background-color alpha($ui-solarized-dark-borderColor, 20%)
color $ui-solarized-dark-text-color
[id=export-wrap]
button
color $ui-dark-inactive-text-color
&:hover
background-color alpha($ui-solarized-dark-borderColor, 20%)
color $ui-solarized-ark-text-color
p
color $ui-dark-inactive-text-color
&:hover
color $ui-solarized-ark-text-color
body[data-theme="monokai"]
.control-infoButton-panel
background-color $ui-monokai-noteList-backgroundColor
.control-infoButton-panel-trash
background-color $ui-monokai-noteList-backgroundColor
.modification-date
color $ui-monokai-text-color
.modification-date-desc
color $ui-inactive-text-color
.infoPanel-defaul-count
color $ui-monokai-text-color
.infoPanel-sub-count
color $ui-inactive-text-color
.infoPanel-default
color $ui-monokai-text-color
.infoPanel-sub
color $ui-inactive-text-color
.infoPanel-noteLink
background-color alpha($ui-monokai-borderColor, 20%)
color $ui-monokai-text-color
[id=export-wrap]
button
color $ui-dark-inactive-text-color
&:hover
background-color alpha($ui-monokai-borderColor, 20%)
color $ui-monokai-text-color
p
color $ui-dark-inactive-text-color
&:hover
color $ui-monokai-text-color
body[data-theme="dracula"]
.control-infoButton-panel
background-color $ui-dracula-noteList-backgroundColor
.control-infoButton-panel-trash
background-color $ui-dracula-noteList-backgroundColor
.modification-date
color $ui-dracula-text-color
.modification-date-desc
color $ui-inactive-text-color
.infoPanel-defaul-count
color $ui-dracula-text-color
.infoPanel-sub-count
color $ui-inactive-text-color
.infoPanel-default
color $ui-dracula-text-color
.infoPanel-sub
color $ui-inactive-text-color
.infoPanel-noteLink
background-color alpha($ui-dracula-borderColor, 20%)
color $ui-dracula-text-color
[id=export-wrap]
button
color $ui-dark-inactive-text-color
&:hover
background-color alpha($ui-dracula-borderColor, 20%)
color $ui-dracula-text-color
p
color $ui-dark-inactive-text-color
&:hover
color $ui-dracula-text-color

View File

@@ -5,9 +5,20 @@ import styles from './InfoPanel.styl'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
const InfoPanelTrashed = ({ const InfoPanelTrashed = ({
storageName, folderName, updatedAt, createdAt, exportAsMd, exportAsTxt, exportAsHtml, exportAsPdf storageName,
folderName,
updatedAt,
createdAt,
exportAsMd,
exportAsTxt,
exportAsHtml,
exportAsPdf
}) => ( }) => (
<div className='infoPanel' styleName='control-infoButton-panel-trash' style={{display: 'none'}}> <div
className='infoPanel'
styleName='control-infoButton-panel-trash'
style={{ display: 'none' }}
>
<div> <div>
<p styleName='modification-date'>{updatedAt}</p> <p styleName='modification-date'>{updatedAt}</p>
<p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p> <p styleName='modification-date-desc'>{i18n.__('MODIFICATION DATE')}</p>
@@ -21,7 +32,10 @@ const InfoPanelTrashed = ({
</div> </div>
<div> <div>
<p styleName='infoPanel-default'><text styleName='infoPanel-trash'>Trash</text>{folderName}</p> <p styleName='infoPanel-default'>
<text styleName='infoPanel-trash'>Trash</text>
{folderName}
</p>
<p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p> <p styleName='infoPanel-sub'>{i18n.__('FOLDER')}</p>
</div> </div>
@@ -31,22 +45,34 @@ const InfoPanelTrashed = ({
</div> </div>
<div id='export-wrap'> <div id='export-wrap'>
<button styleName='export--enable' onClick={(e) => exportAsMd(e, 'export-md')}> <button
styleName='export--enable'
onClick={e => exportAsMd(e, 'export-md')}
>
<i className='fa fa-file-code-o' /> <i className='fa fa-file-code-o' />
<p>.md</p> <p>.md</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsTxt(e, 'export-txt')}> <button
styleName='export--enable'
onClick={e => exportAsTxt(e, 'export-txt')}
>
<i className='fa fa-file-text-o' /> <i className='fa fa-file-text-o' />
<p>.txt</p> <p>.txt</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsHtml(e, 'export-html')}> <button
styleName='export--enable'
onClick={e => exportAsHtml(e, 'export-html')}
>
<i className='fa fa-html5' /> <i className='fa fa-html5' />
<p>.html</p> <p>.html</p>
</button> </button>
<button styleName='export--enable' onClick={(e) => exportAsPdf(e, 'export-pdf')}> <button
styleName='export--enable'
onClick={e => exportAsPdf(e, 'export-pdf')}
>
<i className='fa fa-file-pdf-o' /> <i className='fa fa-file-pdf-o' />
<p>.pdf</p> <p>.pdf</p>
</button> </button>

View File

@@ -1,3 +1,4 @@
/* eslint-disable camelcase */
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
@@ -31,60 +32,74 @@ import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import markdownToc from 'browser/lib/markdown-toc-generator' import markdownToc from 'browser/lib/markdown-toc-generator'
import queryString from 'query-string' import queryString from 'query-string'
import { replace } from 'connected-react-router' import { replace } from 'connected-react-router'
import ToggleDirectionButton from 'browser/main/Detail/ToggleDirectionButton'
class MarkdownNoteDetail extends React.Component { class MarkdownNoteDetail extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
isMovingNote: false, isMovingNote: false,
note: Object.assign({ note: Object.assign(
{
title: '', title: '',
content: '', content: '',
linesHighlighted: [] linesHighlighted: []
}, props.note), },
props.note
),
isLockButtonShown: props.config.editor.type !== 'SPLIT', isLockButtonShown: props.config.editor.type !== 'SPLIT',
isLocked: false, isLocked: false,
editorType: props.config.editor.type, editorType: props.config.editor.type,
switchPreview: props.config.editor.switchPreview switchPreview: props.config.editor.switchPreview,
RTL: false
} }
this.dispatchTimer = null this.dispatchTimer = null
this.toggleLockButton = this.handleToggleLockButton.bind(this) this.toggleLockButton = this.handleToggleLockButton.bind(this)
this.generateToc = () => this.handleGenerateToc() this.generateToc = this.handleGenerateToc.bind(this)
this.handleUpdateContent = this.handleUpdateContent.bind(this)
this.handleSwitchStackDirection = this.handleSwitchStackDirection.bind(this)
} }
focus () { focus() {
this.refs.content.focus() this.refs.content.focus()
} }
componentDidMount () { componentDidMount() {
ee.on('editor:orientation', this.handleSwitchStackDirection)
ee.on('topbar:togglelockbutton', this.toggleLockButton) ee.on('topbar:togglelockbutton', this.toggleLockButton)
ee.on('topbar:toggledirectionbutton', () => this.handleSwitchDirection())
ee.on('topbar:togglemodebutton', () => { ee.on('topbar:togglemodebutton', () => {
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT' const reversedType =
this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
this.handleSwitchMode(reversedType) this.handleSwitchMode(reversedType)
}) })
ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this)) ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
ee.on('code:generate-toc', this.generateToc) ee.on('code:generate-toc', this.generateToc)
} }
componentWillReceiveProps (nextProps) { UNSAFE_componentWillReceiveProps(nextProps) {
const isNewNote = nextProps.note.key !== this.props.note.key const isNewNote = nextProps.note.key !== this.props.note.key
const hasDeletedTags = nextProps.note.tags.length < this.props.note.tags.length const hasDeletedTags =
nextProps.note.tags.length < this.props.note.tags.length
if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) { if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
if (this.saveQueue != null) this.saveNow() if (this.saveQueue != null) this.saveNow()
this.setState({ this.setState(
note: Object.assign({linesHighlighted: []}, nextProps.note) {
}, () => { note: Object.assign({ linesHighlighted: [] }, nextProps.note)
},
() => {
this.refs.content.reload() this.refs.content.reload()
if (this.refs.tags) this.refs.tags.reset() if (this.refs.tags) this.refs.tags.reset()
}) }
)
} }
// Focus content if using blur or double click // 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 // --> Moved here from componentDidMount so a re-render during search won't set focus to the editor
const {switchPreview} = nextProps.config.editor const { switchPreview } = nextProps.config.editor
if (this.state.switchPreview !== switchPreview) { if (this.state.switchPreview !== switchPreview) {
this.setState({ this.setState({
@@ -97,23 +112,28 @@ class MarkdownNoteDetail extends React.Component {
} }
} }
componentWillUnmount () { componentWillUnmount() {
ee.off('topbar:togglelockbutton', this.toggleLockButton) ee.off('topbar:togglelockbutton', this.toggleLockButton)
ee.on('topbar:toggledirectionbutton', this.handleSwitchDirection)
ee.off('code:generate-toc', this.generateToc) ee.off('code:generate-toc', this.generateToc)
if (this.saveQueue != null) this.saveNow() if (this.saveQueue != null) this.saveNow()
} }
handleUpdateTag () { handleUpdateTag() {
const { note } = this.state const { note } = this.state
if (this.refs.tags) note.tags = this.refs.tags.value if (this.refs.tags) note.tags = this.refs.tags.value
this.updateNote(note) this.updateNote(note)
} }
handleUpdateContent () { handleUpdateContent() {
const { note } = this.state const { note } = this.state
note.content = this.refs.content.value note.content = this.refs.content.value
let title = 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 = striptags(title)
title = markdown.strip(title) title = markdown.strip(title)
note.title = title note.title = title
@@ -121,28 +141,26 @@ class MarkdownNoteDetail extends React.Component {
this.updateNote(note) this.updateNote(note)
} }
updateNote (note) { updateNote(note) {
note.updatedAt = new Date() note.updatedAt = new Date()
this.setState({note}, () => { this.setState({ note }, () => {
this.save() this.save()
}) })
} }
save () { save() {
clearTimeout(this.saveQueue) clearTimeout(this.saveQueue)
this.saveQueue = setTimeout(() => { this.saveQueue = setTimeout(() => {
this.saveNow() this.saveNow()
}, 1000) }, 1000)
} }
saveNow () { saveNow() {
const { note, dispatch } = this.props const { note, dispatch } = this.props
clearTimeout(this.saveQueue) clearTimeout(this.saveQueue)
this.saveQueue = null this.saveQueue = null
dataApi dataApi.updateNote(note.storage, note.key, this.state.note).then(note => {
.updateNote(note.storage, note.key, this.state.note)
.then((note) => {
dispatch({ dispatch({
type: 'UPDATE_NOTE', type: 'UPDATE_NOTE',
note: note note: note
@@ -151,8 +169,7 @@ class MarkdownNoteDetail extends React.Component {
}) })
} }
handleFolderChange (e) { handleFolderChange(e) {
const { dispatch } = this.props
const { note } = this.state const { note } = this.state
const value = this.refs.folder.value const value = this.refs.folder.value
const splitted = value.split('-') const splitted = value.split('-')
@@ -161,64 +178,71 @@ class MarkdownNoteDetail extends React.Component {
dataApi dataApi
.moveNote(note.storage, note.key, newStorageKey, newFolderKey) .moveNote(note.storage, note.key, newStorageKey, newFolderKey)
.then((newNote) => { .then(newNote => {
this.setState({ this.setState(
{
isMovingNote: true, isMovingNote: true,
note: Object.assign({}, newNote) note: Object.assign({}, newNote)
}, () => { },
() => {
const { dispatch, location } = this.props const { dispatch, location } = this.props
dispatch({ dispatch({
type: 'MOVE_NOTE', type: 'MOVE_NOTE',
originNote: note, originNote: note,
note: newNote note: newNote
}) })
dispatch(replace({ dispatch(
replace({
pathname: location.pathname, pathname: location.pathname,
search: queryString.stringify({ search: queryString.stringify({
key: newNote.key key: newNote.key
}) })
})) })
)
this.setState({ this.setState({
isMovingNote: false isMovingNote: false
}) })
}) }
)
}) })
} }
handleStarButtonClick (e) { handleStarButtonClick(e) {
const { note } = this.state const { note } = this.state
if (!note.isStarred) AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR') if (!note.isStarred)
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_STAR')
note.isStarred = !note.isStarred note.isStarred = !note.isStarred
this.setState({ this.setState(
{
note note
}, () => { },
() => {
this.save() this.save()
}) }
)
} }
exportAsFile () { exportAsFile() {}
} exportAsMd() {
exportAsMd () {
ee.emit('export:save-md') ee.emit('export:save-md')
} }
exportAsTxt () { exportAsTxt() {
ee.emit('export:save-text') ee.emit('export:save-text')
} }
exportAsHtml () { exportAsHtml() {
ee.emit('export:save-html') ee.emit('export:save-html')
} }
exportAsPdf () { exportAsPdf() {
ee.emit('export:save-pdf') ee.emit('export:save-pdf')
} }
handleKeyDown (e) { handleKeyDown(e) {
switch (e.keyCode) { switch (e.keyCode) {
// tab key // tab key
case 9: case 9:
@@ -228,7 +252,11 @@ class MarkdownNoteDetail extends React.Component {
} else if (e.ctrlKey && e.shiftKey) { } else if (e.ctrlKey && e.shiftKey) {
e.preventDefault() e.preventDefault()
this.jumpPrevTab() this.jumpPrevTab()
} else if (!e.ctrlKey && !e.shiftKey && e.target === this.refs.description) { } else if (
!e.ctrlKey &&
!e.shiftKey &&
e.target === this.refs.description
) {
e.preventDefault() e.preventDefault()
this.focusEditor() this.focusEditor()
} }
@@ -236,9 +264,8 @@ class MarkdownNoteDetail extends React.Component {
// I key // I key
case 73: case 73:
{ {
const isSuper = global.process.platform === 'darwin' const isSuper =
? e.metaKey global.process.platform === 'darwin' ? e.metaKey : e.ctrlKey
: e.ctrlKey
if (isSuper) { if (isSuper) {
e.preventDefault() e.preventDefault()
this.handleInfoButtonClick(e) this.handleInfoButtonClick(e)
@@ -248,17 +275,17 @@ class MarkdownNoteDetail extends React.Component {
} }
} }
handleTrashButtonClick (e) { handleTrashButtonClick(e) {
const { note } = this.state const { note } = this.state
const { isTrashed } = note const { isTrashed } = note
const { confirmDeletion } = this.props.config.ui const { confirmDeletion } = this.props.config.ui
if (isTrashed) { if (isTrashed) {
if (confirmDeleteNote(confirmDeletion, true)) { if (confirmDeleteNote(confirmDeletion, true)) {
const {note, dispatch} = this.props const { note, dispatch } = this.props
dataApi dataApi
.deleteNote(note.storage, note.key) .deleteNote(note.storage, note.key)
.then((data) => { .then(data => {
const dispatchHandler = () => { const dispatchHandler = () => {
dispatch({ dispatch({
type: 'DELETE_NOTE', type: 'DELETE_NOTE',
@@ -274,103 +301,139 @@ class MarkdownNoteDetail extends React.Component {
if (confirmDeleteNote(confirmDeletion, false)) { if (confirmDeleteNote(confirmDeletion, false)) {
note.isTrashed = true note.isTrashed = true
this.setState({ this.setState(
{
note note
}, () => { },
() => {
this.save() this.save()
}) }
)
ee.emit('list:next') ee.emit('list:next')
} }
} }
} }
handleUndoButtonClick (e) { handleUndoButtonClick(e) {
const { note } = this.state const { note } = this.state
note.isTrashed = false note.isTrashed = false
this.setState({ this.setState(
{
note note
}, () => { },
() => {
this.save() this.save()
this.refs.content.reload() this.refs.content.reload()
ee.emit('list:next') ee.emit('list:next')
}) }
)
} }
handleFullScreenButton (e) { handleFullScreenButton(e) {
ee.emit('editor:fullscreen') ee.emit('editor:fullscreen')
} }
handleLockButtonMouseDown (e) { handleLockButtonMouseDown(e) {
e.preventDefault() e.preventDefault()
ee.emit('editor:lock') ee.emit('editor:lock')
this.setState({ isLocked: !this.state.isLocked }) this.setState({ isLocked: !this.state.isLocked })
if (this.state.isLocked) this.focus() if (this.state.isLocked) this.focus()
} }
getToggleLockButton () { getToggleLockButton() {
return this.state.isLocked ? '../resources/icon/icon-lock.svg' : '../resources/icon/icon-unlock.svg' return this.state.isLocked
? '../resources/icon/icon-lock.svg'
: '../resources/icon/icon-unlock.svg'
} }
handleDeleteKeyDown (e) { handleDeleteKeyDown(e) {
if (e.keyCode === 27) this.handleDeleteCancelButtonClick(e) if (e.keyCode === 27) this.handleDeleteCancelButtonClick(e)
} }
handleToggleLockButton (event, noteStatus) { handleToggleLockButton(event, noteStatus) {
// first argument event is not used // first argument event is not used
if (noteStatus === 'CODE') { if (noteStatus === 'CODE') {
this.setState({isLockButtonShown: true}) this.setState({ isLockButtonShown: true })
} else { } else {
this.setState({isLockButtonShown: false}) this.setState({ isLockButtonShown: false })
} }
} }
handleGenerateToc () { handleGenerateToc() {
const editor = this.refs.content.refs.code.editor const editor = this.refs.content.refs.code.editor
markdownToc.generateInEditor(editor) markdownToc.generateInEditor(editor)
} }
handleFocus (e) { handleFocus(e) {
this.focus() this.focus()
} }
handleInfoButtonClick (e) { handleInfoButtonClick(e) {
const infoPanel = document.querySelector('.infoPanel') const infoPanel = document.querySelector('.infoPanel')
if (infoPanel.style) infoPanel.style.display = infoPanel.style.display === 'none' ? 'inline' : 'none' if (infoPanel.style)
infoPanel.style.display =
infoPanel.style.display === 'none' ? 'inline' : 'none'
} }
print (e) { print(e) {
ee.emit('print') ee.emit('print')
} }
handleSwitchMode (type) { handleSwitchMode(type) {
// If in split mode, hide the lock button // If in split mode, hide the lock button
this.setState({ editorType: type, isLockButtonShown: !(type === 'SPLIT') }, () => { this.setState(
{ editorType: type, isLockButtonShown: type !== 'SPLIT' },
() => {
this.focus() this.focus()
const newConfig = Object.assign({}, this.props.config) const newConfig = Object.assign({}, this.props.config)
newConfig.editor.type = type newConfig.editor.type = type
ConfigManager.set(newConfig) ConfigManager.set(newConfig)
}) }
)
} }
handleDeleteNote () { handleSwitchStackDirection() {
this.setState(
prevState => ({ isStacking: !prevState.isStacking }),
() => {
this.focus()
const newConfig = Object.assign({}, this.props.config)
newConfig.ui.isStacking = this.state.isStacking
ConfigManager.set(newConfig)
}
)
}
handleSwitchDirection() {
if (!this.props.config.editor.rtlEnabled) {
return
}
// If in split mode, hide the lock button
const direction = this.state.RTL
this.setState({ RTL: !direction })
}
handleDeleteNote() {
this.handleTrashButtonClick() this.handleTrashButtonClick()
} }
handleClearTodo () { handleClearTodo() {
const { note } = this.state const { note } = this.state
const splitted = note.content.split('\n') const splitted = note.content.split('\n')
const clearTodoContent = splitted.map((line) => { const clearTodoContent = splitted
.map(line => {
const trimmedLine = line.trim() const trimmedLine = line.trim()
if (trimmedLine.match(/\[x\]/i)) { if (trimmedLine.match(/\[x\]/i)) {
return line.replace(/\[x\]/i, '[ ]') return line.replace(/\[x\]/i, '[ ]')
} else { } else {
return line return line
} }
}).join('\n') })
.join('\n')
note.content = clearTodoContent note.content = clearTodoContent
this.refs.content.setValue(note.content) this.refs.content.setValue(note.content)
@@ -378,12 +441,13 @@ class MarkdownNoteDetail extends React.Component {
this.updateNote(note) this.updateNote(note)
} }
renderEditor () { renderEditor() {
const { config, ignorePreviewPointerEvents } = this.props const { config, ignorePreviewPointerEvents } = this.props
const { note } = this.state const { note, isStacking } = this.state
if (this.state.editorType === 'EDITOR_PREVIEW') { if (this.state.editorType === 'EDITOR_PREVIEW') {
return <MarkdownEditor return (
<MarkdownEditor
ref='content' ref='content'
styleName='body-noteEditor' styleName='body-noteEditor'
config={config} config={config}
@@ -391,53 +455,69 @@ class MarkdownNoteDetail extends React.Component {
storageKey={note.storage} storageKey={note.storage}
noteKey={note.key} noteKey={note.key}
linesHighlighted={note.linesHighlighted} linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)} onChange={this.handleUpdateContent}
isLocked={this.state.isLocked} isLocked={this.state.isLocked}
ignorePreviewPointerEvents={ignorePreviewPointerEvents} ignorePreviewPointerEvents={ignorePreviewPointerEvents}
RTL={config.editor.rtlEnabled && this.state.RTL}
/> />
)
} else { } else {
return <MarkdownSplitEditor return (
<MarkdownSplitEditor
ref='content' ref='content'
config={config} config={config}
value={note.content} value={note.content}
storageKey={note.storage} storageKey={note.storage}
noteKey={note.key} noteKey={note.key}
isStacking={isStacking}
linesHighlighted={note.linesHighlighted} linesHighlighted={note.linesHighlighted}
onChange={this.handleUpdateContent.bind(this)} onChange={this.handleUpdateContent}
ignorePreviewPointerEvents={ignorePreviewPointerEvents} ignorePreviewPointerEvents={ignorePreviewPointerEvents}
RTL={config.editor.rtlEnabled && this.state.RTL}
/> />
)
} }
} }
render () { render() {
const { data, location, config } = this.props const { data, dispatch, location, config } = this.props
const { note, editorType } = this.state const { note, editorType } = this.state
const storageKey = note.storage const storageKey = note.storage
const folderKey = note.folder const folderKey = note.folder
const options = [] const options = []
data.storageMap.forEach((storage, index) => { data.storageMap.forEach((storage, index) => {
storage.folders.forEach((folder) => { storage.folders.forEach(folder => {
options.push({ options.push({
storage: storage, storage: storage,
folder: folder folder: folder
}) })
}) })
}) })
const currentOption = options.filter((option) => option.storage.key === storageKey && option.folder.key === folderKey)[0]
const trashTopBar = <div styleName='info'> const currentOption = _.find(
options,
option =>
option.storage.key === storageKey && option.folder.key === folderKey
)
// currentOption may be undefined
const storageName = _.get(currentOption, 'storage.name') || ''
const folderName = _.get(currentOption, 'folder.name') || ''
const trashTopBar = (
<div styleName='info'>
<div styleName='info-left'> <div styleName='info-left'>
<RestoreButton onClick={(e) => this.handleUndoButtonClick(e)} /> <RestoreButton onClick={e => this.handleUndoButtonClick(e)} />
</div> </div>
<div styleName='info-right'> <div styleName='info-right'>
<PermanentDeleteButton onClick={(e) => this.handleTrashButtonClick(e)} /> <PermanentDeleteButton
<InfoButton onClick={e => this.handleTrashButtonClick(e)}
onClick={(e) => this.handleInfoButtonClick(e)}
/> />
<InfoButton onClick={e => this.handleInfoButtonClick(e)} />
<InfoPanelTrashed <InfoPanelTrashed
storageName={currentOption.storage.name} storageName={storageName}
folderName={currentOption.folder.name} folderName={folderName}
updatedAt={formatDate(note.updatedAt)} updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)} createdAt={formatDate(note.createdAt)}
exportAsHtml={this.exportAsHtml} exportAsHtml={this.exportAsHtml}
@@ -447,15 +527,18 @@ class MarkdownNoteDetail extends React.Component {
/> />
</div> </div>
</div> </div>
)
const detailTopBar = <div styleName='info'> const detailTopBar = (
<div styleName='info'>
<div styleName='info-left'> <div styleName='info-left'>
<div> <div>
<FolderSelect styleName='info-left-top-folderSelect' <FolderSelect
styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder} value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder' ref='folder'
data={data} data={data}
onChange={(e) => this.handleFolderChange(e)} onChange={e => this.handleFolderChange(e)}
/> />
</div> </div>
@@ -465,72 +548,88 @@ class MarkdownNoteDetail extends React.Component {
saveTagsAlphabetically={config.ui.saveTagsAlphabetically} saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
showTagsAlphabetically={config.ui.showTagsAlphabetically} showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data} data={data}
dispatch={dispatch}
onChange={this.handleUpdateTag.bind(this)} onChange={this.handleUpdateTag.bind(this)}
coloredTags={config.coloredTags} coloredTags={config.coloredTags}
/> />
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} /> <TodoListPercentage
onClearCheckboxClick={e => this.handleClearTodo(e)}
percentageOfTodo={getTodoPercentageOfCompleted(note.content)}
/>
</div> </div>
<div styleName='info-right'> <div styleName='info-right'>
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} /> <ToggleModeButton
onClick={e => this.handleSwitchMode(e)}
editorType={editorType}
/>
{this.props.config.editor.rtlEnabled && (
<ToggleDirectionButton
onClick={e => this.handleSwitchDirection(e)}
isRTL={this.state.RTL}
/>
)}
<StarButton <StarButton
onClick={(e) => this.handleStarButtonClick(e)} onClick={e => this.handleStarButtonClick(e)}
isActive={note.isStarred} isActive={note.isStarred}
/> />
{(() => { {(() => {
const imgSrc = `${this.getToggleLockButton()}` const imgSrc = `${this.getToggleLockButton()}`
const lockButtonComponent = const lockButtonComponent = (
<button styleName='control-lockButton' <button
onFocus={(e) => this.handleFocus(e)} styleName='control-lockButton'
onMouseDown={(e) => this.handleLockButtonMouseDown(e)} onFocus={e => this.handleFocus(e)}
onMouseDown={e => this.handleLockButtonMouseDown(e)}
> >
<img src={imgSrc} /> <img src={imgSrc} />
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>} {this.state.isLocked ? (
<span styleName='tooltip'>Unlock</span>
) : (
<span styleName='tooltip'>Lock</span>
)}
</button> </button>
return (
this.state.isLockButtonShown ? lockButtonComponent : ''
) )
return this.state.isLockButtonShown ? lockButtonComponent : ''
})()} })()}
<FullscreenButton onClick={(e) => this.handleFullScreenButton(e)} /> <FullscreenButton onClick={e => this.handleFullScreenButton(e)} />
<TrashButton onClick={(e) => this.handleTrashButtonClick(e)} /> <TrashButton onClick={e => this.handleTrashButtonClick(e)} />
<InfoButton <InfoButton onClick={e => this.handleInfoButtonClick(e)} />
onClick={(e) => this.handleInfoButtonClick(e)}
/>
<InfoPanel <InfoPanel
storageName={currentOption.storage.name} storageName={storageName}
folderName={currentOption.folder.name} folderName={folderName}
noteLink={`[${note.title}](:note:${queryString.parse(location.search).key})`} noteLink={`[${note.title}](:note:${
queryString.parse(location.search).key
})`}
updatedAt={formatDate(note.updatedAt)} updatedAt={formatDate(note.updatedAt)}
createdAt={formatDate(note.createdAt)} createdAt={formatDate(note.createdAt)}
exportAsMd={this.exportAsMd} exportAsMd={this.exportAsMd}
exportAsTxt={this.exportAsTxt} exportAsTxt={this.exportAsTxt}
exportAsHtml={this.exportAsHtml} exportAsHtml={this.exportAsHtml}
exportAsPdf={this.exportAsPdf} exportAsPdf={this.exportAsPdf}
wordCount={note.content.split(' ').length} wordCount={note.content.trim().split(/\s+/g).length}
letterCount={note.content.replace(/\r?\n/g, '').length} letterCount={note.content.replace(/\r?\n/g, '').length}
type={note.type} type={note.type}
print={this.print} print={this.print}
/> />
</div> </div>
</div> </div>
)
return ( return (
<div className='NoteDetail' <div
className='NoteDetail'
style={this.props.style} style={this.props.style}
styleName='root' styleName='root'
onKeyDown={(e) => this.handleKeyDown(e)} onKeyDown={e => this.handleKeyDown(e)}
> >
{location.pathname === '/trashed' ? trashTopBar : detailTopBar} {location.pathname === '/trashed' ? trashTopBar : detailTopBar}
<div styleName='body'> <div styleName='body'>{this.renderEditor()}</div>
{this.renderEditor()}
</div>
<StatusBar <StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])} {..._.pick(this.props, ['config', 'location', 'dispatch'])}
@@ -544,9 +643,7 @@ class MarkdownNoteDetail extends React.Component {
MarkdownNoteDetail.propTypes = { MarkdownNoteDetail.propTypes = {
dispatch: PropTypes.func, dispatch: PropTypes.func,
repositories: PropTypes.array, repositories: PropTypes.array,
note: PropTypes.shape({ note: PropTypes.shape({}),
}),
style: PropTypes.shape({ style: PropTypes.shape({
left: PropTypes.number left: PropTypes.number
}), }),

View File

@@ -15,7 +15,7 @@
.control-lockButton .control-lockButton
topBarButtonRight() topBarButtonRight()
position absolute position absolute
right 225px right 265px
&:hover .tooltip &:hover .tooltip
opacity 1 opacity 1
@@ -66,26 +66,14 @@ body[data-theme="dark"]
.control-fullScreenButton .control-fullScreenButton
topBarButtonDark() topBarButtonDark()
apply-theme(theme)
body[data-theme="solarized-dark"] body[data-theme={theme}]
.root .root
border-left 1px solid $ui-solarized-dark-borderColor border-left 1px solid get-theme-var(theme, 'borderColor')
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color get-theme-var(theme, 'noteDetail-backgroundColor')
body[data-theme="monokai"] for theme in 'solarized-dark' 'dracula'
.root apply-theme(theme)
border-left 1px solid $ui-monokai-borderColor
background-color $ui-monokai-noteDetail-backgroundColor
body[data-theme="dracula"] for theme in $themes
.root apply-theme(theme)
border-left 1px solid $ui-dracula-borderColor
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

@@ -15,6 +15,14 @@ $info-margin-under-border = 30px
padding 0 20px padding 0 20px
z-index 99 z-index 99
.info > div
> button
-webkit-user-drag none
user-select none
> img, span
-webkit-user-drag none
user-select none
.info-left .info-left
padding 0 10px padding 0 10px
width 100% width 100%
@@ -94,17 +102,14 @@ body[data-theme="dark"]
.undo-button .undo-button
topBarButtonDark() topBarButtonDark()
body[data-theme="solarized-dark"] apply-theme(theme)
body[data-theme={theme}]
.info .info
border-color $ui-solarized-dark-borderColor border-color get-theme-var(theme, 'borderColor')
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color get-theme-var(theme, 'noteDetail-backgroundColor')
body[data-theme="monokai"] for theme in 'solarized-dark' 'dracula'
.info apply-theme(theme)
border-color $ui-monokai-borderColor
background-color $ui-monokai-noteDetail-backgroundColor
body[data-theme="dracula"] for theme in $themes
.info apply-theme(theme)
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteDetail-backgroundColor

View File

@@ -4,12 +4,8 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './TrashButton.styl' import styles from './TrashButton.styl'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
const PermanentDeleteButton = ({ const PermanentDeleteButton = ({ onClick }) => (
onClick <button styleName='control-trashButton--in-trash' onClick={e => onClick(e)}>
}) => (
<button styleName='control-trashButton--in-trash'
onClick={(e) => onClick(e)}
>
<img src='../resources/icon/icon-trash.svg' /> <img src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span> <span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
</button> </button>

View File

@@ -4,12 +4,8 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './RestoreButton.styl' import styles from './RestoreButton.styl'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
const RestoreButton = ({ const RestoreButton = ({ onClick }) => (
onClick <button styleName='control-restoreButton' onClick={onClick}>
}) => (
<button styleName='control-restoreButton'
onClick={onClick}
>
<i className='fa fa-undo fa-fw' styleName='iconRestore' /> <i className='fa fa-undo fa-fw' styleName='iconRestore' />
<span styleName='tooltip'>{i18n.__('Restore')}</span> <span styleName='tooltip'>{i18n.__('Restore')}</span>
</button> </button>

File diff suppressed because it is too large Load Diff

View File

@@ -156,78 +156,35 @@ body[data-theme="dark"]
.control-fullScreenButton .control-fullScreenButton
topBarButtonDark() topBarButtonDark()
body[data-theme="solarized-dark"] apply-theme(theme)
body[data-theme={theme}]
.root .root
border-left 1px solid $ui-solarized-dark-borderColor border-left 1px solid get-theme-var(theme, 'borderColor')
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color get-theme-var(theme, 'noteDetail-backgroundColor')
.body .body
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color get-theme-var(theme, 'noteDetail-backgroundColor')
.body .description textarea .body .description textarea
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color get-theme-var(theme, 'noteDetail-backgroundColor')
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
border 1px solid $ui-solarized-dark-borderColor border 1px solid get-theme-var(theme, 'borderColor')
.tabList .tabButton .tabList .tabButton
border-color $ui-solarized-dark-borderColor border-color get-theme-var(theme, 'borderColor')
.tabButton .tabButton
&:hover &:hover
color $ui-solarized-dark-button--active-color color get-theme-var(theme, 'text-color')
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color get-theme-var(theme, 'noteDetail-backgroundColor')
transition 0.15s transition 0.15s
.tabList .tabList
background-color $ui-solarized-dark-noteDetail-backgroundColor background-color get-theme-var(theme, 'noteDetail-backgroundColor')
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
body[data-theme="monokai"] for theme in 'solarized-dark' 'dracula'
.root apply-theme(theme)
border-left 1px solid $ui-monokai-borderColor
background-color $ui-monokai-noteDetail-backgroundColor
.body for theme in $themes
background-color $ui-monokai-noteDetail-backgroundColor apply-theme(theme)
.body .description textarea
background-color $ui-monokai-noteDetail-backgroundColor
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
body[data-theme="dracula"]
.root
border-left 1px solid $ui-dracula-borderColor
background-color $ui-dracula-noteDetail-backgroundColor
.body
background-color $ui-dracula-noteDetail-backgroundColor
.body .description textarea
background-color $ui-dracula-noteDetail-backgroundColor
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

@@ -6,7 +6,7 @@ import _ from 'lodash'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
class StarButton extends React.Component { class StarButton extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
@@ -14,47 +14,51 @@ class StarButton extends React.Component {
} }
} }
handleMouseDown (e) { handleMouseDown(e) {
this.setState({ this.setState({
isActive: true isActive: true
}) })
} }
handleMouseUp (e) { handleMouseUp(e) {
this.setState({ this.setState({
isActive: false isActive: false
}) })
} }
handleMouseLeave (e) { handleMouseLeave(e) {
this.setState({ this.setState({
isActive: false isActive: false
}) })
} }
render () { render() {
const { className } = this.props const { className } = this.props
return ( return (
<button className={_.isString(className) <button
? 'StarButton ' + className className={
: 'StarButton' _.isString(className) ? 'StarButton ' + className : 'StarButton'
} }
styleName={this.state.isActive || this.props.isActive styleName={
? 'root--active' this.state.isActive || this.props.isActive ? 'root--active' : 'root'
: 'root'
} }
onMouseDown={(e) => this.handleMouseDown(e)} onMouseDown={e => this.handleMouseDown(e)}
onMouseUp={(e) => this.handleMouseUp(e)} onMouseUp={e => this.handleMouseUp(e)}
onMouseLeave={(e) => this.handleMouseLeave(e)} onMouseLeave={e => this.handleMouseLeave(e)}
onClick={this.props.onClick}> onClick={this.props.onClick}
<img styleName='icon' >
src={this.state.isActive || this.props.isActive <img
styleName='icon'
src={
this.state.isActive || this.props.isActive
? '../resources/icon/icon-starred.svg' ? '../resources/icon/icon-starred.svg'
: '../resources/icon/icon-star.svg' : '../resources/icon/icon-star.svg'
} }
/> />
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Star')}</span> <span lang={i18n.locale} styleName='tooltip'>
{i18n.__('Star')}
</span>
</button> </button>
) )
} }

View File

@@ -8,9 +8,10 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import ee from 'browser/main/lib/eventEmitter' import ee from 'browser/main/lib/eventEmitter'
import Autosuggest from 'react-autosuggest' import Autosuggest from 'react-autosuggest'
import { push } from 'connected-react-router'
class TagSelect extends React.Component { class TagSelect extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
@@ -19,15 +20,20 @@ class TagSelect extends React.Component {
} }
this.handleAddTag = this.handleAddTag.bind(this) this.handleAddTag = this.handleAddTag.bind(this)
this.handleRenameTag = this.handleRenameTag.bind(this)
this.onInputBlur = this.onInputBlur.bind(this) this.onInputBlur = this.onInputBlur.bind(this)
this.onInputChange = this.onInputChange.bind(this) this.onInputChange = this.onInputChange.bind(this)
this.onInputKeyDown = this.onInputKeyDown.bind(this) this.onInputKeyDown = this.onInputKeyDown.bind(this)
this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this) this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(
this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this) this
)
this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(
this
)
this.onSuggestionSelected = this.onSuggestionSelected.bind(this) this.onSuggestionSelected = this.onSuggestionSelected.bind(this)
} }
addNewTag (newTag) { addNewTag(newTag) {
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG') AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
newTag = newTag.trim().replace(/ +/g, '_') newTag = newTag.trim().replace(/ +/g, '_')
@@ -43,9 +49,7 @@ class TagSelect extends React.Component {
} }
let { value } = this.props let { value } = this.props
value = _.isArray(value) value = _.isArray(value) ? value.slice() : []
? value.slice()
: []
if (!_.includes(value, newTag)) { if (!_.includes(value, newTag)) {
value.push(newTag) value.push(newTag)
@@ -55,68 +59,87 @@ class TagSelect extends React.Component {
value = _.sortBy(value) value = _.sortBy(value)
} }
this.setState({ this.setState(
{
newTag: '' newTag: ''
}, () => { },
() => {
this.value = value this.value = value
this.props.onChange() this.props.onChange()
}) }
)
} }
buildSuggestions () { buildSuggestions() {
this.suggestions = _.sortBy(this.props.data.tagNoteMap.map( this.suggestions = _.sortBy(
(tag, name) => ({ this.props.data.tagNoteMap
.map((tag, name) => ({
name, name,
nameLC: name.toLowerCase(), nameLC: name.toLowerCase(),
size: tag.size size: tag.size
}) }))
).filter( .filter(tag => tag.size > 0),
tag => tag.size > 0 ['name']
), ['name']) )
} }
componentDidMount () { componentDidMount() {
this.value = this.props.value this.value = this.props.value
this.buildSuggestions() this.buildSuggestions()
ee.on('editor:add-tag', this.handleAddTag) ee.on('editor:add-tag', this.handleAddTag)
ee.on('sidebar:rename-tag', this.handleRenameTag)
} }
componentDidUpdate () { componentDidUpdate() {
this.value = this.props.value this.value = this.props.value
} }
componentWillUnmount () { componentWillUnmount() {
ee.off('editor:add-tag', this.handleAddTag) ee.off('editor:add-tag', this.handleAddTag)
ee.off('sidebar:rename-tag', this.handleRenameTag)
} }
handleAddTag () { handleAddTag() {
this.refs.newTag.input.focus() this.refs.newTag.input.focus()
} }
handleTagLabelClick (tag) { handleRenameTag(event, tagChange) {
const { router } = this.context const { value } = this.props
router.push(`/tags/${tag}`) const { tag, updatedTag } = tagChange
const newTags = value.slice()
newTags[value.indexOf(tag)] = updatedTag
this.value = newTags
this.props.onChange()
} }
handleTagRemoveButtonClick (tag) { handleTagLabelClick(tag) {
const { dispatch } = this.props
// Note: `tag` requires encoding later.
// E.g. % in tag is a problem (see issue #3170) - encodeURIComponent(tag) is not working.
dispatch(push(`/tags/${tag}`))
}
handleTagRemoveButtonClick(tag) {
this.removeTagByCallback((value, tag) => { this.removeTagByCallback((value, tag) => {
value.splice(value.indexOf(tag), 1) value.splice(value.indexOf(tag), 1)
}, tag) }, tag)
} }
onInputBlur (e) { onInputBlur(e) {
this.submitNewTag() this.submitNewTag()
} }
onInputChange (e, { newValue, method }) { onInputChange(e, { newValue, method }) {
this.setState({ this.setState({
newTag: newValue newTag: newValue
}) })
} }
onInputKeyDown (e) { onInputKeyDown(e) {
switch (e.keyCode) { switch (e.keyCode) {
case 9: case 9:
e.preventDefault() e.preventDefault()
@@ -132,17 +155,18 @@ class TagSelect extends React.Component {
} }
} }
onSuggestionsClearRequested () { onSuggestionsClearRequested() {
this.setState({ this.setState({
suggestions: [] suggestions: []
}) })
} }
onSuggestionsFetchRequested ({ value }) { onSuggestionsFetchRequested({ value }) {
const valueLC = value.toLowerCase() const valueLC = value.toLowerCase()
const suggestions = _.filter( const suggestions = _.filter(
this.suggestions, this.suggestions,
tag => !_.includes(this.value, tag.name) && tag.nameLC.indexOf(valueLC) !== -1 tag =>
!_.includes(this.value, tag.name) && tag.nameLC.indexOf(valueLC) !== -1
) )
this.setState({ this.setState({
@@ -150,22 +174,20 @@ class TagSelect extends React.Component {
}) })
} }
onSuggestionSelected (event, { suggestion, suggestionValue }) { onSuggestionSelected(event, { suggestion, suggestionValue }) {
this.addNewTag(suggestionValue) this.addNewTag(suggestionValue)
} }
removeLastTag () { removeLastTag() {
this.removeTagByCallback((value) => { this.removeTagByCallback(value => {
value.pop() value.pop()
}) })
} }
removeTagByCallback (callback, tag = null) { removeTagByCallback(callback, tag = null) {
let { value } = this.props let { value } = this.props
value = _.isArray(value) value = _.isArray(value) ? value.slice() : []
? value.slice()
: []
callback(value, tag) callback(value, tag)
value = _.uniq(value) value = _.uniq(value)
@@ -173,7 +195,7 @@ class TagSelect extends React.Component {
this.props.onChange() this.props.onChange()
} }
reset () { reset() {
this.buildSuggestions() this.buildSuggestions()
this.setState({ this.setState({
@@ -181,21 +203,22 @@ class TagSelect extends React.Component {
}) })
} }
submitNewTag () { submitNewTag() {
this.addNewTag(this.refs.newTag.input.value) this.addNewTag(this.refs.newTag.input.value)
} }
render () { render() {
const { value, className, showTagsAlphabetically, coloredTags } = this.props const { value, className, showTagsAlphabetically, coloredTags } = this.props
const tagList = _.isArray(value) const tagList = _.isArray(value)
? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => { ? (showTagsAlphabetically ? _.sortBy(value) : value).map(tag => {
const wrapperStyle = {} const wrapperStyle = {}
const textStyle = {} const textStyle = {}
const BLACK = '#333333' const BLACK = '#333333'
const WHITE = '#f1f1f1' const WHITE = '#f1f1f1'
const color = coloredTags[tag] const color = coloredTags[tag]
const invertedColor = color && invertColor(color, { black: BLACK, white: WHITE }) const invertedColor =
color && invertColor(color, { black: BLACK, white: WHITE })
let iconRemove = '../resources/icon/icon-x.svg' let iconRemove = '../resources/icon/icon-x.svg'
if (color) { if (color) {
wrapperStyle.backgroundColor = color wrapperStyle.backgroundColor = color
@@ -205,15 +228,23 @@ class TagSelect extends React.Component {
iconRemove = '../resources/icon/icon-x-light.svg' iconRemove = '../resources/icon/icon-x-light.svg'
} }
return ( return (
<span styleName='tag' <span styleName='tag' key={tag} style={wrapperStyle}>
key={tag} <span
style={wrapperStyle} styleName='tag-label'
style={textStyle}
onClick={e => this.handleTagLabelClick(tag)}
> >
<span styleName='tag-label' style={textStyle} onClick={(e) => this.handleTagLabelClick(tag)}>#{tag}</span> #{tag}
<button styleName='tag-removeButton' </span>
onClick={(e) => this.handleTagRemoveButtonClick(tag)} <button
styleName='tag-removeButton'
onClick={e => this.handleTagRemoveButtonClick(tag)}
> >
<img className='tag-removeButton-icon' src={iconRemove} width='8px' /> <img
className='tag-removeButton-icon'
src={iconRemove}
width='8px'
/>
</button> </button>
</span> </span>
) )
@@ -223,9 +254,9 @@ class TagSelect extends React.Component {
const { newTag, suggestions } = this.state const { newTag, suggestions } = this.state
return ( return (
<div className={_.isString(className) <div
? 'TagSelect ' + className className={
: 'TagSelect' _.isString(className) ? 'TagSelect ' + className : 'TagSelect'
} }
styleName='root' styleName='root'
> >
@@ -237,11 +268,7 @@ class TagSelect extends React.Component {
onSuggestionsClearRequested={this.onSuggestionsClearRequested} onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected} onSuggestionSelected={this.onSuggestionSelected}
getSuggestionValue={suggestion => suggestion.name} getSuggestionValue={suggestion => suggestion.name}
renderSuggestion={suggestion => ( renderSuggestion={suggestion => <div>{suggestion.name}</div>}
<div>
{suggestion.name}
</div>
)}
inputProps={{ inputProps={{
placeholder: i18n.__('Add tag...'), placeholder: i18n.__('Add tag...'),
value: newTag, value: newTag,
@@ -255,11 +282,8 @@ class TagSelect extends React.Component {
} }
} }
TagSelect.contextTypes = {
router: PropTypes.shape({})
}
TagSelect.propTypes = { TagSelect.propTypes = {
dispatch: PropTypes.func,
className: PropTypes.string, className: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string), value: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func, onChange: PropTypes.func,

View File

@@ -54,35 +54,20 @@ body[data-theme="dark"]
.tag-label .tag-label
color $ui-dark-text-color color $ui-dark-text-color
body[data-theme="solarized-dark"] apply-theme(theme)
body[data-theme={theme}]
.tag .tag
background-color $ui-solarized-dark-tag-backgroundColor background-color get-theme-var(theme, 'tag-backgroundColor')
.tag-removeButton .tag-removeButton
border-color $ui-button--focus-borderColor border-color $ui-button--focus-borderColor
background-color transparent background-color transparent
.tag-label .tag-label
color $ui-solarized-dark-text-color color get-theme-var(theme, 'text-color')
body[data-theme="monokai"] for theme in 'solarized-dark' 'dracula'
.tag apply-theme(theme)
background-color $ui-monokai-tag-backgroundColor
.tag-removeButton for theme in $themes
border-color $ui-button--focus-borderColor apply-theme(theme)
background-color transparent
.tag-label
color $ui-monokai-text-color
body[data-theme="dracula"]
.tag
background-color $ui-dracula-tag-backgroundColor
.tag-removeButton
border-color $ui-dracula-button--focus-borderColor
background-color transparent
.tag-label
color $ui-dracula-borderColor

View File

@@ -0,0 +1,26 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './ToggleDirectionButton.styl'
import i18n from 'browser/lib/i18n'
const ToggleDirectionButton = ({ onClick, isRTL }) => (
<div styleName='control-toggleModeButton'>
<div styleName={isRTL ? 'active' : undefined} onClick={() => onClick()}>
<img src={!isRTL ? '../resources/icon/icon-left-to-right.svg' : ''} />
</div>
<div styleName={!isRTL ? 'active' : undefined} onClick={() => onClick()}>
<img src={!isRTL ? '' : '../resources/icon/icon-right-to-left.svg'} />
</div>
<span lang={i18n.locale} styleName='tooltip'>
{i18n.__('Toggle Direction')}
</span>
</div>
)
ToggleDirectionButton.propTypes = {
onClick: PropTypes.func.isRequired,
isRTL: PropTypes.bool.isRequired
}
export default CSSModules(ToggleDirectionButton, styles)

View File

@@ -0,0 +1,85 @@
.control-toggleModeButton
height 25px
border-radius 50px
background-color #F4F4F4
width 52px
display flex
align-items center
position: relative
top 2px
margin-left 5px
.active
background-color #1EC38B
width 33px
height 24px
box-shadow 2px 0px 7px #eee
z-index 1
div
width 40px
height 100%
border-radius 50%
display flex
align-items center
justify-content center
cursor pointer
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 33px
left -10px
z-index 200
width 80px
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.tooltip:lang(ja)
@extend .tooltip
left -8px
width 70px
.control-toggleModeButton
-webkit-user-drag none
user-select none
> div img
-webkit-user-drag none
user-select none
body[data-theme="dark"]
.control-fullScreenButton
topBarButtonDark()
.control-toggleModeButton
background-color #3A404C
.active
background-color #1EC38B
box-shadow 2px 0px 7px #444444
body[data-theme="solarized-dark"]
.control-toggleModeButton
background-color #002B36
.active
background-color #1EC38B
box-shadow 2px 0px 7px #222222
apply-theme(theme)
body[data-theme={theme}]
.control-toggleModeButton
background-color get-theme-var(theme, 'borderColor')
.active
background-color get-theme-var(theme, 'active-color')
box-shadow 2px 0px 7px #222222
for theme in 'dracula'
apply-theme(theme)
for theme in $themes
apply-theme(theme)

View File

@@ -4,17 +4,35 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './ToggleModeButton.styl' import styles from './ToggleModeButton.styl'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
const ToggleModeButton = ({ const ToggleModeButton = ({ onClick, editorType }) => (
onClick, editorType
}) => (
<div styleName='control-toggleModeButton'> <div styleName='control-toggleModeButton'>
<div styleName={editorType === 'SPLIT' ? 'active' : undefined} onClick={() => onClick('SPLIT')}> <div
<img src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} /> styleName={editorType === 'SPLIT' ? 'active' : undefined}
onClick={() => onClick('SPLIT')}
>
<img
src={
editorType === 'EDITOR_PREVIEW'
? '../resources/icon/icon-mode-markdown-off-active.svg'
: ''
}
/>
</div> </div>
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : undefined} onClick={() => onClick('EDITOR_PREVIEW')}> <div
<img src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} /> styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : undefined}
onClick={() => onClick('EDITOR_PREVIEW')}
>
<img
src={
editorType === 'EDITOR_PREVIEW'
? ''
: '../resources/icon/icon-mode-split-on-active.svg'
}
/>
</div> </div>
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Toggle Mode')}</span> <span lang={i18n.locale} styleName='tooltip'>
{i18n.__('Toggle Mode')}
</span>
</div> </div>
) )

View File

@@ -26,6 +26,13 @@
&:hover .tooltip &:hover .tooltip
opacity 1 opacity 1
.control-toggleModeButton
-webkit-user-drag none
user-select none
> div img
-webkit-user-drag none
user-select none
.tooltip .tooltip
tooltip() tooltip()
position absolute position absolute
@@ -62,16 +69,16 @@ body[data-theme="solarized-dark"]
background-color #1EC38B background-color #1EC38B
box-shadow 2px 0px 7px #222222 box-shadow 2px 0px 7px #222222
body[data-theme="monokai"] apply-theme(theme)
body[data-theme={theme}]
.control-toggleModeButton .control-toggleModeButton
background-color #373831 background-color get-theme-var(theme, 'borderColor')
.active .active
background-color #f92672 background-color get-theme-var(theme, 'active-color')
box-shadow 2px 0px 7px #222222 box-shadow 2px 0px 7px #222222
body[data-theme="dracula"] for theme in 'dracula'
.control-toggleModeButton apply-theme(theme)
background-color #44475a
.active for theme in $themes
background-color #bd93f9 apply-theme(theme)
box-shadow 2px 0px 7px #222222

View File

@@ -4,14 +4,12 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './TrashButton.styl' import styles from './TrashButton.styl'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
const TrashButton = ({ const TrashButton = ({ onClick }) => (
onClick <button styleName='control-trashButton' onClick={e => onClick(e)}>
}) => (
<button styleName='control-trashButton'
onClick={(e) => onClick(e)}
>
<img src='../resources/icon/icon-trash.svg' /> <img src='../resources/icon/icon-trash.svg' />
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Trash')}</span> <span lang={i18n.locale} styleName='tooltip'>
{i18n.__('Trash')}
</span>
</button> </button>
) )

View File

@@ -15,7 +15,7 @@ import queryString from 'query-string'
const OSX = global.process.platform === 'darwin' const OSX = global.process.platform === 'darwin'
class Detail extends React.Component { class Detail extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props)
this.focusHandler = () => { this.focusHandler = () => {
@@ -26,43 +26,55 @@ class Detail extends React.Component {
} }
} }
componentDidMount () { componentDidMount() {
ee.on('detail:focus', this.focusHandler) ee.on('detail:focus', this.focusHandler)
ee.on('detail:delete', this.deleteHandler) ee.on('detail:delete', this.deleteHandler)
} }
componentWillUnmount () { componentWillUnmount() {
ee.off('detail:focus', this.focusHandler) ee.off('detail:focus', this.focusHandler)
ee.off('detail:delete', this.deleteHandler) ee.off('detail:delete', this.deleteHandler)
} }
render () { render() {
const { location, data, match: { params }, config } = this.props const {
const noteKey = location.search !== '' && queryString.parse(location.search).key location,
data,
match: { params },
config
} = this.props
const noteKey =
location.search !== '' && queryString.parse(location.search).key
let note = null let note = null
if (location.search !== '') { if (location.search !== '') {
const allNotes = data.noteMap.map(note => note) const allNotes = data.noteMap.map(note => note)
const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey)) const trashedNotes = data.trashedSet
.toJS()
.map(uniqueKey => data.noteMap.get(uniqueKey))
let displayedNotes = allNotes let displayedNotes = allNotes
if (location.pathname.match(/\/searched/)) { if (location.pathname.match(/\/searched/)) {
const searchStr = params.searchword const searchStr = params.searchword
displayedNotes = searchStr === undefined || searchStr === '' ? allNotes displayedNotes =
searchStr === undefined || searchStr === ''
? allNotes
: searchFromNotes(allNotes, searchStr) : searchFromNotes(allNotes, searchStr)
} } else if (location.pathname.match(/^\/tags/)) {
if (location.pathname.match(/\/tags/)) {
const listOfTags = params.tagname.split(' ') const listOfTags = params.tagname.split(' ')
displayedNotes = data.noteMap.map(note => note).filter(note => displayedNotes = data.noteMap
listOfTags.every(tag => note.tags.includes(tag)) .map(note => note)
) .filter(note => listOfTags.every(tag => note.tags.includes(tag)))
} }
if (location.pathname.match(/\/trashed/)) { if (location.pathname.match(/^\/trashed/)) {
displayedNotes = trashedNotes displayedNotes = trashedNotes
} else { } else {
displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key) displayedNotes = _.differenceWith(
displayedNotes,
trashedNotes,
(note, trashed) => note.key === trashed.key
)
} }
const noteKeys = displayedNotes.map(note => note.key) const noteKeys = displayedNotes.map(note => note.key)
@@ -73,12 +85,12 @@ class Detail extends React.Component {
if (note == null) { if (note == null) {
return ( return (
<div styleName='root' <div styleName='root' style={this.props.style} tabIndex='0'>
style={this.props.style}
tabIndex='0'
>
<div styleName='empty'> <div styleName='empty'>
<div styleName='empty-message'>{OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')} + N<br />{i18n.__('to create a new note')}</div> <div styleName='empty-message'>
{OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')} + N<br />
{i18n.__('to create a new note')}
</div>
</div> </div>
<StatusBar <StatusBar
{..._.pick(this.props, ['config', 'location', 'dispatch'])} {..._.pick(this.props, ['config', 'location', 'dispatch'])}

View File

@@ -16,13 +16,15 @@ import { store } from 'browser/main/store'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import { getLocales } from 'browser/lib/Languages' import { getLocales } from 'browser/lib/Languages'
import applyShortcuts from 'browser/main/lib/shortcutManager' import applyShortcuts from 'browser/main/lib/shortcutManager'
import { chooseTheme, applyTheme } from 'browser/main/lib/ThemeManager'
import { push } from 'connected-react-router' import { push } from 'connected-react-router'
const path = require('path') const path = require('path')
const electron = require('electron') const electron = require('electron')
const { remote } = electron const { remote } = electron
class Main extends React.Component { class Main extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props)
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
@@ -44,7 +46,7 @@ class Main extends React.Component {
this.toggleFullScreen = () => this.handleFullScreenButton() this.toggleFullScreen = () => this.handleFullScreenButton()
} }
getChildContext () { getChildContext() {
const { status, config } = this.props const { status, config } = this.props
return { return {
@@ -53,7 +55,7 @@ class Main extends React.Component {
} }
} }
init () { init() {
dataApi dataApi
.addStorage({ .addStorage({
name: 'My Storage Location', name: 'My Storage Location',
@@ -91,18 +93,21 @@ class Main extends React.Component {
type: 'SNIPPET_NOTE', type: 'SNIPPET_NOTE',
folder: data.storage.folders[0].key, folder: data.storage.folders[0].key,
title: 'Snippet note example', title: 'Snippet note example',
description: 'Snippet note example\nYou can store a series of snippets as a single note, like Gist.', description:
'Snippet note example\nYou can store a series of snippets as a single note, like Gist.',
snippets: [ snippets: [
{ {
name: 'example.html', name: 'example.html',
mode: '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: [] linesHighlighted: []
}, },
{ {
name: 'example.js', name: 'example.js',
mode: 'javascript', mode: 'javascript',
content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)", content:
"var boostnote = document.getElementById('hello').innerHTML\n\nconsole.log(boostnote)",
linesHighlighted: [] linesHighlighted: []
} }
] ]
@@ -118,7 +123,8 @@ class Main extends React.Component {
type: 'MARKDOWN_NOTE', type: 'MARKDOWN_NOTE',
folder: data.storage.folders[0].key, folder: data.storage.folders[0].key,
title: 'Welcome to Boostnote!', title: 'Welcome to Boostnote!',
content: '# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)' content:
'# Welcome to Boostnote!\n## Click here to edit markdown :wave:\n\n<iframe width="560" height="315" src="https://www.youtube.com/embed/L0qNPLsvmyM" frameborder="0" allowfullscreen></iframe>\n\n## Docs :memo:\n- [Boostnote | Boost your happiness, productivity and creativity.](https://hackernoon.com/boostnote-boost-your-happiness-productivity-and-creativity-315034efeebe)\n- [Cloud Syncing & Backups](https://github.com/BoostIO/Boostnote/wiki/Cloud-Syncing-and-Backup)\n- [How to sync your data across Desktop and Mobile apps](https://github.com/BoostIO/Boostnote/wiki/Sync-Data-Across-Desktop-and-Mobile-apps)\n- [Convert data from **Evernote** to Boostnote.](https://github.com/BoostIO/Boostnote/wiki/Evernote)\n- [Keyboard Shortcuts](https://github.com/BoostIO/Boostnote/wiki/Keyboard-Shortcuts)\n- [Keymaps in Editor mode](https://github.com/BoostIO/Boostnote/wiki/Keymaps-in-Editor-mode)\n- [How to set syntax highlight in Snippet note](https://github.com/BoostIO/Boostnote/wiki/Syntax-Highlighting)\n\n---\n\n## Article Archive :books:\n- [Reddit English](http://bit.ly/2mOJPu7)\n- [Reddit Spanish](https://www.reddit.com/r/boostnote_es/)\n- [Reddit Chinese](https://www.reddit.com/r/boostnote_cn/)\n- [Reddit Japanese](https://www.reddit.com/r/boostnote_jp/)\n\n---\n\n## Community :beers:\n- [GitHub](http://bit.ly/2AWWzkD)\n- [Twitter](http://bit.ly/2z8BUJZ)\n- [Facebook Group](http://bit.ly/2jcca8t)'
}) })
.then(note => { .then(note => {
store.dispatch({ store.dispatch({
@@ -139,16 +145,16 @@ class Main extends React.Component {
}) })
} }
componentDidMount () { componentDidMount() {
const { dispatch, config } = this.props const { dispatch, config } = this.props
const supportedThemes = ['dark', 'white', 'solarized-dark', 'monokai', 'dracula'] this.refreshTheme = setInterval(() => {
const conf = ConfigManager.get()
chooseTheme(conf)
}, 5 * 1000)
if (supportedThemes.indexOf(config.ui.theme) !== -1) { chooseTheme(config)
document.body.setAttribute('data-theme', config.ui.theme) applyTheme(config.ui.theme)
} else {
document.body.setAttribute('data-theme', 'default')
}
if (getLocales().indexOf(config.ui.language) !== -1) { if (getLocales().indexOf(config.ui.language) !== -1) {
i18n.setLocale(config.ui.language) i18n.setLocale(config.ui.language)
@@ -169,41 +175,56 @@ class Main extends React.Component {
} }
}) })
// eslint-disable-next-line no-undef
delete CodeMirror.keyMap.emacs['Ctrl-V'] delete CodeMirror.keyMap.emacs['Ctrl-V']
eventEmitter.on('editor:fullscreen', this.toggleFullScreen) eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
eventEmitter.on('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this)) eventEmitter.on(
'menubar:togglemenubar',
this.toggleMenuBarVisible.bind(this)
)
eventEmitter.on('dispatch:push', this.changeRoutePush.bind(this))
} }
componentWillUnmount () { componentWillUnmount() {
eventEmitter.off('editor:fullscreen', this.toggleFullScreen) eventEmitter.off('editor:fullscreen', this.toggleFullScreen)
eventEmitter.off('menubar:togglemenubar', this.toggleMenuBarVisible.bind(this)) eventEmitter.off(
'menubar:togglemenubar',
this.toggleMenuBarVisible.bind(this)
)
eventEmitter.off('dispatch:push', this.changeRoutePush.bind(this))
clearInterval(this.refreshTheme)
} }
toggleMenuBarVisible () { changeRoutePush(event, destination) {
const { dispatch } = this.props
dispatch(push(destination))
}
toggleMenuBarVisible() {
const { config } = this.props const { config } = this.props
const { ui } = config const { ui } = config
const newUI = Object.assign(ui, {showMenuBar: !ui.showMenuBar}) const newUI = Object.assign(ui, { showMenuBar: !ui.showMenuBar })
const newConfig = Object.assign(config, newUI) const newConfig = Object.assign(config, newUI)
ConfigManager.set(newConfig) ConfigManager.set(newConfig)
} }
handleLeftSlideMouseDown (e) { handleLeftSlideMouseDown(e) {
e.preventDefault() e.preventDefault()
this.setState({ this.setState({
isLeftSliderFocused: true isLeftSliderFocused: true
}) })
} }
handleRightSlideMouseDown (e) { handleRightSlideMouseDown(e) {
e.preventDefault() e.preventDefault()
this.setState({ this.setState({
isRightSliderFocused: true isRightSliderFocused: true
}) })
} }
handleMouseUp (e) { handleMouseUp(e) {
// Change width of NoteList component. // Change width of NoteList component.
if (this.state.isRightSliderFocused) { if (this.state.isRightSliderFocused) {
this.setState( this.setState(
@@ -243,7 +264,7 @@ class Main extends React.Component {
} }
} }
handleMouseMove (e) { handleMouseMove(e) {
if (this.state.isRightSliderFocused) { if (this.state.isRightSliderFocused) {
const offset = this.refs.body.getBoundingClientRect().left const offset = this.refs.body.getBoundingClientRect().left
let newListWidth = e.pageX - offset let newListWidth = e.pageX - offset
@@ -269,7 +290,7 @@ class Main extends React.Component {
} }
} }
handleFullScreenButton (e) { handleFullScreenButton(e) {
this.setState({ fullScreen: !this.state.fullScreen }, () => { this.setState({ fullScreen: !this.state.fullScreen }, () => {
const noteDetail = document.querySelector('.NoteDetail') const noteDetail = document.querySelector('.NoteDetail')
const noteList = document.querySelector('.NoteList') const noteList = document.querySelector('.NoteList')
@@ -283,7 +304,7 @@ class Main extends React.Component {
}) })
} }
hideLeftLists (noteDetail, noteList, mainBody) { hideLeftLists(noteDetail, noteList, mainBody) {
this.setState({ noteDetailWidth: noteDetail.style.left }) this.setState({ noteDetailWidth: noteDetail.style.left })
this.setState({ mainBodyWidth: mainBody.style.left }) this.setState({ mainBodyWidth: mainBody.style.left })
noteDetail.style.left = '0px' noteDetail.style.left = '0px'
@@ -291,13 +312,13 @@ class Main extends React.Component {
noteList.style.display = 'none' noteList.style.display = 'none'
} }
showLeftLists (noteDetail, noteList, mainBody) { showLeftLists(noteDetail, noteList, mainBody) {
noteDetail.style.left = this.state.noteDetailWidth noteDetail.style.left = this.state.noteDetailWidth
mainBody.style.left = this.state.mainBodyWidth mainBody.style.left = this.state.mainBodyWidth
noteList.style.display = 'inline' noteList.style.display = 'inline'
} }
render () { render() {
const { config } = this.props const { config } = this.props
// the width of the navigation bar when it is folded/collapsed // the width of the navigation bar when it is folded/collapsed
@@ -311,10 +332,16 @@ class Main extends React.Component {
onMouseUp={e => this.handleMouseUp(e)} onMouseUp={e => this.handleMouseUp(e)}
> >
<SideNav <SideNav
{..._.pick(this.props, ['dispatch', 'data', 'config', 'match', 'location'])} {..._.pick(this.props, [
'dispatch',
'data',
'config',
'match',
'location'
])}
width={this.state.navWidth} width={this.state.navWidth}
/> />
{!config.isSideNavFolded && {!config.isSideNavFolded && (
<div <div
styleName={ styleName={
this.state.isLeftSliderFocused ? 'slider--active' : 'slider' this.state.isLeftSliderFocused ? 'slider--active' : 'slider'
@@ -324,7 +351,8 @@ class Main extends React.Component {
draggable='false' draggable='false'
> >
<div styleName='slider-hitbox' /> <div styleName='slider-hitbox' />
</div>} </div>
)}
<div <div
styleName={config.isSideNavFolded ? 'body--expanded' : 'body'} styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
id='main-body' id='main-body'

View File

@@ -72,14 +72,13 @@ body[data-theme="dark"]
.control-newNoteButton-tooltip .control-newNoteButton-tooltip
darkTooltip() darkTooltip()
body[data-theme="solarized-dark"] apply-theme(theme)
body[data-theme={theme}]
.root, .root--expanded .root, .root--expanded
background-color $ui-solarized-dark-noteList-backgroundColor background-color get-theme-var(theme, 'noteList-backgroundColor')
body[data-theme="monokai"] for theme in 'solarized-dark' 'dracula'
.root, .root--expanded apply-theme(theme)
background-color $ui-monokai-noteList-backgroundColor
body[data-theme="dracula"] for theme in $themes
.root, .root--expanded apply-theme(theme)
background-color $ui-dracula-noteList-backgroundColor

View File

@@ -15,30 +15,48 @@ const { dialog } = remote
const OSX = window.process.platform === 'darwin' const OSX = window.process.platform === 'darwin'
class NewNoteButton extends React.Component { class NewNoteButton extends React.Component {
constructor (props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {}
}
this.handleNewNoteButtonClick = this.handleNewNoteButtonClick.bind(this) this.handleNewNoteButtonClick = this.handleNewNoteButtonClick.bind(this)
} }
componentDidMount () { componentDidMount() {
eventEmitter.on('top:new-note', this.handleNewNoteButtonClick) eventEmitter.on('top:new-note', this.handleNewNoteButtonClick)
} }
componentWillUnmount () { componentWillUnmount() {
eventEmitter.off('top:new-note', this.handleNewNoteButtonClick) eventEmitter.off('top:new-note', this.handleNewNoteButtonClick)
} }
handleNewNoteButtonClick (e) { handleNewNoteButtonClick(e) {
const { location, dispatch, match: { params }, config } = this.props const {
location,
dispatch,
match: { params },
config
} = this.props
const { storage, folder } = this.resolveTargetFolder() const { storage, folder } = this.resolveTargetFolder()
if (config.ui.defaultNote === 'MARKDOWN_NOTE') { if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
createMarkdownNote(storage.key, folder.key, dispatch, location, params, config) createMarkdownNote(
storage.key,
folder.key,
dispatch,
location,
params,
config
)
} else if (config.ui.defaultNote === 'SNIPPET_NOTE') { } else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
createSnippetNote(storage.key, folder.key, dispatch, location, params, config) createSnippetNote(
storage.key,
folder.key,
dispatch,
location,
params,
config
)
} else { } else {
modal.open(NewNoteModal, { modal.open(NewNoteModal, {
storage: storage.key, storage: storage.key,
@@ -51,8 +69,11 @@ class NewNoteButton extends React.Component {
} }
} }
resolveTargetFolder () { resolveTargetFolder() {
const { data, match: { params } } = this.props const {
data,
match: { params }
} = this.props
let storage = data.storageMap.get(params.storageKey) let storage = data.storageMap.get(params.storageKey)
// Find first storage // Find first storage
if (storage == null) { if (storage == null) {
@@ -62,9 +83,12 @@ class NewNoteButton extends React.Component {
} }
} }
if (storage == null) this.showMessageBox(i18n.__('No storage to create a note')) if (storage == null)
const folder = _.find(storage.folders, {key: params.folderKey}) || storage.folders[0] this.showMessageBox(i18n.__('No storage to create a note'))
if (folder == null) this.showMessageBox(i18n.__('No folder to create a note')) const folder =
_.find(storage.folders, { key: params.folderKey }) || storage.folders[0]
if (folder == null)
this.showMessageBox(i18n.__('No folder to create a note'))
return { return {
storage, storage,
@@ -72,7 +96,7 @@ class NewNoteButton extends React.Component {
} }
} }
showMessageBox (message) { showMessageBox(message) {
dialog.showMessageBox(remote.getCurrentWindow(), { dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'warning', type: 'warning',
message: message, message: message,
@@ -80,16 +104,19 @@ class NewNoteButton extends React.Component {
}) })
} }
render () { render() {
const { config, style } = this.props const { config, style } = this.props
return ( return (
<div className='NewNoteButton' <div
className='NewNoteButton'
styleName={config.isSideNavFolded ? 'root--expanded' : 'root'} styleName={config.isSideNavFolded ? 'root--expanded' : 'root'}
style={style} style={style}
> >
<div styleName='control'> <div styleName='control'>
<button styleName='control-newNoteButton' <button
onClick={this.handleNewNoteButtonClick}> styleName='control-newNoteButton'
onClick={this.handleNewNoteButtonClick}
>
<img src='../resources/icon/icon-newnote.svg' /> <img src='../resources/icon/icon-newnote.svg' />
<span styleName='control-newNoteButton-tooltip'> <span styleName='control-newNoteButton-tooltip'>
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N {i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N

View File

@@ -59,106 +59,34 @@ $control-height = 30px
top $control-height top $control-height
overflow auto overflow auto
body[data-theme="white"] apply-theme(theme)
body[data-theme={theme}]
.root .root
background-color $ui-white-noteList-backgroundColor border-color get-theme-var(theme, 'borderColor')
background-color get-theme-var(theme, 'noteList-backgroundColor')
.control .control
background-color $ui-white-noteList-backgroundColor background-color get-theme-var(theme, 'noteList-backgroundColor')
border-color get-theme-var(theme, 'borderColor')
body[data-theme="dark"]
.root
border-color $ui-dark-borderColor
background-color $ui-dark-noteList-backgroundColor
.control
background-color $ui-dark-noteList-backgroundColor
border-color $ui-dark-borderColor
.control-sortBy-select .control-sortBy-select
&:hover &:hover
transition 0.2s transition 0.2s
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
background-color: get-theme-var(theme, 'noteList-backgroundColor')
.control-button .control-button
color $ui-dark-inactive-text-color color get-theme-var(theme, 'inactive-text-color')
&:hover &:hover
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
.control-button--active .control-button--active
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
&:active &:active
color $ui-dark-text-color color get-theme-var(theme, 'text-color')
for theme in 'white' 'dark' 'solarized-dark' 'dracula'
apply-theme(theme)
body[data-theme="solarized-dark"] for theme in $themes
.root apply-theme(theme)
border-color $ui-solarized-dark-borderColor
background-color $ui-solarized-dark-noteList-backgroundColor
.control
background-color $ui-solarized-dark-noteList-backgroundColor
border-color $ui-solarized-dark-borderColor
.control-sortBy-select
&:hover
transition 0.2s
color $ui-solarized-dark-text-color
.control-button
color $ui-solarized-dark-inactive-text-color
&:hover
color $ui-solarized-dark-text-color
.control-button--active
color $ui-solarized-dark-text-color
&:active
color $ui-solarized-dark-text-color
body[data-theme="monokai"]
.root
border-color $ui-monokai-borderColor
background-color $ui-monokai-noteList-backgroundColor
.control
background-color $ui-monokai-noteList-backgroundColor
border-color $ui-monokai-borderColor
.control-sortBy-select
&:hover
transition 0.2s
color $ui-monokai-text-color
.control-button
color $ui-monokai-inactive-text-color
&:hover
color $ui-monokai-text-color
.control-button--active
color $ui-monokai-text-color
&:active
color $ui-monokai-text-color
body[data-theme="dracula"]
.root
border-color $ui-dracula-borderColor
background-color $ui-dracula-noteList-backgroundColor
.control
background-color $ui-dracula-noteList-backgroundColor
border-color $ui-dracula-borderColor
.control-sortBy-select
&:hover
transition 0.2s
color $ui-dracula-text-color
.control-button
color $ui-dracula-inactive-text-color
&:hover
color $ui-dracula-text-color
.control-button--active
color $ui-dracula-text-color
&:active
color $ui-dracula-text-color

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