mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-14 10:16:26 +00:00
Compare commits
405 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f644d21c61 | ||
|
|
2af52c193c | ||
|
|
ae840ea689 | ||
|
|
79ee674ad9 | ||
|
|
4d77053313 | ||
|
|
235e88f115 | ||
|
|
021bac5b68 | ||
|
|
60c4cacfbc | ||
|
|
30e6fc516b | ||
|
|
1388e7d7f4 | ||
|
|
b79b123c65 | ||
|
|
10d3adbe40 | ||
|
|
e100e080da | ||
|
|
d97bbe7918 | ||
|
|
3864d73d1a | ||
|
|
82987cd53c | ||
|
|
aa56aad46e | ||
|
|
ad7b155a99 | ||
|
|
f1ca06daf5 | ||
|
|
3df743f47b | ||
|
|
637225c2bc | ||
|
|
480c05650b | ||
|
|
fa265d769c | ||
|
|
4a197a5c90 | ||
|
|
23702056fd | ||
|
|
2a774d20f4 | ||
|
|
fdb1ef540d | ||
|
|
62a8ffb4ae | ||
|
|
1e3cf6f374 | ||
|
|
48c29dd7d9 | ||
|
|
e30727ab27 | ||
|
|
157eb5f87b | ||
|
|
8f290c2a6d | ||
|
|
47f6a217e8 | ||
|
|
69691bdf2a | ||
|
|
522a37b8e3 | ||
|
|
53aa142d94 | ||
|
|
61bd591ee8 | ||
|
|
fd2b438c67 | ||
|
|
5855cdfb26 | ||
|
|
a63266b0e9 | ||
|
|
fe19d96088 | ||
|
|
37933782d2 | ||
|
|
1dcc51e5a4 | ||
|
|
eb61ce2cf2 | ||
|
|
c5bcfe6ab3 | ||
|
|
00ed0d79ec | ||
|
|
cfc84f3e78 | ||
|
|
27e5010d8e | ||
|
|
efa00728d6 | ||
|
|
106caf2b3e | ||
|
|
3dca6c1fd6 | ||
|
|
bd56055593 | ||
|
|
dbd2c7049e | ||
|
|
07aa775de1 | ||
|
|
d0c5592855 | ||
|
|
b03ad0cfe0 | ||
|
|
a43b0e6a57 | ||
|
|
12176473ff | ||
|
|
1614c1452f | ||
|
|
d38f16d732 | ||
|
|
d7a2296909 | ||
|
|
d62d1d670b | ||
|
|
f18e8c5a38 | ||
|
|
5144c0ecdc | ||
|
|
642c62d8ce | ||
|
|
f92da16544 | ||
|
|
6e8b370e54 | ||
|
|
1ecf1e0413 | ||
|
|
1ea1482aad | ||
|
|
aeded9ac0e | ||
|
|
1cec872273 | ||
|
|
e550295644 | ||
|
|
70da43eeb7 | ||
|
|
7e9fb6be32 | ||
|
|
258ed4e866 | ||
|
|
b8992362c2 | ||
|
|
60fcf2734a | ||
|
|
ae9e83cf66 | ||
|
|
b8d1e37cce | ||
|
|
6ecd1e5ea5 | ||
|
|
52956503f1 | ||
|
|
148feac43e | ||
|
|
3272033c63 | ||
|
|
73d1bdf84b | ||
|
|
0e38f61c85 | ||
|
|
9d84fe7719 | ||
|
|
5aaecfc0fe | ||
|
|
ff3026686f | ||
|
|
d64dafc715 | ||
|
|
9e67880456 | ||
|
|
7484f6e6a6 | ||
|
|
d8d5810d7c | ||
|
|
4a167aa3d7 | ||
|
|
ced3460673 | ||
|
|
1da477d1d1 | ||
|
|
a4ad3896f0 | ||
|
|
e536d203d2 | ||
|
|
ca038937e9 | ||
|
|
342d0862c6 | ||
|
|
3b11285bd5 | ||
|
|
3c404f3678 | ||
|
|
eb37be1381 | ||
|
|
0e29e8ac76 | ||
|
|
71ae42056f | ||
|
|
a9bad53209 | ||
|
|
eee340366e | ||
|
|
a2bc1a5d2d | ||
|
|
3610d5ea93 | ||
|
|
a75f8e5fdf | ||
|
|
1821a5c678 | ||
|
|
5c3a62b9c5 | ||
|
|
851f57c1f5 | ||
|
|
2cd3f8c6ee | ||
|
|
a331d82cb5 | ||
|
|
6ef2ec4ed2 | ||
|
|
29ed26a503 | ||
|
|
7b3d5ab1ae | ||
|
|
6f2f6e9567 | ||
|
|
9a0bc984d4 | ||
|
|
1922c8dbf8 | ||
|
|
a841449771 | ||
|
|
ff4ef16375 | ||
|
|
33f6926916 | ||
|
|
0b9635c160 | ||
|
|
555ae327b6 | ||
|
|
944c79ec61 | ||
|
|
623688c28e | ||
|
|
8936d8b517 | ||
|
|
14fec7473a | ||
|
|
33b40bf35f | ||
|
|
52bea8f808 | ||
|
|
39ce706f3e | ||
|
|
d0171a8933 | ||
|
|
9f75d2fe4b | ||
|
|
9dfc6c2bc1 | ||
|
|
025e778252 | ||
|
|
c761f631a1 | ||
|
|
e173117a44 | ||
|
|
1ff4206bed | ||
|
|
babc5626a9 | ||
|
|
b624c9a4d2 | ||
|
|
d83feafcb2 | ||
|
|
cc1cb5fbc7 | ||
|
|
80666fed1a | ||
|
|
db2c6c99f7 | ||
|
|
3f1fa44ee7 | ||
|
|
4717e4fe3f | ||
|
|
ffc390d49d | ||
|
|
002e7ab9dd | ||
|
|
ca69bd69f2 | ||
|
|
3e2a366dc6 | ||
|
|
d365aaf270 | ||
|
|
818ee16e39 | ||
|
|
533caba717 | ||
|
|
cae6fd45b3 | ||
|
|
8c268be823 | ||
|
|
17845428bd | ||
|
|
efd1b3cd3c | ||
|
|
2ccd00a378 | ||
|
|
0123a99b5d | ||
|
|
ac744fbd90 | ||
|
|
10a1104073 | ||
|
|
a39a856f69 | ||
|
|
e6e69b4fd2 | ||
|
|
b4de1b49f2 | ||
|
|
c47428b27f | ||
|
|
d267a78416 | ||
|
|
04bb04a6a9 | ||
|
|
8eb535169f | ||
|
|
e0e0fbf739 | ||
|
|
4f8e8ae7b9 | ||
|
|
15b77482ac | ||
|
|
7420363adf | ||
|
|
1666e3a58a | ||
|
|
adbe85cc33 | ||
|
|
89d8d36ec3 | ||
|
|
a631adacb5 | ||
|
|
3384d1b7c3 | ||
|
|
f70de60672 | ||
|
|
cfd54c3f0e | ||
|
|
b29c0fe8cb | ||
|
|
5c1e5e0fcc | ||
|
|
8ec56390c4 | ||
|
|
2ad27e175c | ||
|
|
da1bd3f1fd | ||
|
|
dd913279d7 | ||
|
|
1246a677d1 | ||
|
|
43f2fc0740 | ||
|
|
b821209807 | ||
|
|
39fc5da98f | ||
|
|
504b6af3f6 | ||
|
|
b3ede3230c | ||
|
|
7035503fa7 | ||
|
|
7d147fd040 | ||
|
|
2a44e0b7eb | ||
|
|
686b9bc82c | ||
|
|
0c1497a255 | ||
|
|
496090610f | ||
|
|
16177754d5 | ||
|
|
f41f4939bc | ||
|
|
610503472a | ||
|
|
5dcd74b3b0 | ||
|
|
305825da78 | ||
|
|
205451a31d | ||
|
|
f1ae04fd07 | ||
|
|
4ba82275b9 | ||
|
|
093920173e | ||
|
|
2c3d95a4db | ||
|
|
76928e43a3 | ||
|
|
a816c5dc23 | ||
|
|
5ef84e4bfc | ||
|
|
fbdc9c9f8d | ||
|
|
df0c6a3b94 | ||
|
|
dfe0d74845 | ||
|
|
b9edd0238d | ||
|
|
bf72237b38 | ||
|
|
a24f6e80c7 | ||
|
|
297c764fe1 | ||
|
|
b03c2a1f80 | ||
|
|
7af77384e7 | ||
|
|
bacbfc8615 | ||
|
|
c8466e9fa6 | ||
|
|
4a231d6fdb | ||
|
|
4e80e1dd03 | ||
|
|
e0b18c6868 | ||
|
|
15b9f8e13f | ||
|
|
03b8dbbc44 | ||
|
|
80af8dcf80 | ||
|
|
62b2856d29 | ||
|
|
7ab3ce91a1 | ||
|
|
e040aeef55 | ||
|
|
46df5a8fa7 | ||
|
|
f61fbbaead | ||
|
|
480f515114 | ||
|
|
e206e6babf | ||
|
|
eae4b52aa1 | ||
|
|
933f75f1ee | ||
|
|
f3c72e561a | ||
|
|
44d6374cfe | ||
|
|
eba13800ff | ||
|
|
3942492f32 | ||
|
|
fe323d5764 | ||
|
|
4740edfb1f | ||
|
|
671dff060d | ||
|
|
4b0dc08426 | ||
|
|
10ffa35b29 | ||
|
|
57fadacda0 | ||
|
|
5c186f30a8 | ||
|
|
0a205f77b0 | ||
|
|
75a0f4373c | ||
|
|
189b245b1d | ||
|
|
0eae47c8be | ||
|
|
00607cb704 | ||
|
|
f9d5c86245 | ||
|
|
a6a1291d0e | ||
|
|
49db1c8244 | ||
|
|
dbe1721d50 | ||
|
|
b55420e935 | ||
|
|
2706df2b24 | ||
|
|
90f791de1b | ||
|
|
c9db3f98d1 | ||
|
|
bdfe233472 | ||
|
|
d340aeb77d | ||
|
|
2da1105ff8 | ||
|
|
e6e5036474 | ||
|
|
609a1709c5 | ||
|
|
2bb9607eea | ||
|
|
ca32d05bb2 | ||
|
|
67a016add0 | ||
|
|
ea41dbb3bc | ||
|
|
637090d259 | ||
|
|
911d65131a | ||
|
|
f4203263bb | ||
|
|
79df5249ef | ||
|
|
ac71093888 | ||
|
|
d15e0f9fe5 | ||
|
|
d3861caf28 | ||
|
|
1c8e379fdd | ||
|
|
d9783490ec | ||
|
|
bb32c3a8d3 | ||
|
|
bab7ec388c | ||
|
|
e2957192d0 | ||
|
|
3994c78365 | ||
|
|
bfd4d7ffe1 | ||
|
|
cb2f18c078 | ||
|
|
69a032c1cf | ||
|
|
a287afb3e9 | ||
|
|
da81f10e04 | ||
|
|
6603f46678 | ||
|
|
fdfa3bb8f5 | ||
|
|
da204a27c5 | ||
|
|
e11a68afba | ||
|
|
168b0f82dd | ||
|
|
6715a54da2 | ||
|
|
6b32b3ae80 | ||
|
|
6a2242725d | ||
|
|
e9070fadab | ||
|
|
1117e1b724 | ||
|
|
d015b18c66 | ||
|
|
01641b5af4 | ||
|
|
7c0c81207b | ||
|
|
c844b60941 | ||
|
|
9f8246a26a | ||
|
|
6d5141b60f | ||
|
|
8b4a9dd325 | ||
|
|
5006aaae38 | ||
|
|
1bb841d5c5 | ||
|
|
f57c4f390d | ||
|
|
646151e020 | ||
|
|
20f573c477 | ||
|
|
094e4c5da8 | ||
|
|
7716880a6c | ||
|
|
71605fb8fe | ||
|
|
fa9d8b8881 | ||
|
|
aa0566b8ca | ||
|
|
2a838ebb0b | ||
|
|
fabc975b20 | ||
|
|
3bdc88cecb | ||
|
|
a591001761 | ||
|
|
53923c9c87 | ||
|
|
ede733888d | ||
|
|
a79db03093 | ||
|
|
5c8254a9c4 | ||
|
|
2f7b62f710 | ||
|
|
a19c13eb3c | ||
|
|
64d4cd84af | ||
|
|
2bbcb8ca89 | ||
|
|
07e810a231 | ||
|
|
48beb184df | ||
|
|
f195e87568 | ||
|
|
13d44ae56a | ||
|
|
00b4874d09 | ||
|
|
5bb90babbc | ||
|
|
7cde30d352 | ||
|
|
73fbf49ba4 | ||
|
|
b39ef5948b | ||
|
|
7cf9dda821 | ||
|
|
657806c8cf | ||
|
|
d61a218808 | ||
|
|
039f73711a | ||
|
|
6e885acf8c | ||
|
|
9ef07cea7a | ||
|
|
9e3b321aaf | ||
|
|
21560701ea | ||
|
|
4556375174 | ||
|
|
91b5398b5a | ||
|
|
eeb8016992 | ||
|
|
736106be3a | ||
|
|
f400568dc0 | ||
|
|
0ca96cba6e | ||
|
|
df4d837026 | ||
|
|
760f84d7fa | ||
|
|
ab35c3557f | ||
|
|
174a315e3f | ||
|
|
0834313456 | ||
|
|
ce3b29085f | ||
|
|
b93d7a204f | ||
|
|
df931e10c0 | ||
|
|
9572cb2d33 | ||
|
|
51e836f32a | ||
|
|
7fefbd88d0 | ||
|
|
cb956c5508 | ||
|
|
47b0086bf8 | ||
|
|
3c14cc219e | ||
|
|
b8d66e4a95 | ||
|
|
7804a22984 | ||
|
|
bfc1c93153 | ||
|
|
5bf3824f28 | ||
|
|
dac23e38d9 | ||
|
|
404faf8a0b | ||
|
|
4a7b0f4711 | ||
|
|
dd62fca45d | ||
|
|
79fb04126c | ||
|
|
39c9574ae3 | ||
|
|
38af257adf | ||
|
|
5aae9a4722 | ||
|
|
cfe3cae88d | ||
|
|
612de84ac6 | ||
|
|
33be597ef0 | ||
|
|
cc26fd80d7 | ||
|
|
c227a1ffec | ||
|
|
f0df787bbe | ||
|
|
09188bed48 | ||
|
|
4a3bcaba06 | ||
|
|
1d1ab65edd | ||
|
|
7330cdaf1c | ||
|
|
050a1fb6cf | ||
|
|
1e8397cf17 | ||
|
|
59b53ece2b | ||
|
|
16c62cd46f | ||
|
|
eff56c2514 | ||
|
|
ee6b9a223f | ||
|
|
acc6ea434a | ||
|
|
1e5a7356f4 | ||
|
|
4c8342c19d | ||
|
|
dad5232ecb | ||
|
|
be972781ee | ||
|
|
58fbc298b1 | ||
|
|
7de7772339 | ||
|
|
ad847a2f5d | ||
|
|
4f79f52524 | ||
|
|
2c7f24cb8c | ||
|
|
398ebae2ba | ||
|
|
5de176757d |
@@ -22,7 +22,9 @@
|
||||
"fontSize": "14",
|
||||
"lineNumber": true
|
||||
},
|
||||
"sortBy": "UPDATED_AT",
|
||||
"sortBy": {
|
||||
"default": "UPDATED_AT"
|
||||
},
|
||||
"sortTagsBy": "ALPHABETICAL",
|
||||
"ui": {
|
||||
"defaultNote": "ALWAYS_ASK",
|
||||
|
||||
16
.editorconfig
Normal file
16
.editorconfig
Normal file
@@ -0,0 +1,16 @@
|
||||
# EditorConfig is awesome: http://EditorConfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Space indentation
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
# The indent size used in the `package.json` file cannot be changed
|
||||
# https://github.com/npm/npm/pull/3180#issuecomment-16336516
|
||||
[{*.yml,*.yaml,package.json}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
41
.vscode/launch.json
vendored
Normal file
41
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "BoostNote Main",
|
||||
"protocol": "inspector",
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
|
||||
"runtimeArgs": [
|
||||
"--remote-debugging-port=9223",
|
||||
"--hot",
|
||||
"${workspaceFolder}/index.js"
|
||||
],
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceFolder}/node_modeules/.bin/electron.cmd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "attach",
|
||||
"name": "BoostNote Renderer",
|
||||
"port": 9223,
|
||||
"webRoot": "${workspaceFolder}",
|
||||
"sourceMapPathOverrides": {
|
||||
"webpack:///./~/*": "${webRoot}/node_modules/*",
|
||||
"webpack:///*": "${webRoot}/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "BostNote All",
|
||||
"configurations": ["BoostNote Main", "BoostNote Renderer"]
|
||||
}
|
||||
]
|
||||
}
|
||||
27
.vscode/tasks.json
vendored
Normal file
27
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Build Boostnote",
|
||||
"group": "build",
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
},
|
||||
"problemMatcher": {
|
||||
"pattern":[
|
||||
{
|
||||
"regexp": "^([^\\\\s].*)\\\\((\\\\d+,\\\\d+)\\\\):\\\\s*(.*)$",
|
||||
"file": 1,
|
||||
"location": 2,
|
||||
"message": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,15 +1,25 @@
|
||||
# Current behavior
|
||||
|
||||
<!--
|
||||
Please paste some **screenshots** with the **developer tool** open (console tab) when you report a bug.
|
||||
Let us know what is currently happening.
|
||||
|
||||
If your issue is regarding boostnote mobile, move to https://github.com/BoostIO/boostnote-mobile.
|
||||
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.
|
||||
-->
|
||||
|
||||
# Expected behavior
|
||||
|
||||
<!--
|
||||
Let us know what you think should happen!
|
||||
-->
|
||||
|
||||
# Steps to reproduce
|
||||
|
||||
<!--
|
||||
Please be thorough, issues we can reproduce are easier to fix!
|
||||
-->
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
@@ -5,7 +5,7 @@ import CodeMirror from 'codemirror'
|
||||
import 'codemirror-mode-elixir'
|
||||
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
|
||||
import convertModeName from 'browser/lib/convertModeName'
|
||||
import { options, TableEditor } from '@susisu/mte-kernel'
|
||||
import { options, TableEditor, Alignment } from '@susisu/mte-kernel'
|
||||
import TextEditorInterface from 'browser/lib/TextEditorInterface'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import iconv from 'iconv-lite'
|
||||
@@ -14,18 +14,23 @@ import consts from 'browser/lib/consts'
|
||||
import fs from 'fs'
|
||||
const { ipcRenderer } = require('electron')
|
||||
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
|
||||
import TurndownService from 'turndown'
|
||||
import { gfm } from 'turndown-plugin-gfm'
|
||||
|
||||
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||
|
||||
const buildCMRulers = (rulers, enableRulers) =>
|
||||
enableRulers ? rulers.map(ruler => ({column: ruler})) : []
|
||||
(enableRulers ? rulers.map(ruler => ({ column: ruler })) : [])
|
||||
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
||||
this.changeHandler = (e) => this.handleChange(e)
|
||||
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {
|
||||
leading: false,
|
||||
trailing: true
|
||||
})
|
||||
this.changeHandler = e => this.handleChange(e)
|
||||
this.focusHandler = () => {
|
||||
ipcRenderer.send('editor:focused', true)
|
||||
}
|
||||
@@ -41,17 +46,25 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
this.props.onBlur != null && this.props.onBlur(e)
|
||||
|
||||
const {storageKey, noteKey} = this.props
|
||||
attachmentManagement.deleteAttachmentsNotPresentInNote(this.editor.getValue(), storageKey, noteKey)
|
||||
const { storageKey, noteKey } = this.props
|
||||
attachmentManagement.deleteAttachmentsNotPresentInNote(
|
||||
this.editor.getValue(),
|
||||
storageKey,
|
||||
noteKey
|
||||
)
|
||||
}
|
||||
this.pasteHandler = (editor, e) => this.handlePaste(editor, e)
|
||||
this.loadStyleHandler = (e) => {
|
||||
this.loadStyleHandler = e => {
|
||||
this.editor.refresh()
|
||||
}
|
||||
this.searchHandler = (e, msg) => this.handleSearch(msg)
|
||||
this.searchState = null
|
||||
this.scrollToLineHandeler = this.scrollToLine.bind(this)
|
||||
|
||||
this.formatTable = () => this.handleFormatTable()
|
||||
this.editorActivityHandler = () => this.handleEditorActivity()
|
||||
|
||||
this.turndownService = new TurndownService()
|
||||
}
|
||||
|
||||
handleSearch (msg) {
|
||||
@@ -66,7 +79,10 @@ export default class CodeEditor extends React.Component {
|
||||
cm.addOverlay(component.searchState)
|
||||
|
||||
function makeOverlay (query, style) {
|
||||
query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'), 'gi')
|
||||
query = new RegExp(
|
||||
query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'),
|
||||
'gi'
|
||||
)
|
||||
return {
|
||||
token: function (stream) {
|
||||
query.lastIndex = stream.pos
|
||||
@@ -89,9 +105,32 @@ export default class CodeEditor extends React.Component {
|
||||
this.tableEditor.formatAll(options({textWidthOptions: {}}))
|
||||
}
|
||||
|
||||
handleEditorActivity () {
|
||||
if (!this.textEditorInterface.transaction) {
|
||||
this.updateTableEditorState()
|
||||
}
|
||||
}
|
||||
|
||||
updateTableEditorState () {
|
||||
const active = this.tableEditor.cursorIsInTable(this.tableEditorOptions)
|
||||
if (active) {
|
||||
if (this.extraKeysMode !== 'editor') {
|
||||
this.extraKeysMode = 'editor'
|
||||
this.editor.setOption('extraKeys', this.editorKeyMap)
|
||||
}
|
||||
} else {
|
||||
if (this.extraKeysMode !== 'default') {
|
||||
this.extraKeysMode = 'default'
|
||||
this.editor.setOption('extraKeys', this.defaultKeyMap)
|
||||
this.tableEditor.resetSmartCursor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
const { rulers, enableRulers } = this.props
|
||||
const expandSnippet = this.expandSnippet.bind(this)
|
||||
eventEmitter.on('line:jump', this.scrollToLineHandeler)
|
||||
|
||||
const defaultSnippet = [
|
||||
{
|
||||
@@ -102,9 +141,66 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
]
|
||||
if (!fs.existsSync(consts.SNIPPET_FILE)) {
|
||||
fs.writeFileSync(consts.SNIPPET_FILE, JSON.stringify(defaultSnippet, null, 4), 'utf8')
|
||||
fs.writeFileSync(
|
||||
consts.SNIPPET_FILE,
|
||||
JSON.stringify(defaultSnippet, null, 4),
|
||||
'utf8'
|
||||
)
|
||||
}
|
||||
|
||||
this.defaultKeyMap = CodeMirror.normalizeKeyMap({
|
||||
Tab: function (cm) {
|
||||
const cursor = cm.getCursor()
|
||||
const line = cm.getLine(cursor.line)
|
||||
const cursorPosition = cursor.ch
|
||||
const charBeforeCursor = line.substr(cursorPosition - 1, 1)
|
||||
if (cm.somethingSelected()) cm.indentSelection('add')
|
||||
else {
|
||||
const tabs = cm.getOption('indentWithTabs')
|
||||
if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) {
|
||||
cm.execCommand('goLineStart')
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
cm.execCommand('goLineEnd')
|
||||
} else if (
|
||||
!charBeforeCursor.match(/\t|\s|\r|\n/) &&
|
||||
cursor.ch > 1
|
||||
) {
|
||||
// text expansion on tab key if the char before is alphabet
|
||||
const snippets = JSON.parse(
|
||||
fs.readFileSync(consts.SNIPPET_FILE, 'utf8')
|
||||
)
|
||||
if (expandSnippet(line, cursor, cm, snippets) === false) {
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'Cmd-T': function (cm) {
|
||||
// Do nothing
|
||||
},
|
||||
Enter: 'boostNewLineAndIndentContinueMarkdownList',
|
||||
'Ctrl-C': cm => {
|
||||
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
||||
document.execCommand('copy')
|
||||
}
|
||||
return CodeMirror.Pass
|
||||
}
|
||||
})
|
||||
|
||||
this.value = this.props.value
|
||||
this.editor = CodeMirror(this.refs.root, {
|
||||
rulers: buildCMRulers(rulers, enableRulers),
|
||||
@@ -127,53 +223,7 @@ export default class CodeEditor extends React.Component {
|
||||
explode: '[]{}``$$',
|
||||
override: true
|
||||
},
|
||||
extraKeys: {
|
||||
Tab: function (cm) {
|
||||
const cursor = cm.getCursor()
|
||||
const line = cm.getLine(cursor.line)
|
||||
const cursorPosition = cursor.ch
|
||||
const charBeforeCursor = line.substr(cursorPosition - 1, 1)
|
||||
if (cm.somethingSelected()) cm.indentSelection('add')
|
||||
else {
|
||||
const tabs = cm.getOption('indentWithTabs')
|
||||
if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) {
|
||||
cm.execCommand('goLineStart')
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
cm.execCommand('goLineEnd')
|
||||
} else if (!charBeforeCursor.match(/\t|\s|\r|\n/) && cursor.ch > 1) {
|
||||
// text expansion on tab key if the char before is alphabet
|
||||
const snippets = JSON.parse(fs.readFileSync(consts.SNIPPET_FILE, 'utf8'))
|
||||
if (expandSnippet(line, cursor, cm, snippets) === false) {
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
'Cmd-T': function (cm) {
|
||||
// Do nothing
|
||||
},
|
||||
Enter: 'boostNewLineAndIndentContinueMarkdownList',
|
||||
'Ctrl-C': (cm) => {
|
||||
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
|
||||
document.execCommand('copy')
|
||||
}
|
||||
return CodeMirror.Pass
|
||||
}
|
||||
}
|
||||
extraKeys: this.defaultKeyMap
|
||||
})
|
||||
|
||||
this.setMode(this.props.mode)
|
||||
@@ -196,12 +246,70 @@ export default class CodeEditor extends React.Component {
|
||||
CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor)
|
||||
CodeMirror.Vim.map('ZZ', ':q', 'normal')
|
||||
|
||||
this.tableEditor = new TableEditor(new TextEditorInterface(this.editor))
|
||||
this.textEditorInterface = new TextEditorInterface(this.editor)
|
||||
this.tableEditor = new TableEditor(this.textEditorInterface)
|
||||
eventEmitter.on('code:format-table', this.formatTable)
|
||||
|
||||
this.tableEditorOptions = options({
|
||||
smartCursor: true
|
||||
})
|
||||
|
||||
this.editorKeyMap = CodeMirror.normalizeKeyMap({
|
||||
'Tab': () => { this.tableEditor.nextCell(this.tableEditorOptions) },
|
||||
'Shift-Tab': () => { this.tableEditor.previousCell(this.tableEditorOptions) },
|
||||
'Enter': () => { this.tableEditor.nextRow(this.tableEditorOptions) },
|
||||
'Ctrl-Enter': () => { this.tableEditor.escape(this.tableEditorOptions) },
|
||||
'Cmd-Enter': () => { this.tableEditor.escape(this.tableEditorOptions) },
|
||||
'Shift-Ctrl-Left': () => { this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) },
|
||||
'Shift-Cmd-Left': () => { this.tableEditor.alignColumn(Alignment.LEFT, this.tableEditorOptions) },
|
||||
'Shift-Ctrl-Right': () => { this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) },
|
||||
'Shift-Cmd-Right': () => { this.tableEditor.alignColumn(Alignment.RIGHT, this.tableEditorOptions) },
|
||||
'Shift-Ctrl-Up': () => { this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) },
|
||||
'Shift-Cmd-Up': () => { this.tableEditor.alignColumn(Alignment.CENTER, this.tableEditorOptions) },
|
||||
'Shift-Ctrl-Down': () => { this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) },
|
||||
'Shift-Cmd-Down': () => { this.tableEditor.alignColumn(Alignment.NONE, this.tableEditorOptions) },
|
||||
'Ctrl-Left': () => { this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) },
|
||||
'Cmd-Left': () => { this.tableEditor.moveFocus(0, -1, this.tableEditorOptions) },
|
||||
'Ctrl-Right': () => { this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) },
|
||||
'Cmd-Right': () => { this.tableEditor.moveFocus(0, 1, this.tableEditorOptions) },
|
||||
'Ctrl-Up': () => { this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) },
|
||||
'Cmd-Up': () => { this.tableEditor.moveFocus(-1, 0, this.tableEditorOptions) },
|
||||
'Ctrl-Down': () => { this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) },
|
||||
'Cmd-Down': () => { this.tableEditor.moveFocus(1, 0, this.tableEditorOptions) },
|
||||
'Ctrl-K Ctrl-I': () => { this.tableEditor.insertRow(this.tableEditorOptions) },
|
||||
'Cmd-K Cmd-I': () => { this.tableEditor.insertRow(this.tableEditorOptions) },
|
||||
'Ctrl-L Ctrl-I': () => { this.tableEditor.deleteRow(this.tableEditorOptions) },
|
||||
'Cmd-L Cmd-I': () => { this.tableEditor.deleteRow(this.tableEditorOptions) },
|
||||
'Ctrl-K Ctrl-J': () => { this.tableEditor.insertColumn(this.tableEditorOptions) },
|
||||
'Cmd-K Cmd-J': () => { this.tableEditor.insertColumn(this.tableEditorOptions) },
|
||||
'Ctrl-L Ctrl-J': () => { this.tableEditor.deleteColumn(this.tableEditorOptions) },
|
||||
'Cmd-L Cmd-J': () => { this.tableEditor.deleteColumn(this.tableEditorOptions) },
|
||||
'Alt-Shift-Ctrl-Left': () => { this.tableEditor.moveColumn(-1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Cmd-Left': () => { this.tableEditor.moveColumn(-1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Ctrl-Right': () => { this.tableEditor.moveColumn(1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Cmd-Right': () => { this.tableEditor.moveColumn(1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Ctrl-Up': () => { this.tableEditor.moveRow(-1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Cmd-Up': () => { this.tableEditor.moveRow(-1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Ctrl-Down': () => { this.tableEditor.moveRow(1, this.tableEditorOptions) },
|
||||
'Alt-Shift-Cmd-Down': () => { this.tableEditor.moveRow(1, this.tableEditorOptions) }
|
||||
})
|
||||
|
||||
if (this.props.enableTableEditor) {
|
||||
this.editor.on('cursorActivity', this.editorActivityHandler)
|
||||
this.editor.on('changes', this.editorActivityHandler)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
clientWidth: this.refs.root.clientWidth
|
||||
})
|
||||
}
|
||||
|
||||
expandSnippet (line, cursor, cm, snippets) {
|
||||
const wordBeforeCursor = this.getWordBeforeCursor(line, cursor.line, cursor.ch)
|
||||
const wordBeforeCursor = this.getWordBeforeCursor(
|
||||
line,
|
||||
cursor.line,
|
||||
cursor.ch
|
||||
)
|
||||
const templateCursorString = ':{}'
|
||||
for (let i = 0; i < snippets.length; i++) {
|
||||
if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) {
|
||||
@@ -209,19 +317,28 @@ export default class CodeEditor extends React.Component {
|
||||
const snippetLines = snippets[i].content.split('\n')
|
||||
let cursorLineNumber = 0
|
||||
let cursorLinePosition = 0
|
||||
|
||||
let cursorIndex
|
||||
for (let j = 0; j < snippetLines.length; j++) {
|
||||
const cursorIndex = snippetLines[j].indexOf(templateCursorString)
|
||||
cursorIndex = snippetLines[j].indexOf(templateCursorString)
|
||||
|
||||
if (cursorIndex !== -1) {
|
||||
cursorLineNumber = j
|
||||
cursorLinePosition = cursorIndex
|
||||
cm.replaceRange(
|
||||
snippets[i].content.replace(templateCursorString, ''),
|
||||
wordBeforeCursor.range.from,
|
||||
wordBeforeCursor.range.to
|
||||
)
|
||||
cm.setCursor({ line: cursor.line + cursorLineNumber, ch: cursorLinePosition })
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
cm.replaceRange(
|
||||
snippets[i].content.replace(templateCursorString, ''),
|
||||
wordBeforeCursor.range.from,
|
||||
wordBeforeCursor.range.to
|
||||
)
|
||||
cm.setCursor({
|
||||
line: cursor.line + cursorLineNumber,
|
||||
ch: cursorLinePosition + cursor.ch - wordBeforeCursor.text.length
|
||||
})
|
||||
} else {
|
||||
cm.replaceRange(
|
||||
snippets[i].content,
|
||||
@@ -261,8 +378,8 @@ export default class CodeEditor extends React.Component {
|
||||
return {
|
||||
text: wordBeforeCursor,
|
||||
range: {
|
||||
from: {line: lineNumber, ch: originCursorPosition},
|
||||
to: {line: lineNumber, ch: cursorPosition}
|
||||
from: { line: lineNumber, ch: originCursorPosition },
|
||||
to: { line: lineNumber, ch: cursorPosition }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,7 +403,7 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
let needRefresh = false
|
||||
const {rulers, enableRulers} = this.props
|
||||
const { rulers, enableRulers } = this.props
|
||||
if (prevProps.mode !== this.props.mode) {
|
||||
this.setMode(this.props.mode)
|
||||
}
|
||||
@@ -304,7 +421,10 @@ export default class CodeEditor extends React.Component {
|
||||
needRefresh = true
|
||||
}
|
||||
|
||||
if (prevProps.enableRulers !== enableRulers || prevProps.rulers !== rulers) {
|
||||
if (
|
||||
prevProps.enableRulers !== enableRulers ||
|
||||
prevProps.rulers !== rulers
|
||||
) {
|
||||
this.editor.setOption('rulers', buildCMRulers(rulers, enableRulers))
|
||||
}
|
||||
|
||||
@@ -324,6 +444,27 @@ export default class CodeEditor extends React.Component {
|
||||
this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd)
|
||||
}
|
||||
|
||||
if (prevProps.enableTableEditor !== this.props.enableTableEditor) {
|
||||
if (this.props.enableTableEditor) {
|
||||
this.editor.on('cursorActivity', this.editorActivityHandler)
|
||||
this.editor.on('changes', this.editorActivityHandler)
|
||||
} else {
|
||||
this.editor.off('cursorActivity', this.editorActivityHandler)
|
||||
this.editor.off('changes', this.editorActivityHandler)
|
||||
}
|
||||
|
||||
this.extraKeysMode = 'default'
|
||||
this.editor.setOption('extraKeys', this.defaultKeyMap)
|
||||
}
|
||||
|
||||
if (this.state.clientWidth !== this.refs.root.clientWidth) {
|
||||
this.setState({
|
||||
clientWidth: this.refs.root.clientWidth
|
||||
})
|
||||
|
||||
needRefresh = true
|
||||
}
|
||||
|
||||
if (needRefresh) {
|
||||
this.editor.refresh()
|
||||
}
|
||||
@@ -344,10 +485,14 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
moveCursorTo (row, col) {
|
||||
}
|
||||
moveCursorTo (row, col) {}
|
||||
|
||||
scrollToLine (num) {
|
||||
scrollToLine (event, num) {
|
||||
const cursor = {
|
||||
line: num,
|
||||
ch: 1
|
||||
}
|
||||
this.editor.setCursor(cursor)
|
||||
}
|
||||
|
||||
focus () {
|
||||
@@ -376,8 +521,13 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
handleDropImage (dropEvent) {
|
||||
dropEvent.preventDefault()
|
||||
const {storageKey, noteKey} = this.props
|
||||
attachmentManagement.handleAttachmentDrop(this, storageKey, noteKey, dropEvent)
|
||||
const { storageKey, noteKey } = this.props
|
||||
attachmentManagement.handleAttachmentDrop(
|
||||
this,
|
||||
storageKey,
|
||||
noteKey,
|
||||
dropEvent
|
||||
)
|
||||
}
|
||||
|
||||
insertAttachmentMd (imageMd) {
|
||||
@@ -386,34 +536,48 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
handlePaste (editor, e) {
|
||||
const clipboardData = e.clipboardData
|
||||
const {storageKey, noteKey} = this.props
|
||||
const { storageKey, noteKey } = this.props
|
||||
const dataTransferItem = clipboardData.items[0]
|
||||
const pastedTxt = clipboardData.getData('text')
|
||||
const isURL = (str) => {
|
||||
const isURL = str => {
|
||||
const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
|
||||
return matcher.test(str)
|
||||
}
|
||||
const isInLinkTag = (editor) => {
|
||||
const isInLinkTag = editor => {
|
||||
const startCursor = editor.getCursor('start')
|
||||
const prevChar = editor.getRange(
|
||||
{line: startCursor.line, ch: startCursor.ch - 2},
|
||||
{line: startCursor.line, ch: startCursor.ch}
|
||||
{ line: startCursor.line, ch: startCursor.ch - 2 },
|
||||
{ line: startCursor.line, ch: startCursor.ch }
|
||||
)
|
||||
const endCursor = editor.getCursor('end')
|
||||
const nextChar = editor.getRange(
|
||||
{line: endCursor.line, ch: endCursor.ch},
|
||||
{line: endCursor.line, ch: endCursor.ch + 1}
|
||||
{ line: endCursor.line, ch: endCursor.ch },
|
||||
{ line: endCursor.line, ch: endCursor.ch + 1 }
|
||||
)
|
||||
return prevChar === '](' && nextChar === ')'
|
||||
}
|
||||
if (dataTransferItem.type.match('image')) {
|
||||
attachmentManagement.handlePastImageEvent(this, storageKey, noteKey, dataTransferItem)
|
||||
} else if (this.props.fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
|
||||
|
||||
const pastedHtml = clipboardData.getData('text/html')
|
||||
if (pastedHtml !== '') {
|
||||
this.handlePasteHtml(e, editor, pastedHtml)
|
||||
} else if (dataTransferItem.type.match('image')) {
|
||||
attachmentManagement.handlePastImageEvent(
|
||||
this,
|
||||
storageKey,
|
||||
noteKey,
|
||||
dataTransferItem
|
||||
)
|
||||
} else if (
|
||||
this.props.fetchUrlTitle &&
|
||||
isURL(pastedTxt) &&
|
||||
!isInLinkTag(editor)
|
||||
) {
|
||||
this.handlePasteUrl(e, editor, pastedTxt)
|
||||
}
|
||||
if (attachmentManagement.isAttachmentLink(pastedTxt)) {
|
||||
attachmentManagement.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
|
||||
.then((modifiedText) => {
|
||||
attachmentManagement
|
||||
.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
|
||||
.then(modifiedText => {
|
||||
this.editor.replaceSelection(modifiedText)
|
||||
})
|
||||
e.preventDefault()
|
||||
@@ -431,40 +595,59 @@ export default class CodeEditor extends React.Component {
|
||||
const taggedUrl = `<${pastedTxt}>`
|
||||
editor.replaceSelection(taggedUrl)
|
||||
|
||||
const isImageReponse = (response) => {
|
||||
return response.headers.has('content-type') &&
|
||||
const isImageReponse = response => {
|
||||
return (
|
||||
response.headers.has('content-type') &&
|
||||
response.headers.get('content-type').match(/^image\/.+$/)
|
||||
)
|
||||
}
|
||||
const replaceTaggedUrl = (replacement) => {
|
||||
const replaceTaggedUrl = replacement => {
|
||||
const value = editor.getValue()
|
||||
const cursor = editor.getCursor()
|
||||
const newValue = value.replace(taggedUrl, replacement)
|
||||
const newCursor = Object.assign({}, cursor, { ch: cursor.ch + newValue.length - value.length })
|
||||
const newCursor = Object.assign({}, cursor, {
|
||||
ch: cursor.ch + newValue.length - value.length
|
||||
})
|
||||
editor.setValue(newValue)
|
||||
editor.setCursor(newCursor)
|
||||
}
|
||||
|
||||
fetch(pastedTxt, {
|
||||
method: 'get'
|
||||
}).then((response) => {
|
||||
if (isImageReponse(response)) {
|
||||
return this.mapImageResponse(response, pastedTxt)
|
||||
} else {
|
||||
return this.mapNormalResponse(response, pastedTxt)
|
||||
}
|
||||
}).then((replacement) => {
|
||||
replaceTaggedUrl(replacement)
|
||||
}).catch((e) => {
|
||||
replaceTaggedUrl(pastedTxt)
|
||||
})
|
||||
.then(response => {
|
||||
if (isImageReponse(response)) {
|
||||
return this.mapImageResponse(response, pastedTxt)
|
||||
} else {
|
||||
return this.mapNormalResponse(response, pastedTxt)
|
||||
}
|
||||
})
|
||||
.then(replacement => {
|
||||
replaceTaggedUrl(replacement)
|
||||
})
|
||||
.catch(e => {
|
||||
replaceTaggedUrl(pastedTxt)
|
||||
})
|
||||
}
|
||||
|
||||
handlePasteHtml (e, editor, pastedHtml) {
|
||||
e.preventDefault()
|
||||
const markdown = this.turndownService.turndown(pastedHtml)
|
||||
editor.replaceSelection(markdown)
|
||||
}
|
||||
|
||||
mapNormalResponse (response, pastedTxt) {
|
||||
return this.decodeResponse(response).then((body) => {
|
||||
return this.decodeResponse(response).then(body => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const parsedBody = (new window.DOMParser()).parseFromString(body, 'text/html')
|
||||
const linkWithTitle = `[${parsedBody.title}](${pastedTxt})`
|
||||
const parsedBody = new window.DOMParser().parseFromString(
|
||||
body,
|
||||
'text/html'
|
||||
)
|
||||
const escapePipe = (str) => {
|
||||
return str.replace('|', '\\|')
|
||||
}
|
||||
const linkWithTitle = `[${escapePipe(parsedBody.title)}](${pastedTxt})`
|
||||
resolve(linkWithTitle)
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
@@ -491,10 +674,13 @@ export default class CodeEditor extends React.Component {
|
||||
const _charset = headers.has('content-type')
|
||||
? this.extractContentTypeCharset(headers.get('content-type'))
|
||||
: undefined
|
||||
return response.arrayBuffer().then((buff) => {
|
||||
return response.arrayBuffer().then(buff => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const charset = _charset !== undefined && iconv.encodingExists(_charset) ? _charset : 'utf-8'
|
||||
const charset = _charset !== undefined &&
|
||||
iconv.encodingExists(_charset)
|
||||
? _charset
|
||||
: 'utf-8'
|
||||
resolve(iconv.decode(new Buffer(buff), charset).toString())
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
@@ -504,11 +690,14 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
extractContentTypeCharset (contentType) {
|
||||
return contentType.split(';').filter((str) => {
|
||||
return str.trim().toLowerCase().startsWith('charset')
|
||||
}).map((str) => {
|
||||
return str.replace(/['"]/g, '').split('=')[1]
|
||||
})[0]
|
||||
return contentType
|
||||
.split(';')
|
||||
.filter(str => {
|
||||
return str.trim().toLowerCase().startsWith('charset')
|
||||
})
|
||||
.map(str => {
|
||||
return str.replace(/['"]/g, '').split('=')[1]
|
||||
})[0]
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -517,10 +706,7 @@ export default class CodeEditor extends React.Component {
|
||||
const width = this.props.width
|
||||
return (
|
||||
<div
|
||||
className={className == null
|
||||
? 'CodeEditor'
|
||||
: `CodeEditor ${className}`
|
||||
}
|
||||
className={className == null ? 'CodeEditor' : `CodeEditor ${className}`}
|
||||
ref='root'
|
||||
tabIndex='-1'
|
||||
style={{
|
||||
@@ -528,7 +714,7 @@ export default class CodeEditor extends React.Component {
|
||||
fontSize: fontSize,
|
||||
width: width
|
||||
}}
|
||||
onDrop={(e) => this.handleDropImage(e)}
|
||||
onDrop={e => this.handleDropImage(e)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import CodeEditor from 'browser/components/CodeEditor'
|
||||
import MarkdownPreview from 'browser/components/MarkdownPreview'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
|
||||
class MarkdownEditor extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -18,7 +19,7 @@ class MarkdownEditor extends React.Component {
|
||||
this.supportMdSelectionBold = [16, 17, 186]
|
||||
|
||||
this.state = {
|
||||
status: 'PREVIEW',
|
||||
status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'PREVIEW',
|
||||
renderValue: props.value,
|
||||
keyPressed: new Set(),
|
||||
isLocked: false
|
||||
@@ -64,6 +65,10 @@ class MarkdownEditor extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
setValue (value) {
|
||||
this.refs.code.setValue(value)
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
this.value = this.refs.code.value
|
||||
this.props.onChange(e)
|
||||
@@ -72,9 +77,7 @@ class MarkdownEditor extends React.Component {
|
||||
handleContextMenu (e) {
|
||||
const { config } = this.props
|
||||
if (config.editor.switchPreview === 'RIGHTCLICK') {
|
||||
const newStatus = this.state.status === 'PREVIEW'
|
||||
? 'CODE'
|
||||
: 'PREVIEW'
|
||||
const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW'
|
||||
this.setState({
|
||||
status: newStatus
|
||||
}, () => {
|
||||
@@ -84,6 +87,10 @@ class MarkdownEditor extends React.Component {
|
||||
this.refs.preview.focus()
|
||||
}
|
||||
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
|
||||
|
||||
const newConfig = Object.assign({}, config)
|
||||
newConfig.editor.delfaultStatus = newStatus
|
||||
ConfigManager.set(newConfig)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -140,8 +147,8 @@ class MarkdownEditor extends React.Component {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const idMatch = /checkbox-([0-9]+)/
|
||||
const checkedMatch = /\[x\]/i
|
||||
const uncheckedMatch = /\[ \]/
|
||||
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
|
||||
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
|
||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||
const lines = this.refs.code.value
|
||||
@@ -150,10 +157,10 @@ class MarkdownEditor extends React.Component {
|
||||
const targetLine = lines[lineIndex]
|
||||
|
||||
if (targetLine.match(checkedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
|
||||
lines[lineIndex] = targetLine.replace(checkedMatch, '- [ ]')
|
||||
}
|
||||
if (targetLine.match(uncheckedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
|
||||
lines[lineIndex] = targetLine.replace(uncheckedMatch, '- [x]')
|
||||
}
|
||||
this.refs.code.setValue(lines.join('\n'))
|
||||
}
|
||||
@@ -250,7 +257,7 @@ class MarkdownEditor extends React.Component {
|
||||
: 'codeEditor--hide'
|
||||
}
|
||||
ref='code'
|
||||
mode='GitHub Flavored Markdown'
|
||||
mode='Boost Flavored Markdown'
|
||||
value={value}
|
||||
theme={config.editor.theme}
|
||||
keyMap={config.editor.keyMap}
|
||||
@@ -265,6 +272,7 @@ class MarkdownEditor extends React.Component {
|
||||
storageKey={storageKey}
|
||||
noteKey={noteKey}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
enableTableEditor={config.editor.enableTableEditor}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
onBlur={(e) => this.handleBlur(e)}
|
||||
/>
|
||||
@@ -299,6 +307,7 @@ class MarkdownEditor extends React.Component {
|
||||
noteKey={noteKey}
|
||||
customCSS={config.preview.customCSS}
|
||||
allowCustomCSS={config.preview.allowCustomCSS}
|
||||
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
.preview
|
||||
display block
|
||||
absolute top bottom left right
|
||||
z-index 100
|
||||
background-color white
|
||||
height 100%
|
||||
width 100%
|
||||
|
||||
@@ -17,8 +17,12 @@ import copy from 'copy-to-clipboard'
|
||||
import mdurl from 'mdurl'
|
||||
import exportNote from 'browser/main/lib/dataApi/exportNote'
|
||||
import { escapeHtmlCharacters } from 'browser/lib/utils'
|
||||
import yaml from 'js-yaml'
|
||||
import context from 'browser/lib/context'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import fs from 'fs'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { remote, shell } = require('electron')
|
||||
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
||||
|
||||
const { app } = remote
|
||||
@@ -27,16 +31,27 @@ const fileUrl = require('file-url')
|
||||
|
||||
const dialog = remote.dialog
|
||||
|
||||
const uri2path = require('file-uri-to-path')
|
||||
|
||||
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
||||
const appPath = fileUrl(process.env.NODE_ENV === 'production'
|
||||
? app.getAppPath()
|
||||
: path.resolve())
|
||||
const appPath = fileUrl(
|
||||
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
|
||||
)
|
||||
const CSS_FILES = [
|
||||
`${appPath}/node_modules/katex/dist/katex.min.css`,
|
||||
`${appPath}/node_modules/codemirror/lib/codemirror.css`
|
||||
]
|
||||
|
||||
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS) {
|
||||
function buildStyle (
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
) {
|
||||
return `
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
@@ -66,7 +81,6 @@ function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scro
|
||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
|
||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
|
||||
}
|
||||
${allowCustomCSS ? customCSS : ''}
|
||||
${markdownStyle}
|
||||
|
||||
body {
|
||||
@@ -74,6 +88,11 @@ body {
|
||||
font-size: ${fontSize}px;
|
||||
${scrollPastEnd && 'padding-bottom: 90vh;'}
|
||||
}
|
||||
@media print {
|
||||
body {
|
||||
padding-bottom: initial;
|
||||
}
|
||||
}
|
||||
code {
|
||||
font-family: '${codeBlockFontFamily.join("','")}';
|
||||
background-color: rgba(0,0,0,0.04);
|
||||
@@ -130,6 +149,8 @@ body p {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
|
||||
${allowCustomCSS ? customCSS : ''}
|
||||
`
|
||||
}
|
||||
|
||||
@@ -152,7 +173,6 @@ const scrollBarDarkStyle = `
|
||||
}
|
||||
`
|
||||
|
||||
const { shell } = require('electron')
|
||||
const OSX = global.process.platform === 'darwin'
|
||||
|
||||
const defaultFontFamily = ['helvetica', 'arial', 'sans-serif']
|
||||
@@ -160,17 +180,27 @@ if (!OSX) {
|
||||
defaultFontFamily.unshift('Microsoft YaHei')
|
||||
defaultFontFamily.unshift('meiryo')
|
||||
}
|
||||
const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||
const defaultCodeBlockFontFamily = [
|
||||
'Monaco',
|
||||
'Menlo',
|
||||
'Ubuntu Mono',
|
||||
'Consolas',
|
||||
'source-code-pro',
|
||||
'monospace'
|
||||
]
|
||||
export default class MarkdownPreview extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.contextMenuHandler = (e) => this.handleContextMenu(e)
|
||||
this.mouseDownHandler = (e) => this.handleMouseDown(e)
|
||||
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
||||
this.DoubleClickHandler = (e) => this.handleDoubleClick(e)
|
||||
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
||||
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
||||
this.contextMenuHandler = e => this.handleContextMenu(e)
|
||||
this.mouseDownHandler = e => this.handleMouseDown(e)
|
||||
this.mouseUpHandler = e => this.handleMouseUp(e)
|
||||
this.DoubleClickHandler = e => this.handleDoubleClick(e)
|
||||
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {
|
||||
leading: false,
|
||||
trailing: true
|
||||
})
|
||||
this.checkboxClickHandler = e => this.handleCheckboxClick(e)
|
||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
||||
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
|
||||
@@ -200,8 +230,32 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleContextMenu (e) {
|
||||
this.props.onContextMenu(e)
|
||||
handleContextMenu (event) {
|
||||
// If a contextMenu handler was passed to us, use it instead of the self-defined one -> return
|
||||
if (_.isFunction(this.props.onContextMenu)) {
|
||||
this.props.onContextMenu(event)
|
||||
return
|
||||
}
|
||||
// No contextMenu was passed to us -> execute our own link-opener
|
||||
if (event.target.tagName.toLowerCase() === 'a') {
|
||||
const href = event.target.href
|
||||
const isLocalFile = href.startsWith('file:')
|
||||
if (isLocalFile) {
|
||||
const absPath = uri2path(href)
|
||||
try {
|
||||
if (fs.lstatSync(absPath).isFile()) {
|
||||
context.popup([
|
||||
{
|
||||
label: i18n.__('Show in explorer'),
|
||||
click: (e) => shell.showItemInFolder(absPath)
|
||||
}
|
||||
])
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error while evaluating if the file is locally available', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleDoubleClick (e) {
|
||||
@@ -232,36 +286,83 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
handleSaveAsMd () {
|
||||
this.exportAsDocument('md')
|
||||
this.exportAsDocument('md', (noteContent, exportTasks) => {
|
||||
let result = noteContent
|
||||
if (this.props && this.props.storagePath && this.props.noteKey) {
|
||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
||||
noteContent,
|
||||
this.props.storagePath
|
||||
)
|
||||
attachmentsAbsolutePaths.forEach(attachment => {
|
||||
exportTasks.push({
|
||||
src: attachment,
|
||||
dst: attachmentManagement.DESTINATION_FOLDER
|
||||
})
|
||||
})
|
||||
result = attachmentManagement.removeStorageAndNoteReferences(
|
||||
noteContent,
|
||||
this.props.noteKey
|
||||
)
|
||||
}
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
handleSaveAsHtml () {
|
||||
this.exportAsDocument('html', (noteContent, exportTasks) => {
|
||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams()
|
||||
|
||||
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS)
|
||||
let body = this.markdown.render(escapeHtmlCharacters(noteContent, { detectCodeBlock: true }))
|
||||
const {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
codeBlockTheme,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
} = this.getStyleParams()
|
||||
|
||||
const inlineStyles = buildStyle(
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
)
|
||||
let body = this.markdown.render(noteContent)
|
||||
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(noteContent, this.props.storagePath)
|
||||
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
|
||||
noteContent,
|
||||
this.props.storagePath
|
||||
)
|
||||
|
||||
files.forEach((file) => {
|
||||
file = file.replace('file://', '')
|
||||
files.forEach(file => {
|
||||
if (global.process.platform === 'win32') {
|
||||
file = file.replace('file:///', '')
|
||||
} else {
|
||||
file = file.replace('file://', '')
|
||||
}
|
||||
exportTasks.push({
|
||||
src: file,
|
||||
dst: 'css'
|
||||
})
|
||||
})
|
||||
attachmentsAbsolutePaths.forEach((attachment) => {
|
||||
attachmentsAbsolutePaths.forEach(attachment => {
|
||||
exportTasks.push({
|
||||
src: attachment,
|
||||
dst: attachmentManagement.DESTINATION_FOLDER
|
||||
})
|
||||
})
|
||||
body = attachmentManagement.removeStorageAndNoteReferences(body, this.props.noteKey)
|
||||
body = attachmentManagement.removeStorageAndNoteReferences(
|
||||
body,
|
||||
this.props.noteKey
|
||||
)
|
||||
|
||||
let styles = ''
|
||||
files.forEach((file) => {
|
||||
files.forEach(file => {
|
||||
styles += `<link rel="stylesheet" href="css/${path.basename(file)}">`
|
||||
})
|
||||
|
||||
@@ -283,48 +384,53 @@ export default class MarkdownPreview extends React.Component {
|
||||
|
||||
exportAsDocument (fileType, contentFormatter) {
|
||||
const options = {
|
||||
filters: [
|
||||
{name: 'Documents', extensions: [fileType]}
|
||||
],
|
||||
filters: [{ name: 'Documents', extensions: [fileType] }],
|
||||
properties: ['openFile', 'createDirectory']
|
||||
}
|
||||
|
||||
dialog.showSaveDialog(remote.getCurrentWindow(), options,
|
||||
(filename) => {
|
||||
if (filename) {
|
||||
const content = this.props.value
|
||||
const storage = this.props.storagePath
|
||||
dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
|
||||
if (filename) {
|
||||
const content = this.props.value
|
||||
const storage = this.props.storagePath
|
||||
|
||||
exportNote(storage, content, filename, contentFormatter)
|
||||
.then((res) => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {type: 'info', message: `Exported to ${filename}`})
|
||||
}).catch((err) => {
|
||||
dialog.showErrorBox('Export error', err ? err.message || err : 'Unexpected error during export')
|
||||
throw err
|
||||
})
|
||||
}
|
||||
})
|
||||
exportNote(storage, content, filename, contentFormatter)
|
||||
.then(res => {
|
||||
dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
type: 'info',
|
||||
message: `Exported to ${filename}`
|
||||
})
|
||||
})
|
||||
.catch(err => {
|
||||
dialog.showErrorBox(
|
||||
'Export error',
|
||||
err ? err.message || err : 'Unexpected error during export'
|
||||
)
|
||||
throw err
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fixDecodedURI (node) {
|
||||
if (node && node.children.length === 1 && typeof node.children[0] === 'string') {
|
||||
if (
|
||||
node &&
|
||||
node.children.length === 1 &&
|
||||
typeof node.children[0] === 'string'
|
||||
) {
|
||||
const { innerText, href } = node
|
||||
|
||||
node.innerText = mdurl.decode(href) === innerText
|
||||
? href
|
||||
: innerText
|
||||
node.innerText = mdurl.decode(href) === innerText ? href : innerText
|
||||
}
|
||||
}
|
||||
|
||||
getScrollBarStyle () {
|
||||
const {
|
||||
theme
|
||||
} = this.props
|
||||
const { theme } = this.props
|
||||
|
||||
switch (theme) {
|
||||
case 'dark':
|
||||
case 'solarized-dark':
|
||||
case 'monokai':
|
||||
case 'dracula':
|
||||
return scrollBarDarkStyle
|
||||
default:
|
||||
return scrollBarStyle
|
||||
@@ -333,7 +439,10 @@ export default class MarkdownPreview extends React.Component {
|
||||
|
||||
componentDidMount () {
|
||||
this.refs.root.setAttribute('sandbox', 'allow-scripts')
|
||||
this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
|
||||
this.refs.root.contentWindow.document.body.addEventListener(
|
||||
'contextmenu',
|
||||
this.contextMenuHandler
|
||||
)
|
||||
|
||||
let styles = `
|
||||
<style id='style'></style>
|
||||
@@ -344,7 +453,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
</style>
|
||||
`
|
||||
|
||||
CSS_FILES.forEach((file) => {
|
||||
CSS_FILES.forEach(file => {
|
||||
styles += `<link rel="stylesheet" href="${file}">`
|
||||
})
|
||||
|
||||
@@ -352,12 +461,30 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.rewriteIframe()
|
||||
this.applyStyle()
|
||||
|
||||
this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('dblclick', this.DoubleClickHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener('scroll', this.scrollHandler)
|
||||
this.refs.root.contentWindow.document.addEventListener(
|
||||
'mousedown',
|
||||
this.mouseDownHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.addEventListener(
|
||||
'mouseup',
|
||||
this.mouseUpHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.addEventListener(
|
||||
'dblclick',
|
||||
this.DoubleClickHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.addEventListener(
|
||||
'drop',
|
||||
this.preventImageDroppedHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.addEventListener(
|
||||
'dragover',
|
||||
this.preventImageDroppedHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.addEventListener(
|
||||
'scroll',
|
||||
this.scrollHandler
|
||||
)
|
||||
eventEmitter.on('export:save-text', this.saveAsTextHandler)
|
||||
eventEmitter.on('export:save-md', this.saveAsMdHandler)
|
||||
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
|
||||
@@ -365,13 +492,34 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('dblclick', this.DoubleClickHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler)
|
||||
this.refs.root.contentWindow.document.removeEventListener('scroll', this.scrollHandler)
|
||||
this.refs.root.contentWindow.document.body.removeEventListener(
|
||||
'contextmenu',
|
||||
this.contextMenuHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.removeEventListener(
|
||||
'mousedown',
|
||||
this.mouseDownHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.removeEventListener(
|
||||
'mouseup',
|
||||
this.mouseUpHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.removeEventListener(
|
||||
'dblclick',
|
||||
this.DoubleClickHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.removeEventListener(
|
||||
'drop',
|
||||
this.preventImageDroppedHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.removeEventListener(
|
||||
'dragover',
|
||||
this.preventImageDroppedHandler
|
||||
)
|
||||
this.refs.root.contentWindow.document.removeEventListener(
|
||||
'scroll',
|
||||
this.scrollHandler
|
||||
)
|
||||
eventEmitter.off('export:save-text', this.saveAsTextHandler)
|
||||
eventEmitter.off('export:save-md', this.saveAsMdHandler)
|
||||
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
|
||||
@@ -380,14 +528,18 @@ export default class MarkdownPreview extends React.Component {
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
||||
if (prevProps.smartQuotes !== this.props.smartQuotes ||
|
||||
prevProps.sanitize !== this.props.sanitize ||
|
||||
prevProps.smartArrows !== this.props.smartArrows ||
|
||||
prevProps.breaks !== this.props.breaks) {
|
||||
if (
|
||||
prevProps.smartQuotes !== this.props.smartQuotes ||
|
||||
prevProps.sanitize !== this.props.sanitize ||
|
||||
prevProps.smartArrows !== this.props.smartArrows ||
|
||||
prevProps.breaks !== this.props.breaks ||
|
||||
prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox
|
||||
) {
|
||||
this.initMarkdown()
|
||||
this.rewriteIframe()
|
||||
}
|
||||
if (prevProps.fontFamily !== this.props.fontFamily ||
|
||||
if (
|
||||
prevProps.fontFamily !== this.props.fontFamily ||
|
||||
prevProps.fontSize !== this.props.fontSize ||
|
||||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
|
||||
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
|
||||
@@ -396,34 +548,82 @@ export default class MarkdownPreview extends React.Component {
|
||||
prevProps.theme !== this.props.theme ||
|
||||
prevProps.scrollPastEnd !== this.props.scrollPastEnd ||
|
||||
prevProps.allowCustomCSS !== this.props.allowCustomCSS ||
|
||||
prevProps.customCSS !== this.props.customCSS) {
|
||||
prevProps.customCSS !== this.props.customCSS
|
||||
) {
|
||||
this.applyStyle()
|
||||
this.rewriteIframe()
|
||||
}
|
||||
}
|
||||
|
||||
getStyleParams () {
|
||||
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS } = this.props
|
||||
const {
|
||||
fontSize,
|
||||
lineNumber,
|
||||
codeBlockTheme,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
} = this.props
|
||||
let { fontFamily, codeBlockFontFamily } = this.props
|
||||
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
||||
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
|
||||
: defaultFontFamily
|
||||
codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
|
||||
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
|
||||
: defaultCodeBlockFontFamily
|
||||
? fontFamily
|
||||
.split(',')
|
||||
.map(fontName => fontName.trim())
|
||||
.concat(defaultFontFamily)
|
||||
: defaultFontFamily
|
||||
codeBlockFontFamily = _.isString(codeBlockFontFamily) &&
|
||||
codeBlockFontFamily.trim().length > 0
|
||||
? codeBlockFontFamily
|
||||
.split(',')
|
||||
.map(fontName => fontName.trim())
|
||||
.concat(defaultCodeBlockFontFamily)
|
||||
: defaultCodeBlockFontFamily
|
||||
|
||||
return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS}
|
||||
return {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
codeBlockTheme,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
}
|
||||
}
|
||||
|
||||
applyStyle () {
|
||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams()
|
||||
const {
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
codeBlockTheme,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
} = this.getStyleParams()
|
||||
|
||||
this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme)
|
||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS)
|
||||
this.getWindow().document.getElementById(
|
||||
'codeTheme'
|
||||
).href = this.GetCodeThemeLink(codeBlockTheme)
|
||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
|
||||
fontFamily,
|
||||
fontSize,
|
||||
codeBlockFontFamily,
|
||||
lineNumber,
|
||||
scrollPastEnd,
|
||||
theme,
|
||||
allowCustomCSS,
|
||||
customCSS
|
||||
)
|
||||
}
|
||||
|
||||
GetCodeThemeLink (theme) {
|
||||
theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
|
||||
theme = consts.THEMES.some(_theme => _theme === theme) &&
|
||||
theme !== 'default'
|
||||
? theme
|
||||
: 'elegant'
|
||||
return theme.startsWith('solarized')
|
||||
@@ -432,71 +632,92 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
rewriteIframe () {
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
||||
el.removeEventListener('click', this.checkboxClickHandler)
|
||||
})
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll(
|
||||
'input[type="checkbox"]'
|
||||
),
|
||||
el => {
|
||||
el.removeEventListener('click', this.checkboxClickHandler)
|
||||
}
|
||||
)
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
el.removeEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('a'),
|
||||
el => {
|
||||
el.removeEventListener('click', this.linkClickHandler)
|
||||
}
|
||||
)
|
||||
|
||||
const { theme, indentSize, showCopyNotification, storagePath, noteKey } = this.props
|
||||
const {
|
||||
theme,
|
||||
indentSize,
|
||||
showCopyNotification,
|
||||
storagePath,
|
||||
noteKey
|
||||
} = this.props
|
||||
let { value, codeBlockTheme } = this.props
|
||||
|
||||
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
||||
|
||||
const codeBlocks = value.match(/(```)(.|[\n])*?(```)/g)
|
||||
if (codeBlocks !== null) {
|
||||
codeBlocks.forEach((codeBlock) => {
|
||||
value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
|
||||
})
|
||||
}
|
||||
const renderedHTML = this.markdown.render(value)
|
||||
attachmentManagement.migrateAttachments(value, storagePath, noteKey)
|
||||
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath)
|
||||
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(
|
||||
renderedHTML,
|
||||
storagePath
|
||||
)
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll(
|
||||
'input[type="checkbox"]'
|
||||
),
|
||||
el => {
|
||||
el.addEventListener('click', this.checkboxClickHandler)
|
||||
}
|
||||
)
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
||||
el.addEventListener('click', this.checkboxClickHandler)
|
||||
})
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('a'),
|
||||
el => {
|
||||
this.fixDecodedURI(el)
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
}
|
||||
)
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
this.fixDecodedURI(el)
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
|
||||
codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
|
||||
codeBlockTheme = consts.THEMES.some(_theme => _theme === codeBlockTheme)
|
||||
? codeBlockTheme
|
||||
: 'default'
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.code code'), (el) => {
|
||||
let syntax = CodeMirror.findModeByName(convertModeName(el.className))
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
CodeMirror.requireMode(syntax.mode, () => {
|
||||
const content = htmlTextHelper.decodeEntities(el.innerHTML)
|
||||
const copyIcon = document.createElement('i')
|
||||
copyIcon.innerHTML = '<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
|
||||
copyIcon.onclick = (e) => {
|
||||
copy(content)
|
||||
if (showCopyNotification) {
|
||||
this.notify('Saved to Clipboard!', {
|
||||
body: 'Paste it wherever you want!',
|
||||
silent: true
|
||||
})
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('.code code'),
|
||||
el => {
|
||||
let syntax = CodeMirror.findModeByName(convertModeName(el.className))
|
||||
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
|
||||
CodeMirror.requireMode(syntax.mode, () => {
|
||||
const content = htmlTextHelper.decodeEntities(el.innerHTML)
|
||||
const copyIcon = document.createElement('i')
|
||||
copyIcon.innerHTML =
|
||||
'<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
|
||||
copyIcon.onclick = e => {
|
||||
copy(content)
|
||||
if (showCopyNotification) {
|
||||
this.notify('Saved to Clipboard!', {
|
||||
body: 'Paste it wherever you want!',
|
||||
silent: true
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
el.parentNode.appendChild(copyIcon)
|
||||
el.innerHTML = ''
|
||||
if (codeBlockTheme.indexOf('solarized') === 0) {
|
||||
const [refThema, color] = codeBlockTheme.split(' ')
|
||||
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
|
||||
} else {
|
||||
el.parentNode.className += ` cm-s-${codeBlockTheme}`
|
||||
}
|
||||
CodeMirror.runMode(content, syntax.mime, el, {
|
||||
tabSize: indentSize
|
||||
el.parentNode.appendChild(copyIcon)
|
||||
el.innerHTML = ''
|
||||
if (codeBlockTheme.indexOf('solarized') === 0) {
|
||||
const [refThema, color] = codeBlockTheme.split(' ')
|
||||
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
|
||||
} else {
|
||||
el.parentNode.className += ` cm-s-${codeBlockTheme}`
|
||||
}
|
||||
CodeMirror.runMode(content, syntax.mime, el, {
|
||||
tabSize: indentSize
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
const opts = {}
|
||||
// if (this.props.theme === 'dark') {
|
||||
// opts['font-color'] = '#DDD'
|
||||
@@ -504,50 +725,65 @@ export default class MarkdownPreview extends React.Component {
|
||||
// opts['element-color'] = '#DDD'
|
||||
// opts['fill'] = '#3A404C'
|
||||
// }
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.flowchart'), (el) => {
|
||||
Raphael.setWindow(this.getWindow())
|
||||
try {
|
||||
const diagram = flowchart.parse(htmlTextHelper.decodeEntities(el.innerHTML))
|
||||
el.innerHTML = ''
|
||||
diagram.drawSVG(el, opts)
|
||||
_.forEach(el.querySelectorAll('a'), (el) => {
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
el.className = 'flowchart-error'
|
||||
el.innerHTML = 'Flowchart parse error: ' + e.message
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('.flowchart'),
|
||||
el => {
|
||||
Raphael.setWindow(this.getWindow())
|
||||
try {
|
||||
const diagram = flowchart.parse(
|
||||
htmlTextHelper.decodeEntities(el.innerHTML)
|
||||
)
|
||||
el.innerHTML = ''
|
||||
diagram.drawSVG(el, opts)
|
||||
_.forEach(el.querySelectorAll('a'), el => {
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
} catch (e) {
|
||||
el.className = 'flowchart-error'
|
||||
el.innerHTML = 'Flowchart parse error: ' + e.message
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('.sequence'), (el) => {
|
||||
Raphael.setWindow(this.getWindow())
|
||||
try {
|
||||
const diagram = SequenceDiagram.parse(htmlTextHelper.decodeEntities(el.innerHTML))
|
||||
el.innerHTML = ''
|
||||
diagram.drawSVG(el, {theme: 'simple'})
|
||||
_.forEach(el.querySelectorAll('a'), (el) => {
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
el.className = 'sequence-error'
|
||||
el.innerHTML = 'Sequence diagram parse error: ' + e.message
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('.sequence'),
|
||||
el => {
|
||||
Raphael.setWindow(this.getWindow())
|
||||
try {
|
||||
const diagram = SequenceDiagram.parse(
|
||||
htmlTextHelper.decodeEntities(el.innerHTML)
|
||||
)
|
||||
el.innerHTML = ''
|
||||
diagram.drawSVG(el, { theme: 'simple' })
|
||||
_.forEach(el.querySelectorAll('a'), el => {
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
} catch (e) {
|
||||
el.className = 'sequence-error'
|
||||
el.innerHTML = 'Sequence diagram parse error: ' + e.message
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('.chart'),
|
||||
(el) => {
|
||||
el => {
|
||||
try {
|
||||
const chartConfig = JSON.parse(el.innerHTML)
|
||||
const format = el.attributes.getNamedItem('data-format').value
|
||||
const chartConfig = format === 'yaml' ? yaml.load(el.innerHTML) : JSON.parse(el.innerHTML)
|
||||
el.innerHTML = ''
|
||||
var canvas = document.createElement('canvas')
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
el.appendChild(canvas)
|
||||
/* eslint-disable no-new */
|
||||
new Chart(canvas, chartConfig)
|
||||
|
||||
const height = el.attributes.getNamedItem('data-height')
|
||||
if (height && height.value !== 'undefined') {
|
||||
el.style.height = height.value + 'vh'
|
||||
canvas.height = height.value + 'vh'
|
||||
}
|
||||
|
||||
const chart = new Chart(canvas, chartConfig)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
el.className = 'chart-error'
|
||||
el.innerHTML = 'chartjs diagram parse error: ' + e.message
|
||||
}
|
||||
@@ -555,8 +791,8 @@ export default class MarkdownPreview extends React.Component {
|
||||
)
|
||||
_.forEach(
|
||||
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
|
||||
(el) => {
|
||||
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML))
|
||||
el => {
|
||||
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -570,7 +806,9 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
scrollTo (targetRow) {
|
||||
const blocks = this.getWindow().document.querySelectorAll('body>[data-line]')
|
||||
const blocks = this.getWindow().document.querySelectorAll(
|
||||
'body>[data-line]'
|
||||
)
|
||||
|
||||
for (let index = 0; index < blocks.length; index++) {
|
||||
let block = blocks[index]
|
||||
@@ -590,7 +828,11 @@ export default class MarkdownPreview extends React.Component {
|
||||
|
||||
notify (title, options) {
|
||||
if (global.process.platform === 'win32') {
|
||||
options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
|
||||
options.icon = path.join(
|
||||
'file://',
|
||||
global.__dirname,
|
||||
'../../resources/app.png'
|
||||
)
|
||||
}
|
||||
return new window.Notification(title, options)
|
||||
}
|
||||
@@ -605,7 +847,9 @@ export default class MarkdownPreview extends React.Component {
|
||||
const regexNoteInternalLink = /main.html#(.+)/
|
||||
if (regexNoteInternalLink.test(linkHash)) {
|
||||
const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1])
|
||||
const targetElement = this.refs.root.contentWindow.document.getElementById(targetId)
|
||||
const targetElement = this.refs.root.contentWindow.document.getElementById(
|
||||
targetId
|
||||
)
|
||||
|
||||
if (targetElement != null) {
|
||||
this.getWindow().scrollTo(0, targetElement.offsetTop)
|
||||
@@ -623,6 +867,15 @@ export default class MarkdownPreview extends React.Component {
|
||||
return
|
||||
}
|
||||
|
||||
const regexIsLine = /^:line:[0-9]/
|
||||
if (regexIsLine.test(linkHash)) {
|
||||
const numberPattern = /\d+/g
|
||||
|
||||
const lineNumber = parseInt(linkHash.match(numberPattern)[0])
|
||||
eventEmitter.emit('line:jump', lineNumber)
|
||||
return
|
||||
}
|
||||
|
||||
// this will match the old link format storage.key-note.key
|
||||
// e.g.
|
||||
// 877f99c3268608328037-1c211eb7dcb463de6490
|
||||
@@ -639,9 +892,9 @@ export default class MarkdownPreview extends React.Component {
|
||||
render () {
|
||||
const { className, style, tabIndex } = this.props
|
||||
return (
|
||||
<iframe className={className != null
|
||||
? 'MarkdownPreview ' + className
|
||||
: 'MarkdownPreview'
|
||||
<iframe
|
||||
className={
|
||||
className != null ? 'MarkdownPreview ' + className : 'MarkdownPreview'
|
||||
}
|
||||
style={style}
|
||||
tabIndex={tabIndex}
|
||||
|
||||
@@ -20,12 +20,18 @@ class MarkdownSplitEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
setValue (value) {
|
||||
this.refs.code.setValue(value)
|
||||
}
|
||||
|
||||
handleOnChange () {
|
||||
this.value = this.refs.code.value
|
||||
this.props.onChange()
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
if (!this.props.config.preview.scrollSync) return
|
||||
|
||||
const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document')
|
||||
const codeDoc = _.get(this, 'refs.code.editor.doc')
|
||||
let srcTop, srcHeight, targetTop, targetHeight
|
||||
@@ -72,8 +78,8 @@ class MarkdownSplitEditor extends React.Component {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const idMatch = /checkbox-([0-9]+)/
|
||||
const checkedMatch = /\[x\]/i
|
||||
const uncheckedMatch = /\[ \]/
|
||||
const checkedMatch = /^\s*[\+\-\*] \[x\]/i
|
||||
const uncheckedMatch = /^\s*[\+\-\*] \[ \]/
|
||||
if (idMatch.test(e.target.getAttribute('id'))) {
|
||||
const lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
|
||||
const lines = this.refs.code.value
|
||||
@@ -82,10 +88,10 @@ class MarkdownSplitEditor extends React.Component {
|
||||
const targetLine = lines[lineIndex]
|
||||
|
||||
if (targetLine.match(checkedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
|
||||
lines[lineIndex] = targetLine.replace(checkedMatch, '- [ ]')
|
||||
}
|
||||
if (targetLine.match(uncheckedMatch)) {
|
||||
lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
|
||||
lines[lineIndex] = targetLine.replace(uncheckedMatch, '- [x]')
|
||||
}
|
||||
this.refs.code.setValue(lines.join('\n'))
|
||||
}
|
||||
@@ -145,7 +151,7 @@ class MarkdownSplitEditor extends React.Component {
|
||||
styleName='codeEditor'
|
||||
ref='code'
|
||||
width={this.state.codeEditorWidthInPercent + '%'}
|
||||
mode='GitHub Flavored Markdown'
|
||||
mode='Boost Flavored Markdown'
|
||||
value={value}
|
||||
theme={config.editor.theme}
|
||||
keyMap={config.editor.keyMap}
|
||||
@@ -158,6 +164,7 @@ class MarkdownSplitEditor extends React.Component {
|
||||
rulers={config.editor.rulers}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
enableTableEditor={config.editor.enableTableEditor}
|
||||
storageKey={storageKey}
|
||||
noteKey={noteKey}
|
||||
onChange={this.handleOnChange.bind(this)}
|
||||
@@ -191,6 +198,7 @@ class MarkdownSplitEditor extends React.Component {
|
||||
noteKey={noteKey}
|
||||
customCSS={config.preview.customCSS}
|
||||
allowCustomCSS={config.preview.allowCustomCSS}
|
||||
lineThroughCheckbox={config.preview.lineThroughCheckbox}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -24,18 +24,19 @@ const TagElement = ({ tagName }) => (
|
||||
/**
|
||||
* @description Tag element list component.
|
||||
* @param {Array|null} tags
|
||||
* @param {boolean} showTagsAlphabetically
|
||||
* @return {React.Component}
|
||||
*/
|
||||
const TagElementList = (tags) => {
|
||||
const TagElementList = (tags, showTagsAlphabetically) => {
|
||||
if (!isArray(tags)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const tagElements = tags.map(tag => (
|
||||
TagElement({tagName: tag})
|
||||
))
|
||||
|
||||
return tagElements
|
||||
if (showTagsAlphabetically) {
|
||||
return _.sortBy(tags).map(tag => TagElement({ tagName: tag }))
|
||||
} else {
|
||||
return tags.map(tag => TagElement({ tagName: tag }))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,12 +58,11 @@ const NoteItem = ({
|
||||
pathname,
|
||||
storageName,
|
||||
folderName,
|
||||
viewType
|
||||
viewType,
|
||||
showTagsAlphabetically
|
||||
}) => (
|
||||
<div styleName={isActive
|
||||
? 'item--active'
|
||||
: 'item'
|
||||
}
|
||||
<div
|
||||
styleName={isActive ? 'item--active' : 'item'}
|
||||
key={note.key}
|
||||
onClick={e => handleNoteClick(e, note.key)}
|
||||
onContextMenu={e => handleNoteContextMenu(e, note.key)}
|
||||
@@ -72,42 +72,52 @@ const NoteItem = ({
|
||||
<div styleName='item-wrapper'>
|
||||
{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-file-text-o' />
|
||||
}
|
||||
: <i styleName='item-title-icon' className='fa fa-fw fa-file-text-o' />}
|
||||
<div styleName='item-title'>
|
||||
{note.title.trim().length > 0
|
||||
? note.title
|
||||
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>
|
||||
}
|
||||
: <span styleName='item-title-empty'>{i18n.__('Empty note')}</span>}
|
||||
</div>
|
||||
{['ALL', 'STORAGE'].includes(viewType) && <div styleName='item-middle'>
|
||||
<div styleName='item-middle'>
|
||||
<div styleName='item-middle-time'>{dateDisplay}</div>
|
||||
<div styleName='item-middle-app-meta'>
|
||||
<div title={viewType === 'ALL' ? storageName : viewType === 'STORAGE' ? folderName : null} styleName='item-middle-app-meta-label'>
|
||||
<div
|
||||
title={
|
||||
viewType === 'ALL'
|
||||
? storageName
|
||||
: viewType === 'STORAGE' ? folderName : null
|
||||
}
|
||||
styleName='item-middle-app-meta-label'
|
||||
>
|
||||
{viewType === 'ALL' && storageName}
|
||||
{viewType === 'STORAGE' && folderName}
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
<div styleName='item-bottom'>
|
||||
<div styleName='item-bottom-tagList'>
|
||||
{note.tags.length > 0
|
||||
? TagElementList(note.tags)
|
||||
: <span style={{ fontStyle: 'italic', opacity: 0.5 }} styleName='item-bottom-tagList-empty'>{i18n.__('No tags')}</span>
|
||||
}
|
||||
? TagElementList(note.tags, showTagsAlphabetically)
|
||||
: <span
|
||||
style={{ fontStyle: 'italic', opacity: 0.5 }}
|
||||
styleName='item-bottom-tagList-empty'
|
||||
>
|
||||
{i18n.__('No tags')}
|
||||
</span>}
|
||||
</div>
|
||||
<div>
|
||||
{note.isStarred
|
||||
? <img styleName='item-star' src='../resources/icon/icon-starred.svg' /> : ''
|
||||
}
|
||||
? <img
|
||||
styleName='item-star'
|
||||
src='../resources/icon/icon-starred.svg'
|
||||
/>
|
||||
: ''}
|
||||
{note.isPinned && !pathname.match(/\/starred|\/trash/)
|
||||
? <i styleName='item-pin' className='fa fa-thumb-tack' /> : ''
|
||||
}
|
||||
? <i styleName='item-pin' className='fa fa-thumb-tack' />
|
||||
: ''}
|
||||
{note.type === 'MARKDOWN_NOTE'
|
||||
? <TodoProcess todoStatus={getTodoStatus(note.content)} />
|
||||
: ''
|
||||
}
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -368,13 +368,13 @@ body[data-theme="monokai"]
|
||||
.item-title
|
||||
.item-title-icon
|
||||
.item-bottom-time
|
||||
color $ui-monokai-text-color
|
||||
color $ui-monokai-active-color
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(white, 10%)
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
// background-color alpha($ui-monokai-button--active-backgroundColor, 60%)
|
||||
color #c0392b
|
||||
color #f92672
|
||||
.item-bottom-tagList-item
|
||||
background-color alpha(#fff, 20%)
|
||||
|
||||
@@ -394,3 +394,76 @@ body[data-theme="monokai"]
|
||||
.item-bottom-tagList-empty
|
||||
color $ui-inactive-text-color
|
||||
vertical-align middle
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
border-color $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.item
|
||||
border-color $ui-dracula-borderColor
|
||||
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
|
||||
@@ -240,7 +240,7 @@ body[data-theme="monokai"]
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
transition 0.15s
|
||||
color $ui-solarized-dark-text-color
|
||||
color $ui-monokai-text-color
|
||||
.item-simple-bottom-tagList-item
|
||||
transition 0.15s
|
||||
background-color alpha(#fff, 20%)
|
||||
@@ -286,3 +286,67 @@ body[data-theme="monokai"]
|
||||
.item-simple-right-storageName
|
||||
padding-left 4px
|
||||
opacity 0.4
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
border-color $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.item-simple
|
||||
border-color $ui-dracula-borderColor
|
||||
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
|
||||
@@ -51,4 +51,15 @@ body[data-theme="monokai"]
|
||||
border none
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
&:hover
|
||||
color #5CB85C
|
||||
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
|
||||
@@ -263,4 +263,46 @@ body[data-theme="monokai"]
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
.menu-button-label
|
||||
color $ui-monokai-text-color
|
||||
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
|
||||
@@ -136,4 +136,92 @@ body[data-theme="solarized-dark"]
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.deleteButton
|
||||
color alpha($ui-solarized-dark-text-color, 30%)
|
||||
color alpha($ui-solarized-dark-text-color, 30%)
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.root
|
||||
color $ui-monokai-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color darken($ui-monokai-noteDetail-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.root--active
|
||||
color $ui-monokai-text-color
|
||||
border-color $ui-monokai-borderColor
|
||||
&:hover
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-monokai-text-color
|
||||
&:hover
|
||||
background-color darken($ui-monokai-noteDetail-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.button
|
||||
border none
|
||||
color $ui-monokai-text-color
|
||||
background-color transparent
|
||||
transition color background-color 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
.input
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.deleteButton
|
||||
color alpha($ui-monokai-text-color, 30%)
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
border-color $ui-dark-borderColor
|
||||
&:hover
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
background-color darken($ui-dracula-noteDetail-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.root--active
|
||||
color $ui-dracula-text-color
|
||||
border-color $ui-dracula-borderColor
|
||||
&:hover
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
.deleteButton
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
background-color darken($ui-dracula-noteDetail-backgroundColor, 15%)
|
||||
&:active
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
|
||||
.button
|
||||
border none
|
||||
color $ui-dracula-text-color
|
||||
background-color transparent
|
||||
transition color background-color 0.15s
|
||||
border-left 4px solid transparent
|
||||
&:hover
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
|
||||
.input
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.deleteButton
|
||||
color alpha($ui-dracula-text-color, 30%)
|
||||
@@ -54,9 +54,8 @@ const StorageItem = ({
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
{!isFolded && (
|
||||
<DraggableIcon className={styles['folderList-item-reorder']} />
|
||||
)}
|
||||
{!isFolded &&
|
||||
<DraggableIcon className={styles['folderList-item-reorder']} />}
|
||||
<span
|
||||
styleName={
|
||||
isFolded ? 'folderList-item-name--folded' : 'folderList-item-name'
|
||||
@@ -72,12 +71,10 @@ const StorageItem = ({
|
||||
: folderName}
|
||||
</span>
|
||||
{!isFolded &&
|
||||
_.isNumber(noteCount) && (
|
||||
<span styleName='folderList-item-noteCount'>{noteCount}</span>
|
||||
)}
|
||||
{isFolded && (
|
||||
<span styleName='folderList-item-tooltip'>{folderName}</span>
|
||||
)}
|
||||
_.isNumber(noteCount) &&
|
||||
<span styleName='folderList-item-noteCount'>{noteCount}</span>}
|
||||
{isFolded &&
|
||||
<span styleName='folderList-item-tooltip'>{folderName}</span>}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -156,4 +156,23 @@ body[data-theme="monokai"]
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
&:hover
|
||||
color $ui-monokai-text-color
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
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
|
||||
@@ -7,18 +7,18 @@ import styles from './StorageList.styl'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
|
||||
/**
|
||||
* @param {Array} storgaeList
|
||||
* @param {Array} storageList
|
||||
*/
|
||||
|
||||
const StorageList = ({storageList, isFolded}) => (
|
||||
<div styleName={isFolded ? 'storageList-folded' : 'storageList'}>
|
||||
{storageList.length > 0 ? storageList : (
|
||||
<div styleName='storgaeList-empty'>No storage mount.</div>
|
||||
<div styleName='storageList-empty'>No storage mount.</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
StorageList.propTypes = {
|
||||
storgaeList: PropTypes.arrayOf(PropTypes.element).isRequired
|
||||
storageList: PropTypes.arrayOf(PropTypes.element).isRequired
|
||||
}
|
||||
export default CSSModules(StorageList, styles)
|
||||
|
||||
@@ -14,8 +14,8 @@ import CSSModules from 'browser/lib/CSSModules'
|
||||
* @param {bool} isRelated
|
||||
*/
|
||||
|
||||
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isActive, isRelated, count}) => (
|
||||
<div styleName='tagList-itemContainer'>
|
||||
const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count}) => (
|
||||
<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'} />
|
||||
@@ -25,7 +25,7 @@ const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isAc
|
||||
<button styleName={isActive ? 'tagList-item-active' : 'tagList-item'} onClick={() => handleClickTagListItem(name)}>
|
||||
<span styleName='tagList-item-name'>
|
||||
{`# ${name}`}
|
||||
<span styleName='tagList-item-count'>{count}</span>
|
||||
<span styleName='tagList-item-count'>{count !== 0 ? count : ''}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@ import styles from './TodoListPercentage.styl'
|
||||
*/
|
||||
|
||||
const TodoListPercentage = ({
|
||||
percentageOfTodo
|
||||
percentageOfTodo, onClearCheckboxClick
|
||||
}) => (
|
||||
<div styleName='percentageBar' style={{display: isNaN(percentageOfTodo) ? 'none' : ''}}>
|
||||
<div styleName='progressBar' style={{width: `${percentageOfTodo}%`}}>
|
||||
@@ -20,11 +20,15 @@ const TodoListPercentage = ({
|
||||
<p styleName='percentageText'>{percentageOfTodo}%</p>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='todoClear'>
|
||||
<p styleName='todoClearText' onClick={(e) => onClearCheckboxClick(e)}>clear</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
TodoListPercentage.propTypes = {
|
||||
percentageOfTodo: PropTypes.number.isRequired
|
||||
percentageOfTodo: PropTypes.number.isRequired,
|
||||
onClearCheckboxClick: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
export default CSSModules(TodoListPercentage, styles)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.percentageBar
|
||||
display: flex
|
||||
position absolute
|
||||
top 72px
|
||||
right 0px
|
||||
@@ -30,6 +31,20 @@
|
||||
color #f4f4f4
|
||||
font-weight 600
|
||||
|
||||
.todoClear
|
||||
display flex
|
||||
justify-content: flex-end
|
||||
position absolute
|
||||
z-index 120
|
||||
width 100%
|
||||
height 100%
|
||||
padding 2px 10px
|
||||
|
||||
.todoClearText
|
||||
color #f4f4f4
|
||||
cursor pointer
|
||||
font-weight 500
|
||||
|
||||
body[data-theme="dark"]
|
||||
.percentageBar
|
||||
background-color #444444
|
||||
@@ -39,7 +54,10 @@ body[data-theme="dark"]
|
||||
|
||||
.percentageText
|
||||
color $ui-dark-text-color
|
||||
|
||||
|
||||
.todoClearText
|
||||
color $ui-dark-text-color
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.percentageBar
|
||||
background-color #002b36
|
||||
@@ -50,12 +68,28 @@ body[data-theme="solarized-dark"]
|
||||
.percentageText
|
||||
color #fdf6e3
|
||||
|
||||
.todoClearText
|
||||
color #fdf6e3
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.percentageBar
|
||||
background-color #f92672
|
||||
background-color: $ui-monokai-borderColor
|
||||
|
||||
.progressBar
|
||||
background-color: #373831
|
||||
background-color $ui-monokai-active-color
|
||||
|
||||
.percentageText
|
||||
color #fdf6e3
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.percentageBar
|
||||
background-color $ui-dracula-borderColor
|
||||
|
||||
.progressBar
|
||||
background-color: $ui-dracula-active-color
|
||||
|
||||
.percentageText
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.percentageText
|
||||
color $ui-dracula-text-color
|
||||
|
||||
@@ -80,6 +80,9 @@ li
|
||||
&.checked
|
||||
text-decoration line-through
|
||||
opacity 0.5
|
||||
&.taskListItem.checked
|
||||
text-decoration line-through
|
||||
opacity 0.5
|
||||
div.math-rendered
|
||||
text-align center
|
||||
.math-failed
|
||||
@@ -206,41 +209,39 @@ code
|
||||
text-decoration none
|
||||
margin-right 2px
|
||||
pre
|
||||
padding 0.5em !important
|
||||
padding 0.5rem !important
|
||||
border solid 1px #D1D1D1
|
||||
border-radius 5px
|
||||
overflow-x auto
|
||||
margin 0 0 1em
|
||||
margin 0 0 1rem
|
||||
display flex
|
||||
line-height 1.4em
|
||||
&.flowchart, &.sequence, &.chart
|
||||
display flex
|
||||
justify-content center
|
||||
background-color white
|
||||
&.CodeMirror
|
||||
height initial
|
||||
flex-wrap wrap
|
||||
&>code
|
||||
flex 1
|
||||
overflow-x auto
|
||||
code
|
||||
background-color inherit
|
||||
margin 0
|
||||
padding 0
|
||||
border none
|
||||
border-radius 0
|
||||
&.CodeMirror
|
||||
height initial
|
||||
flex-wrap wrap
|
||||
&>code
|
||||
flex 1
|
||||
overflow-x auto
|
||||
&.mermaid svg
|
||||
max-width 100% !important
|
||||
&>span.filename
|
||||
width 100%
|
||||
border-radius: 5px 0px 0px 0px
|
||||
margin -8px 100% 8px -8px
|
||||
padding 0px 6px
|
||||
margin -0.5rem 100% 0.5rem -0.5rem
|
||||
padding 0.125rem 0.375rem
|
||||
background-color #777;
|
||||
color white
|
||||
&:empty
|
||||
display none
|
||||
&>span.lineNumber
|
||||
display none
|
||||
font-size 1em
|
||||
padding 0.5em 0
|
||||
margin -0.5em 0.5em -0.5em -0.5em
|
||||
padding 0.5rem 0
|
||||
margin -0.5rem 0.5rem -0.5rem -0.5rem
|
||||
border-right 1px solid
|
||||
text-align right
|
||||
border-top-left-radius 4px
|
||||
@@ -257,6 +258,7 @@ table
|
||||
display block
|
||||
width 100%
|
||||
margin 0 0 1em
|
||||
overflow-x auto
|
||||
thead
|
||||
tr
|
||||
background-color tableHeadBgColor
|
||||
@@ -360,7 +362,7 @@ for name, val in admonition_types
|
||||
.admonition.{name}
|
||||
@extend $admonition
|
||||
border-left-color: val[color]
|
||||
|
||||
|
||||
.admonition.{name}>.admonition-title
|
||||
@extend $admonition-title
|
||||
border-bottom-color: .1rem solid rgba(val[color], 0.2)
|
||||
@@ -371,6 +373,49 @@ for name, val in admonition_types
|
||||
color: val[color]
|
||||
content: val[icon]
|
||||
|
||||
dl
|
||||
margin 2rem 0
|
||||
padding 0
|
||||
display flex
|
||||
width 100%
|
||||
flex-wrap wrap
|
||||
align-items flex-start
|
||||
border-bottom 1px solid borderColor
|
||||
background-color tableHeadBgColor
|
||||
|
||||
dt
|
||||
border-top 1px solid borderColor
|
||||
font-weight bold
|
||||
text-align right
|
||||
overflow hidden
|
||||
flex-basis 20%
|
||||
padding 0.4rem 0.9rem
|
||||
box-sizing border-box
|
||||
|
||||
dd
|
||||
border-top 1px solid borderColor
|
||||
flex-basis 80%
|
||||
padding 0.4rem 0.9rem
|
||||
min-height 2.5rem
|
||||
background-color $ui-noteDetail-backgroundColor
|
||||
box-sizing border-box
|
||||
|
||||
dd + dd
|
||||
margin-left 20%
|
||||
|
||||
pre.fence
|
||||
flex-wrap wrap
|
||||
|
||||
.chart, .flowchart, .mermaid, .sequence
|
||||
display flex
|
||||
justify-content center
|
||||
background-color white
|
||||
max-width 100%
|
||||
flex-grow 1
|
||||
|
||||
canvas, svg
|
||||
max-width 100% !important
|
||||
|
||||
themeDarkBackground = darken(#21252B, 10%)
|
||||
themeDarkText = #f9f9f9
|
||||
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
||||
@@ -421,6 +466,14 @@ body[data-theme="dark"]
|
||||
kbd
|
||||
background-color themeDarkBorder
|
||||
color themeDarkText
|
||||
dl
|
||||
border-color themeDarkBorder
|
||||
background-color themeDarkTableHead
|
||||
dt
|
||||
border-color themeDarkBorder
|
||||
dd
|
||||
border-color themeDarkBorder
|
||||
background-color themeDarkPreview
|
||||
|
||||
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
|
||||
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
|
||||
@@ -448,6 +501,14 @@ body[data-theme="solarized-dark"]
|
||||
border-color themeSolarizedDarkTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeSolarizedDarkTableBorder
|
||||
dl
|
||||
border-color themeDarkBorder
|
||||
background-color themeSolarizedDarkTableHead
|
||||
dt
|
||||
border-color themeDarkBorder
|
||||
dd
|
||||
border-color themeDarkBorder
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
|
||||
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
|
||||
@@ -476,4 +537,49 @@ body[data-theme="monokai"]
|
||||
&:last-child
|
||||
border-right solid 1px themeMonokaiTableBorder
|
||||
kbd
|
||||
background-color themeDarkBackground
|
||||
background-color themeDarkBackground
|
||||
dl
|
||||
border-color themeDarkBorder
|
||||
background-color themeMonokaiTableHead
|
||||
dt
|
||||
border-color themeDarkBorder
|
||||
dd
|
||||
border-color themeDarkBorder
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
themeDraculaTableOdd = $ui-dracula-noteDetail-backgroundColor
|
||||
themeDraculaTableEven = darken($ui-dracula-noteDetail-backgroundColor, 10%)
|
||||
themeDraculaTableHead = themeDraculaTableEven
|
||||
themeDraculaTableBorder = themeDarkBorder
|
||||
|
||||
body[data-theme="dracula"]
|
||||
color $ui-dracula-text-color
|
||||
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
|
||||
border-color themeDarkBorder
|
||||
background-color themeDraculaTableHead
|
||||
dt
|
||||
border-color themeDarkBorder
|
||||
dd
|
||||
border-color themeDarkBorder
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
|
||||
@@ -1,25 +1,40 @@
|
||||
import mermaidAPI from 'mermaid'
|
||||
|
||||
// fixes bad styling in the mermaid dark theme
|
||||
const darkThemeStyling = `
|
||||
.loopText tspan {
|
||||
fill: white;
|
||||
}`
|
||||
|
||||
function getRandomInt (min, max) {
|
||||
return Math.floor(Math.random() * (max - min)) + min
|
||||
}
|
||||
|
||||
function getId () {
|
||||
var pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
var id = 'm-'
|
||||
for (var i = 0; i < 7; i++) {
|
||||
const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
||||
let id = 'm-'
|
||||
for (let i = 0; i < 7; i++) {
|
||||
id += pool[getRandomInt(0, 16)]
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
function render (element, content) {
|
||||
function render (element, content, theme) {
|
||||
try {
|
||||
const height = element.attributes.getNamedItem('data-height')
|
||||
if (height && height.value !== 'undefined') {
|
||||
element.style.height = height.value + 'vh'
|
||||
}
|
||||
let isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula'
|
||||
mermaidAPI.initialize({
|
||||
theme: isDarkTheme ? 'dark' : 'default',
|
||||
themeCSS: isDarkTheme ? darkThemeStyling : '',
|
||||
useMaxWidth: false
|
||||
})
|
||||
mermaidAPI.render(getId(), content, (svgGraph) => {
|
||||
element.innerHTML = svgGraph
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
element.className = 'mermaid-error'
|
||||
element.innerHTML = 'mermaid diagram parse error: ' + e.message
|
||||
}
|
||||
|
||||
@@ -48,8 +48,12 @@ const languages = [
|
||||
locale: 'pl'
|
||||
},
|
||||
{
|
||||
name: 'Portuguese',
|
||||
locale: 'pt'
|
||||
name: 'Portuguese (PT-BR)',
|
||||
locale: 'pt-BR'
|
||||
},
|
||||
{
|
||||
name: 'Portuguese (PT-PT)',
|
||||
locale: 'pt-PT'
|
||||
},
|
||||
{
|
||||
name: 'Russian',
|
||||
@@ -61,6 +65,9 @@ const languages = [
|
||||
}, {
|
||||
name: 'Turkish',
|
||||
locale: 'tr'
|
||||
}, {
|
||||
name: 'Thai',
|
||||
locale: 'th'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1,53 +1,113 @@
|
||||
import { Point } from '@susisu/mte-kernel'
|
||||
|
||||
export default class TextEditorInterface {
|
||||
constructor (editor) {
|
||||
this.editor = editor
|
||||
}
|
||||
|
||||
getCursorPosition () {
|
||||
const pos = this.editor.getCursor()
|
||||
return new Point(pos.line, pos.ch)
|
||||
}
|
||||
|
||||
setCursorPosition (pos) {
|
||||
this.editor.setCursor({line: pos.row, ch: pos.column})
|
||||
}
|
||||
|
||||
setSelectionRange (range) {
|
||||
this.editor.setSelection({
|
||||
anchor: {line: range.start.row, ch: range.start.column},
|
||||
head: {line: range.end.row, ch: range.end.column}
|
||||
})
|
||||
}
|
||||
|
||||
getLastRow () {
|
||||
return this.editor.lastLine()
|
||||
}
|
||||
|
||||
acceptsTableEdit (row) {
|
||||
return true
|
||||
}
|
||||
|
||||
getLine (row) {
|
||||
return this.editor.getLine(row)
|
||||
}
|
||||
|
||||
insertLine (row, line) {
|
||||
this.editor.replaceRange(line, {line: row, ch: 0})
|
||||
}
|
||||
|
||||
deleteLine (row) {
|
||||
this.editor.replaceRange('', {line: row, ch: 0}, {line: row, ch: this.editor.getLine(row).length})
|
||||
}
|
||||
|
||||
replaceLines (startRow, endRow, lines) {
|
||||
endRow-- // because endRow is a first line after a table.
|
||||
const endRowCh = this.editor.getLine(endRow).length
|
||||
this.editor.replaceRange(lines, {line: startRow, ch: 0}, {line: endRow, ch: endRowCh})
|
||||
}
|
||||
|
||||
transact (func) {
|
||||
func()
|
||||
}
|
||||
}
|
||||
import { Point } from '@susisu/mte-kernel'
|
||||
|
||||
export default class TextEditorInterface {
|
||||
constructor (editor) {
|
||||
this.editor = editor
|
||||
this.doc = editor.getDoc()
|
||||
this.transaction = false
|
||||
}
|
||||
|
||||
getCursorPosition () {
|
||||
const { line, ch } = this.doc.getCursor()
|
||||
return new Point(line, ch)
|
||||
}
|
||||
|
||||
setCursorPosition (pos) {
|
||||
this.doc.setCursor({
|
||||
line: pos.row,
|
||||
ch: pos.column
|
||||
})
|
||||
}
|
||||
|
||||
setSelectionRange (range) {
|
||||
this.doc.setSelection(
|
||||
{ line: range.start.row, ch: range.start.column },
|
||||
{ line: range.end.row, ch: range.end.column }
|
||||
)
|
||||
}
|
||||
|
||||
getLastRow () {
|
||||
return this.doc.lineCount() - 1
|
||||
}
|
||||
|
||||
acceptsTableEdit () {
|
||||
return true
|
||||
}
|
||||
|
||||
getLine (row) {
|
||||
return this.doc.getLine(row)
|
||||
}
|
||||
|
||||
insertLine (row, line) {
|
||||
const lastRow = this.getLastRow()
|
||||
if (row > lastRow) {
|
||||
const lastLine = this.getLine(lastRow)
|
||||
this.doc.replaceRange(
|
||||
'\n' + line,
|
||||
{ line: lastRow, ch: lastLine.length },
|
||||
{ line: lastRow, ch: lastLine.length }
|
||||
)
|
||||
} else {
|
||||
this.doc.replaceRange(
|
||||
line + '\n',
|
||||
{ line: row, ch: 0 },
|
||||
{ line: row, ch: 0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
deleteLine (row) {
|
||||
const lastRow = this.getLastRow()
|
||||
if (row >= lastRow) {
|
||||
if (lastRow > 0) {
|
||||
const preLastLine = this.getLine(lastRow - 1)
|
||||
const lastLine = this.getLine(lastRow)
|
||||
this.doc.replaceRange(
|
||||
'',
|
||||
{ line: lastRow - 1, ch: preLastLine.length },
|
||||
{ line: lastRow, ch: lastLine.length }
|
||||
)
|
||||
} else {
|
||||
const lastLine = this.getLine(lastRow)
|
||||
this.doc.replaceRange(
|
||||
'',
|
||||
{ line: lastRow, ch: 0 },
|
||||
{ line: lastRow, ch: lastLine.length }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
this.doc.replaceRange(
|
||||
'',
|
||||
{ line: row, ch: 0 },
|
||||
{ line: row + 1, ch: 0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
replaceLines (startRow, endRow, lines) {
|
||||
const lastRow = this.getLastRow()
|
||||
if (endRow > lastRow) {
|
||||
const lastLine = this.getLine(lastRow)
|
||||
this.doc.replaceRange(
|
||||
lines.join('\n'),
|
||||
{ line: startRow, ch: 0 },
|
||||
{ line: lastRow, ch: lastLine.length }
|
||||
)
|
||||
} else {
|
||||
this.doc.replaceRange(
|
||||
lines.join('\n') + '\n',
|
||||
{ line: startRow, ch: 0 },
|
||||
{ line: endRow, ch: 0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
transact (func) {
|
||||
this.transaction = true
|
||||
func()
|
||||
this.transaction = false
|
||||
if (this.onDidFinishTransaction) {
|
||||
this.onDidFinishTransaction.call(undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,37 @@
|
||||
export function findNoteTitle (value) {
|
||||
export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleField = 'title') {
|
||||
const splitted = value.split('\n')
|
||||
let title = null
|
||||
let isInsideCodeBlock = false
|
||||
|
||||
splitted.some((line, index) => {
|
||||
const trimmedLine = line.trim()
|
||||
const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
|
||||
if (trimmedLine.match('```')) {
|
||||
isInsideCodeBlock = !isInsideCodeBlock
|
||||
if (splitted[0] === '---') {
|
||||
let line = 0
|
||||
while (++line < splitted.length) {
|
||||
if (enableFrontMatterTitle && splitted[line].startsWith(frontMatterTitleField + ':')) {
|
||||
title = splitted[line].substring(frontMatterTitleField.length + 1).trim()
|
||||
|
||||
break
|
||||
}
|
||||
if (splitted[line] === '---') {
|
||||
splitted.splice(0, line + 1)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) {
|
||||
title = trimmedLine
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (title === null) {
|
||||
splitted.some((line, index) => {
|
||||
const trimmedLine = line.trim()
|
||||
const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
|
||||
if (trimmedLine.match('```')) {
|
||||
isInsideCodeBlock = !isInsideCodeBlock
|
||||
}
|
||||
if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) {
|
||||
title = trimmedLine
|
||||
return true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (title === null) {
|
||||
title = ''
|
||||
|
||||
@@ -5,10 +5,10 @@ export function getTodoStatus (content) {
|
||||
|
||||
splitted.forEach((line) => {
|
||||
const trimmedLine = line.trim()
|
||||
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./)) {
|
||||
if (trimmedLine.match(/^[\+\-\*] \[(\s|x)\] ./i)) {
|
||||
numberOfTodo++
|
||||
}
|
||||
if (trimmedLine.match(/^[\+\-\*] \[x\] ./)) {
|
||||
if (trimmedLine.match(/^[\+\-\*] \[x\] ./i)) {
|
||||
numberOfCompletedTodo++
|
||||
}
|
||||
})
|
||||
|
||||
232
browser/lib/markdown-it-deflist.js
Normal file
232
browser/lib/markdown-it-deflist.js
Normal file
@@ -0,0 +1,232 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = function definitionListPlugin (md) {
|
||||
var isSpace = md.utils.isSpace
|
||||
|
||||
// Search `[:~][\n ]`, returns next pos after marker on success
|
||||
// or -1 on fail.
|
||||
function skipMarker (state, line) {
|
||||
let start = state.bMarks[line] + state.tShift[line]
|
||||
const max = state.eMarks[line]
|
||||
|
||||
if (start >= max) { return -1 }
|
||||
|
||||
// Check bullet
|
||||
const marker = state.src.charCodeAt(start++)
|
||||
if (marker !== 0x7E/* ~ */ && marker !== 0x3A/* : */) { return -1 }
|
||||
|
||||
const pos = state.skipSpaces(start)
|
||||
|
||||
// require space after ":"
|
||||
if (start === pos) { return -1 }
|
||||
|
||||
return start
|
||||
}
|
||||
|
||||
function markTightParagraphs (state, idx) {
|
||||
const level = state.level + 2
|
||||
|
||||
let i
|
||||
let l
|
||||
for (i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
|
||||
if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') {
|
||||
state.tokens[i + 2].hidden = true
|
||||
state.tokens[i].hidden = true
|
||||
i += 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deflist (state, startLine, endLine, silent) {
|
||||
var ch,
|
||||
contentStart,
|
||||
ddLine,
|
||||
dtLine,
|
||||
itemLines,
|
||||
listLines,
|
||||
listTokIdx,
|
||||
max,
|
||||
newEndLine,
|
||||
nextLine,
|
||||
offset,
|
||||
oldDDIndent,
|
||||
oldIndent,
|
||||
oldLineMax,
|
||||
oldParentType,
|
||||
oldSCount,
|
||||
oldTShift,
|
||||
oldTight,
|
||||
pos,
|
||||
prevEmptyEnd,
|
||||
tight,
|
||||
token
|
||||
|
||||
if (silent) {
|
||||
// quirk: validation mode validates a dd block only, not a whole deflist
|
||||
if (state.ddIndent < 0) { return false }
|
||||
return skipMarker(state, startLine) >= 0
|
||||
}
|
||||
|
||||
nextLine = startLine + 1
|
||||
if (nextLine >= endLine) { return false }
|
||||
|
||||
if (state.isEmpty(nextLine)) {
|
||||
nextLine++
|
||||
if (nextLine >= endLine) { return false }
|
||||
}
|
||||
|
||||
if (state.sCount[nextLine] < state.blkIndent) { return false }
|
||||
contentStart = skipMarker(state, nextLine)
|
||||
if (contentStart < 0) { return false }
|
||||
|
||||
// Start list
|
||||
listTokIdx = state.tokens.length
|
||||
tight = true
|
||||
|
||||
token = state.push('dl_open', 'dl', 1)
|
||||
token.map = listLines = [ startLine, 0 ]
|
||||
|
||||
//
|
||||
// Iterate list items
|
||||
//
|
||||
|
||||
dtLine = startLine
|
||||
ddLine = nextLine
|
||||
|
||||
// One definition list can contain multiple DTs,
|
||||
// and one DT can be followed by multiple DDs.
|
||||
//
|
||||
// Thus, there is two loops here, and label is
|
||||
// needed to break out of the second one
|
||||
//
|
||||
/* eslint no-labels:0,block-scoped-var:0 */
|
||||
OUTER:
|
||||
for (;;) {
|
||||
prevEmptyEnd = false
|
||||
|
||||
token = state.push('dt_open', 'dt', 1)
|
||||
token.map = [ dtLine, dtLine ]
|
||||
|
||||
token = state.push('inline', '', 0)
|
||||
token.map = [ dtLine, dtLine ]
|
||||
token.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim()
|
||||
token.children = []
|
||||
|
||||
token = state.push('dt_close', 'dt', -1)
|
||||
|
||||
for (;;) {
|
||||
token = state.push('dd_open', 'dd', 1)
|
||||
token.map = itemLines = [ ddLine, 0 ]
|
||||
|
||||
pos = contentStart
|
||||
max = state.eMarks[ddLine]
|
||||
offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine])
|
||||
|
||||
while (pos < max) {
|
||||
ch = state.src.charCodeAt(pos)
|
||||
|
||||
if (isSpace(ch)) {
|
||||
if (ch === 0x09) {
|
||||
offset += 4 - offset % 4
|
||||
} else {
|
||||
offset++
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
pos++
|
||||
}
|
||||
|
||||
contentStart = pos
|
||||
|
||||
oldTight = state.tight
|
||||
oldDDIndent = state.ddIndent
|
||||
oldIndent = state.blkIndent
|
||||
oldTShift = state.tShift[ddLine]
|
||||
oldSCount = state.sCount[ddLine]
|
||||
oldParentType = state.parentType
|
||||
state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2
|
||||
state.tShift[ddLine] = contentStart - state.bMarks[ddLine]
|
||||
state.sCount[ddLine] = offset
|
||||
state.tight = true
|
||||
state.parentType = 'deflist'
|
||||
|
||||
newEndLine = ddLine
|
||||
while (++newEndLine < endLine && (state.sCount[newEndLine] >= state.sCount[ddLine] || state.isEmpty(newEndLine))) {
|
||||
}
|
||||
|
||||
oldLineMax = state.lineMax
|
||||
state.lineMax = newEndLine
|
||||
|
||||
state.md.block.tokenize(state, ddLine, newEndLine, true)
|
||||
|
||||
state.lineMax = oldLineMax
|
||||
|
||||
// If any of list item is tight, mark list as tight
|
||||
if (!state.tight || prevEmptyEnd) {
|
||||
tight = false
|
||||
}
|
||||
// Item become loose if finish with empty line,
|
||||
// but we should filter last element, because it means list finish
|
||||
prevEmptyEnd = (state.line - ddLine) > 1 && state.isEmpty(state.line - 1)
|
||||
|
||||
state.tShift[ddLine] = oldTShift
|
||||
state.sCount[ddLine] = oldSCount
|
||||
state.tight = oldTight
|
||||
state.parentType = oldParentType
|
||||
state.blkIndent = oldIndent
|
||||
state.ddIndent = oldDDIndent
|
||||
|
||||
token = state.push('dd_close', 'dd', -1)
|
||||
|
||||
itemLines[1] = nextLine = state.line
|
||||
|
||||
if (nextLine >= endLine) { break OUTER }
|
||||
|
||||
if (state.sCount[nextLine] < state.blkIndent) { break OUTER }
|
||||
contentStart = skipMarker(state, nextLine)
|
||||
if (contentStart < 0) { break }
|
||||
|
||||
ddLine = nextLine
|
||||
|
||||
// go to the next loop iteration:
|
||||
// insert DD tag and repeat checking
|
||||
}
|
||||
|
||||
if (nextLine >= endLine) { break }
|
||||
dtLine = nextLine
|
||||
|
||||
if (state.isEmpty(dtLine)) { break }
|
||||
if (state.sCount[dtLine] < state.blkIndent) { break }
|
||||
|
||||
ddLine = dtLine + 1
|
||||
if (ddLine >= endLine) { break }
|
||||
if (state.isEmpty(ddLine)) { ddLine++ }
|
||||
if (ddLine >= endLine) { break }
|
||||
|
||||
if (state.sCount[ddLine] < state.blkIndent) { break }
|
||||
contentStart = skipMarker(state, ddLine)
|
||||
if (contentStart < 0) { break }
|
||||
|
||||
// go to the next loop iteration:
|
||||
// insert DT and DD tags and repeat checking
|
||||
}
|
||||
|
||||
// Finilize list
|
||||
token = state.push('dl_close', 'dl', -1)
|
||||
|
||||
listLines[1] = nextLine
|
||||
|
||||
state.line = nextLine
|
||||
|
||||
// mark paragraphs tight if needed
|
||||
if (tight) {
|
||||
markTightParagraphs(state, listTokIdx)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
md.block.ruler.before('paragraph', 'deflist', deflist, { alt: [ 'paragraph', 'reference' ] })
|
||||
}
|
||||
132
browser/lib/markdown-it-fence.js
Normal file
132
browser/lib/markdown-it-fence.js
Normal file
@@ -0,0 +1,132 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = function (md, renderers, defaultRenderer) {
|
||||
const paramsRE = /^[ \t]*([\w+#-]+)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/
|
||||
|
||||
function fence (state, startLine, endLine) {
|
||||
let pos = state.bMarks[startLine] + state.tShift[startLine]
|
||||
let max = state.eMarks[startLine]
|
||||
|
||||
if (state.sCount[startLine] - state.blkIndent >= 4 || pos + 3 > max) {
|
||||
return false
|
||||
}
|
||||
|
||||
const marker = state.src.charCodeAt(pos)
|
||||
if (!(marker === 96 || marker === 126)) {
|
||||
return false
|
||||
}
|
||||
|
||||
let mem = pos
|
||||
pos = state.skipChars(pos, marker)
|
||||
|
||||
let len = pos - mem
|
||||
if (len < 3) {
|
||||
return false
|
||||
}
|
||||
|
||||
const markup = state.src.slice(mem, pos)
|
||||
const params = state.src.slice(pos, max)
|
||||
|
||||
let nextLine = startLine
|
||||
let haveEndMarker = false
|
||||
|
||||
while (true) {
|
||||
nextLine++
|
||||
if (nextLine >= endLine) {
|
||||
break
|
||||
}
|
||||
|
||||
pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]
|
||||
max = state.eMarks[nextLine]
|
||||
|
||||
if (pos < max && state.sCount[nextLine] < state.blkIndent) {
|
||||
break
|
||||
}
|
||||
if (state.src.charCodeAt(pos) !== marker || state.sCount[nextLine] - state.blkIndent >= 4) {
|
||||
continue
|
||||
}
|
||||
|
||||
pos = state.skipChars(pos, marker)
|
||||
|
||||
if (pos - mem < len) {
|
||||
continue
|
||||
}
|
||||
|
||||
pos = state.skipSpaces(pos)
|
||||
|
||||
if (pos >= max) {
|
||||
haveEndMarker = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
len = state.sCount[startLine]
|
||||
state.line = nextLine + (haveEndMarker ? 1 : 0)
|
||||
|
||||
const parameters = {}
|
||||
let langType = ''
|
||||
let fileName = ''
|
||||
let firstLineNumber = 1
|
||||
|
||||
let match = paramsRE.exec(params)
|
||||
if (match) {
|
||||
if (match[1]) {
|
||||
langType = match[1]
|
||||
}
|
||||
if (match[3]) {
|
||||
fileName = match[3]
|
||||
}
|
||||
if (match[4]) {
|
||||
firstLineNumber = parseInt(match[4], 10)
|
||||
}
|
||||
|
||||
if (match[2]) {
|
||||
const params = match[2]
|
||||
const regex = /(\w[-\w]*)(?:=(?:'(.*?[^\\])?'|"(.*?[^\\])?"|([^'"][^\s]*)))?/g
|
||||
|
||||
let name, value
|
||||
while ((match = regex.exec(params))) {
|
||||
name = match[1]
|
||||
value = match[2] || match[3] || match[4] || null
|
||||
|
||||
const height = /^(\d+)h$/.exec(name)
|
||||
if (height && !value) {
|
||||
parameters.height = height[1]
|
||||
} else {
|
||||
parameters[name] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let token
|
||||
if (renderers[langType]) {
|
||||
token = state.push(`${langType}_fence`, 'div', 0)
|
||||
} else {
|
||||
token = state.push('_fence', 'code', 0)
|
||||
}
|
||||
|
||||
token.langType = langType
|
||||
token.fileName = fileName
|
||||
token.firstLineNumber = firstLineNumber
|
||||
token.parameters = parameters
|
||||
|
||||
token.content = state.getLines(startLine + 1, nextLine, len, true)
|
||||
token.markup = markup
|
||||
token.map = [startLine, state.line]
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
md.block.ruler.before('fence', '_fence', fence, {
|
||||
alt: ['paragraph', 'reference', 'blockquote', 'list']
|
||||
})
|
||||
|
||||
for (const name in renderers) {
|
||||
md.renderer.rules[`${name}_fence`] = (tokens, index) => renderers[name](tokens[index])
|
||||
}
|
||||
|
||||
if (defaultRenderer) {
|
||||
md.renderer.rules['_fence'] = (tokens, index) => defaultRenderer(tokens[index])
|
||||
}
|
||||
}
|
||||
24
browser/lib/markdown-it-frontmatter.js
Normal file
24
browser/lib/markdown-it-frontmatter.js
Normal file
@@ -0,0 +1,24 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = function frontMatterPlugin (md) {
|
||||
function frontmatter (state, startLine, endLine, silent) {
|
||||
if (startLine !== 0 || state.src.substr(startLine, state.eMarks[0]) !== '---') {
|
||||
return false
|
||||
}
|
||||
|
||||
let line = 0
|
||||
while (++line < state.lineMax) {
|
||||
if (state.src.substring(state.bMarks[line], state.eMarks[line]) === '---') {
|
||||
state.line = line + 1
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
md.block.ruler.before('table', 'frontmatter', frontmatter, {
|
||||
alt: [ 'paragraph', 'reference', 'blockquote', 'list' ]
|
||||
})
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
'use strict'
|
||||
|
||||
import sanitizeHtml from 'sanitize-html'
|
||||
import { escapeHtmlCharacters } from './utils'
|
||||
import url from 'url'
|
||||
|
||||
module.exports = function sanitizePlugin (md, options) {
|
||||
options = options || {}
|
||||
@@ -8,19 +10,115 @@ module.exports = function sanitizePlugin (md, options) {
|
||||
md.core.ruler.after('linkify', 'sanitize_inline', state => {
|
||||
for (let tokenIdx = 0; tokenIdx < state.tokens.length; tokenIdx++) {
|
||||
if (state.tokens[tokenIdx].type === 'html_block') {
|
||||
state.tokens[tokenIdx].content = sanitizeHtml(state.tokens[tokenIdx].content, options)
|
||||
state.tokens[tokenIdx].content = sanitizeHtml(
|
||||
state.tokens[tokenIdx].content,
|
||||
options
|
||||
)
|
||||
}
|
||||
if (state.tokens[tokenIdx].type === 'fence') {
|
||||
state.tokens[tokenIdx].content = state.tokens[tokenIdx].content.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
|
||||
if (state.tokens[tokenIdx].type === '_fence') {
|
||||
// escapeHtmlCharacters has better performance
|
||||
state.tokens[tokenIdx].content = escapeHtmlCharacters(
|
||||
state.tokens[tokenIdx].content,
|
||||
{ skipSingleQuote: true }
|
||||
)
|
||||
}
|
||||
if (state.tokens[tokenIdx].type === 'inline') {
|
||||
const inlineTokens = state.tokens[tokenIdx].children
|
||||
for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
|
||||
if (inlineTokens[childIdx].type === 'html_inline') {
|
||||
inlineTokens[childIdx].content = sanitizeHtml(inlineTokens[childIdx].content, options)
|
||||
inlineTokens[childIdx].content = sanitizeInline(
|
||||
inlineTokens[childIdx].content,
|
||||
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 attributesRegex = /([A-Z][A-Z0-9]*)(?:=("|')([^\2]+?)\2)?/ig
|
||||
|
||||
function sanitizeInline (html, options) {
|
||||
let match = tagRegex.exec(html)
|
||||
if (!match) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const { allowedTags, allowedAttributes, selfClosing, allowedSchemesAppliedToAttributes } = options
|
||||
|
||||
if (match[1] !== undefined) {
|
||||
// opening tag
|
||||
const tag = match[1].toLowerCase()
|
||||
if (allowedTags.indexOf(tag) === -1) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const attributes = match[2]
|
||||
|
||||
let attrs = ''
|
||||
let name
|
||||
let value
|
||||
|
||||
while ((match = attributesRegex.exec(attributes))) {
|
||||
name = match[1].toLowerCase()
|
||||
value = match[3]
|
||||
|
||||
if (allowedAttributes['*'].indexOf(name) !== -1 || (allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)) {
|
||||
if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) {
|
||||
if (naughtyHRef(value, options) || (tag === 'iframe' && name === 'src' && naughtyIFrame(value, options))) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
attrs += ` ${name}`
|
||||
if (match[2]) {
|
||||
attrs += `="${value}"`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selfClosing.indexOf(tag) === -1) {
|
||||
return '<' + tag + attrs + '>'
|
||||
} else {
|
||||
return '<' + tag + attrs + ' />'
|
||||
}
|
||||
} else {
|
||||
// closing tag
|
||||
if (allowedTags.indexOf(match[4].toLowerCase()) !== -1) {
|
||||
return html
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function naughtyHRef (href, options) {
|
||||
// href = href.replace(/[\x00-\x20]+/g, '')
|
||||
href = href.replace(/<\!\-\-.*?\-\-\>/g, '')
|
||||
|
||||
const matches = href.match(/^([a-zA-Z]+)\:/)
|
||||
if (!matches) {
|
||||
if (href.match(/^[\/\\]{2}/)) {
|
||||
return !options.allowProtocolRelative
|
||||
}
|
||||
|
||||
// No scheme
|
||||
return false
|
||||
}
|
||||
|
||||
const scheme = matches[1].toLowerCase()
|
||||
|
||||
return options.allowedSchemes.indexOf(scheme) === -1
|
||||
}
|
||||
|
||||
function naughtyIFrame (src, options) {
|
||||
try {
|
||||
const parsed = url.parse(src, false, true)
|
||||
|
||||
return options.allowedIframeHostnames.index(parsed.hostname) === -1
|
||||
} catch (e) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
100
browser/lib/markdown-toc-generator.js
Normal file
100
browser/lib/markdown-toc-generator.js
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @fileoverview Markdown table of contents generator
|
||||
*/
|
||||
|
||||
import toc from 'markdown-toc'
|
||||
import diacritics from 'diacritics-map'
|
||||
import stripColor from 'strip-color'
|
||||
|
||||
const EOL = require('os').EOL
|
||||
|
||||
/**
|
||||
* @caseSensitiveSlugify Custom slugify function
|
||||
* Same implementation that the original used by markdown-toc (node_modules/markdown-toc/lib/utils.js),
|
||||
* but keeps original case to properly handle https://github.com/BoostIO/Boostnote/issues/2067
|
||||
*/
|
||||
function caseSensitiveSlugify (str) {
|
||||
function replaceDiacritics (str) {
|
||||
return str.replace(/[À-ž]/g, function (ch) {
|
||||
return diacritics[ch] || ch
|
||||
})
|
||||
}
|
||||
|
||||
function getTitle (str) {
|
||||
if (/^\[[^\]]+\]\(/.test(str)) {
|
||||
var m = /^\[([^\]]+)\]/.exec(str)
|
||||
if (m) return m[1]
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
str = getTitle(str)
|
||||
str = stripColor(str)
|
||||
// str = str.toLowerCase() //let's be case sensitive
|
||||
|
||||
// `.split()` is often (but not always) faster than `.replace()`
|
||||
str = str.split(' ').join('-')
|
||||
str = str.split(/\t/).join('--')
|
||||
str = str.split(/<\/?[^>]+>/).join('')
|
||||
str = str.split(/[|$&`~=\\\/@+*!?({[\]})<>=.,;:'"^]/).join('')
|
||||
str = str.split(/[。?!,、;:“”【】()〔〕[]﹃﹄“ ”‘’﹁﹂—…-~《》〈〉「」]/).join('')
|
||||
str = replaceDiacritics(str)
|
||||
return str
|
||||
}
|
||||
|
||||
const TOC_MARKER_START = '<!-- toc -->'
|
||||
const TOC_MARKER_END = '<!-- tocstop -->'
|
||||
|
||||
/**
|
||||
* Takes care of proper updating given editor with TOC.
|
||||
* If TOC doesn't exit in the editor, it's inserted at current caret position.
|
||||
* Otherwise,TOC is updated in place.
|
||||
* @param editor CodeMirror editor to be updated with TOC
|
||||
*/
|
||||
export function generateInEditor (editor) {
|
||||
const tocRegex = new RegExp(`${TOC_MARKER_START}[\\s\\S]*?${TOC_MARKER_END}`)
|
||||
|
||||
function tocExistsInEditor () {
|
||||
return tocRegex.test(editor.getValue())
|
||||
}
|
||||
|
||||
function updateExistingToc () {
|
||||
const toc = generate(editor.getValue())
|
||||
const search = editor.getSearchCursor(tocRegex)
|
||||
while (search.findNext()) {
|
||||
search.replace(toc)
|
||||
}
|
||||
}
|
||||
|
||||
function addTocAtCursorPosition () {
|
||||
const toc = generate(editor.getRange(editor.getCursor(), {line: Infinity}))
|
||||
editor.replaceRange(wrapTocWithEol(toc, editor), editor.getCursor())
|
||||
}
|
||||
|
||||
if (tocExistsInEditor()) {
|
||||
updateExistingToc()
|
||||
} else {
|
||||
addTocAtCursorPosition()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates MD TOC based on MD document passed as string.
|
||||
* @param markdownText MD document
|
||||
* @returns generatedTOC String containing generated TOC
|
||||
*/
|
||||
export function generate (markdownText) {
|
||||
const generatedToc = toc(markdownText, {slugify: caseSensitiveSlugify})
|
||||
return TOC_MARKER_START + EOL + EOL + generatedToc.content + EOL + EOL + TOC_MARKER_END
|
||||
}
|
||||
|
||||
function wrapTocWithEol (toc, editor) {
|
||||
const leftWrap = editor.getCursor().ch === 0 ? '' : EOL
|
||||
const rightWrap = editor.getLine(editor.getCursor().line).length === editor.getCursor().ch ? '' : EOL
|
||||
return leftWrap + toc + rightWrap
|
||||
}
|
||||
|
||||
export default {
|
||||
generate,
|
||||
generateInEditor
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import _ from 'lodash'
|
||||
import ConfigManager from 'browser/main/lib/ConfigManager'
|
||||
import katex from 'katex'
|
||||
import { lastFindInArray } from './utils'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
|
||||
function createGutter (str, firstLineNumber) {
|
||||
if (Number.isNaN(firstLineNumber)) firstLineNumber = 1
|
||||
@@ -27,32 +28,6 @@ class Markdown {
|
||||
html: true,
|
||||
xhtmlOut: true,
|
||||
breaks: config.preview.breaks,
|
||||
highlight: function (str, lang) {
|
||||
const delimiter = ':'
|
||||
const langInfo = lang.split(delimiter)
|
||||
const langType = langInfo[0]
|
||||
const fileName = langInfo[1] || ''
|
||||
const firstLineNumber = parseInt(langInfo[2], 10)
|
||||
|
||||
if (langType === 'flowchart') {
|
||||
return `<pre class="flowchart">${str}</pre>`
|
||||
}
|
||||
if (langType === 'sequence') {
|
||||
return `<pre class="sequence">${str}</pre>`
|
||||
}
|
||||
if (langType === 'chart') {
|
||||
return `<pre class="chart">${str}</pre>`
|
||||
}
|
||||
if (langType === 'mermaid') {
|
||||
return `<pre class="mermaid">${str}</pre>`
|
||||
}
|
||||
return '<pre class="code CodeMirror">' +
|
||||
'<span class="filename">' + fileName + '</span>' +
|
||||
createGutter(str, firstLineNumber) +
|
||||
'<code class="' + langType + '">' +
|
||||
str +
|
||||
'</code></pre>'
|
||||
},
|
||||
sanitize: 'STRICT'
|
||||
}
|
||||
|
||||
@@ -105,7 +80,11 @@ class Markdown {
|
||||
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
|
||||
'input': ['type', 'id', 'checked']
|
||||
},
|
||||
allowedIframeHostnames: ['www.youtube.com']
|
||||
allowedIframeHostnames: ['www.youtube.com'],
|
||||
selfClosing: [ 'img', 'br', 'hr', 'input' ],
|
||||
allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
|
||||
allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ],
|
||||
allowProtocolRelative: true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -148,7 +127,49 @@ class Markdown {
|
||||
}
|
||||
})
|
||||
this.md.use(require('markdown-it-kbd'))
|
||||
this.md.use(require('markdown-it-admonition'))
|
||||
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']})
|
||||
this.md.use(require('markdown-it-abbr'))
|
||||
this.md.use(require('markdown-it-sub'))
|
||||
this.md.use(require('markdown-it-sup'))
|
||||
this.md.use(require('./markdown-it-deflist'))
|
||||
this.md.use(require('./markdown-it-frontmatter'))
|
||||
|
||||
this.md.use(require('./markdown-it-fence'), {
|
||||
chart: token => {
|
||||
if (token.parameters.hasOwnProperty('yaml')) {
|
||||
token.parameters.format = 'yaml'
|
||||
}
|
||||
|
||||
return `<pre class="fence" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="chart" data-height="${token.parameters.height}" data-format="${token.parameters.format || 'json'}">${token.content}</div>
|
||||
</pre>`
|
||||
},
|
||||
flowchart: token => {
|
||||
return `<pre class="fence" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="flowchart" data-height="${token.parameters.height}">${token.content}</div>
|
||||
</pre>`
|
||||
},
|
||||
mermaid: token => {
|
||||
return `<pre class="fence" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="mermaid" data-height="${token.parameters.height}">${token.content}</div>
|
||||
</pre>`
|
||||
},
|
||||
sequence: token => {
|
||||
return `<pre class="fence" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
<div class="sequence" data-height="${token.parameters.height}">${token.content}</div>
|
||||
</pre>`
|
||||
}
|
||||
}, token => {
|
||||
return `<pre class="code CodeMirror" data-line="${token.map[0]}">
|
||||
<span class="filename">${token.fileName}</span>
|
||||
${createGutter(token.content, token.firstLineNumber)}
|
||||
<code class="${token.langType}">${token.content}</code>
|
||||
</pre>`
|
||||
})
|
||||
|
||||
const deflate = require('markdown-it-plantuml/lib/deflate')
|
||||
this.md.use(require('markdown-it-plantuml'), '', {
|
||||
@@ -163,6 +184,22 @@ class Markdown {
|
||||
}
|
||||
})
|
||||
|
||||
// Ditaa support
|
||||
this.md.use(require('markdown-it-plantuml'), {
|
||||
openMarker: '@startditaa',
|
||||
closeMarker: '@endditaa',
|
||||
generateSource: function (umlCode) {
|
||||
const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
|
||||
// Currently PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment.
|
||||
const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/png'
|
||||
const s = unescape(encodeURIComponent(umlCode))
|
||||
const zippedCode = deflate.encode64(
|
||||
deflate.zip_deflate(`@startditaa\n${s}\n@endditaa`, 9)
|
||||
)
|
||||
return `${serverAddress}/${zippedCode}`
|
||||
}
|
||||
})
|
||||
|
||||
// Override task item
|
||||
this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
|
||||
let content, terminate, i, l, token
|
||||
@@ -205,7 +242,11 @@ class Markdown {
|
||||
if (!liToken.attrs) {
|
||||
liToken.attrs = []
|
||||
}
|
||||
liToken.attrs.push(['class', 'taskListItem'])
|
||||
if (config.preview.lineThroughCheckbox) {
|
||||
liToken.attrs.push(['class', `taskListItem${match[1] !== ' ' ? ' checked' : ''}`])
|
||||
} else {
|
||||
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>`
|
||||
}
|
||||
@@ -230,9 +271,12 @@ class Markdown {
|
||||
this.md.renderer.render = (tokens, options, env) => {
|
||||
tokens.forEach((token) => {
|
||||
switch (token.type) {
|
||||
case 'heading_open':
|
||||
case 'paragraph_open':
|
||||
case 'blockquote_open':
|
||||
case 'dd_open':
|
||||
case 'dt_open':
|
||||
case 'heading_open':
|
||||
case 'list_item_open':
|
||||
case 'paragraph_open':
|
||||
case 'table_open':
|
||||
token.attrPush(['data-line', token.map[0]])
|
||||
}
|
||||
|
||||
62
browser/lib/newNote.js
Normal file
62
browser/lib/newNote.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { hashHistory } from 'react-router'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
|
||||
export function createMarkdownNote (storage, folder, dispatch, location) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||
return dataApi
|
||||
.createNote(storage, {
|
||||
type: 'MARKDOWN_NOTE',
|
||||
folder: folder,
|
||||
title: '',
|
||||
content: ''
|
||||
})
|
||||
.then(note => {
|
||||
const noteHash = note.key
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: { key: noteHash }
|
||||
})
|
||||
ee.emit('list:jump', noteHash)
|
||||
ee.emit('detail:focus')
|
||||
})
|
||||
}
|
||||
|
||||
export function createSnippetNote (storage, folder, dispatch, location, config) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||
return dataApi
|
||||
.createNote(storage, {
|
||||
type: 'SNIPPET_NOTE',
|
||||
folder: folder,
|
||||
title: '',
|
||||
description: '',
|
||||
snippets: [
|
||||
{
|
||||
name: '',
|
||||
mode: config.editor.snippetDefaultLanguage || 'text',
|
||||
content: ''
|
||||
}
|
||||
]
|
||||
})
|
||||
.then(note => {
|
||||
const noteHash = note.key
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: { key: noteHash }
|
||||
})
|
||||
ee.emit('list:jump', noteHash)
|
||||
ee.emit('detail:focus')
|
||||
})
|
||||
}
|
||||
@@ -23,7 +23,9 @@ function findByWordOrTag (notes, block) {
|
||||
return true
|
||||
}
|
||||
if (note.type === 'SNIPPET_NOTE') {
|
||||
return note.description.match(wordRegExp)
|
||||
return note.description.match(wordRegExp) || note.snippets.some((snippet) => {
|
||||
return snippet.name.match(wordRegExp) || snippet.content.match(wordRegExp)
|
||||
})
|
||||
} else if (note.type === 'MARKDOWN_NOTE') {
|
||||
return note.content.match(wordRegExp)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,12 @@ export function lastFindInArray (array, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
export function escapeHtmlCharacters (html, opt = { detectCodeBlock: false }) {
|
||||
export function escapeHtmlCharacters (
|
||||
html,
|
||||
opt = { detectCodeBlock: false, skipSingleQuote: false }
|
||||
) {
|
||||
const matchHtmlRegExp = /["'&<>]/g
|
||||
const matchCodeBlockRegExp = /```/g
|
||||
const escapes = ['"', '&', ''', '<', '>']
|
||||
let match = null
|
||||
const replaceAt = (str, index, replace) =>
|
||||
@@ -15,11 +19,18 @@ export function escapeHtmlCharacters (html, opt = { detectCodeBlock: false }) {
|
||||
replace +
|
||||
str.substr(index + replace.length - (replace.length - 1))
|
||||
|
||||
// detecting code block
|
||||
while ((match = matchHtmlRegExp.exec(html)) != null) {
|
||||
while ((match = matchHtmlRegExp.exec(html)) !== null) {
|
||||
const current = { char: match[0], index: match.index }
|
||||
const codeBlockIndexs = []
|
||||
let openCodeBlock = null
|
||||
// if the detectCodeBlock option is activated then this function should skip
|
||||
// characters that needed to be escape but located in code block
|
||||
if (opt.detectCodeBlock) {
|
||||
// position of the nearest line start
|
||||
// The first type of code block is lines that start with 4 spaces
|
||||
// Here we check for the \n character located before the character that
|
||||
// needed to be escape. It means we check for the begining of the line that
|
||||
// contain that character, then we check if there are 4 spaces next to the
|
||||
// \n character (the line start with 4 spaces)
|
||||
let previousLineEnd = current.index - 1
|
||||
while (html[previousLineEnd] !== '\n' && previousLineEnd !== -1) {
|
||||
previousLineEnd--
|
||||
@@ -31,16 +42,54 @@ export function escapeHtmlCharacters (html, opt = { detectCodeBlock: false }) {
|
||||
html[previousLineEnd + 3] === ' ' &&
|
||||
html[previousLineEnd + 4] === ' '
|
||||
) {
|
||||
// so skip it
|
||||
// skip the current character
|
||||
continue
|
||||
}
|
||||
// The second type of code block is lines that wrapped in ```
|
||||
// We will get the position of each ```
|
||||
// then push it into an array
|
||||
// then the array returned will be like this:
|
||||
// [startCodeblock, endCodeBlock, startCodeBlock, endCodeBlock]
|
||||
while ((openCodeBlock = matchCodeBlockRegExp.exec(html)) !== null) {
|
||||
codeBlockIndexs.push(openCodeBlock.index)
|
||||
}
|
||||
let shouldSkipChar = false
|
||||
// we loop through the array of positions
|
||||
// we skip 2 element as the i index position is the position of ``` that
|
||||
// open the codeblock and the i + 1 is the position of the ``` that close
|
||||
// the code block
|
||||
for (let i = 0; i < codeBlockIndexs.length; i += 2) {
|
||||
// the i index position is the position of the ``` that open code block
|
||||
// so we have to + 2 as that position is the position of the first ` in the ````
|
||||
// but we need to make sure that the position current character is larger
|
||||
// that the last ` in the ``` that open the code block so we have to take
|
||||
// the position of the first ` and + 2
|
||||
// the i + 1 index position is the closing ``` so the char must less than it
|
||||
if (
|
||||
current.index > codeBlockIndexs[i] + 2 &&
|
||||
current.index < codeBlockIndexs[i + 1]
|
||||
) {
|
||||
// skip it
|
||||
shouldSkipChar = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (shouldSkipChar) {
|
||||
// skip the current character
|
||||
continue
|
||||
}
|
||||
}
|
||||
// otherwise, escape it !!!
|
||||
if (current.char === '&') {
|
||||
// when escaping character & we have to be becareful as the & could be a part
|
||||
// of an escaped character like " will be came &quot;
|
||||
let nextStr = ''
|
||||
let nextIndex = current.index
|
||||
let escapedStr = false
|
||||
// maximum length of an escape string is 5. For example ('"')
|
||||
// maximum length of an escaped string is 5. For example ('"')
|
||||
// we take the next 5 character of the next string if it is one of the string:
|
||||
// ['"', '&', ''', '<', '>'] then we will not escape the & character
|
||||
// as it is a part of the escaped string and should not be escaped
|
||||
while (nextStr.length <= 5) {
|
||||
nextStr += html[nextIndex]
|
||||
nextIndex++
|
||||
@@ -55,7 +104,7 @@ export function escapeHtmlCharacters (html, opt = { detectCodeBlock: false }) {
|
||||
}
|
||||
} else if (current.char === '"') {
|
||||
html = replaceAt(html, current.index, '"')
|
||||
} else if (current.char === "'") {
|
||||
} else if (current.char === "'" && !opt.skipSingleQuote) {
|
||||
html = replaceAt(html, current.index, ''')
|
||||
} else if (current.char === '<') {
|
||||
html = replaceAt(html, current.index, '<')
|
||||
|
||||
@@ -23,7 +23,7 @@ body[data-theme="dark"]
|
||||
border-left 1px solid $ui-dark-borderColor
|
||||
.empty-message
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
@@ -37,3 +37,10 @@ body[data-theme="monokai"]
|
||||
border-left 1px solid $ui-monokai-borderColor
|
||||
.empty-message
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
border-left 1px solid $ui-dracula-borderColor
|
||||
.empty-message
|
||||
color $ui-dracula-text-color
|
||||
@@ -36,7 +36,7 @@
|
||||
height 34px
|
||||
width 20px
|
||||
line-height 34px
|
||||
|
||||
|
||||
.search-input
|
||||
vertical-align middle
|
||||
position relative
|
||||
@@ -71,7 +71,7 @@
|
||||
overflow ellipsis
|
||||
cursor pointer
|
||||
&:hover
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
background-color $ui-button--hover-backgroundColor
|
||||
|
||||
.search-optionList-item--active
|
||||
@extend .search-optionList-item
|
||||
@@ -159,3 +159,29 @@ body[data-theme="monokai"]
|
||||
color $ui-monokai-button--active-color
|
||||
.search-optionList-item-name-surfix
|
||||
color $ui-monokai-inactive-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
color #f8f8f2
|
||||
background-color $ui-dark-button--hover-backgroundColor
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.search-optionList
|
||||
color #f8f8f2
|
||||
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
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
.control-infoButton-panel-trash
|
||||
z-index 200
|
||||
margin-top 0px
|
||||
top 50px
|
||||
right 0px
|
||||
position absolute
|
||||
padding 20px 25px 0 25px
|
||||
@@ -256,3 +257,43 @@ body[data-theme="monokai"]
|
||||
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
|
||||
@@ -29,6 +29,7 @@ import { formatDate } from 'browser/lib/date-formatter'
|
||||
import { getTodoPercentageOfCompleted } from 'browser/lib/getTodoStatus'
|
||||
import striptags from 'striptags'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
import markdownToc from 'browser/lib/markdown-toc-generator'
|
||||
|
||||
class MarkdownNoteDetail extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -47,6 +48,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
this.dispatchTimer = null
|
||||
|
||||
this.toggleLockButton = this.handleToggleLockButton.bind(this)
|
||||
this.generateToc = () => this.handleGenerateToc()
|
||||
}
|
||||
|
||||
focus () {
|
||||
@@ -59,10 +61,14 @@ class MarkdownNoteDetail extends React.Component {
|
||||
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
|
||||
this.handleSwitchMode(reversedType)
|
||||
})
|
||||
ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
|
||||
ee.on('code:generate-toc', this.generateToc)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.note.key !== this.props.note.key && !this.state.isMovingNote) {
|
||||
const isNewNote = nextProps.note.key !== this.props.note.key
|
||||
const hasDeletedTags = nextProps.note.tags.length < this.props.note.tags.length
|
||||
if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
this.setState({
|
||||
note: Object.assign({}, nextProps.note)
|
||||
@@ -75,6 +81,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
|
||||
componentWillUnmount () {
|
||||
ee.off('topbar:togglelockbutton', this.toggleLockButton)
|
||||
ee.off('code:generate-toc', this.generateToc)
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
}
|
||||
|
||||
@@ -87,7 +94,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
handleUpdateContent () {
|
||||
const { note } = this.state
|
||||
note.content = this.refs.content.value
|
||||
note.title = markdown.strip(striptags(findNoteTitle(note.content)))
|
||||
note.title = markdown.strip(striptags(findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField)))
|
||||
this.updateNote(note)
|
||||
}
|
||||
|
||||
@@ -262,6 +269,11 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleGenerateToc () {
|
||||
const editor = this.refs.content.refs.code.editor
|
||||
markdownToc.generateInEditor(editor)
|
||||
}
|
||||
|
||||
handleFocus (e) {
|
||||
this.focus()
|
||||
}
|
||||
@@ -284,9 +296,33 @@ class MarkdownNoteDetail extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleDeleteNote () {
|
||||
this.handleTrashButtonClick()
|
||||
}
|
||||
|
||||
handleClearTodo () {
|
||||
const { note } = this.state
|
||||
const splitted = note.content.split('\n')
|
||||
|
||||
const clearTodoContent = splitted.map((line) => {
|
||||
const trimmedLine = line.trim()
|
||||
if (trimmedLine.match(/\[x\]/i)) {
|
||||
return line.replace(/\[x\]/i, '[ ]')
|
||||
} else {
|
||||
return line
|
||||
}
|
||||
}).join('\n')
|
||||
|
||||
note.content = clearTodoContent
|
||||
this.refs.content.setValue(note.content)
|
||||
|
||||
this.updateNote(note)
|
||||
}
|
||||
|
||||
renderEditor () {
|
||||
const { config, ignorePreviewPointerEvents } = this.props
|
||||
const { note } = this.state
|
||||
|
||||
if (this.state.editorType === 'EDITOR_PREVIEW') {
|
||||
return <MarkdownEditor
|
||||
ref='content'
|
||||
@@ -312,7 +348,7 @@ class MarkdownNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { data, location } = this.props
|
||||
const { data, location, config } = this.props
|
||||
const { note, editorType } = this.state
|
||||
const storageKey = note.storage
|
||||
const folderKey = note.folder
|
||||
@@ -363,9 +399,12 @@ class MarkdownNoteDetail extends React.Component {
|
||||
<TagSelect
|
||||
ref='tags'
|
||||
value={this.state.note.tags}
|
||||
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||
data={data}
|
||||
onChange={this.handleUpdateTag.bind(this)}
|
||||
/>
|
||||
<TodoListPercentage percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
||||
<TodoListPercentage onClearCheckboxClick={(e) => this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
|
||||
</div>
|
||||
<div styleName='info-right'>
|
||||
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
|
||||
|
||||
@@ -76,3 +76,8 @@ body[data-theme="monokai"]
|
||||
.root
|
||||
border-left 1px solid $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
border-left 1px solid $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
@@ -13,6 +13,7 @@ $info-margin-under-border = 30px
|
||||
display flex
|
||||
align-items center
|
||||
padding 0 20px
|
||||
z-index 99
|
||||
|
||||
.info-left
|
||||
padding 0 10px
|
||||
@@ -97,8 +98,13 @@ body[data-theme="solarized-dark"]
|
||||
.info
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-noteDetail-backgroundColor
|
||||
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.info
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.info
|
||||
border-color $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
@@ -29,6 +29,7 @@ import InfoPanelTrashed from './InfoPanelTrashed'
|
||||
import { formatDate } from 'browser/lib/date-formatter'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
import markdownToc from 'browser/lib/markdown-toc-generator'
|
||||
|
||||
const electron = require('electron')
|
||||
const { remote } = electron
|
||||
@@ -52,6 +53,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
this.scrollToNextTabThreshold = 0.7
|
||||
this.generateToc = () => this.handleGenerateToc()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
@@ -65,6 +67,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
enableLeftArrow: allTabs.offsetLeft !== 0
|
||||
})
|
||||
}
|
||||
ee.on('code:generate-toc', this.generateToc)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
@@ -91,6 +94,16 @@ class SnippetNoteDetail extends React.Component {
|
||||
|
||||
componentWillUnmount () {
|
||||
if (this.saveQueue != null) this.saveNow()
|
||||
ee.off('code:generate-toc', this.generateToc)
|
||||
}
|
||||
|
||||
handleGenerateToc () {
|
||||
const { note, snippetIndex } = this.state
|
||||
const currentMode = note.snippets[snippetIndex].mode
|
||||
if (currentMode.includes('Markdown')) {
|
||||
const currentEditor = this.refs[`code-${snippetIndex}`].refs.code.editor
|
||||
markdownToc.generateInEditor(currentEditor)
|
||||
}
|
||||
}
|
||||
|
||||
handleChange (e) {
|
||||
@@ -99,7 +112,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
if (this.refs.tags) note.tags = this.refs.tags.value
|
||||
note.description = this.refs.description.value
|
||||
note.updatedAt = new Date()
|
||||
note.title = findNoteTitle(note.description)
|
||||
note.title = findNoteTitle(note.description, false)
|
||||
|
||||
this.setState({
|
||||
note
|
||||
@@ -341,12 +354,10 @@ class SnippetNoteDetail extends React.Component {
|
||||
this.refs['code-' + this.state.snippetIndex].reload()
|
||||
|
||||
if (this.visibleTabs.offsetWidth > this.allTabs.scrollWidth) {
|
||||
console.log('no need for arrows')
|
||||
this.moveTabBarBy(0)
|
||||
} else {
|
||||
const lastTab = this.allTabs.lastChild
|
||||
if (lastTab.offsetLeft + lastTab.offsetWidth < this.visibleTabs.offsetWidth) {
|
||||
console.log('need to scroll')
|
||||
const width = this.visibleTabs.offsetWidth
|
||||
const newLeft = lastTab.offsetLeft + lastTab.offsetWidth - width
|
||||
this.moveTabBarBy(newLeft > 0 ? -newLeft : 0)
|
||||
@@ -441,7 +452,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
const isSuper = global.process.platform === 'darwin'
|
||||
? e.metaKey
|
||||
: e.ctrlKey
|
||||
if (isSuper) {
|
||||
if (isSuper && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault()
|
||||
this.addSnippet()
|
||||
}
|
||||
@@ -573,11 +584,12 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
addSnippet () {
|
||||
const { config } = this.props
|
||||
const { note } = this.state
|
||||
|
||||
note.snippets = note.snippets.concat([{
|
||||
name: '',
|
||||
mode: 'Plain Text',
|
||||
mode: config.editor.snippetDefaultLanguage || 'text',
|
||||
content: ''
|
||||
}])
|
||||
const snippetIndex = note.snippets.length - 1
|
||||
@@ -613,7 +625,6 @@ class SnippetNoteDetail extends React.Component {
|
||||
}
|
||||
|
||||
focusEditor () {
|
||||
console.log('code-' + this.state.snippetIndex)
|
||||
this.refs['code-' + this.state.snippetIndex].focus()
|
||||
}
|
||||
|
||||
@@ -691,6 +702,7 @@ class SnippetNoteDetail extends React.Component {
|
||||
keyMap={config.editor.keyMap}
|
||||
scrollPastEnd={config.editor.scrollPastEnd}
|
||||
fetchUrlTitle={config.editor.fetchUrlTitle}
|
||||
enableTableEditor={config.editor.enableTableEditor}
|
||||
onChange={(e) => this.handleCodeChange(index)(e)}
|
||||
ref={'code-' + index}
|
||||
/>
|
||||
@@ -744,6 +756,9 @@ class SnippetNoteDetail extends React.Component {
|
||||
<TagSelect
|
||||
ref='tags'
|
||||
value={this.state.note.tags}
|
||||
saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
|
||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||
data={data}
|
||||
onChange={(e) => this.handleChange(e)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -169,4 +169,21 @@ body[data-theme="monokai"]
|
||||
|
||||
.tabList
|
||||
background-color $ui-monokai-noteDetail-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
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
|
||||
background-color $ui-dracula-noteDetail-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
@@ -5,61 +5,34 @@ import styles from './TagSelect.styl'
|
||||
import _ from 'lodash'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import Autosuggest from 'react-autosuggest'
|
||||
|
||||
class TagSelect extends React.Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
newTag: ''
|
||||
newTag: '',
|
||||
suggestions: []
|
||||
}
|
||||
|
||||
this.handleAddTag = this.handleAddTag.bind(this)
|
||||
this.onInputBlur = this.onInputBlur.bind(this)
|
||||
this.onInputChange = this.onInputChange.bind(this)
|
||||
this.onInputKeyDown = this.onInputKeyDown.bind(this)
|
||||
this.onSuggestionsClearRequested = this.onSuggestionsClearRequested.bind(this)
|
||||
this.onSuggestionsFetchRequested = this.onSuggestionsFetchRequested.bind(this)
|
||||
this.onSuggestionSelected = this.onSuggestionSelected.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.value = this.props.value
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this.value = this.props.value
|
||||
}
|
||||
|
||||
handleNewTagInputKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
case 9:
|
||||
e.preventDefault()
|
||||
this.submitTag()
|
||||
break
|
||||
case 13:
|
||||
this.submitTag()
|
||||
break
|
||||
case 8:
|
||||
if (this.refs.newTag.value.length === 0) {
|
||||
this.removeLastTag()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleNewTagBlur (e) {
|
||||
this.submitTag()
|
||||
}
|
||||
|
||||
removeLastTag () {
|
||||
this.removeTagByCallback((value) => {
|
||||
value.pop()
|
||||
})
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.setState({
|
||||
newTag: ''
|
||||
})
|
||||
}
|
||||
|
||||
submitTag () {
|
||||
addNewTag (newTag) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_TAG')
|
||||
let { value } = this.props
|
||||
let newTag = this.refs.newTag.value.trim().replace(/ +/g, '_')
|
||||
newTag = newTag.charAt(0) === '#' ? newTag.substring(1) : newTag
|
||||
|
||||
newTag = newTag.trim().replace(/ +/g, '_')
|
||||
if (newTag.charAt(0) === '#') {
|
||||
newTag.substring(1)
|
||||
}
|
||||
|
||||
if (newTag.length <= 0) {
|
||||
this.setState({
|
||||
@@ -68,6 +41,7 @@ class TagSelect extends React.Component {
|
||||
return
|
||||
}
|
||||
|
||||
let { value } = this.props
|
||||
value = _.isArray(value)
|
||||
? value.slice()
|
||||
: []
|
||||
@@ -82,10 +56,41 @@ class TagSelect extends React.Component {
|
||||
})
|
||||
}
|
||||
|
||||
handleNewTagInputChange (e) {
|
||||
this.setState({
|
||||
newTag: this.refs.newTag.value
|
||||
})
|
||||
buildSuggestions () {
|
||||
this.suggestions = _.sortBy(this.props.data.tagNoteMap.map(
|
||||
(tag, name) => ({
|
||||
name,
|
||||
nameLC: name.toLowerCase(),
|
||||
size: tag.size
|
||||
})
|
||||
).filter(
|
||||
tag => tag.size > 0
|
||||
), ['name'])
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.value = this.props.value
|
||||
|
||||
this.buildSuggestions()
|
||||
|
||||
ee.on('editor:add-tag', this.handleAddTag)
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this.value = this.props.value
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
ee.off('editor:add-tag', this.handleAddTag)
|
||||
}
|
||||
|
||||
handleAddTag () {
|
||||
this.refs.newTag.input.focus()
|
||||
}
|
||||
|
||||
handleTagLabelClick (tag) {
|
||||
const { router } = this.context
|
||||
router.push(`/tags/${tag}`)
|
||||
}
|
||||
|
||||
handleTagRemoveButtonClick (tag) {
|
||||
@@ -94,6 +99,60 @@ class TagSelect extends React.Component {
|
||||
}, tag)
|
||||
}
|
||||
|
||||
onInputBlur (e) {
|
||||
this.submitNewTag()
|
||||
}
|
||||
|
||||
onInputChange (e, { newValue, method }) {
|
||||
this.setState({
|
||||
newTag: newValue
|
||||
})
|
||||
}
|
||||
|
||||
onInputKeyDown (e) {
|
||||
switch (e.keyCode) {
|
||||
case 9:
|
||||
e.preventDefault()
|
||||
this.submitNewTag()
|
||||
break
|
||||
case 13:
|
||||
this.submitNewTag()
|
||||
break
|
||||
case 8:
|
||||
if (this.state.newTag.length === 0) {
|
||||
this.removeLastTag()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSuggestionsClearRequested () {
|
||||
this.setState({
|
||||
suggestions: []
|
||||
})
|
||||
}
|
||||
|
||||
onSuggestionsFetchRequested ({ value }) {
|
||||
const valueLC = value.toLowerCase()
|
||||
const suggestions = _.filter(
|
||||
this.suggestions,
|
||||
tag => !_.includes(this.value, tag.name) && tag.nameLC.indexOf(valueLC) !== -1
|
||||
)
|
||||
|
||||
this.setState({
|
||||
suggestions
|
||||
})
|
||||
}
|
||||
|
||||
onSuggestionSelected (event, { suggestion, suggestionValue }) {
|
||||
this.addNewTag(suggestionValue)
|
||||
}
|
||||
|
||||
removeLastTag () {
|
||||
this.removeTagByCallback((value) => {
|
||||
value.pop()
|
||||
})
|
||||
}
|
||||
|
||||
removeTagByCallback (callback, tag = null) {
|
||||
let { value } = this.props
|
||||
|
||||
@@ -107,16 +166,28 @@ class TagSelect extends React.Component {
|
||||
this.props.onChange()
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.buildSuggestions()
|
||||
|
||||
this.setState({
|
||||
newTag: ''
|
||||
})
|
||||
}
|
||||
|
||||
submitNewTag () {
|
||||
this.addNewTag(this.refs.newTag.input.value)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value, className } = this.props
|
||||
const { value, className, showTagsAlphabetically } = this.props
|
||||
|
||||
const tagList = _.isArray(value)
|
||||
? value.map((tag) => {
|
||||
? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => {
|
||||
return (
|
||||
<span styleName='tag'
|
||||
key={tag}
|
||||
>
|
||||
<span styleName='tag-label'>#{tag}</span>
|
||||
<span styleName='tag-label' onClick={(e) => this.handleTagLabelClick(tag)}>#{tag}</span>
|
||||
<button styleName='tag-removeButton'
|
||||
onClick={(e) => this.handleTagRemoveButtonClick(tag)}
|
||||
>
|
||||
@@ -127,6 +198,8 @@ class TagSelect extends React.Component {
|
||||
})
|
||||
: []
|
||||
|
||||
const { newTag, suggestions } = this.state
|
||||
|
||||
return (
|
||||
<div className={_.isString(className)
|
||||
? 'TagSelect ' + className
|
||||
@@ -135,24 +208,39 @@ class TagSelect extends React.Component {
|
||||
styleName='root'
|
||||
>
|
||||
{tagList}
|
||||
<input styleName='newTag'
|
||||
<Autosuggest
|
||||
ref='newTag'
|
||||
value={this.state.newTag}
|
||||
placeholder={i18n.__('Add tag...')}
|
||||
onChange={(e) => this.handleNewTagInputChange(e)}
|
||||
onKeyDown={(e) => this.handleNewTagInputKeyDown(e)}
|
||||
onBlur={(e) => this.handleNewTagBlur(e)}
|
||||
suggestions={suggestions}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
onSuggestionSelected={this.onSuggestionSelected}
|
||||
getSuggestionValue={suggestion => suggestion.name}
|
||||
renderSuggestion={suggestion => (
|
||||
<div>
|
||||
{suggestion.name}
|
||||
</div>
|
||||
)}
|
||||
inputProps={{
|
||||
placeholder: i18n.__('Add tag...'),
|
||||
value: newTag,
|
||||
onChange: this.onInputChange,
|
||||
onKeyDown: this.onInputKeyDown,
|
||||
onBlur: this.onInputBlur
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TagSelect.contextTypes = {
|
||||
router: PropTypes.shape({})
|
||||
}
|
||||
|
||||
TagSelect.propTypes = {
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.arrayOf(PropTypes.string),
|
||||
onChange: PropTypes.func
|
||||
|
||||
}
|
||||
|
||||
export default CSSModules(TagSelect, styles)
|
||||
|
||||
@@ -39,16 +39,9 @@
|
||||
|
||||
.tag-label
|
||||
font-size 13px
|
||||
color: $ui-text-color
|
||||
color $ui-text-color
|
||||
padding 4px 16px 4px 8px
|
||||
|
||||
.newTag
|
||||
box-sizing border-box
|
||||
border none
|
||||
background-color transparent
|
||||
outline none
|
||||
padding 0 4px
|
||||
font-size 13px
|
||||
cursor pointer
|
||||
|
||||
body[data-theme="dark"]
|
||||
.tag
|
||||
@@ -62,11 +55,6 @@ body[data-theme="dark"]
|
||||
.tag-label
|
||||
color $ui-dark-text-color
|
||||
|
||||
.newTag
|
||||
border-color none
|
||||
background-color transparent
|
||||
color $ui-dark-text-color
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.tag
|
||||
background-color $ui-solarized-dark-tag-backgroundColor
|
||||
@@ -78,14 +66,9 @@ body[data-theme="solarized-dark"]
|
||||
.tag-label
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.newTag
|
||||
border-color none
|
||||
background-color transparent
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.tag
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
background-color $ui-monokai-tag-backgroundColor
|
||||
|
||||
.tag-removeButton
|
||||
border-color $ui-button--focus-borderColor
|
||||
@@ -93,8 +76,14 @@ body[data-theme="monokai"]
|
||||
|
||||
.tag-label
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.newTag
|
||||
border-color none
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.tag
|
||||
background-color $ui-dracula-tag-backgroundColor
|
||||
|
||||
.tag-removeButton
|
||||
border-color $ui-dracula-button--focus-borderColor
|
||||
background-color transparent
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.tag-label
|
||||
color $ui-dracula-borderColor
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './ToggleModeButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const ToggleModeButton = ({
|
||||
onClick, editorType
|
||||
}) => (
|
||||
<div styleName='control-toggleModeButton'>
|
||||
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
|
||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
|
||||
</div>
|
||||
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
|
||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
||||
</div>
|
||||
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
ToggleModeButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
editorType: PropTypes.string.Required
|
||||
}
|
||||
|
||||
export default CSSModules(ToggleModeButton, styles)
|
||||
import PropTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './ToggleModeButton.styl'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
|
||||
const ToggleModeButton = ({
|
||||
onClick, editorType
|
||||
}) => (
|
||||
<div styleName='control-toggleModeButton'>
|
||||
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}>
|
||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
|
||||
</div>
|
||||
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}>
|
||||
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
|
||||
</div>
|
||||
<span styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
ToggleModeButton.propTypes = {
|
||||
onClick: PropTypes.func.isRequired,
|
||||
editorType: PropTypes.string.Required
|
||||
}
|
||||
|
||||
export default CSSModules(ToggleModeButton, styles)
|
||||
|
||||
@@ -59,7 +59,14 @@ body[data-theme="solarized-dark"]
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.control-toggleModeButton
|
||||
background-color #272822
|
||||
background-color #373831
|
||||
.active
|
||||
background-color #1EC38B
|
||||
background-color #f92672
|
||||
box-shadow 2px 0px 7px #222222
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.control-toggleModeButton
|
||||
background-color #44475a
|
||||
.active
|
||||
background-color #bd93f9
|
||||
box-shadow 2px 0px 7px #222222
|
||||
|
||||
@@ -22,7 +22,6 @@ const electron = require('electron')
|
||||
const { remote } = electron
|
||||
|
||||
class Main extends React.Component {
|
||||
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
@@ -57,13 +56,13 @@ class Main extends React.Component {
|
||||
init () {
|
||||
dataApi
|
||||
.addStorage({
|
||||
name: 'My Storage',
|
||||
name: 'My Storage Location',
|
||||
path: path.join(remote.app.getPath('home'), 'Boostnote')
|
||||
})
|
||||
.then((data) => {
|
||||
.then(data => {
|
||||
return data
|
||||
})
|
||||
.then((data) => {
|
||||
.then(data => {
|
||||
if (data.storage.folders[0] != null) {
|
||||
return data
|
||||
} else {
|
||||
@@ -72,7 +71,7 @@ class Main extends React.Component {
|
||||
color: '#1278BD',
|
||||
name: 'Default'
|
||||
})
|
||||
.then((_data) => {
|
||||
.then(_data => {
|
||||
return {
|
||||
storage: _data.storage,
|
||||
notes: data.notes
|
||||
@@ -80,8 +79,7 @@ class Main extends React.Component {
|
||||
})
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
console.log(data)
|
||||
.then(data => {
|
||||
store.dispatch({
|
||||
type: 'ADD_STORAGE',
|
||||
storage: data.storage,
|
||||
@@ -98,16 +96,16 @@ class Main extends React.Component {
|
||||
{
|
||||
name: 'example.html',
|
||||
mode: 'html',
|
||||
content: '<html>\n<body>\n<h1 id=\'hello\'>Enjoy Boostnote!</h1>\n</body>\n</html>'
|
||||
content: "<html>\n<body>\n<h1 id='hello'>Enjoy Boostnote!</h1>\n</body>\n</html>"
|
||||
},
|
||||
{
|
||||
name: 'example.js',
|
||||
mode: 'javascript',
|
||||
content: 'var boostnote = document.getElementById(\'enjoy\').innerHTML\n\nconsole.log(boostnote)'
|
||||
content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)"
|
||||
}
|
||||
]
|
||||
})
|
||||
.then((note) => {
|
||||
.then(note => {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
@@ -120,7 +118,7 @@ class Main extends React.Component {
|
||||
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)'
|
||||
})
|
||||
.then((note) => {
|
||||
.then(note => {
|
||||
store.dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
@@ -131,10 +129,10 @@ class Main extends React.Component {
|
||||
.then(defaultMarkdownNote)
|
||||
.then(() => data.storage)
|
||||
})
|
||||
.then((storage) => {
|
||||
.then(storage => {
|
||||
hashHistory.push('/storages/' + storage.key)
|
||||
})
|
||||
.catch((err) => {
|
||||
.catch(err => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
@@ -142,12 +140,7 @@ class Main extends React.Component {
|
||||
componentDidMount () {
|
||||
const { dispatch, config } = this.props
|
||||
|
||||
const supportedThemes = [
|
||||
'dark',
|
||||
'white',
|
||||
'solarized-dark',
|
||||
'monokai'
|
||||
]
|
||||
const supportedThemes = ['dark', 'white', 'solarized-dark', 'monokai', 'dracula']
|
||||
|
||||
if (supportedThemes.indexOf(config.ui.theme) !== -1) {
|
||||
document.body.setAttribute('data-theme', config.ui.theme)
|
||||
@@ -162,19 +155,20 @@ class Main extends React.Component {
|
||||
}
|
||||
applyShortcuts()
|
||||
// Reload all data
|
||||
dataApi.init()
|
||||
.then((data) => {
|
||||
dispatch({
|
||||
type: 'INIT_ALL',
|
||||
storages: data.storages,
|
||||
notes: data.notes
|
||||
})
|
||||
|
||||
if (data.storages.length < 1) {
|
||||
this.init()
|
||||
}
|
||||
dataApi.init().then(data => {
|
||||
dispatch({
|
||||
type: 'INIT_ALL',
|
||||
storages: data.storages,
|
||||
notes: data.notes
|
||||
})
|
||||
|
||||
if (data.storages.length < 1) {
|
||||
this.init()
|
||||
}
|
||||
})
|
||||
|
||||
delete CodeMirror.keyMap.emacs['Ctrl-V']
|
||||
|
||||
eventEmitter.on('editor:fullscreen', this.toggleFullScreen)
|
||||
}
|
||||
|
||||
@@ -199,34 +193,40 @@ class Main extends React.Component {
|
||||
handleMouseUp (e) {
|
||||
// Change width of NoteList component.
|
||||
if (this.state.isRightSliderFocused) {
|
||||
this.setState({
|
||||
isRightSliderFocused: false
|
||||
}, () => {
|
||||
const { dispatch } = this.props
|
||||
const newListWidth = this.state.listWidth
|
||||
// TODO: ConfigManager should dispatch itself.
|
||||
ConfigManager.set({listWidth: newListWidth})
|
||||
dispatch({
|
||||
type: 'SET_LIST_WIDTH',
|
||||
listWidth: newListWidth
|
||||
})
|
||||
})
|
||||
this.setState(
|
||||
{
|
||||
isRightSliderFocused: false
|
||||
},
|
||||
() => {
|
||||
const { dispatch } = this.props
|
||||
const newListWidth = this.state.listWidth
|
||||
// TODO: ConfigManager should dispatch itself.
|
||||
ConfigManager.set({ listWidth: newListWidth })
|
||||
dispatch({
|
||||
type: 'SET_LIST_WIDTH',
|
||||
listWidth: newListWidth
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Change width of SideNav component.
|
||||
if (this.state.isLeftSliderFocused) {
|
||||
this.setState({
|
||||
isLeftSliderFocused: false
|
||||
}, () => {
|
||||
const { dispatch } = this.props
|
||||
const navWidth = this.state.navWidth
|
||||
// TODO: ConfigManager should dispatch itself.
|
||||
ConfigManager.set({ navWidth })
|
||||
dispatch({
|
||||
type: 'SET_NAV_WIDTH',
|
||||
navWidth
|
||||
})
|
||||
})
|
||||
this.setState(
|
||||
{
|
||||
isLeftSliderFocused: false
|
||||
},
|
||||
() => {
|
||||
const { dispatch } = this.props
|
||||
const navWidth = this.state.navWidth
|
||||
// TODO: ConfigManager should dispatch itself.
|
||||
ConfigManager.set({ navWidth })
|
||||
dispatch({
|
||||
type: 'SET_NAV_WIDTH',
|
||||
navWidth
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,8 +271,8 @@ class Main extends React.Component {
|
||||
}
|
||||
|
||||
hideLeftLists (noteDetail, noteList, mainBody) {
|
||||
this.setState({noteDetailWidth: noteDetail.style.left})
|
||||
this.setState({mainBodyWidth: mainBody.style.left})
|
||||
this.setState({ noteDetailWidth: noteDetail.style.left })
|
||||
this.setState({ mainBodyWidth: mainBody.style.left })
|
||||
noteDetail.style.left = '0px'
|
||||
mainBody.style.left = '0px'
|
||||
noteList.style.display = 'none'
|
||||
@@ -294,33 +294,36 @@ class Main extends React.Component {
|
||||
<div
|
||||
className='Main'
|
||||
styleName='root'
|
||||
onMouseMove={(e) => this.handleMouseMove(e)}
|
||||
onMouseUp={(e) => this.handleMouseUp(e)}
|
||||
onMouseMove={e => this.handleMouseMove(e)}
|
||||
onMouseUp={e => this.handleMouseUp(e)}
|
||||
>
|
||||
<SideNav
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
'config',
|
||||
'location'
|
||||
])}
|
||||
{..._.pick(this.props, ['dispatch', 'data', 'config', 'params', 'location'])}
|
||||
width={this.state.navWidth}
|
||||
/>
|
||||
{!config.isSideNavFolded &&
|
||||
<div styleName={this.state.isLeftSliderFocused ? 'slider--active' : 'slider'}
|
||||
style={{left: this.state.navWidth}}
|
||||
onMouseDown={(e) => this.handleLeftSlideMouseDown(e)}
|
||||
<div
|
||||
styleName={
|
||||
this.state.isLeftSliderFocused ? 'slider--active' : 'slider'
|
||||
}
|
||||
style={{ left: this.state.navWidth }}
|
||||
onMouseDown={e => this.handleLeftSlideMouseDown(e)}
|
||||
draggable='false'
|
||||
>
|
||||
<div styleName='slider-hitbox' />
|
||||
</div>
|
||||
}
|
||||
<div styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
|
||||
</div>}
|
||||
<div
|
||||
styleName={config.isSideNavFolded ? 'body--expanded' : 'body'}
|
||||
id='main-body'
|
||||
ref='body'
|
||||
style={{left: config.isSideNavFolded ? foldedNavigationWidth : this.state.navWidth}}
|
||||
style={{
|
||||
left: config.isSideNavFolded
|
||||
? foldedNavigationWidth
|
||||
: this.state.navWidth
|
||||
}}
|
||||
>
|
||||
<TopBar style={{width: this.state.listWidth}}
|
||||
<TopBar
|
||||
style={{ width: this.state.listWidth }}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'config',
|
||||
@@ -329,7 +332,8 @@ class Main extends React.Component {
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
<NoteList style={{width: this.state.listWidth}}
|
||||
<NoteList
|
||||
style={{ width: this.state.listWidth }}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
@@ -338,15 +342,20 @@ class Main extends React.Component {
|
||||
'location'
|
||||
])}
|
||||
/>
|
||||
<div styleName={this.state.isRightSliderFocused ? 'slider-right--active' : 'slider-right'}
|
||||
style={{left: this.state.listWidth - 1}}
|
||||
onMouseDown={(e) => this.handleRightSlideMouseDown(e)}
|
||||
<div
|
||||
styleName={
|
||||
this.state.isRightSliderFocused
|
||||
? 'slider-right--active'
|
||||
: 'slider-right'
|
||||
}
|
||||
style={{ left: this.state.listWidth - 1 }}
|
||||
onMouseDown={e => this.handleRightSlideMouseDown(e)}
|
||||
draggable='false'
|
||||
>
|
||||
<div styleName='slider-hitbox' />
|
||||
</div>
|
||||
<Detail
|
||||
style={{left: this.state.listWidth}}
|
||||
style={{ left: this.state.listWidth }}
|
||||
{..._.pick(this.props, [
|
||||
'dispatch',
|
||||
'data',
|
||||
@@ -374,4 +383,4 @@ Main.propTypes = {
|
||||
data: PropTypes.shape({}).isRequired
|
||||
}
|
||||
|
||||
export default connect((x) => x)(CSSModules(Main, styles))
|
||||
export default connect(x => x)(CSSModules(Main, styles))
|
||||
|
||||
@@ -79,3 +79,7 @@ body[data-theme="solarized-dark"]
|
||||
body[data-theme="monokai"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
@@ -7,6 +7,7 @@ import modal from 'browser/main/lib/modal'
|
||||
import NewNoteModal from 'browser/main/modals/NewNoteModal'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote'
|
||||
|
||||
const { remote } = require('electron')
|
||||
const { dialog } = remote
|
||||
@@ -34,15 +35,22 @@ class NewNoteButton extends React.Component {
|
||||
}
|
||||
|
||||
handleNewNoteButtonClick (e) {
|
||||
const { location, dispatch } = this.props
|
||||
const { location, dispatch, config } = this.props
|
||||
const { storage, folder } = this.resolveTargetFolder()
|
||||
|
||||
modal.open(NewNoteModal, {
|
||||
storage: storage.key,
|
||||
folder: folder.key,
|
||||
dispatch,
|
||||
location
|
||||
})
|
||||
if (config.ui.defaultNote === 'MARKDOWN_NOTE') {
|
||||
createMarkdownNote(storage.key, folder.key, dispatch, location)
|
||||
} else if (config.ui.defaultNote === 'SNIPPET_NOTE') {
|
||||
createSnippetNote(storage.key, folder.key, dispatch, location, config)
|
||||
} else {
|
||||
modal.open(NewNoteModal, {
|
||||
storage: storage.key,
|
||||
folder: folder.key,
|
||||
dispatch,
|
||||
location,
|
||||
config
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
resolveTargetFolder () {
|
||||
|
||||
@@ -84,7 +84,7 @@ body[data-theme="dark"]
|
||||
color $ui-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-dark-text-color
|
||||
|
||||
|
||||
.control-button--active
|
||||
color $ui-dark-text-color
|
||||
&:active
|
||||
@@ -109,7 +109,7 @@ body[data-theme="solarized-dark"]
|
||||
color $ui-solarized-dark-inactive-text-color
|
||||
&:hover
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
|
||||
.control-button--active
|
||||
color $ui-solarized-dark-text-color
|
||||
&:active
|
||||
@@ -138,3 +138,27 @@ body[data-theme="monokai"]
|
||||
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
|
||||
@@ -56,7 +56,6 @@ class NoteList extends React.Component {
|
||||
super(props)
|
||||
|
||||
this.selectNextNoteHandler = () => {
|
||||
console.log('fired next')
|
||||
this.selectNextNote()
|
||||
}
|
||||
this.selectPriorNoteHandler = () => {
|
||||
@@ -80,10 +79,13 @@ class NoteList extends React.Component {
|
||||
this.getViewType = this.getViewType.bind(this)
|
||||
this.restoreNote = this.restoreNote.bind(this)
|
||||
this.copyNoteLink = this.copyNoteLink.bind(this)
|
||||
this.navigate = this.navigate.bind(this)
|
||||
|
||||
// TODO: not Selected noteKeys but SelectedNote(for reusing)
|
||||
this.state = {
|
||||
ctrlKeyDown: false,
|
||||
shiftKeyDown: false,
|
||||
prevShiftNoteIndex: -1,
|
||||
selectedNoteKeys: []
|
||||
}
|
||||
|
||||
@@ -98,6 +100,7 @@ class NoteList extends React.Component {
|
||||
ee.on('list:isMarkdownNote', this.alertIfSnippetHandler)
|
||||
ee.on('import:file', this.importFromFileHandler)
|
||||
ee.on('list:jump', this.jumpNoteByHash)
|
||||
ee.on('list:navigate', this.navigate)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
@@ -265,7 +268,7 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
handleNoteListKeyDown (e) {
|
||||
if (e.metaKey || e.ctrlKey) return true
|
||||
if (e.metaKey) return true
|
||||
|
||||
// A key
|
||||
if (e.keyCode === 65 && !e.shiftKey) {
|
||||
@@ -305,6 +308,8 @@ class NoteList extends React.Component {
|
||||
|
||||
if (e.shiftKey) {
|
||||
this.setState({ shiftKeyDown: true })
|
||||
} else if (e.ctrlKey) {
|
||||
this.setState({ ctrlKeyDown: true })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,6 +317,10 @@ class NoteList extends React.Component {
|
||||
if (!e.shiftKey) {
|
||||
this.setState({ shiftKeyDown: false })
|
||||
}
|
||||
|
||||
if (!e.ctrlKey) {
|
||||
this.setState({ ctrlKeyDown: false })
|
||||
}
|
||||
}
|
||||
|
||||
getNotes () {
|
||||
@@ -388,25 +397,65 @@ class NoteList extends React.Component {
|
||||
return pinnedNotes.concat(unpinnedNotes)
|
||||
}
|
||||
|
||||
getNoteIndexByKey (noteKey) {
|
||||
return this.notes.findIndex((note) => {
|
||||
if (!note) return -1
|
||||
|
||||
return note.key === noteKey
|
||||
})
|
||||
}
|
||||
|
||||
handleNoteClick (e, uniqueKey) {
|
||||
const { router } = this.context
|
||||
const { location } = this.props
|
||||
let { selectedNoteKeys } = this.state
|
||||
const { shiftKeyDown } = this.state
|
||||
let { selectedNoteKeys, prevShiftNoteIndex } = this.state
|
||||
const { ctrlKeyDown, shiftKeyDown } = this.state
|
||||
const hasSelectedNoteKey = selectedNoteKeys.length > 0
|
||||
|
||||
if (shiftKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
||||
if (ctrlKeyDown && selectedNoteKeys.includes(uniqueKey)) {
|
||||
const newSelectedNoteKeys = selectedNoteKeys.filter((noteKey) => noteKey !== uniqueKey)
|
||||
this.setState({
|
||||
selectedNoteKeys: newSelectedNoteKeys
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!shiftKeyDown) {
|
||||
if (!ctrlKeyDown && !shiftKeyDown) {
|
||||
selectedNoteKeys = []
|
||||
}
|
||||
|
||||
if (!shiftKeyDown) {
|
||||
prevShiftNoteIndex = -1
|
||||
}
|
||||
|
||||
selectedNoteKeys.push(uniqueKey)
|
||||
|
||||
if (shiftKeyDown && hasSelectedNoteKey) {
|
||||
let firstShiftNoteIndex = this.getNoteIndexByKey(selectedNoteKeys[0])
|
||||
// Shift selection can either start from first note in the exisiting selectedNoteKeys
|
||||
// or previous first shift note index
|
||||
firstShiftNoteIndex = firstShiftNoteIndex > prevShiftNoteIndex
|
||||
? firstShiftNoteIndex : prevShiftNoteIndex
|
||||
|
||||
const lastShiftNoteIndex = this.getNoteIndexByKey(uniqueKey)
|
||||
|
||||
const startIndex = firstShiftNoteIndex < lastShiftNoteIndex
|
||||
? firstShiftNoteIndex : lastShiftNoteIndex
|
||||
const endIndex = firstShiftNoteIndex > lastShiftNoteIndex
|
||||
? firstShiftNoteIndex : lastShiftNoteIndex
|
||||
|
||||
selectedNoteKeys = []
|
||||
for (let i = startIndex; i <= endIndex; i++) {
|
||||
selectedNoteKeys.push(this.notes[i].key)
|
||||
}
|
||||
|
||||
if (prevShiftNoteIndex < 0) {
|
||||
prevShiftNoteIndex = firstShiftNoteIndex
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
selectedNoteKeys
|
||||
selectedNoteKeys,
|
||||
prevShiftNoteIndex
|
||||
})
|
||||
|
||||
router.push({
|
||||
@@ -418,10 +467,10 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
handleSortByChange (e) {
|
||||
const { dispatch } = this.props
|
||||
const { dispatch, params: { folderKey } } = this.props
|
||||
|
||||
const config = {
|
||||
sortBy: e.target.value
|
||||
[folderKey]: { sortBy: e.target.value }
|
||||
}
|
||||
|
||||
ConfigManager.set(config)
|
||||
@@ -517,7 +566,7 @@ class NoteList extends React.Component {
|
||||
click: this.cloneNote.bind(this)
|
||||
}, {
|
||||
label: copyNoteLink,
|
||||
click: this.copyNoteLink(note)
|
||||
click: this.copyNoteLink.bind(this, note)
|
||||
})
|
||||
if (note.type === 'MARKDOWN_NOTE') {
|
||||
if (note.blog && note.blog.blogLink && note.blog.blogId) {
|
||||
@@ -614,7 +663,6 @@ class NoteList extends React.Component {
|
||||
.catch((err) => {
|
||||
console.error('Cannot Delete note: ' + err)
|
||||
})
|
||||
console.log('Notes were all deleted')
|
||||
} else {
|
||||
if (!confirmDeleteNote(confirmDeletion, false)) return
|
||||
|
||||
@@ -634,7 +682,6 @@ class NoteList extends React.Component {
|
||||
})
|
||||
})
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
|
||||
console.log('Notes went to trash')
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Notes could not go to trash: ' + err)
|
||||
@@ -687,6 +734,16 @@ class NoteList extends React.Component {
|
||||
return copy(noteLink)
|
||||
}
|
||||
|
||||
navigate (sender, pathname) {
|
||||
const { router } = this.context
|
||||
router.push({
|
||||
pathname,
|
||||
query: {
|
||||
// key: noteKey
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
save (note) {
|
||||
const { dispatch } = this.props
|
||||
dataApi
|
||||
@@ -909,12 +966,13 @@ class NoteList extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { location, config } = this.props
|
||||
const { location, config, params: { folderKey } } = this.props
|
||||
let { notes } = this.props
|
||||
const { selectedNoteKeys } = this.state
|
||||
const sortFunc = config.sortBy === 'CREATED_AT'
|
||||
const sortBy = _.get(config, [folderKey, 'sortBy'], config.sortBy.default)
|
||||
const sortFunc = sortBy === 'CREATED_AT'
|
||||
? sortByCreatedAt
|
||||
: config.sortBy === 'ALPHABETICAL'
|
||||
: sortBy === 'ALPHABETICAL'
|
||||
? sortByAlphabetical
|
||||
: sortByUpdatedAt
|
||||
const sortedNotes = location.pathname.match(/\/starred|\/trash/)
|
||||
@@ -965,7 +1023,7 @@ class NoteList extends React.Component {
|
||||
notes.length === 1 ||
|
||||
(autoSelectFirst && index === 0)
|
||||
const dateDisplay = moment(
|
||||
config.sortBy === 'CREATED_AT'
|
||||
sortBy === 'CREATED_AT'
|
||||
? note.createdAt : note.updatedAt
|
||||
).fromNow('D')
|
||||
|
||||
@@ -983,6 +1041,7 @@ class NoteList extends React.Component {
|
||||
folderName={this.getNoteFolder(note).name}
|
||||
storageName={this.getNoteStorage(note).name}
|
||||
viewType={viewType}
|
||||
showTagsAlphabetically={config.ui.showTagsAlphabetically}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1014,7 +1073,7 @@ class NoteList extends React.Component {
|
||||
<i className='fa fa-angle-down' />
|
||||
<select styleName='control-sortBy-select'
|
||||
title={i18n.__('Select filter mode')}
|
||||
value={config.sortBy}
|
||||
value={sortBy}
|
||||
onChange={(e) => this.handleSortByChange(e)}
|
||||
>
|
||||
<option title='Sort by update time' value='UPDATED_AT'>{i18n.__('Updated')}</option>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
text-align center
|
||||
|
||||
|
||||
|
||||
|
||||
.top-menu-label
|
||||
margin-left 5px
|
||||
overflow ellipsis
|
||||
@@ -122,3 +122,8 @@ body[data-theme="monokai"]
|
||||
.root, .root--folded
|
||||
background-color $ui-monokai-backgroundColor
|
||||
border-right 1px solid $ui-monokai-borderColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root, .root--folded
|
||||
background-color $ui-dracula-backgroundColor
|
||||
border-right 1px solid $ui-dracula-borderColor
|
||||
@@ -38,6 +38,22 @@ class StorageItem extends React.Component {
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export Storage'),
|
||||
submenu: [
|
||||
{
|
||||
label: i18n.__('Export as txt'),
|
||||
click: (e) => this.handleExportStorageClick(e, 'txt')
|
||||
},
|
||||
{
|
||||
label: i18n.__('Export as md'),
|
||||
click: (e) => this.handleExportStorageClick(e, 'md')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
{
|
||||
label: i18n.__('Unlink Storage'),
|
||||
click: (e) => this.handleUnlinkStorageClick(e)
|
||||
@@ -68,6 +84,30 @@ class StorageItem extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleExportStorageClick (e, fileType) {
|
||||
const options = {
|
||||
properties: ['openDirectory', 'createDirectory'],
|
||||
buttonLabel: i18n.__('Select directory'),
|
||||
title: i18n.__('Select a folder to export the files to'),
|
||||
multiSelections: false
|
||||
}
|
||||
dialog.showOpenDialog(remote.getCurrentWindow(), options,
|
||||
(paths) => {
|
||||
if (paths && paths.length === 1) {
|
||||
const { storage, dispatch } = this.props
|
||||
dataApi
|
||||
.exportStorage(storage.key, fileType, paths[0])
|
||||
.then(data => {
|
||||
dispatch({
|
||||
type: 'EXPORT_STORAGE',
|
||||
storage: data.storage,
|
||||
fileType: data.fileType
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
handleToggleButtonClick (e) {
|
||||
const { storage, dispatch } = this.props
|
||||
const isOpen = !this.state.isOpen
|
||||
|
||||
@@ -18,6 +18,12 @@ import TagButton from './TagButton'
|
||||
import {SortableContainer} from 'react-sortable-hoc'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import context from 'browser/lib/context'
|
||||
import { remote } from 'electron'
|
||||
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
|
||||
|
||||
function matchActiveTags (tags, activeTags) {
|
||||
return _.every(activeTags, v => tags.indexOf(v) >= 0)
|
||||
}
|
||||
|
||||
class SideNav extends React.Component {
|
||||
// TODO: should not use electron stuff v0.7
|
||||
@@ -30,6 +36,52 @@ class SideNav extends React.Component {
|
||||
EventEmitter.off('side:preferences', this.handleMenuButtonClick)
|
||||
}
|
||||
|
||||
deleteTag (tag) {
|
||||
const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
|
||||
ype: 'warning',
|
||||
message: i18n.__('Confirm tag deletion'),
|
||||
detail: i18n.__('This will permanently remove this tag.'),
|
||||
buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
|
||||
})
|
||||
|
||||
if (selectedButton === 0) {
|
||||
const { data, dispatch, location, params } = this.props
|
||||
|
||||
const notes = data.noteMap
|
||||
.map(note => note)
|
||||
.filter(note => note.tags.indexOf(tag) !== -1)
|
||||
.map(note => {
|
||||
note = Object.assign({}, note)
|
||||
note.tags = note.tags.slice()
|
||||
|
||||
note.tags.splice(note.tags.indexOf(tag), 1)
|
||||
|
||||
return note
|
||||
})
|
||||
|
||||
Promise
|
||||
.all(notes.map(note => dataApi.updateNote(note.storage, note.key, note)))
|
||||
.then(updatedNotes => {
|
||||
updatedNotes.forEach(note => {
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note
|
||||
})
|
||||
})
|
||||
|
||||
if (location.pathname.match('/tags')) {
|
||||
const tags = params.tagname.split(' ')
|
||||
const index = tags.indexOf(tag)
|
||||
if (index !== -1) {
|
||||
tags.splice(index, 1)
|
||||
|
||||
this.context.router.push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleMenuButtonClick (e) {
|
||||
openModal(PreferencesModal)
|
||||
}
|
||||
@@ -44,6 +96,17 @@ class SideNav extends React.Component {
|
||||
router.push('/starred')
|
||||
}
|
||||
|
||||
handleTagContextMenu (e, tag) {
|
||||
const menu = []
|
||||
|
||||
menu.push({
|
||||
label: i18n.__('Delete Tag'),
|
||||
click: this.deleteTag.bind(this, tag)
|
||||
})
|
||||
|
||||
context.popup(menu)
|
||||
}
|
||||
|
||||
handleToggleButtonClick (e) {
|
||||
const { dispatch, config } = this.props
|
||||
|
||||
@@ -144,12 +207,20 @@ class SideNav extends React.Component {
|
||||
|
||||
tagListComponent () {
|
||||
const { data, location, config } = this.props
|
||||
const relatedTags = this.getRelatedTags(this.getActiveTags(location.pathname), data.noteMap)
|
||||
const activeTags = this.getActiveTags(location.pathname)
|
||||
const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
|
||||
let tagList = _.sortBy(data.tagNoteMap.map(
|
||||
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
|
||||
), ['name']).filter(
|
||||
).filter(
|
||||
tag => tag.size > 0
|
||||
)
|
||||
), ['name'])
|
||||
if (config.ui.enableLiveNoteCounts && activeTags.length !== 0) {
|
||||
const notesTags = data.noteMap.map(note => note.tags)
|
||||
tagList = tagList.map(tag => {
|
||||
tag.size = notesTags.filter(tags => tags.includes(tag.name) && matchActiveTags(tags, activeTags)).length
|
||||
return tag
|
||||
})
|
||||
}
|
||||
if (config.sortTagsBy === 'COUNTER') {
|
||||
tagList = _.sortBy(tagList, item => (0 - item.size))
|
||||
}
|
||||
@@ -165,6 +236,7 @@ class SideNav extends React.Component {
|
||||
name={tag.name}
|
||||
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
|
||||
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
|
||||
handleContextMenu={this.handleTagContextMenu.bind(this)}
|
||||
isActive={this.getTagActive(location.pathname, tag.name)}
|
||||
isRelated={tag.related}
|
||||
key={tag.name}
|
||||
@@ -198,12 +270,12 @@ class SideNav extends React.Component {
|
||||
const tags = pathSegments[pathSegments.length - 1]
|
||||
return (tags === 'alltags')
|
||||
? []
|
||||
: tags.split(' ')
|
||||
: decodeURIComponent(tags).split(' ')
|
||||
}
|
||||
|
||||
handleClickTagListItem (name) {
|
||||
const { router } = this.context
|
||||
router.push(`/tags/${name}`)
|
||||
router.push(`/tags/${encodeURIComponent(name)}`)
|
||||
}
|
||||
|
||||
handleSortTagsByChange (e) {
|
||||
@@ -230,7 +302,7 @@ class SideNav extends React.Component {
|
||||
} else {
|
||||
listOfTags.push(tag)
|
||||
}
|
||||
router.push(`/tags/${listOfTags.join(' ')}`)
|
||||
router.push(`/tags/${encodeURIComponent(listOfTags.join(' '))}`)
|
||||
}
|
||||
|
||||
emptyTrash (entries) {
|
||||
@@ -238,6 +310,8 @@ class SideNav extends React.Component {
|
||||
const deletionPromises = entries.map((note) => {
|
||||
return dataApi.deleteNote(note.storage, note.key)
|
||||
})
|
||||
const { confirmDeletion } = this.props.config.ui
|
||||
if (!confirmDeleteNote(confirmDeletion, true)) return
|
||||
Promise.all(deletionPromises)
|
||||
.then((arrayOfStorageAndNoteKeys) => {
|
||||
arrayOfStorageAndNoteKeys.forEach(({ storageKey, noteKey }) => {
|
||||
@@ -247,7 +321,6 @@ class SideNav extends React.Component {
|
||||
.catch((err) => {
|
||||
console.error('Cannot Delete note: ' + err)
|
||||
})
|
||||
console.log('Trash emptied')
|
||||
}
|
||||
|
||||
handleFilterButtonContextMenu (event) {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
color $ui-active-color
|
||||
span
|
||||
margin-left 5px
|
||||
|
||||
|
||||
.update
|
||||
navButtonColor()
|
||||
height 24px
|
||||
@@ -80,3 +80,14 @@ body[data-theme="monokai"]
|
||||
color $ui-monokai-active-color
|
||||
&:active
|
||||
color $ui-monokai-active-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
navButtonColor()
|
||||
.zoom
|
||||
border-color $ui-dark-borderColor
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
transition 0.15s
|
||||
color $ui-dracula-active-color
|
||||
&:active
|
||||
color $ui-dracula-active-color
|
||||
@@ -256,3 +256,25 @@ body[data-theme="monokai"]
|
||||
input
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root, .root--expanded
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.control
|
||||
border-color $ui-dracula-borderColor
|
||||
.control-search
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.control-search-icon
|
||||
absolute top bottom left
|
||||
line-height 32px
|
||||
width 35px
|
||||
color $ui-dracula-inactive-text-color
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
|
||||
.control-search-input
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
input
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
@@ -18,6 +18,9 @@ body
|
||||
::-webkit-scrollbar
|
||||
width 12px
|
||||
|
||||
::-webkit-scrollbar-corner
|
||||
background-color: transparent;
|
||||
|
||||
::-webkit-scrollbar-thumb
|
||||
background-color rgba(0, 0, 0, 0.15)
|
||||
|
||||
@@ -132,6 +135,15 @@ body[data-theme="dark"]
|
||||
.CodeMirror-foldgutter-folded:after
|
||||
content: "\25B8"
|
||||
|
||||
.CodeMirror-hover
|
||||
padding 2px 4px 0 4px
|
||||
position absolute
|
||||
z-index 99
|
||||
|
||||
.CodeMirror-hyperlink
|
||||
cursor pointer
|
||||
|
||||
|
||||
.sortableItemHelper
|
||||
z-index modalZIndex + 5
|
||||
|
||||
@@ -152,3 +164,18 @@ body[data-theme="monokai"]
|
||||
background-color $ui-monokai-backgroundColor
|
||||
.sortableItemHelper
|
||||
color: $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
::-webkit-scrollbar-thumb
|
||||
background-color rgba(0, 0, 0, 0.3)
|
||||
.ModalBase
|
||||
.modalBack
|
||||
background-color $ui-dracula-backgroundColor
|
||||
.sortableItemHelper
|
||||
color: $ui-dracula-text-color
|
||||
|
||||
body[data-theme="default"]
|
||||
.SideNav ::-webkit-scrollbar-thumb
|
||||
background-color rgba(255, 255, 255, 0.3)
|
||||
|
||||
@import '../styles/Detail/TagSelect.styl'
|
||||
@@ -45,7 +45,6 @@ function initAwsMobileAnalytics () {
|
||||
if (getSendEventCond()) return
|
||||
AWS.config.credentials.get((err) => {
|
||||
if (!err) {
|
||||
console.log('Cognito Identity ID: ' + AWS.config.credentials.identityId)
|
||||
recordDynamicCustomEvent('APP_STARTED')
|
||||
recordStaticCustomEvent()
|
||||
}
|
||||
@@ -58,7 +57,7 @@ function recordDynamicCustomEvent (type, options = {}) {
|
||||
mobileAnalyticsClient.recordEvent(type, options)
|
||||
} catch (analyticsError) {
|
||||
if (analyticsError instanceof ReferenceError) {
|
||||
console.log(analyticsError.name + ': ' + analyticsError.message)
|
||||
console.error(analyticsError.name + ': ' + analyticsError.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,7 +70,7 @@ function recordStaticCustomEvent () {
|
||||
})
|
||||
} catch (analyticsError) {
|
||||
if (analyticsError instanceof ReferenceError) {
|
||||
console.log(analyticsError.name + ': ' + analyticsError.message)
|
||||
console.error(analyticsError.name + ': ' + analyticsError.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,16 @@ export const DEFAULT_CONFIG = {
|
||||
isSideNavFolded: false,
|
||||
listWidth: 280,
|
||||
navWidth: 200,
|
||||
sortBy: 'UPDATED_AT', // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL'
|
||||
sortBy: {
|
||||
default: 'UPDATED_AT' // 'CREATED_AT', 'UPDATED_AT', 'APLHABETICAL'
|
||||
},
|
||||
sortTagsBy: 'ALPHABETICAL', // 'ALPHABETICAL', 'COUNTER'
|
||||
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
|
||||
amaEnabled: true,
|
||||
hotkey: {
|
||||
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
|
||||
toggleMode: OSX ? 'Command + M' : 'Ctrl + M'
|
||||
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
|
||||
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace'
|
||||
},
|
||||
ui: {
|
||||
language: 'en',
|
||||
@@ -41,10 +44,14 @@ export const DEFAULT_CONFIG = {
|
||||
enableRulers: false,
|
||||
rulers: [80, 120],
|
||||
displayLineNumbers: true,
|
||||
switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR
|
||||
switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
|
||||
delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE'
|
||||
scrollPastEnd: false,
|
||||
type: 'SPLIT',
|
||||
fetchUrlTitle: true
|
||||
type: 'SPLIT', // 'SPLIT', 'EDITOR_PREVIEW'
|
||||
fetchUrlTitle: true,
|
||||
enableTableEditor: false,
|
||||
enableFrontMatterTitle: true,
|
||||
frontMatterTitleField: 'title'
|
||||
},
|
||||
preview: {
|
||||
fontSize: '14',
|
||||
@@ -57,12 +64,14 @@ export const DEFAULT_CONFIG = {
|
||||
latexBlockClose: '$$',
|
||||
plantUMLServerAddress: 'http://www.plantuml.com/plantuml',
|
||||
scrollPastEnd: false,
|
||||
scrollSync: true,
|
||||
smartQuotes: true,
|
||||
breaks: true,
|
||||
smartArrows: false,
|
||||
allowCustomCSS: false,
|
||||
customCSS: '',
|
||||
sanitize: 'STRICT' // 'STRICT', 'ALLOW_STYLES', 'NONE'
|
||||
sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE'
|
||||
lineThroughCheckbox: true
|
||||
},
|
||||
blog: {
|
||||
type: 'wordpress', // Available value: wordpress, add more types in the future plz
|
||||
@@ -144,6 +153,8 @@ function set (updates) {
|
||||
document.body.setAttribute('data-theme', 'solarized-dark')
|
||||
} else if (newConfig.ui.theme === 'monokai') {
|
||||
document.body.setAttribute('data-theme', 'monokai')
|
||||
} else if (newConfig.ui.theme === 'dracula') {
|
||||
document.body.setAttribute('data-theme', 'dracula')
|
||||
} else {
|
||||
document.body.setAttribute('data-theme', 'default')
|
||||
}
|
||||
@@ -192,6 +203,7 @@ function rewriteHotkey (config) {
|
||||
const keys = [...Object.keys(config.hotkey)]
|
||||
keys.forEach(key => {
|
||||
config.hotkey[key] = config.hotkey[key].replace(/Cmd/g, 'Command')
|
||||
config.hotkey[key] = config.hotkey[key].replace(/Opt\s/g, 'Option ')
|
||||
})
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -11,6 +11,118 @@ import i18n from 'browser/lib/i18n'
|
||||
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
|
||||
const DESTINATION_FOLDER = 'attachments'
|
||||
const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(path.win32.sep)
|
||||
/**
|
||||
* @description
|
||||
* Create a Image element to get the real size of image.
|
||||
* @param {File} file the File object dropped.
|
||||
* @returns {Promise<Image>} Image element created
|
||||
*/
|
||||
function getImage (file) {
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader()
|
||||
const img = new Image()
|
||||
img.onload = () => resolve(img)
|
||||
reader.onload = e => {
|
||||
img.src = e.target.result
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description
|
||||
* Get the orientation info from iamges's EXIF data.
|
||||
* case 1: The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
|
||||
* case 2: The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.
|
||||
* case 3: The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.
|
||||
* case 4: The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.
|
||||
* case 5: The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.
|
||||
* case 6: The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.
|
||||
* case 7: The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.
|
||||
* case 8: The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.
|
||||
* Other: reserved
|
||||
* ref: http://sylvana.net/jpegcrop/exif_orientation.html
|
||||
* @param {File} file the File object dropped.
|
||||
* @returns {Promise<Number>} Orientation info
|
||||
*/
|
||||
function getOrientation (file) {
|
||||
const getData = arrayBuffer => {
|
||||
const view = new DataView(arrayBuffer)
|
||||
|
||||
// Not start with SOI(Start of image) Marker return fail value
|
||||
if (view.getUint16(0, false) !== 0xFFD8) return -2
|
||||
const length = view.byteLength
|
||||
let offset = 2
|
||||
while (offset < length) {
|
||||
const marker = view.getUint16(offset, false)
|
||||
offset += 2
|
||||
// Loop and seed for APP1 Marker
|
||||
if (marker === 0xFFE1) {
|
||||
// return fail value if it isn't EXIF data
|
||||
if (view.getUint32(offset += 2, false) !== 0x45786966) {
|
||||
return -1
|
||||
}
|
||||
// Read TIFF header,
|
||||
// First 2bytes defines byte align of TIFF data.
|
||||
// If it is 0x4949="II", it means "Intel" type byte align.
|
||||
// If it is 0x4d4d="MM", it means "Motorola" type byte align
|
||||
const little = view.getUint16(offset += 6, false) === 0x4949
|
||||
offset += view.getUint32(offset + 4, little)
|
||||
const tags = view.getUint16(offset, little) // Get TAG number
|
||||
offset += 2
|
||||
for (let i = 0; i < tags; i++) {
|
||||
// Loop to find Orientation TAG and return the value
|
||||
if (view.getUint16(offset + (i * 12), little) === 0x0112) {
|
||||
return view.getUint16(offset + (i * 12) + 8, little)
|
||||
}
|
||||
}
|
||||
} else if ((marker & 0xFF00) !== 0xFF00) { // If not start with 0xFF, not a Marker
|
||||
break
|
||||
} else {
|
||||
offset += view.getUint16(offset, false)
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = event => resolve(getData(event.target.result))
|
||||
reader.readAsArrayBuffer(file.slice(0, 64 * 1024))
|
||||
})
|
||||
}
|
||||
/**
|
||||
* @description
|
||||
* Rotate image file to correct direction.
|
||||
* Create a canvas and draw the image with correct direction, then export to base64 format.
|
||||
* @param {*} file the File object dropped.
|
||||
* @return {String} Base64 encoded image.
|
||||
*/
|
||||
function fixRotate (file) {
|
||||
return Promise.all([getImage(file), getOrientation(file)])
|
||||
.then(([img, orientation]) => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (orientation > 4 && orientation < 9) {
|
||||
canvas.width = img.height
|
||||
canvas.height = img.width
|
||||
} else {
|
||||
canvas.width = img.width
|
||||
canvas.height = img.height
|
||||
}
|
||||
switch (orientation) {
|
||||
case 2: ctx.transform(-1, 0, 0, 1, img.width, 0); break
|
||||
case 3: ctx.transform(-1, 0, 0, -1, img.width, img.height); break
|
||||
case 4: ctx.transform(1, 0, 0, -1, 0, img.height); break
|
||||
case 5: ctx.transform(0, 1, 1, 0, 0, 0); break
|
||||
case 6: ctx.transform(0, 1, -1, 0, img.height, 0); break
|
||||
case 7: ctx.transform(0, -1, -1, 0, img.height, img.width); break
|
||||
case 8: ctx.transform(0, -1, 1, 0, 0, img.width); break
|
||||
default: break
|
||||
}
|
||||
ctx.drawImage(img, 0, 0)
|
||||
return canvas.toDataURL()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @description
|
||||
@@ -38,26 +150,34 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr
|
||||
}
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(sourceFilePath)) {
|
||||
reject('source file does not exist')
|
||||
const isBase64 = typeof sourceFilePath === 'object' && sourceFilePath.type === 'base64'
|
||||
if (!fs.existsSync(sourceFilePath) && !isBase64) {
|
||||
return reject('source file does not exist')
|
||||
}
|
||||
|
||||
const targetStorage = findStorage.findStorage(storageKey)
|
||||
|
||||
const inputFileStream = fs.createReadStream(sourceFilePath)
|
||||
let destinationName
|
||||
if (useRandomName) {
|
||||
destinationName = `${uniqueSlug()}${path.extname(sourceFilePath)}`
|
||||
destinationName = `${uniqueSlug()}${path.extname(sourceFilePath.sourceFilePath || sourceFilePath)}`
|
||||
} else {
|
||||
destinationName = path.basename(sourceFilePath)
|
||||
destinationName = path.basename(sourceFilePath.sourceFilePath || sourceFilePath)
|
||||
}
|
||||
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
||||
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
||||
const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName))
|
||||
inputFileStream.pipe(outputFile)
|
||||
inputFileStream.on('end', () => {
|
||||
resolve(destinationName)
|
||||
})
|
||||
|
||||
if (isBase64) {
|
||||
const base64Data = sourceFilePath.data.replace(/^data:image\/\w+;base64,/, '')
|
||||
const dataBuffer = new Buffer(base64Data, 'base64')
|
||||
outputFile.write(dataBuffer, () => {
|
||||
resolve(destinationName)
|
||||
})
|
||||
} else {
|
||||
const inputFileStream = fs.createReadStream(sourceFilePath)
|
||||
inputFileStream.pipe(outputFile)
|
||||
inputFileStream.on('end', () => {
|
||||
resolve(destinationName)
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
return reject(e)
|
||||
}
|
||||
@@ -82,9 +202,9 @@ function createAttachmentDestinationFolder (destinationStoragePath, noteKey) {
|
||||
* @param noteKey Key of the current note
|
||||
*/
|
||||
function migrateAttachments (markdownContent, storagePath, noteKey) {
|
||||
if (sander.existsSync(path.join(storagePath, 'images'))) {
|
||||
if (noteKey !== undefined && sander.existsSync(path.join(storagePath, 'images'))) {
|
||||
const attachments = getAttachmentsInMarkdownContent(markdownContent) || []
|
||||
if (attachments !== []) {
|
||||
if (attachments.length) {
|
||||
createAttachmentDestinationFolder(storagePath, noteKey)
|
||||
}
|
||||
for (const attachment of attachments) {
|
||||
@@ -137,10 +257,17 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
||||
const filePath = file.path
|
||||
const originalFileName = path.basename(filePath)
|
||||
const fileType = file['type']
|
||||
|
||||
copyAttachment(filePath, storageKey, noteKey).then((fileName) => {
|
||||
const showPreview = fileType.startsWith('image')
|
||||
const imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), showPreview)
|
||||
const isImage = fileType.startsWith('image')
|
||||
let promise
|
||||
if (isImage) {
|
||||
promise = fixRotate(file).then(base64data => {
|
||||
return copyAttachment({type: 'base64', data: base64data, sourceFilePath: filePath}, storageKey, noteKey)
|
||||
})
|
||||
} else {
|
||||
promise = copyAttachment(filePath, storageKey, noteKey)
|
||||
}
|
||||
promise.then((fileName) => {
|
||||
const imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), isImage)
|
||||
codeEditor.insertAttachmentMd(imageMd)
|
||||
})
|
||||
}
|
||||
@@ -402,7 +529,6 @@ function handleAttachmentLinkPaste (storageKey, noteKey, linkText) {
|
||||
return modifiedLinkText
|
||||
})
|
||||
} else {
|
||||
console.log('One if the parameters was null -> Do nothing..')
|
||||
return Promise.resolve(linkText)
|
||||
}
|
||||
}
|
||||
|
||||
63
browser/main/lib/dataApi/exportStorage.js
Normal file
63
browser/main/lib/dataApi/exportStorage.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import { findStorage } from 'browser/lib/findStorage'
|
||||
import resolveStorageData from './resolveStorageData'
|
||||
import resolveStorageNotes from './resolveStorageNotes'
|
||||
import filenamify from 'filenamify'
|
||||
import * as path from 'path'
|
||||
import * as fs from 'fs'
|
||||
|
||||
/**
|
||||
* @param {String} storageKey
|
||||
* @param {String} fileType
|
||||
* @param {String} exportDir
|
||||
*
|
||||
* @return {Object}
|
||||
* ```
|
||||
* {
|
||||
* storage: Object,
|
||||
* fileType: String,
|
||||
* exportDir: String
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
|
||||
function exportStorage (storageKey, fileType, exportDir) {
|
||||
let targetStorage
|
||||
try {
|
||||
targetStorage = findStorage(storageKey)
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
|
||||
return resolveStorageData(targetStorage)
|
||||
.then(storage => (
|
||||
resolveStorageNotes(storage).then(notes => ({storage, notes}))
|
||||
))
|
||||
.then(function exportNotes (data) {
|
||||
const { storage, notes } = data
|
||||
const folderNamesMapping = {}
|
||||
storage.folders.forEach(folder => {
|
||||
const folderExportedDir = path.join(exportDir, filenamify(folder.name, {replacement: '_'}))
|
||||
folderNamesMapping[folder.key] = folderExportedDir
|
||||
// make sure directory exists
|
||||
try {
|
||||
fs.mkdirSync(folderExportedDir)
|
||||
} catch (e) {}
|
||||
})
|
||||
notes
|
||||
.filter(note => !note.isTrashed && note.type === 'MARKDOWN_NOTE')
|
||||
.forEach(markdownNote => {
|
||||
const folderExportedDir = folderNamesMapping[markdownNote.folder]
|
||||
const snippetName = `${filenamify(markdownNote.title, {replacement: '_'})}.${fileType}`
|
||||
const notePath = path.join(folderExportedDir, snippetName)
|
||||
fs.writeFileSync(notePath, markdownNote.content)
|
||||
})
|
||||
|
||||
return {
|
||||
storage,
|
||||
fileType,
|
||||
exportDir
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = exportStorage
|
||||
@@ -9,6 +9,7 @@ const dataApi = {
|
||||
deleteFolder: require('./deleteFolder'),
|
||||
reorderFolder: require('./reorderFolder'),
|
||||
exportFolder: require('./exportFolder'),
|
||||
exportStorage: require('./exportStorage'),
|
||||
createNote: require('./createNote'),
|
||||
updateNote: require('./updateNote'),
|
||||
deleteNote: require('./deleteNote'),
|
||||
|
||||
@@ -14,7 +14,6 @@ function renameStorage (key, name) {
|
||||
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
|
||||
} catch (err) {
|
||||
console.log('error got')
|
||||
console.error(err)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
|
||||
@@ -31,13 +31,9 @@ function resolveStorageData (storageCache) {
|
||||
|
||||
const version = parseInt(storage.version, 10)
|
||||
if (version >= 1) {
|
||||
if (version > 1) {
|
||||
console.log('The repository version is newer than one of current app.')
|
||||
}
|
||||
return Promise.resolve(storage)
|
||||
}
|
||||
|
||||
console.log('Transform Legacy storage', storage.path)
|
||||
return migrateFromV6Storage(storage.path)
|
||||
.then(() => storage)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ function resolveStorageNotes (storage) {
|
||||
notePathList = sander.readdirSync(notesDirPath)
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log(notesDirPath, ' doesn\'t exist.')
|
||||
console.error(notesDirPath, ' doesn\'t exist.')
|
||||
sander.mkdirSync(notesDirPath)
|
||||
} else {
|
||||
console.warn('Failed to find note dir', notesDirPath, err)
|
||||
|
||||
@@ -12,7 +12,6 @@ function toggleStorage (key, isOpen) {
|
||||
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
|
||||
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
|
||||
} catch (err) {
|
||||
console.log('error got')
|
||||
console.error(err)
|
||||
return Promise.reject(err)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ function once (name, listener) {
|
||||
}
|
||||
|
||||
function emit (name, ...args) {
|
||||
console.log(name)
|
||||
remote.getCurrentWindow().webContents.send(name, ...args)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,14 +14,13 @@ nodeIpc.connectTo(
|
||||
path.join(app.getPath('userData'), 'boostnote.service'),
|
||||
function () {
|
||||
nodeIpc.of.node.on('error', function (err) {
|
||||
console.log(err)
|
||||
console.error(err)
|
||||
})
|
||||
nodeIpc.of.node.on('connect', function () {
|
||||
console.log('Connected successfully')
|
||||
ipcRenderer.send('config-renew', {config: ConfigManager.get()})
|
||||
})
|
||||
nodeIpc.of.node.on('disconnect', function () {
|
||||
console.log('disconnected')
|
||||
return
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3,5 +3,8 @@ import ee from 'browser/main/lib/eventEmitter'
|
||||
module.exports = {
|
||||
'toggleMode': () => {
|
||||
ee.emit('topbar:togglemodebutton')
|
||||
},
|
||||
'deleteNote': () => {
|
||||
ee.emit('hotkey:deletenote')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,3 +128,29 @@ body[data-theme="monokai"]
|
||||
|
||||
.control-confirmButton
|
||||
colorMonokaiPrimaryButton()
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
modalDracula()
|
||||
width 500px
|
||||
height 270px
|
||||
overflow hidden
|
||||
position relative
|
||||
|
||||
.header
|
||||
background-color transparent
|
||||
border-color $ui-dark-borderColor
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.control-folder-label
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.control-folder-input
|
||||
border 1px solid $ui-input--create-folder-modal
|
||||
color white
|
||||
|
||||
.description
|
||||
color $ui-inactive-text-color
|
||||
|
||||
.control-confirmButton
|
||||
colorDraculaPrimaryButton()
|
||||
@@ -1,12 +1,9 @@
|
||||
import React from 'react'
|
||||
import CSSModules from 'browser/lib/CSSModules'
|
||||
import styles from './NewNoteModal.styl'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import { hashHistory } from 'react-router'
|
||||
import ee from 'browser/main/lib/eventEmitter'
|
||||
import ModalEscButton from 'browser/components/ModalEscButton'
|
||||
import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
|
||||
import i18n from 'browser/lib/i18n'
|
||||
import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote'
|
||||
|
||||
class NewNoteModal extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -24,31 +21,10 @@ class NewNoteModal extends React.Component {
|
||||
}
|
||||
|
||||
handleMarkdownNoteButtonClick (e) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_MARKDOWN')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||
const { storage, folder, dispatch, location } = this.props
|
||||
dataApi
|
||||
.createNote(storage, {
|
||||
type: 'MARKDOWN_NOTE',
|
||||
folder: folder,
|
||||
title: '',
|
||||
content: ''
|
||||
})
|
||||
.then(note => {
|
||||
const noteHash = note.key
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: { key: noteHash }
|
||||
})
|
||||
ee.emit('list:jump', noteHash)
|
||||
ee.emit('detail:focus')
|
||||
setTimeout(this.props.close, 200)
|
||||
})
|
||||
createMarkdownNote(storage, folder, dispatch, location).then(() => {
|
||||
setTimeout(this.props.close, 200)
|
||||
})
|
||||
}
|
||||
|
||||
handleMarkdownNoteButtonKeyDown (e) {
|
||||
@@ -59,38 +35,10 @@ class NewNoteModal extends React.Component {
|
||||
}
|
||||
|
||||
handleSnippetNoteButtonClick (e) {
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_SNIPPET')
|
||||
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('ADD_ALLNOTE')
|
||||
const { storage, folder, dispatch, location } = this.props
|
||||
|
||||
dataApi
|
||||
.createNote(storage, {
|
||||
type: 'SNIPPET_NOTE',
|
||||
folder: folder,
|
||||
title: '',
|
||||
description: '',
|
||||
snippets: [
|
||||
{
|
||||
name: '',
|
||||
mode: 'text',
|
||||
content: ''
|
||||
}
|
||||
]
|
||||
})
|
||||
.then(note => {
|
||||
const noteHash = note.key
|
||||
dispatch({
|
||||
type: 'UPDATE_NOTE',
|
||||
note: note
|
||||
})
|
||||
hashHistory.push({
|
||||
pathname: location.pathname,
|
||||
query: { key: noteHash }
|
||||
})
|
||||
ee.emit('list:jump', noteHash)
|
||||
ee.emit('detail:focus')
|
||||
setTimeout(this.props.close, 200)
|
||||
})
|
||||
const { storage, folder, dispatch, location, config } = this.props
|
||||
createSnippetNote(storage, folder, dispatch, location, config).then(() => {
|
||||
setTimeout(this.props.close, 200)
|
||||
})
|
||||
}
|
||||
|
||||
handleSnippetNoteButtonKeyDown (e) {
|
||||
|
||||
@@ -97,3 +97,20 @@ body[data-theme="monokai"]
|
||||
|
||||
.description
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
background-color transparent
|
||||
|
||||
.header
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.control-button
|
||||
border-color $ui-dracula-borderColor
|
||||
color $ui-dracula-text-color
|
||||
background-color transparent
|
||||
&:focus
|
||||
colorDraculaPrimaryButton()
|
||||
|
||||
.description
|
||||
color $ui-dracula-text-color
|
||||
@@ -43,7 +43,7 @@ class Blog extends React.Component {
|
||||
this.handleSettingError = (err) => {
|
||||
this.setState({BlogAlert: {
|
||||
type: 'error',
|
||||
message: err.message != null ? err.message : i18n.__('Error occurs!')
|
||||
message: err.message != null ? err.message : i18n.__('An error occurred!')
|
||||
}})
|
||||
}
|
||||
this.oldBlog = this.state.config.blog
|
||||
@@ -70,7 +70,7 @@ class Blog extends React.Component {
|
||||
this.props.haveToSave({
|
||||
tab: 'Blog',
|
||||
type: 'warning',
|
||||
message: i18n.__('You have to save!')
|
||||
message: i18n.__('Unsaved Changes!')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
line-height 30px
|
||||
|
||||
.group-section-label
|
||||
width 150px
|
||||
width 200px
|
||||
text-align left
|
||||
margin-right 10px
|
||||
font-size 14px
|
||||
@@ -68,9 +68,9 @@
|
||||
:global
|
||||
.alert
|
||||
display inline-block
|
||||
position absolute
|
||||
top 60px
|
||||
right 15px
|
||||
position fixed
|
||||
top 130px
|
||||
right 100px
|
||||
font-size 14px
|
||||
.success
|
||||
color #1EC38B
|
||||
@@ -127,7 +127,7 @@ colorDarkControl()
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
|
||||
colorSolarizedDarkControl()
|
||||
border none
|
||||
background-color $ui-solarized-dark-button-backgroundColor
|
||||
@@ -138,6 +138,10 @@ colorMonokaiControl()
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
colorDraculaControl()
|
||||
border none
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
@@ -165,27 +169,27 @@ body[data-theme="dark"]
|
||||
.group-section-control
|
||||
select, .group-section-control-input
|
||||
colorDarkControl()
|
||||
|
||||
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.group-header
|
||||
color $ui-solarized-dark-text-color
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
|
||||
.group-header2
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
.group-section-control-input
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
|
||||
.group-control
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
.group-control-leftButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
.group-control-rightButton
|
||||
colorSolarizedDarkPrimaryButton()
|
||||
.group-hint
|
||||
@@ -200,19 +204,19 @@ body[data-theme="monokai"]
|
||||
|
||||
.group-header
|
||||
color $ui-monokai-text-color
|
||||
border-color $ui-monokai-borderColor
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
.group-header2
|
||||
color $ui-monokai-text-color
|
||||
|
||||
.group-section-control-input
|
||||
border-color $ui-monokai-borderColor
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
.group-control
|
||||
border-color $ui-monokai-borderColor
|
||||
border-color $ui-monokai-borderColor
|
||||
.group-control-leftButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-monokai-borderColor
|
||||
border-color $ui-monokai-borderColor
|
||||
.group-control-rightButton
|
||||
colorMonokaiPrimaryButton()
|
||||
.group-hint
|
||||
@@ -220,3 +224,30 @@ body[data-theme="monokai"]
|
||||
.group-section-control
|
||||
select, .group-section-control-input
|
||||
colorMonokaiControl()
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.group-header
|
||||
color $ui-dracula-text-color
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.group-header2
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.group-section-control-input
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.group-control
|
||||
border-color $ui-dracula-borderColor
|
||||
.group-control-leftButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-dracula-borderColor
|
||||
.group-control-rightButton
|
||||
colorDraculaPrimaryButton()
|
||||
.group-hint
|
||||
colorDraculaControl()
|
||||
.group-section-control
|
||||
select, .group-section-control-input
|
||||
colorDraculaControl()
|
||||
@@ -23,21 +23,29 @@ class Crowdfunding extends React.Component {
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='header'>{i18n.__('Crowdfunding')}</div>
|
||||
<p>{i18n.__('Dear everyone,')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('Thank you for using Boostnote!')}</p>
|
||||
<p>{i18n.__('Boostnote is used in about 200 different countries and regions by an awesome community of developers.')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('To continue supporting this growth, and to satisfy community expectations,')}</p>
|
||||
<p>{i18n.__('we would like to invest more time and resources in this project.')}</p>
|
||||
<p>{i18n.__('We launched IssueHunt which is an issue-based crowdfunding / sourcing platform for open source projects.')}</p>
|
||||
<p>{i18n.__('Anyone can put a bounty on not only a bug but also on OSS feature requests listed on IssueHunt. Collected funds will be distributed to project owners and contributors.')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('If you like this project and see its potential, you can help by supporting us on OpenCollective!')}</p>
|
||||
<p>{i18n.__('### Sustainable Open Source Ecosystem')}</p>
|
||||
<p>{i18n.__('We discussed about open-source ecosystem and IssueHunt concept with the Boostnote team repeatedly. We actually also discussed with Matz who father of Ruby.')}</p>
|
||||
<p>{i18n.__('The original reason why we made IssueHunt was to reward our contributors of Boostnote project. We’ve got tons of Github stars and hundred of contributors in two years.')}</p>
|
||||
<p>{i18n.__('We thought that it will be nice if we can pay reward for our contributors.')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('Thanks,')}</p>
|
||||
<p>{i18n.__('Boostnote maintainers')}</p>
|
||||
<p>{i18n.__('### We believe Meritocracy')}</p>
|
||||
<p>{i18n.__('We think developers who has skill and did great things must be rewarded properly.')}</p>
|
||||
<p>{i18n.__('OSS projects are used in everywhere on the internet, but no matter how they great, most of owners of those projects need to have another job to sustain their living.')}</p>
|
||||
<p>{i18n.__('It sometimes looks like exploitation.')}</p>
|
||||
<p>{i18n.__('We’ve realized IssueHunt could enhance sustainability of open-source ecosystem.')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('As same as issues of Boostnote are already funded on IssueHunt, your open-source projects can be also started funding from now.')}</p>
|
||||
<br />
|
||||
<p>{i18n.__('Thank you,')}</p>
|
||||
<p>{i18n.__('The Boostnote Team')}</p>
|
||||
<br />
|
||||
<button styleName='cf-link'>
|
||||
<a href='https://opencollective.com/boostnoteio' onClick={(e) => this.handleLinkClick(e)}>{i18n.__('Support via OpenCollective')}</a>
|
||||
<a href='http://bit.ly/issuehunt-from-boostnote-app' onClick={(e) => this.handleLinkClick(e)}>{i18n.__('See IssueHunt')}</a>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -29,7 +29,7 @@ p
|
||||
body[data-theme="dark"]
|
||||
p
|
||||
color $ui-dark-text-color
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
color $ui-solarized-dark-text-color
|
||||
@@ -41,3 +41,9 @@ body[data-theme="monokai"]
|
||||
color $ui-monokai-text-color
|
||||
p
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
p
|
||||
color $ui-dracula-text-color
|
||||
@@ -2,6 +2,7 @@
|
||||
height 35px
|
||||
box-sizing border-box
|
||||
padding 2.5px 15px
|
||||
display flex
|
||||
&:hover
|
||||
background-color darken(white, 3%)
|
||||
|
||||
@@ -18,7 +19,10 @@
|
||||
border-left solid 2px transparent
|
||||
padding 0 10px
|
||||
line-height 30px
|
||||
float left
|
||||
flex 1
|
||||
white-space nowrap
|
||||
text-overflow ellipsis
|
||||
overflow hidden
|
||||
.folderItem-left-danger
|
||||
color $danger-color
|
||||
font-weight bold
|
||||
@@ -52,7 +56,8 @@
|
||||
outline none
|
||||
|
||||
.folderItem-right
|
||||
float right
|
||||
-webkit-box-flex: 1
|
||||
white-space nowrap
|
||||
|
||||
.folderItem-right-button
|
||||
vertical-align middle
|
||||
@@ -149,3 +154,26 @@ body[data-theme="monokai"]
|
||||
|
||||
.folderItem-right-dangerButton
|
||||
colorMonokaiPrimaryButton()
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.folderItem
|
||||
&:hover
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
|
||||
.folderItem-left-danger
|
||||
color $danger-color
|
||||
|
||||
.folderItem-left-key
|
||||
color $ui-dark-inactive-text-color
|
||||
|
||||
.folderItem-left-colorButton
|
||||
colorDraculaPrimaryButton()
|
||||
|
||||
.folderItem-right-button
|
||||
colorDraculaPrimaryButton()
|
||||
|
||||
.folderItem-right-confirmButton
|
||||
colorDraculaPrimaryButton()
|
||||
|
||||
.folderItem-right-dangerButton
|
||||
colorDraculaPrimaryButton()
|
||||
@@ -28,10 +28,20 @@ class HotkeyTab extends React.Component {
|
||||
}})
|
||||
}
|
||||
this.handleSettingError = (err) => {
|
||||
this.setState({keymapAlert: {
|
||||
type: 'error',
|
||||
message: err.message != null ? err.message : i18n.__('Error occurs!')
|
||||
}})
|
||||
if (
|
||||
this.state.config.hotkey.toggleMain === '' ||
|
||||
this.state.config.hotkey.toggleMode === ''
|
||||
) {
|
||||
this.setState({keymapAlert: {
|
||||
type: 'success',
|
||||
message: i18n.__('Successfully applied!')
|
||||
}})
|
||||
} else {
|
||||
this.setState({keymapAlert: {
|
||||
type: 'error',
|
||||
message: err.message != null ? err.message : i18n.__('An error occurred!')
|
||||
}})
|
||||
}
|
||||
}
|
||||
this.oldHotkey = this.state.config.hotkey
|
||||
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
|
||||
@@ -68,7 +78,8 @@ class HotkeyTab extends React.Component {
|
||||
const { config } = this.state
|
||||
config.hotkey = {
|
||||
toggleMain: this.refs.toggleMain.value,
|
||||
toggleMode: this.refs.toggleMode.value
|
||||
toggleMode: this.refs.toggleMode.value,
|
||||
deleteNote: this.refs.deleteNote.value
|
||||
}
|
||||
this.setState({
|
||||
config
|
||||
@@ -79,7 +90,7 @@ class HotkeyTab extends React.Component {
|
||||
this.props.haveToSave({
|
||||
tab: 'Hotkey',
|
||||
type: 'warning',
|
||||
message: i18n.__('You have to save!')
|
||||
message: i18n.__('Unsaved Changes!')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -117,7 +128,7 @@ class HotkeyTab extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Toggle editor mode')}</div>
|
||||
<div styleName='group-section-label'>{i18n.__('Toggle Editor Mode')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
onChange={(e) => this.handleHotkeyChange(e)}
|
||||
@@ -127,6 +138,17 @@ class HotkeyTab extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Delete Note')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
onChange={(e) => this.handleHotkeyChange(e)}
|
||||
ref='deleteNote'
|
||||
value={config.hotkey.deleteNote}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-control'>
|
||||
<button styleName='group-control-leftButton'
|
||||
onClick={(e) => this.handleHintToggleButtonClick(e)}
|
||||
|
||||
@@ -84,7 +84,7 @@ class InfoTab extends React.Component {
|
||||
>{i18n.__('GitHub')}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href='https://boostlog.io/@junp1234'
|
||||
<a href='https://medium.com/boostnote'
|
||||
onClick={(e) => this.handleLinkClick(e)}
|
||||
>{i18n.__('Blog')}</a>
|
||||
</li>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
body[data-theme="dark"]
|
||||
.root
|
||||
color alpha($tab--dark-text-color, 80%)
|
||||
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
@@ -75,3 +75,10 @@ body[data-theme="monokai"]
|
||||
.list
|
||||
a
|
||||
color $ui-monokai-active-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
.list
|
||||
a
|
||||
color $ui-dracula-active-color
|
||||
|
||||
@@ -90,7 +90,7 @@ body[data-theme="dark"]
|
||||
background-color $dark-primary-button-background--active
|
||||
&:hover
|
||||
color white
|
||||
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.root
|
||||
@@ -139,3 +139,27 @@ body[data-theme="monokai"]
|
||||
background-color $ui-monokai-button--active-backgroundColor
|
||||
&:hover
|
||||
color white
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
background-color transparent
|
||||
.top-bar
|
||||
background-color transparent
|
||||
border-color $ui-dracula-borderColor
|
||||
p
|
||||
color $ui-dracula-text-color
|
||||
.nav
|
||||
background-color transparent
|
||||
border-color $ui-dracula-borderColor
|
||||
.nav-button
|
||||
background-color transparent
|
||||
color $ui-dracula-text-color
|
||||
&:hover
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.nav-button--active
|
||||
@extend .nav-button
|
||||
color $ui-dracula-button--active-color
|
||||
background-color $ui-dracula-button--active-backgroundColor
|
||||
&:hover
|
||||
color #f8f8f2
|
||||
@@ -22,7 +22,7 @@ class SnippetList extends React.Component {
|
||||
reloadSnippetList () {
|
||||
dataApi.fetchSnippet().then(snippets => {
|
||||
this.setState({snippets})
|
||||
this.props.onSnippetSelect(snippets[0])
|
||||
this.props.onSnippetSelect(this.props.currentSnippet)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -55,7 +55,11 @@ class SnippetList extends React.Component {
|
||||
|
||||
defineSnippetStyleName (snippet) {
|
||||
const { currentSnippet } = this.props
|
||||
if (currentSnippet == null) return
|
||||
|
||||
if (currentSnippet == null) {
|
||||
return 'snippet-item'
|
||||
}
|
||||
|
||||
if (currentSnippet.id === snippet.id) {
|
||||
return 'snippet-item-selected'
|
||||
} else {
|
||||
|
||||
@@ -6,6 +6,9 @@ import i18n from 'browser/lib/i18n'
|
||||
import dataApi from 'browser/main/lib/dataApi'
|
||||
import SnippetList from './SnippetList'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import copy from 'copy-to-clipboard'
|
||||
|
||||
const path = require('path')
|
||||
|
||||
class SnippetTab extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -16,6 +19,17 @@ class SnippetTab extends React.Component {
|
||||
this.changeDelay = null
|
||||
}
|
||||
|
||||
notify (title, options) {
|
||||
if (global.process.platform === 'win32') {
|
||||
options.icon = path.join(
|
||||
'file://',
|
||||
global.__dirname,
|
||||
'../../resources/app.png'
|
||||
)
|
||||
}
|
||||
return new window.Notification(title, options)
|
||||
}
|
||||
|
||||
handleSnippetNameOrPrefixChange () {
|
||||
clearTimeout(this.changeDelay)
|
||||
this.changeDelay = setTimeout(() => {
|
||||
@@ -27,12 +41,14 @@ class SnippetTab extends React.Component {
|
||||
|
||||
handleSnippetSelect (snippet) {
|
||||
const { currentSnippet } = this.state
|
||||
if (currentSnippet === null || currentSnippet.id !== snippet.id) {
|
||||
dataApi.fetchSnippet(snippet.id).then(changedSnippet => {
|
||||
// notify the snippet editor to load the content of the new snippet
|
||||
this.snippetEditor.onSnippetChanged(changedSnippet)
|
||||
this.setState({currentSnippet: changedSnippet})
|
||||
})
|
||||
if (snippet !== null) {
|
||||
if (currentSnippet === null || currentSnippet.id !== snippet.id) {
|
||||
dataApi.fetchSnippet(snippet.id).then(changedSnippet => {
|
||||
// notify the snippet editor to load the content of the new snippet
|
||||
this.snippetEditor.onSnippetChanged(changedSnippet)
|
||||
this.setState({currentSnippet: changedSnippet})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +70,17 @@ class SnippetTab extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleCopySnippet (e) {
|
||||
const showCopyNotification = this.props.config.ui.showCopyNotification
|
||||
copy(this.state.currentSnippet.content)
|
||||
if (showCopyNotification) {
|
||||
this.notify('Saved to Clipboard!', {
|
||||
body: 'Paste it wherever you want!',
|
||||
silent: true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { config, storageKey } = this.props
|
||||
const { currentSnippet } = this.state
|
||||
@@ -70,6 +97,13 @@ class SnippetTab extends React.Component {
|
||||
onSnippetDeleted={this.handleDeleteSnippet.bind(this)}
|
||||
currentSnippet={currentSnippet} />
|
||||
<div styleName='snippet-detail' style={{visibility: currentSnippet ? 'visible' : 'hidden'}}>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-control'>
|
||||
<button styleName='group-control-rightButton'
|
||||
onClick={e => this.handleCopySnippet(e)}>{i18n.__('Copy')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Snippet name')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
|
||||
@@ -196,3 +196,19 @@ body[data-theme="monokai"]
|
||||
color white
|
||||
.group-control-button
|
||||
colorMonokaiPrimaryButton()
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.snippets
|
||||
background $ui-dracula-backgroundColor
|
||||
.snippet-item
|
||||
color #f8f8f2
|
||||
&::after
|
||||
background $ui-dracula-borderColor
|
||||
&:hover
|
||||
background darken($ui-dracula-backgroundColor, 5)
|
||||
.snippet-item-selected
|
||||
background darken($ui-dracula-backgroundColor, 5)
|
||||
.snippet-detail
|
||||
color #f8f8f2
|
||||
.group-control-button
|
||||
colorDraculaPrimaryButton()
|
||||
@@ -9,13 +9,17 @@
|
||||
box-sizing border-box
|
||||
border-bottom $default-border
|
||||
margin-bottom 5px
|
||||
display flex
|
||||
|
||||
.header-label
|
||||
float left
|
||||
cursor pointer
|
||||
&:hover
|
||||
.header-label-editButton
|
||||
opacity 1
|
||||
flex 1
|
||||
white-space nowrap
|
||||
text-overflow ellipsis
|
||||
overflow hidden
|
||||
|
||||
.header-label-path
|
||||
color $ui-inactive-text-color
|
||||
@@ -38,8 +42,8 @@
|
||||
outline none
|
||||
|
||||
.header-control
|
||||
float right
|
||||
|
||||
-webkit-box-flex: 1
|
||||
white-space nowrap
|
||||
.header-control-button
|
||||
width 30px
|
||||
height 25px
|
||||
|
||||
@@ -70,7 +70,7 @@ class StoragesTab extends React.Component {
|
||||
})
|
||||
return (
|
||||
<div styleName='list'>
|
||||
<div styleName='header'>{i18n.__('Storages')}</div>
|
||||
<div styleName='header'>{i18n.__('Storage Locations')}</div>
|
||||
{storageList.length > 0
|
||||
? storageList
|
||||
: <div styleName='list-empty'>{i18n.__('No storage found.')}</div>
|
||||
|
||||
@@ -158,7 +158,7 @@ body[data-theme="dark"]
|
||||
.addStorage-body-control-cancelButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-dark-borderColor
|
||||
|
||||
|
||||
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
@@ -236,3 +236,41 @@ body[data-theme="monokai"]
|
||||
.addStorage-body-control-cancelButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-monokai-borderColor
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.root
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.folderList-item
|
||||
border-bottom $ui-dracula-borderColor
|
||||
|
||||
.folderList-empty
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.list-empty
|
||||
color $ui-dracula-text-color
|
||||
.list-control-addStorageButton
|
||||
border-color $ui-dracula-button-backgroundColor
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.addStorage-header
|
||||
color $ui-dracula-text-color
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.addStorage-body-section-name-input
|
||||
border-color $$ui-dracula-borderColor
|
||||
|
||||
.addStorage-body-section-type-description
|
||||
color $ui-dracula-text-color
|
||||
|
||||
.addStorage-body-section-path-button
|
||||
colorPrimaryButton()
|
||||
.addStorage-body-control
|
||||
border-color $ui-dracula-borderColor
|
||||
|
||||
.addStorage-body-control-createButton
|
||||
colorDarkPrimaryButton()
|
||||
.addStorage-body-control-cancelButton
|
||||
colorDarkDefaultButton()
|
||||
border-color $ui-dracula-borderColor
|
||||
@@ -40,7 +40,7 @@ class UiTab extends React.Component {
|
||||
this.handleSettingError = (err) => {
|
||||
this.setState({UiAlert: {
|
||||
type: 'error',
|
||||
message: err.message != null ? err.message : i18n.__('Error occurs!')
|
||||
message: err.message != null ? err.message : i18n.__('An error occurred!')
|
||||
}})
|
||||
}
|
||||
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
|
||||
@@ -67,9 +67,13 @@ class UiTab extends React.Component {
|
||||
ui: {
|
||||
theme: this.refs.uiTheme.value,
|
||||
language: this.refs.uiLanguage.value,
|
||||
defaultNote: this.refs.defaultNote.value,
|
||||
showCopyNotification: this.refs.showCopyNotification.checked,
|
||||
confirmDeletion: this.refs.confirmDeletion.checked,
|
||||
showOnlyRelatedTags: this.refs.showOnlyRelatedTags.checked,
|
||||
showTagsAlphabetically: this.refs.showTagsAlphabetically.checked,
|
||||
saveTagsAlphabetically: this.refs.saveTagsAlphabetically.checked,
|
||||
enableLiveNoteCounts: this.refs.enableLiveNoteCounts.checked,
|
||||
disableDirectWrite: this.refs.uiD2w != null
|
||||
? this.refs.uiD2w.checked
|
||||
: false
|
||||
@@ -85,8 +89,12 @@ class UiTab extends React.Component {
|
||||
displayLineNumbers: this.refs.editorDisplayLineNumbers.checked,
|
||||
switchPreview: this.refs.editorSwitchPreview.value,
|
||||
keyMap: this.refs.editorKeyMap.value,
|
||||
snippetDefaultLanguage: this.refs.editorSnippetDefaultLanguage.value,
|
||||
scrollPastEnd: this.refs.scrollPastEnd.checked,
|
||||
fetchUrlTitle: this.refs.editorFetchUrlTitle.checked
|
||||
fetchUrlTitle: this.refs.editorFetchUrlTitle.checked,
|
||||
enableTableEditor: this.refs.enableTableEditor.checked,
|
||||
enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked,
|
||||
frontMatterTitleField: this.refs.frontMatterTitleField.value
|
||||
},
|
||||
preview: {
|
||||
fontSize: this.refs.previewFontSize.value,
|
||||
@@ -99,11 +107,13 @@ class UiTab extends React.Component {
|
||||
latexBlockClose: this.refs.previewLatexBlockClose.value,
|
||||
plantUMLServerAddress: this.refs.previewPlantUMLServerAddress.value,
|
||||
scrollPastEnd: this.refs.previewScrollPastEnd.checked,
|
||||
scrollSync: this.refs.previewScrollSync.checked,
|
||||
smartQuotes: this.refs.previewSmartQuotes.checked,
|
||||
breaks: this.refs.previewBreaks.checked,
|
||||
smartArrows: this.refs.previewSmartArrows.checked,
|
||||
sanitize: this.refs.previewSanitize.value,
|
||||
allowCustomCSS: this.refs.previewAllowCustomCSS.checked,
|
||||
lineThroughCheckbox: this.refs.lineThroughCheckbox.checked,
|
||||
customCSS: this.customCSSCM.getCodeMirror().getValue()
|
||||
}
|
||||
}
|
||||
@@ -122,7 +132,7 @@ class UiTab extends React.Component {
|
||||
this.props.haveToSave({
|
||||
tab: 'UI',
|
||||
type: 'warning',
|
||||
message: i18n.__('You have to save!')
|
||||
message: i18n.__('Unsaved Changes!')
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -172,7 +182,9 @@ class UiTab extends React.Component {
|
||||
<div styleName='group-header'>{i18n.__('Interface')}</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
{i18n.__('Interface Theme')}
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Interface Theme')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<select value={config.ui.theme}
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
@@ -182,13 +194,16 @@ class UiTab extends React.Component {
|
||||
<option value='white'>{i18n.__('White')}</option>
|
||||
<option value='solarized-dark'>{i18n.__('Solarized Dark')}</option>
|
||||
<option value='monokai'>{i18n.__('Monokai')}</option>
|
||||
<option value='dracula'>{i18n.__('Dracula')}</option>
|
||||
<option value='dark'>{i18n.__('Dark')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
{i18n.__('Language')}
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Language')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<select value={config.ui.language}
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
@@ -201,6 +216,22 @@ class UiTab extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Default New Note')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<select value={config.ui.defaultNote}
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
ref='defaultNote'
|
||||
>
|
||||
<option value='ALWAYS_ASK'>{i18n.__('Always Ask')}</option>
|
||||
<option value='MARKDOWN_NOTE'>{i18n.__('Markdown Note')}</option>
|
||||
<option value='SNIPPET_NOTE'>{i18n.__('Snippet Note')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
@@ -221,16 +252,6 @@ class UiTab extends React.Component {
|
||||
{i18n.__('Show a confirmation dialog when deleting notes')}
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.ui.showOnlyRelatedTags}
|
||||
ref='showOnlyRelatedTags'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Show only related tags')}
|
||||
</label>
|
||||
</div>
|
||||
{
|
||||
global.process.platform === 'win32'
|
||||
? <div styleName='group-checkBoxSection'>
|
||||
@@ -246,6 +267,52 @@ class UiTab extends React.Component {
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
<div styleName='group-header2'>Tags</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.ui.saveTagsAlphabetically}
|
||||
ref='saveTagsAlphabetically'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Save tags of a note in alphabetical order')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.ui.showTagsAlphabetically}
|
||||
ref='showTagsAlphabetically'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Show tags of a note in alphabetical order')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.ui.showOnlyRelatedTags}
|
||||
ref='showOnlyRelatedTags'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Show only related tags')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.ui.enableLiveNoteCounts}
|
||||
ref='enableLiveNoteCounts'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Enable live count of notes')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-header2'>Editor</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
@@ -387,6 +454,47 @@ class UiTab extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Snippet Default Language')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<select value={config.editor.snippetDefaultLanguage}
|
||||
ref='editorSnippetDefaultLanguage'
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
>
|
||||
{
|
||||
_.sortBy(CodeMirror.modeInfo.map(mode => mode.name)).map(name => (<option key={name} value={name}>{name}</option>))
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
{i18n.__('Front matter title field')}
|
||||
</div>
|
||||
<div styleName='group-section-control'>
|
||||
<input styleName='group-section-control-input'
|
||||
ref='frontMatterTitleField'
|
||||
value={config.editor.frontMatterTitleField}
|
||||
onChange={(e) => this.handleUIChange(e)}
|
||||
type='text'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.editor.enableFrontMatterTitle}
|
||||
ref='enableFrontMatterTitle'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Extract title from front matter')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
@@ -420,6 +528,17 @@ class UiTab extends React.Component {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.editor.enableTableEditor}
|
||||
ref='enableTableEditor'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Enable smart table editor')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div styleName='group-header2'>{i18n.__('Preview')}</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>
|
||||
@@ -448,7 +567,7 @@ class UiTab extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-section'>
|
||||
<div styleName='group-section-label'>{i18n.__('Code block Theme')}</div>
|
||||
<div styleName='group-section-label'>{i18n.__('Code Block Theme')}</div>
|
||||
<div styleName='group-section-control'>
|
||||
<select value={config.preview.codeBlockTheme}
|
||||
ref='previewCodeBlockTheme'
|
||||
@@ -462,6 +581,16 @@ class UiTab extends React.Component {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.preview.lineThroughCheckbox}
|
||||
ref='lineThroughCheckbox'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('Allow line through checkbox')}
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
@@ -472,6 +601,16 @@ class UiTab extends React.Component {
|
||||
{i18n.__('Allow preview to scroll past the last line')}
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
checked={this.state.config.preview.scrollSync}
|
||||
ref='previewScrollSync'
|
||||
type='checkbox'
|
||||
/>
|
||||
{i18n.__('When scrolling, synchronize preview with editor')}
|
||||
</label>
|
||||
</div>
|
||||
<div styleName='group-checkBoxSection'>
|
||||
<label>
|
||||
<input onChange={(e) => this.handleUIChange(e)}
|
||||
|
||||
@@ -113,7 +113,6 @@ function data (state = defaultDataMap(), action) {
|
||||
|
||||
// If storage chanced, origin key must be discarded
|
||||
if (originKey !== uniqueKey) {
|
||||
console.log('diffrent storage')
|
||||
// From isStarred
|
||||
if (originNote.isStarred) {
|
||||
state.starredSet = new Set(state.starredSet)
|
||||
@@ -216,16 +215,10 @@ function data (state = defaultDataMap(), action) {
|
||||
return state
|
||||
}
|
||||
case 'UPDATE_FOLDER':
|
||||
state = Object.assign({}, state)
|
||||
state.storageMap = new Map(state.storageMap)
|
||||
state.storageMap.set(action.storage.key, action.storage)
|
||||
return state
|
||||
case 'REORDER_FOLDER':
|
||||
state = Object.assign({}, state)
|
||||
state.storageMap = new Map(state.storageMap)
|
||||
state.storageMap.set(action.storage.key, action.storage)
|
||||
return state
|
||||
case 'EXPORT_FOLDER':
|
||||
case 'RENAME_STORAGE':
|
||||
case 'EXPORT_STORAGE':
|
||||
state = Object.assign({}, state)
|
||||
state.storageMap = new Map(state.storageMap)
|
||||
state.storageMap.set(action.storage.key, action.storage)
|
||||
@@ -355,11 +348,6 @@ function data (state = defaultDataMap(), action) {
|
||||
})
|
||||
}
|
||||
return state
|
||||
case 'RENAME_STORAGE':
|
||||
state = Object.assign({}, state)
|
||||
state.storageMap = new Map(state.storageMap)
|
||||
state.storageMap.set(action.storage.key, action.storage)
|
||||
return state
|
||||
case 'EXPAND_STORAGE':
|
||||
state = Object.assign({}, state)
|
||||
state.storageMap = new Map(state.storageMap)
|
||||
|
||||
126
browser/styles/Detail/TagSelect.styl
Normal file
126
browser/styles/Detail/TagSelect.styl
Normal file
@@ -0,0 +1,126 @@
|
||||
.TagSelect
|
||||
.react-autosuggest__input
|
||||
box-sizing border-box
|
||||
border none
|
||||
background-color transparent
|
||||
outline none
|
||||
padding 0 4px
|
||||
font-size 13px
|
||||
|
||||
ul
|
||||
position fixed
|
||||
z-index 999
|
||||
box-sizing border-box
|
||||
list-style none
|
||||
padding 0
|
||||
margin 0
|
||||
|
||||
border-radius 4px
|
||||
margin .5rem 0 0
|
||||
background-color $ui-noteList-backgroundColor
|
||||
border 1px solid rgba(0,0,0,.3)
|
||||
box-shadow .05em .2em .6em rgba(0,0,0,.2)
|
||||
text-shadow none
|
||||
|
||||
&:empty,
|
||||
&[hidden]
|
||||
display none
|
||||
|
||||
&:before
|
||||
content ""
|
||||
position absolute
|
||||
top -7px
|
||||
left 14px
|
||||
width 0 height 0
|
||||
padding 6px
|
||||
background-color $ui-noteList-backgroundColor
|
||||
border inherit
|
||||
border-right 0
|
||||
border-bottom 0
|
||||
-webkit-transform rotate(45deg)
|
||||
transform rotate(45deg)
|
||||
|
||||
li
|
||||
position relative
|
||||
padding 6px 18px 6px 10px
|
||||
cursor pointer
|
||||
|
||||
li[aria-selected="true"]
|
||||
background-color alpha($ui-button--active-backgroundColor, 40%)
|
||||
color $ui-text-color
|
||||
|
||||
body[data-theme="dark"]
|
||||
.TagSelect
|
||||
.react-autosuggest__input
|
||||
color $ui-dark-text-color
|
||||
|
||||
ul
|
||||
border-color $ui-dark-borderColor
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
&:before
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
li[aria-selected="true"]
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-dark-text-color
|
||||
|
||||
body[data-theme="monokai"]
|
||||
.TagSelect
|
||||
.react-autosuggest__input
|
||||
color $ui-monokai-text-color
|
||||
|
||||
ul
|
||||
border-color $ui-monokai-borderColor
|
||||
background-color $ui-monokai-noteList-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
&:before
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
li[aria-selected="true"]
|
||||
background-color $ui-monokai-button-backgroundColor
|
||||
color $ui-monokai-text-color
|
||||
|
||||
body[data-theme="dracula"]
|
||||
.TagSelect
|
||||
.react-autosuggest__input
|
||||
color $ui-dracula-text-color
|
||||
|
||||
ul
|
||||
border-color $ui-dracula-borderColor
|
||||
background-color $ui-dracula-noteList-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
|
||||
&:before
|
||||
background-color $ui-dark-noteList-backgroundColor
|
||||
|
||||
li[aria-selected="true"]
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
color $ui-dracula-text-color
|
||||
|
||||
body[data-theme="solarized-dark"]
|
||||
.TagSelect
|
||||
.react-autosuggest__input
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
ul
|
||||
border-color $ui-solarized-dark-borderColor
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
&:before
|
||||
background-color $ui-solarized-dark-noteList-backgroundColor
|
||||
|
||||
li[aria-selected="true"]
|
||||
background-color $ui-dark-button--active-backgroundColor
|
||||
color $ui-solarized-dark-text-color
|
||||
|
||||
body[data-theme="white"]
|
||||
.TagSelect
|
||||
ul
|
||||
background-color $ui-white-noteList-backgroundColor
|
||||
|
||||
li[aria-selected="true"]
|
||||
background-color $ui-button--active-backgroundColor
|
||||
@@ -5,7 +5,7 @@ $danger-color = #c9302c
|
||||
$danger-lighten-color = lighten(#c9302c, 5%)
|
||||
|
||||
// Layouts
|
||||
$statusBar-height = 22px
|
||||
$statusBar-height = 28px
|
||||
$sideNav-width = 200px
|
||||
$sideNav--folded-width = 44px
|
||||
$topBar-height = 60px
|
||||
@@ -128,6 +128,16 @@ colorMonokaiPrimaryButton()
|
||||
&:active:hover
|
||||
background-color $dark-primary-button-background--active
|
||||
|
||||
colorDraculaPrimaryButton()
|
||||
color $ui-dracula-text-color
|
||||
background-color $ui-dracula-button-backgroundColor
|
||||
border none
|
||||
&:hover
|
||||
background-color $ui-dracula-button--active-backgroundColor
|
||||
&:active
|
||||
&:active:hover
|
||||
background-color $ui-dracula-button--active-backgroundColor
|
||||
|
||||
|
||||
// Danger button(Brand color)
|
||||
$danger-button-background = #c9302c
|
||||
@@ -369,7 +379,7 @@ $ui-monokai-active-color = #f92672
|
||||
|
||||
$ui-monokai-borderColor = #373831
|
||||
|
||||
$ui-monokai-tag-backgroundColor = #f92672
|
||||
$ui-monokai-tag-backgroundColor = #373831
|
||||
|
||||
$ui-monokai-button-backgroundColor = #373831
|
||||
$ui-monokai-button--active-color = white
|
||||
@@ -377,10 +387,36 @@ $ui-monokai-button--active-backgroundColor = #f92672
|
||||
$ui-monokai-button--hover-backgroundColor = lighten($ui-dark-backgroundColor, 10%)
|
||||
$ui-monokai-button--focus-borderColor = lighten(#369DCD, 25%)
|
||||
|
||||
modalmonokai()
|
||||
modalMonokai()
|
||||
position relative
|
||||
z-index $modal-z-index
|
||||
width 100%
|
||||
background-color $ui-monokai-backgroundColor
|
||||
overflow hidden
|
||||
border-radius $modal-border-radius
|
||||
|
||||
/******* Dracula theme ********/
|
||||
$ui-dracula-backgroundColor = #282a36
|
||||
$ui-dracula-noteList-backgroundColor = #282a36
|
||||
$ui-dracula-noteDetail-backgroundColor = #282a36
|
||||
|
||||
$ui-dracula-text-color = #f8f8f2
|
||||
$ui-dracula-active-color = #bd93f9
|
||||
|
||||
$ui-dracula-borderColor = #44475a
|
||||
|
||||
$ui-dracula-tag-backgroundColor = #8be9fd
|
||||
|
||||
$ui-dracula-button-backgroundColor = #44475a
|
||||
$ui-dracula-button--active-color = #f8f8f2
|
||||
$ui-dracula-button--active-backgroundColor = #bd93f9
|
||||
$ui-dracula-button--hover-backgroundColor = lighten($ui-dracula-backgroundColor, 10%)
|
||||
$ui-dracula-button--focus-borderColor = lighten(#44475a, 25%)
|
||||
|
||||
modalDracula()
|
||||
position relative
|
||||
z-index $modal-z-index
|
||||
width 100%
|
||||
background-color $ui-dracula-backgroundColor
|
||||
overflow hidden
|
||||
border-radius $modal-border-radius
|
||||
@@ -1,18 +1,18 @@
|
||||
# Contributing to Boostnote (English)
|
||||
|
||||
### When you open an issue of a bug report
|
||||
There are no issue template. But there is a request.
|
||||
### When you open an issue or a bug report
|
||||
There is no issue template, but there is a request.
|
||||
|
||||
**Please paste screenshots of Boostnote with developer tool open**
|
||||
**Please paste screenshots of Boostnote with the developer tool open**
|
||||
|
||||
Thank you for your help in advance.
|
||||
Thank you in advance for your help.
|
||||
|
||||
### About copyright of Pull Request
|
||||
### Concerning Copyright
|
||||
|
||||
If you make a pull request, It means you agree to transfer the copyright of the code changes to BoostIO.
|
||||
By making a pull request you agree to transfer ownership of your code to BoostIO.
|
||||
|
||||
It doesn't mean Boostnote will become a paid app. If we want to earn some money, We will try other way, which is some kind of cloud storage, Mobile app integration or some SPECIAL features.
|
||||
Because GPL v3 is too strict to be compatible with any other License, We thought this is needed to replace the license with much freer one(like BSD, MIT) somewhen.
|
||||
This doesn't mean Boostnote will become a paid app. If we want to earn money, we will find other way. Potentially some kind of cloud storage, mobile app integration, or some premium features.
|
||||
GPL v3 is too strict to be compatible with another license, so we thought it might be necessary to replace the license with a more open one (like BSD, MIT) eventually.
|
||||
|
||||
---
|
||||
|
||||
@@ -86,4 +86,23 @@ Pull requestをすることはその変化分のコードの著作権をBoostIO
|
||||
如果您提供了一个Pull Request,这表示您将您所修改的代码的著作权移交给BoostIO。
|
||||
|
||||
这并不表示Boostnote会成为一个需要付费的软件。如果我们想获得收益,我们会尝试一些其他的方法,比如说云存储、绑定手机软件等。
|
||||
因为GPLv3过于严格,不能和其他的一些协议兼容,所以我们有可能在将来会把BoostNote的协议改为一些较为宽松的协议,比如说BSD、MIT。
|
||||
因为GPLv3过于严格,不能和其他的一些协议兼容,所以我们有可能在将来会把BoostNote的协议改为一些较为宽松的协议,比如说BSD、MIT。
|
||||
|
||||
---
|
||||
|
||||
# Contributing to Boostnote (Français)
|
||||
|
||||
### Lorsque vous signalez un problème ou un bug
|
||||
Il n'y a pas de modèle pour un signaler problème. Mais nous vous demandons :
|
||||
|
||||
**Merci de founir une capture d'écran de Boostnote avec l'outil de développement ouvert**
|
||||
(vous pouvez l'ouvrir avec `Ctrl+Shift+I`)
|
||||
|
||||
Merci en avance pour votre aide.
|
||||
|
||||
### À propos des droits d'auteurs et des requêtes (`Pull Request`)
|
||||
|
||||
Si vous faites une requête, vous acceptez de transmettre les modifications du code à BoostIO.
|
||||
|
||||
Cela ne veut pas dire que Boostnote deviendra une application payante. Si nous voulons gagner de l'argent, nous trouverons un autre moyen, comme un service de sauvegarde sur le Cloud, une application mobile ou des options payantes.
|
||||
Puisque GPL v3 est trop strict pour être compatible avec n'importe quelle autre licence, nous pensons avoir un jour besoin de la remplacer avec une licence bien plus libre (comme BSD, MIT).
|
||||
|
||||
76
dev-scripts/dev.js
Normal file
76
dev-scripts/dev.js
Normal file
@@ -0,0 +1,76 @@
|
||||
const webpack = require('webpack')
|
||||
const WebpackDevServer = require('webpack-dev-server')
|
||||
const config = require('../webpack.config')
|
||||
const signale = require('signale')
|
||||
const { spawn } = require('child_process')
|
||||
const electron = require('electron')
|
||||
const port = 8080
|
||||
let server = null
|
||||
let firstRun = true
|
||||
|
||||
const options = {
|
||||
publicPath: config.output.publicPath,
|
||||
hot: true,
|
||||
inline: true,
|
||||
quiet: true
|
||||
}
|
||||
|
||||
function startServer () {
|
||||
config.plugins.push(new webpack.HotModuleReplacementPlugin())
|
||||
config.entry.main.unshift(
|
||||
`webpack-dev-server/client?http://localhost:${port}/`,
|
||||
'webpack/hot/dev-server'
|
||||
)
|
||||
const compiler = webpack(config)
|
||||
server = new WebpackDevServer(compiler, options)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
server.listen(port, 'localhost', function (err) {
|
||||
if (err) {
|
||||
reject(err)
|
||||
}
|
||||
signale.success(`Webpack Dev Server listening at localhost:${port}`)
|
||||
signale.watch(`Waiting for webpack to bundle...`)
|
||||
compiler.plugin('done', stats => {
|
||||
if (!stats.hasErrors()) {
|
||||
signale.success(`Bundle success !`)
|
||||
resolve()
|
||||
} else {
|
||||
if (!firstRun) {
|
||||
console.log(stats.compilation.errors[0])
|
||||
} else {
|
||||
firstRun = false
|
||||
reject(stats.compilation.errors[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function startElectron () {
|
||||
spawn(electron, ['--hot', './index.js'], { stdio: 'inherit' })
|
||||
.on('close', () => {
|
||||
server.close()
|
||||
})
|
||||
.on('error', err => {
|
||||
signale.error(err)
|
||||
server.close()
|
||||
})
|
||||
.on('disconnect', () => {
|
||||
server.close()
|
||||
})
|
||||
.on('exit', () => {
|
||||
server.close()
|
||||
})
|
||||
}
|
||||
|
||||
startServer()
|
||||
.then(() => {
|
||||
startElectron()
|
||||
signale.success('Electron started')
|
||||
})
|
||||
.catch(err => {
|
||||
signale.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -2,10 +2,9 @@
|
||||
This page is also available in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Korean](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [French](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) and [German](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md).
|
||||
|
||||
## Environments
|
||||
* npm: 4.x
|
||||
* node: 7.x
|
||||
|
||||
You should use `npm v4.x` because `$ grunt pre-build` fails on `v5.x`.
|
||||
* npm: 6.x
|
||||
* node: 8.x
|
||||
|
||||
## Development
|
||||
|
||||
@@ -21,17 +20,9 @@ $ yarn
|
||||
Build and run.
|
||||
|
||||
```
|
||||
$ yarn run dev-start
|
||||
$ yarn run dev
|
||||
```
|
||||
|
||||
This command runs `yarn run webpack` and `yarn run hot` in parallel. It is the same as running these commands in two terminals.
|
||||
|
||||
The `webpack` will watch for code changes and then apply them automatically.
|
||||
|
||||
If the following error occurs: `Failed to load resource: net::ERR_CONNECTION_REFUSED`, please reload Boostnote.
|
||||
|
||||

|
||||
|
||||
> ### Notice
|
||||
> There are some cases where you have to refresh the app manually.
|
||||
> 1. When editing a constructor method of a component
|
||||
@@ -44,8 +35,6 @@ You can build the program by using `grunt`. However, we don't recommend this bec
|
||||
|
||||
So, we've prepared a separate script which just makes an executable file.
|
||||
|
||||
This build doesn't work on npm v5.3.0. So you need to use v5.2.0 when you build it.
|
||||
|
||||
```
|
||||
grunt pre-build
|
||||
```
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
Diese Seite ist auch verfügbar in [Japanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Koreanisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russisch](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Vereinfachtem Chinesisch](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [Französisch](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) und [Deutsch](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md).
|
||||
|
||||
## Umgebungen
|
||||
* npm: 4.x
|
||||
* node: 7.x
|
||||
|
||||
Du solltest `npm v4.x` benutzen weil `$ grunt pre-build` scheitert mit Version `v5.x`.
|
||||
* npm: 6.x
|
||||
* node: 8.x
|
||||
|
||||
## Entwicklung
|
||||
|
||||
@@ -21,17 +20,9 @@ $ yarn
|
||||
Bauen und Ausführen.
|
||||
|
||||
```
|
||||
$ yarn run dev-start
|
||||
$ yarn run dev
|
||||
```
|
||||
|
||||
Dieser Befehl startet `yarn run webpack` und `yarn run hot` parallel. Es hat den selben Effekt wie beide Befehle separat in zwei Terminals zu starten.
|
||||
|
||||
Das `webpack` überprüft den Code auf Änderungen und wendet diese dann automatisch an.
|
||||
|
||||
Wenn folgender Fehler passiert: `Failed to load resource: net::ERR_CONNECTION_REFUSED`, bitte Boostnote neu starten.
|
||||
|
||||

|
||||
|
||||
> ### Notiz
|
||||
> Es gibt einige Fälle bei denen die App manuell zu refreshen ist.
|
||||
> 1. Wenn eine "constructor method" einer Komponente manuell editiert wird.
|
||||
@@ -44,11 +35,10 @@ Du kannst das Programm unter Verwendung von `grunt` bauen. Jedoch empfehlen wir
|
||||
|
||||
Deshalb haben wir ein separates Script vorbereitet welches eine ausführbare Datei erstellt.
|
||||
|
||||
Dieser build funktioniert nicht mit npm v5.3.0. Deshalb musst du für den Build die Version v5.2.0 verwenden.
|
||||
|
||||
```
|
||||
grunt pre-build
|
||||
```
|
||||
|
||||
Du findest die ausführbare Datein in dem Verzeichnis `dist`. Beachte, der auto updater funktioniert nicht da die app nicht signiert ist.
|
||||
|
||||
Wenn du es für notwendig erachtest, kannst du codesign or authenticode mit dieser ausführbaren Datei verwenden.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# How to debug Boostnote (Electron app)
|
||||
|
||||
This page is also available in [Japanese](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/debug.md), [Korean](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/debug.md), [Russain](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/debug.md), [Simplified Chinese](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/debug.md), [French](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/debug.md) and [German](https://github.com/BoostIO/Boostnote/blob/master/docs/de/debug.md).
|
||||
|
||||
## Debug with Google Chrome developer Tools
|
||||
Boostnote is an Electron app so it's based on Chromium; developers can use `Developer Tools` just like Google Chrome.
|
||||
|
||||
You can toggle the `Developer Tools` like this:
|
||||
@@ -11,12 +13,24 @@ The `Developer Tools` will look like this:
|
||||
|
||||
When errors occur, the error messages are displayed at the `console`.
|
||||
|
||||
## Debugging
|
||||
### Debugging
|
||||
For example, you can use the `debugger` to set a breakpoint in the code like this:
|
||||
|
||||

|
||||
|
||||
This is just an illustrative example, you should find a way to debug which fits your style.
|
||||
|
||||
## References
|
||||
### References
|
||||
* [Official document of Google Chrome about debugging](https://developer.chrome.com/devtools)
|
||||
|
||||
## Debug with Visual Studio Code
|
||||
|
||||
1. Install **[Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome "Install Debugger for Chrome")** plugin for Visual Studio Code. Then restart it.
|
||||
2. Pressing **Shift+Command+B** or running **Run Build Task** from the global **Terminal** menu, then pick the task named **Build Boostnote**. Or run `yarn run watch` from the terminal.
|
||||
3. When above task is running, open **Debug view** in **Activity Bar** on the side of VS Code or use shortcut **Shift+Command+D**.
|
||||
4. Select the configuration named **Boostnote All** from the **Debug configuration**, then click the green arrow button or press **F5** to start debugging.
|
||||
5. Now you should find **Boostnote** is running. You will see two processes running, one named **Boostnote Main** and the other named **Boostnote Renderer**. Now you can set **debug breakpoints** in vscode. If you find your **breakpoints** is unverified, you need to switch to the appropriate process between **Boostnote Renderer** and **Boostnote Main**.
|
||||
|
||||
### References
|
||||
* [Electron application debugging](https://electronjs.org/docs/tutorial/application-debugging)
|
||||
* [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome)
|
||||
@@ -2,10 +2,9 @@
|
||||
Cette page est également disponible en [Anglais](https://github.com/BoostIO/Boostnote/blob/master/docs/build.md), [Japonais](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [Coréen](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [Russe](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [Chinois Simplifié](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md) et en [Allemand](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md)
|
||||
|
||||
## Environnements
|
||||
* npm: 4.x
|
||||
* node: 7.x
|
||||
|
||||
Il est conseillé d'utiliser `npm v4.x` car `$ grunt pre-build` ne marche pas sur la `v5.x`.
|
||||
* npm: 6.x
|
||||
* node: 8.x
|
||||
|
||||
## Développement
|
||||
|
||||
@@ -20,17 +19,9 @@ $ yarn
|
||||
Build et start
|
||||
|
||||
```
|
||||
$ yarn run dev-start
|
||||
$ yarn run dev
|
||||
```
|
||||
|
||||
Cette commande lance `yarn run webpack` et `yarn run hot` en parallèle. Cela revient au même que si on utilisait ces deux commandes dans 2 terminaux.
|
||||
|
||||
La commande `webpack` va surveiller les changements de code et les appliquer automatiquement.
|
||||
|
||||
Si l'erreur suivante apparait : `Failed to load resource: net::ERR_CONNECTION_REFUSED`, relancez Boostnote.
|
||||
|
||||

|
||||
|
||||
> ### Notice
|
||||
> Il y a certains cas où vous voudrez relancer l'application manuellement.
|
||||
> 1. Quand vous éditez la méthode constructeur dans un composant
|
||||
@@ -43,8 +34,6 @@ Vous pouvez build le programme en utilisant `grunt`. Cependant, nous ne recomman
|
||||
|
||||
Nous avons donc préparé un script séparé qui va rendre un fichier exécutable.
|
||||
|
||||
Le build ne fonctionne pas sur `npm v5.3.0`. Il faut donc utiliser `npm v5.2.0` quand vous faites le build.
|
||||
|
||||
```
|
||||
grunt pre-build
|
||||
```
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user