2017-11-05 02:17:48 +02:00
const React = require ( 'react' ) ;
2017-12-14 20:12:14 +02:00
const Note = require ( 'lib/models/Note.js' ) ;
2018-05-02 16:13:20 +02:00
const BaseItem = require ( 'lib/models/BaseItem.js' ) ;
2018-03-20 01:04:48 +02:00
const BaseModel = require ( 'lib/BaseModel.js' ) ;
2019-07-29 14:13:23 +02:00
const Resource = require ( 'lib/models/Resource.js' ) ;
const Folder = require ( 'lib/models/Folder.js' ) ;
2017-11-30 01:03:10 +02:00
const { time } = require ( 'lib/time-utils.js' ) ;
2017-12-14 20:12:14 +02:00
const Setting = require ( 'lib/models/Setting.js' ) ;
2019-12-17 11:44:48 +02:00
const InteropServiceHelper = require ( '../InteropServiceHelper.js' ) ;
2017-11-11 00:18:00 +02:00
const { IconButton } = require ( './IconButton.min.js' ) ;
2019-07-29 14:13:23 +02:00
const { urlDecode , substrWithEllipsis } = require ( 'lib/string-utils' ) ;
2017-11-30 01:03:10 +02:00
const Toolbar = require ( './Toolbar.min.js' ) ;
2018-11-08 00:16:05 +02:00
const TagList = require ( './TagList.min.js' ) ;
2017-11-05 02:17:48 +02:00
const { connect } = require ( 'react-redux' ) ;
2017-11-07 23:11:14 +02:00
const { _ } = require ( 'lib/locale.js' ) ;
const { reg } = require ( 'lib/registry.js' ) ;
2020-02-12 00:27:34 +02:00
const { MarkupToHtml } = require ( 'lib/joplin-renderer' ) ;
2017-11-05 20:36:27 +02:00
const shared = require ( 'lib/components/shared/note-screen-shared.js' ) ;
2017-11-07 23:11:14 +02:00
const { bridge } = require ( 'electron' ) . remote . require ( './bridge' ) ;
2017-11-08 19:51:55 +02:00
const { themeStyle } = require ( '../theme.js' ) ;
2017-11-10 00:44:10 +02:00
const AceEditor = require ( 'react-ace' ) . default ;
2017-11-11 00:18:00 +02:00
const Menu = bridge ( ) . Menu ;
const MenuItem = bridge ( ) . MenuItem ;
const { shim } = require ( 'lib/shim.js' ) ;
2017-11-30 01:03:10 +02:00
const eventManager = require ( '../eventManager' ) ;
2018-02-07 22:23:17 +02:00
const fs = require ( 'fs-extra' ) ;
2018-05-10 11:45:44 +02:00
const md5 = require ( 'md5' ) ;
const mimeUtils = require ( 'lib/mime-utils.js' ) . mime ;
2018-12-09 02:18:10 +02:00
const ObjectUtils = require ( 'lib/ObjectUtils' ) ;
2018-06-11 01:35:01 +02:00
const urlUtils = require ( 'lib/urlUtils' ) ;
2018-06-12 01:12:06 +02:00
const dialogs = require ( './dialogs' ) ;
2019-01-29 20:02:34 +02:00
const NoteListUtils = require ( './utils/NoteListUtils' ) ;
2018-12-09 02:18:10 +02:00
const NoteSearchBar = require ( './NoteSearchBar.min.js' ) ;
2018-06-14 09:52:12 +02:00
const markdownUtils = require ( 'lib/markdownUtils' ) ;
2018-06-18 20:56:07 +02:00
const ExternalEditWatcher = require ( 'lib/services/ExternalEditWatcher' ) ;
2018-10-08 20:11:53 +02:00
const ResourceFetcher = require ( 'lib/services/ResourceFetcher' ) ;
2018-06-22 20:36:15 +02:00
const { toSystemSlashes , safeFilename } = require ( 'lib/path-utils' ) ;
2018-06-26 01:52:46 +02:00
const { clipboard } = require ( 'electron' ) ;
2018-12-14 00:57:14 +02:00
const SearchEngine = require ( 'lib/services/SearchEngine' ) ;
2019-02-09 01:07:01 +02:00
const NoteTextViewer = require ( './NoteTextViewer.min' ) ;
2019-05-06 22:35:29 +02:00
const NoteRevisionViewer = require ( './NoteRevisionViewer.min' ) ;
2019-07-20 23:13:10 +02:00
const TemplateUtils = require ( 'lib/TemplateUtils' ) ;
2019-12-29 19:58:40 +02:00
const markupLanguageUtils = require ( 'lib/markupLanguageUtils' ) ;
2017-11-10 20:43:54 +02:00
2017-11-11 00:18:00 +02:00
require ( 'brace/mode/markdown' ) ;
2017-11-10 20:43:54 +02:00
// https://ace.c9.io/build/kitchen-sink.html
// https://highlightjs.org/static/demo/
2017-11-10 00:44:10 +02:00
require ( 'brace/theme/chrome' ) ;
2019-07-21 18:27:42 +02:00
require ( 'brace/theme/solarized_light' ) ;
require ( 'brace/theme/solarized_dark' ) ;
2018-11-08 00:37:13 +02:00
require ( 'brace/theme/twilight' ) ;
2019-10-01 13:23:32 +02:00
require ( 'brace/theme/dracula' ) ;
2019-10-30 11:55:45 +02:00
require ( 'brace/theme/chaos' ) ;
2019-11-06 23:51:08 +02:00
require ( 'brace/keybinding/vim' ) ;
require ( 'brace/keybinding/emacs' ) ;
2017-11-05 02:17:48 +02:00
2019-12-03 13:12:47 +02:00
/* eslint-disable-next-line no-undef */
class CustomHighlightRules extends ace . acequire (
'ace/mode/markdown_highlight_rules'
) . MarkdownHighlightRules {
constructor ( ) {
super ( ) ;
if ( Setting . value ( 'markdown.plugin.mark' ) ) {
this . $rules . start . push ( {
// This is actually a highlight `mark`, but Ace has no token name for
// this so we made up our own. Reference for common tokens here:
// https://github.com/ajaxorg/ace/wiki/Creating-or-Extending-an-Edit-Mode#common-tokens
token : 'highlight_mark' ,
regex : '==[^ ](?:.*?[^ ])?==' ,
} ) ;
}
}
}
/* eslint-disable-next-line no-undef */
class CustomMdMode extends ace . acequire ( 'ace/mode/markdown' ) . Mode {
constructor ( ) {
super ( ) ;
this . HighlightRules = CustomHighlightRules ;
}
}
2020-01-06 23:23:22 +02:00
const NOTE _TAG _BAR _FEATURE _ENABLED = true ;
2018-11-24 13:42:50 +02:00
2017-11-05 01:27:13 +02:00
class NoteTextComponent extends React . Component {
2017-11-05 20:36:27 +02:00
constructor ( ) {
super ( ) ;
2018-12-09 02:18:10 +02:00
this . localSearchDefaultState = {
query : '' ,
selectedIndex : 0 ,
resultCount : 0 ,
2020-02-05 12:45:24 +02:00
searching : false ,
2018-12-09 02:18:10 +02:00
} ;
2017-11-05 20:36:27 +02:00
this . state = {
2017-11-10 19:58:17 +02:00
note : null ,
2017-11-05 20:36:27 +02:00
folder : null ,
lastSavedNote : null ,
isLoading : true ,
webviewReady : false ,
2017-11-07 23:11:14 +02:00
scrollHeight : null ,
2018-01-12 21:58:01 +02:00
editorScrollTop : 0 ,
newNote : null ,
2018-11-08 00:16:05 +02:00
noteTags : [ ] ,
2019-05-06 22:35:29 +02:00
showRevisions : false ,
2019-07-17 23:42:53 +02:00
loading : false ,
2018-01-12 21:58:01 +02:00
// If the current note was just created, and the title has never been
// changed by the user, this variable contains that note ID. Used
// to automatically set the title.
newAndNoTitleChangeNoteId : null ,
2018-05-10 13:02:39 +02:00
bodyHtml : '' ,
2019-03-08 19:14:17 +02:00
lastRenderCssFiles : [ ] ,
2019-12-29 19:58:40 +02:00
lastRenderPluginAssets : [ ] ,
2018-06-14 09:52:12 +02:00
lastKeys : [ ] ,
2018-12-09 02:18:10 +02:00
showLocalSearch : false ,
2019-07-29 14:13:23 +02:00
localSearch : Object . assign ( { } , this . localSearchDefaultState ) ,
2017-11-05 20:36:27 +02:00
} ;
2017-11-07 01:56:33 +02:00
2019-02-09 01:07:01 +02:00
this . webviewRef _ = React . createRef ( ) ;
2017-11-07 01:56:33 +02:00
this . lastLoadedNoteId _ = null ;
2017-11-07 23:11:14 +02:00
this . webviewListeners _ = null ;
this . ignoreNextEditorScroll _ = false ;
2017-11-07 23:46:23 +02:00
this . scheduleSaveTimeout _ = null ;
2017-11-10 00:44:10 +02:00
this . restoreScrollTop _ = null ;
2018-05-10 13:02:39 +02:00
this . lastSetHtml _ = '' ;
2018-12-14 00:57:14 +02:00
this . lastSetMarkers _ = '' ;
2018-12-09 02:18:10 +02:00
this . lastSetMarkersOptions _ = { } ;
2018-06-17 03:44:37 +02:00
this . selectionRange _ = null ;
2019-07-17 23:42:53 +02:00
this . lastComponentUpdateNoteId _ = null ;
2018-12-09 02:18:10 +02:00
this . noteSearchBar _ = React . createRef ( ) ;
2019-10-31 10:33:40 +02:00
this . isPrinting _ = false ;
2017-11-10 00:44:10 +02:00
// Complicated but reliable method to get editor content height
// https://github.com/ajaxorg/ace/issues/2046
this . editorMaxScrollTop _ = 0 ;
this . onAfterEditorRender _ = ( ) => {
const r = this . editor _ . editor . renderer ;
this . editorMaxScrollTop _ = Math . max ( 0 , r . layerConfig . maxHeight - r . $size . scrollerHeight ) ;
2017-11-13 02:23:12 +02:00
if ( this . restoreScrollTop _ !== null ) {
2017-11-10 00:44:10 +02:00
this . editorSetScrollTop ( this . restoreScrollTop _ ) ;
this . restoreScrollTop _ = null ;
}
2019-07-29 14:13:23 +02:00
} ;
2017-11-30 01:03:10 +02:00
2019-07-29 14:13:23 +02:00
this . onAlarmChange _ = event => {
if ( event . noteId === this . props . noteId ) this . scheduleReloadNote ( this . props ) ;
} ;
this . onNoteTypeToggle _ = event => {
if ( event . noteId === this . props . noteId ) this . scheduleReloadNote ( this . props ) ;
} ;
this . onTodoToggle _ = event => {
if ( event . noteId === this . props . noteId ) this . scheduleReloadNote ( this . props ) ;
} ;
2018-05-10 11:45:44 +02:00
2018-09-04 19:20:41 +02:00
this . onEditorPaste _ = async ( event = null ) => {
2018-05-10 11:45:44 +02:00
const formats = clipboard . availableFormats ( ) ;
for ( let i = 0 ; i < formats . length ; i ++ ) {
const format = formats [ i ] . toLowerCase ( ) ;
2019-07-29 14:13:23 +02:00
const formatType = format . split ( '/' ) [ 0 ] ;
2018-09-04 19:20:41 +02:00
2018-05-10 11:45:44 +02:00
if ( formatType === 'image' ) {
2018-09-04 19:20:41 +02:00
if ( event ) event . preventDefault ( ) ;
2018-05-10 11:45:44 +02:00
const image = clipboard . readImage ( ) ;
const fileExt = mimeUtils . toFileExtension ( format ) ;
2019-09-19 23:51:18 +02:00
const filePath = ` ${ Setting . value ( 'tempDir' ) } / ${ md5 ( Date . now ( ) ) } . ${ fileExt } ` ;
2018-05-10 11:45:44 +02:00
await shim . writeImageToFile ( image , format , filePath ) ;
await this . commandAttachFile ( [ filePath ] ) ;
await shim . fsDriver ( ) . remove ( filePath ) ;
}
}
2019-07-29 14:13:23 +02:00
} ;
2018-05-10 11:45:44 +02:00
2019-07-29 14:13:23 +02:00
this . onEditorKeyDown _ = event => {
2018-06-14 09:52:12 +02:00
const lastKeys = this . state . lastKeys . slice ( ) ;
lastKeys . push ( event . key ) ;
while ( lastKeys . length > 2 ) lastKeys . splice ( 0 , 1 ) ;
this . setState ( { lastKeys : lastKeys } ) ;
2019-07-29 14:13:23 +02:00
} ;
2018-06-14 09:52:12 +02:00
2019-09-13 00:16:42 +02:00
this . onEditorContextMenu _ = ( ) => {
2018-09-04 19:20:41 +02:00
const menu = new Menu ( ) ;
const selectedText = this . selectedText ( ) ;
const clipboardText = clipboard . readText ( ) ;
2019-07-29 14:13:23 +02:00
menu . append (
new MenuItem ( {
label : _ ( 'Cut' ) ,
enabled : ! ! selectedText ,
click : async ( ) => {
this . editorCutText ( ) ;
} ,
} )
) ;
2018-09-04 19:20:41 +02:00
2019-07-29 14:13:23 +02:00
menu . append (
new MenuItem ( {
label : _ ( 'Copy' ) ,
enabled : ! ! selectedText ,
click : async ( ) => {
this . editorCopyText ( ) ;
} ,
} )
) ;
2018-09-04 19:20:41 +02:00
2019-07-29 14:13:23 +02:00
menu . append (
new MenuItem ( {
label : _ ( 'Paste' ) ,
enabled : true ,
click : async ( ) => {
if ( clipboardText ) {
this . editorPasteText ( ) ;
} else {
// To handle pasting images
this . onEditorPaste _ ( ) ;
}
} ,
} )
) ;
2018-09-04 19:20:41 +02:00
menu . popup ( bridge ( ) . window ( ) ) ;
2019-07-29 14:13:23 +02:00
} ;
2018-09-04 19:20:41 +02:00
2019-07-29 14:13:23 +02:00
this . onDrop _ = async event => {
2018-09-30 21:15:30 +02:00
const dt = event . dataTransfer ;
2019-07-29 12:16:47 +02:00
const createFileURL = event . altKey ;
2018-09-30 21:15:30 +02:00
2019-07-29 14:13:23 +02:00
if ( dt . types . indexOf ( 'text/x-jop-note-ids' ) >= 0 ) {
const noteIds = JSON . parse ( dt . getData ( 'text/x-jop-note-ids' ) ) ;
2018-09-30 21:15:30 +02:00
const linkText = [ ] ;
for ( let i = 0 ; i < noteIds . length ; i ++ ) {
const note = await Note . load ( noteIds [ i ] ) ;
linkText . push ( Note . markdownTag ( note ) ) ;
}
2019-07-29 14:13:23 +02:00
this . wrapSelectionWithStrings ( '' , '' , '' , linkText . join ( '\n' ) ) ;
2018-09-30 21:15:30 +02:00
}
const files = dt . files ;
2018-05-10 11:45:44 +02:00
if ( ! files || ! files . length ) return ;
const filesToAttach = [ ] ;
for ( let i = 0 ; i < files . length ; i ++ ) {
const file = files [ i ] ;
if ( ! file . path ) continue ;
filesToAttach . push ( file . path ) ;
}
2019-07-29 12:16:47 +02:00
await this . commandAttachFile ( filesToAttach , createFileURL ) ;
2019-07-29 14:13:23 +02:00
} ;
2018-06-12 00:47:44 +02:00
2018-06-14 09:52:12 +02:00
const updateSelectionRange = ( ) => {
2018-10-05 20:19:47 +02:00
if ( ! this . rawEditor ( ) ) {
this . selectionRange _ = null ;
return ;
}
2018-06-17 03:44:37 +02:00
2019-07-29 14:13:23 +02:00
const ranges = this . rawEditor ( )
. getSelection ( )
. getAllRanges ( ) ;
2018-06-12 00:47:44 +02:00
if ( ! ranges || ! ranges . length || ! this . state . note ) {
2018-06-17 03:44:37 +02:00
this . selectionRange _ = null ;
2018-06-14 09:52:12 +02:00
} else {
2018-06-17 03:44:37 +02:00
this . selectionRange _ = ranges [ 0 ] ;
2019-10-30 17:40:59 +02:00
if ( process . platform === 'linux' ) {
const textRange = this . textOffsetSelection ( ) ;
if ( textRange . start != textRange . end ) {
clipboard . writeText ( this . state . note . body . slice (
Math . min ( textRange . start , textRange . end ) ,
Math . max ( textRange . end , textRange . start ) ) , 'selection' ) ;
}
}
2018-06-12 00:47:44 +02:00
}
2019-07-29 14:13:23 +02:00
} ;
2018-06-12 00:47:44 +02:00
2019-07-29 14:13:23 +02:00
this . aceEditor _selectionChange = ( ) => {
2018-06-14 09:52:12 +02:00
updateSelectionRange ( ) ;
2019-07-29 14:13:23 +02:00
} ;
2018-06-13 18:53:41 +02:00
2019-09-13 00:16:42 +02:00
this . aceEditor _focus = ( ) => {
2018-06-14 09:52:12 +02:00
updateSelectionRange ( ) ;
2019-07-29 14:13:23 +02:00
} ;
2018-06-18 20:56:07 +02:00
2019-07-29 14:13:23 +02:00
this . externalEditWatcher _noteChange = event => {
2018-06-18 20:56:07 +02:00
if ( ! this . state . note || ! this . state . note . id ) return ;
if ( event . id === this . state . note . id ) {
2019-01-30 20:06:47 +02:00
this . scheduleReloadNote ( this . props ) ;
2018-06-18 20:56:07 +02:00
}
2019-07-29 14:13:23 +02:00
} ;
2018-10-08 20:11:53 +02:00
2019-07-29 14:13:23 +02:00
this . refreshResource = async event => {
2018-10-08 20:11:53 +02:00
if ( ! this . state . note || ! this . state . note . body ) return ;
const resourceIds = await Note . linkedResourceIds ( this . state . note . body ) ;
2019-05-22 16:56:07 +02:00
if ( resourceIds . indexOf ( event . id ) >= 0 ) {
shared . clearResourceCache ( ) ;
2018-10-08 20:11:53 +02:00
this . lastSetHtml _ = '' ;
2019-03-08 19:14:17 +02:00
this . scheduleHtmlUpdate ( ) ;
2018-10-08 20:11:53 +02:00
}
2019-07-29 14:13:23 +02:00
} ;
2018-12-09 02:18:10 +02:00
2019-07-29 14:13:23 +02:00
this . noteSearchBar _change = query => {
this . setState ( {
localSearch : {
query : query ,
selectedIndex : 0 ,
2019-09-07 12:57:31 +02:00
timestamp : Date . now ( ) ,
2020-02-05 12:45:24 +02:00
resultCount : this . state . localSearch . resultCount ,
searching : true ,
2019-07-29 14:13:23 +02:00
} ,
} ) ;
} ;
2018-12-09 02:18:10 +02:00
2019-07-29 14:13:23 +02:00
const noteSearchBarNextPrevious = inc => {
2018-12-09 02:18:10 +02:00
const ls = Object . assign ( { } , this . state . localSearch ) ;
ls . selectedIndex += inc ;
2019-09-07 12:57:31 +02:00
ls . timestamp = Date . now ( ) ;
2018-12-09 02:18:10 +02:00
if ( ls . selectedIndex < 0 ) ls . selectedIndex = ls . resultCount - 1 ;
if ( ls . selectedIndex >= ls . resultCount ) ls . selectedIndex = 0 ;
this . setState ( { localSearch : ls } ) ;
2019-07-29 14:13:23 +02:00
} ;
2018-12-09 02:18:10 +02:00
this . noteSearchBar _next = ( ) => {
noteSearchBarNextPrevious ( + 1 ) ;
2019-07-29 14:13:23 +02:00
} ;
2018-12-09 02:18:10 +02:00
this . noteSearchBar _previous = ( ) => {
noteSearchBarNextPrevious ( - 1 ) ;
2019-07-29 14:13:23 +02:00
} ;
2018-12-09 02:18:10 +02:00
this . noteSearchBar _close = ( ) => {
this . setState ( {
showLocalSearch : false ,
} ) ;
2019-07-29 14:13:23 +02:00
} ;
2019-01-26 20:04:32 +02:00
this . titleField _keyDown = this . titleField _keyDown . bind ( this ) ;
2019-02-09 01:07:01 +02:00
this . webview _ipcMessage = this . webview _ipcMessage . bind ( this ) ;
this . webview _domReady = this . webview _domReady . bind ( this ) ;
2019-05-06 22:35:29 +02:00
this . noteRevisionViewer _onBack = this . noteRevisionViewer _onBack . bind ( this ) ;
2018-05-10 11:45:44 +02:00
}
2018-06-17 03:44:37 +02:00
// Note:
// - What's called "cursor position" is expressed as { row: x, column: y } and is how Ace Editor get/set the cursor position
2018-11-08 00:16:05 +02:00
// - A "range" defines a selection with a start and end cusor position, expressed as { start: <CursorPos>, end: <CursorPos> }
2018-06-17 03:44:37 +02:00
// - A "text offset" below is the absolute position of the cursor in the string, as would be used in the indexOf() function.
// The functions below are used to convert between the different types.
2018-06-13 18:53:41 +02:00
rangeToTextOffsets ( range , body ) {
return {
2018-06-17 03:44:37 +02:00
start : this . cursorPositionToTextOffset ( range . start , body ) ,
end : this . cursorPositionToTextOffset ( range . end , body ) ,
2018-06-13 18:53:41 +02:00
} ;
}
2018-06-17 03:44:37 +02:00
currentTextOffset ( ) {
return this . cursorPositionToTextOffset ( this . editor _ . editor . getCursorPosition ( ) , this . state . note . body ) ;
2018-06-12 00:47:44 +02:00
}
2018-06-17 03:44:37 +02:00
cursorPositionToTextOffset ( cursorPos , body ) {
2018-05-10 11:45:44 +02:00
if ( ! this . editor _ || ! this . editor _ . editor || ! this . state . note || ! this . state . note . body ) return 0 ;
2018-06-12 00:47:44 +02:00
const noteLines = body . split ( '\n' ) ;
2018-05-10 11:45:44 +02:00
let pos = 0 ;
for ( let i = 0 ; i < noteLines . length ; i ++ ) {
if ( i > 0 ) pos ++ ; // Need to add the newline that's been removed in the split() call above
if ( i === cursorPos . row ) {
pos += cursorPos . column ;
break ;
} else {
pos += noteLines [ i ] . length ;
}
}
2018-06-16 17:16:27 +02:00
return pos ;
2017-11-05 20:36:27 +02:00
}
2018-06-17 03:44:37 +02:00
textOffsetToCursorPosition ( offset , body ) {
const lines = body . split ( '\n' ) ;
let row = 0 ;
let currentOffset = 0 ;
for ( let i = 0 ; i < lines . length ; i ++ ) {
const line = lines [ i ] ;
if ( currentOffset + line . length >= offset ) {
return {
row : row ,
column : offset - currentOffset ,
2019-07-29 14:13:23 +02:00
} ;
2018-06-17 03:44:37 +02:00
}
row ++ ;
2018-11-08 00:16:05 +02:00
currentOffset += line . length + 1 ;
2018-06-17 03:44:37 +02:00
}
}
2019-07-16 20:05:47 +02:00
markupToHtml ( ) {
if ( this . markupToHtml _ ) return this . markupToHtml _ ;
2019-12-29 19:58:40 +02:00
this . markupToHtml _ = markupLanguageUtils . newMarkupToHtml ( {
2019-09-19 23:51:18 +02:00
resourceBaseUrl : ` file:// ${ Setting . value ( 'resourceDir' ) } / ` ,
2017-11-21 21:31:21 +02:00
} ) ;
2019-12-29 19:58:40 +02:00
2019-07-16 20:05:47 +02:00
return this . markupToHtml _ ;
2017-11-05 01:27:13 +02:00
}
2019-12-13 03:16:34 +02:00
async UNSAFE _componentWillMount ( ) {
2017-11-10 19:58:17 +02:00
let note = null ;
2018-11-08 00:16:05 +02:00
let noteTags = [ ] ;
2018-01-12 21:58:01 +02:00
if ( this . props . newNote ) {
note = Object . assign ( { } , this . props . newNote ) ;
} else if ( this . props . noteId ) {
2017-11-10 19:58:17 +02:00
note = await Note . load ( this . props . noteId ) ;
2018-11-08 00:16:05 +02:00
noteTags = this . props . noteTags || [ ] ;
2017-11-10 19:58:17 +02:00
}
const folder = note ? Folder . byId ( this . props . folders , note . parent _id ) : null ;
this . setState ( {
lastSavedNote : Object . assign ( { } , note ) ,
note : note ,
folder : folder ,
isLoading : false ,
2019-07-29 14:13:23 +02:00
noteTags : noteTags ,
2017-11-10 19:58:17 +02:00
} ) ;
this . lastLoadedNoteId _ = note ? note . id : null ;
2017-11-30 01:03:10 +02:00
2019-07-16 20:05:47 +02:00
this . updateHtml ( note ? note . markup _language : null , note && note . body ? note . body : '' ) ;
2018-05-21 16:29:35 +02:00
2017-11-30 01:03:10 +02:00
eventManager . on ( 'alarmChange' , this . onAlarmChange _ ) ;
eventManager . on ( 'noteTypeToggle' , this . onNoteTypeToggle _ ) ;
eventManager . on ( 'todoToggle' , this . onTodoToggle _ ) ;
2018-10-08 20:11:53 +02:00
2019-05-22 16:56:07 +02:00
shared . installResourceHandling ( this . refreshResource ) ;
2018-11-21 21:50:50 +02:00
ExternalEditWatcher . instance ( ) . on ( 'noteChange' , this . externalEditWatcher _noteChange ) ;
2017-11-05 18:51:03 +02:00
}
componentWillUnmount ( ) {
2017-11-07 23:46:23 +02:00
this . saveIfNeeded ( ) ;
2019-07-16 20:05:47 +02:00
this . markupToHtml _ = null ;
2017-11-30 01:03:10 +02:00
eventManager . removeListener ( 'alarmChange' , this . onAlarmChange _ ) ;
eventManager . removeListener ( 'noteTypeToggle' , this . onNoteTypeToggle _ ) ;
eventManager . removeListener ( 'todoToggle' , this . onTodoToggle _ ) ;
2018-06-18 20:56:07 +02:00
2019-05-22 16:56:07 +02:00
shared . uninstallResourceHandling ( this . refreshResource ) ;
2018-11-21 21:50:50 +02:00
ExternalEditWatcher . instance ( ) . off ( 'noteChange' , this . externalEditWatcher _noteChange ) ;
2017-11-05 18:51:03 +02:00
}
2019-07-29 14:13:23 +02:00
componentDidUpdate ( ) {
2019-07-17 23:42:53 +02:00
const currentNoteId = this . state . note ? this . state . note . id : null ;
2019-07-21 01:18:51 +02:00
if ( this . lastComponentUpdateNoteId _ !== currentNoteId && this . editor _ ) {
2019-12-03 13:12:47 +02:00
this . editor _ . editor . getSession ( ) . setMode ( new CustomMdMode ( ) ) ;
2019-07-17 23:42:53 +02:00
const undoManager = this . editor _ . editor . getSession ( ) . getUndoManager ( ) ;
undoManager . reset ( ) ;
this . editor _ . editor . getSession ( ) . setUndoManager ( undoManager ) ;
this . lastComponentUpdateNoteId _ = currentNoteId ;
}
2019-06-05 18:41:30 +02:00
}
webviewRef ( ) {
if ( ! this . webviewRef _ . current || ! this . webviewRef _ . current . wrappedInstance ) return null ;
if ( ! this . webviewRef _ . current . wrappedInstance . domReady ( ) ) return null ;
return this . webviewRef _ . current . wrappedInstance ;
}
2019-06-13 09:48:19 +02:00
async saveIfNeeded ( saveIfNewNote = false , options = { } ) {
2019-07-17 23:42:53 +02:00
if ( this . state . loading ) return ;
2018-02-06 21:31:22 +02:00
const forceSave = saveIfNewNote && ( this . state . note && ! this . state . note . id ) ;
2017-11-07 23:46:23 +02:00
if ( this . scheduleSaveTimeout _ ) clearTimeout ( this . scheduleSaveTimeout _ ) ;
this . scheduleSaveTimeout _ = null ;
2018-02-06 21:31:22 +02:00
if ( ! forceSave ) {
if ( ! shared . isModified ( this ) ) return ;
}
2019-06-13 09:48:19 +02:00
await shared . saveNoteButton _press ( this , null , options ) ;
2018-06-18 20:56:07 +02:00
2018-11-21 21:50:50 +02:00
ExternalEditWatcher . instance ( ) . updateNoteFile ( this . state . note ) ;
2017-11-07 23:46:23 +02:00
}
2017-11-08 19:51:55 +02:00
async saveOneProperty ( name , value ) {
2018-01-12 21:58:01 +02:00
if ( this . state . note && ! this . state . note . id ) {
const note = Object . assign ( { } , this . state . note ) ;
note [ name ] = value ;
this . setState ( { note : note } ) ;
this . scheduleSave ( ) ;
} else {
await shared . saveOneProperty ( this , name , value ) ;
}
2017-11-08 19:51:55 +02:00
}
2017-11-07 23:46:23 +02:00
scheduleSave ( ) {
if ( this . scheduleSaveTimeout _ ) clearTimeout ( this . scheduleSaveTimeout _ ) ;
this . scheduleSaveTimeout _ = setTimeout ( ( ) => {
this . saveIfNeeded ( ) ;
} , 500 ) ;
}
2019-01-30 20:06:47 +02:00
scheduleReloadNote ( props , options = null ) {
if ( this . scheduleReloadNoteIID _ ) {
clearTimeout ( this . scheduleReloadNoteIID _ ) ;
this . scheduleReloadNoteIID _ = null ;
}
this . scheduleReloadNoteIID _ = setTimeout ( ( ) => {
this . reloadNote ( props , options ) ;
2019-02-02 12:47:26 +02:00
} , 10 ) ;
2019-01-30 20:06:47 +02:00
}
// Generally, reloadNote() should not be called directly so that it's not called multiple times
// from multiple places within a short interval of time. Instead use scheduleReloadNote() to
// delay reloading a bit and make sure that only one reload operation is performed.
2017-12-01 02:00:18 +02:00
async reloadNote ( props , options = null ) {
if ( ! options ) options = { } ;
if ( ! ( 'noReloadIfLocalChanges' in options ) ) options . noReloadIfLocalChanges = false ;
2018-01-11 22:05:34 +02:00
await this . saveIfNeeded ( ) ;
2019-07-17 23:42:53 +02:00
const defer = ( ) => {
this . setState ( { loading : false } ) ;
2019-07-29 14:13:23 +02:00
} ;
2019-07-17 23:42:53 +02:00
this . setState ( { loading : true } ) ;
2018-01-09 22:26:20 +02:00
const stateNoteId = this . state . note ? this . state . note . id : null ;
2018-01-12 21:58:01 +02:00
let noteId = null ;
let note = null ;
let loadingNewNote = true ;
2018-06-05 10:27:07 +02:00
let parentFolder = null ;
2019-01-31 00:45:28 +02:00
let scrollPercent = 0 ;
2018-01-12 21:58:01 +02:00
if ( props . newNote ) {
2019-07-20 23:13:10 +02:00
// assign new note and prevent body from being null
2019-07-29 14:13:23 +02:00
note = Object . assign ( { } , props . newNote , { body : '' } ) ;
2018-01-12 21:58:01 +02:00
this . lastLoadedNoteId _ = null ;
2019-07-29 14:13:23 +02:00
if ( note . template ) note . body = TemplateUtils . render ( note . template ) ;
2018-01-12 21:58:01 +02:00
} else {
noteId = props . noteId ;
2019-01-31 00:45:28 +02:00
scrollPercent = this . props . lastEditorScrollPercents [ noteId ] ;
if ( ! scrollPercent ) scrollPercent = 0 ;
2018-01-12 21:58:01 +02:00
loadingNewNote = stateNoteId !== noteId ;
this . lastLoadedNoteId _ = noteId ;
note = noteId ? await Note . load ( noteId ) : null ;
2019-07-17 23:42:53 +02:00
if ( noteId !== this . lastLoadedNoteId _ ) return defer ( ) ; // Race condition - current note was changed while this one was loading
if ( options . noReloadIfLocalChanges && this . isModified ( ) ) return defer ( ) ;
2018-01-12 21:58:01 +02:00
// If the note hasn't been changed, exit now
if ( this . state . note && note ) {
let diff = Note . diffObjects ( this . state . note , note ) ;
delete diff . type _ ;
2019-07-17 23:42:53 +02:00
if ( ! Object . getOwnPropertyNames ( diff ) . length ) return defer ( ) ;
2018-01-12 21:58:01 +02:00
}
2017-11-28 22:58:07 +02:00
}
2019-07-16 20:05:47 +02:00
this . markupToHtml _ = null ;
2017-11-28 23:15:22 +02:00
2017-11-12 20:12:05 +02:00
// If we are loading nothing (noteId == null), make sure to
// set webviewReady to false too because the webview component
// is going to be removed in render().
2019-02-09 01:07:01 +02:00
const webviewReady = ! ! this . webviewRef _ . current && this . state . webviewReady && ( ! ! noteId || ! ! props . newNote ) ;
2017-11-10 19:58:17 +02:00
2018-01-09 22:26:20 +02:00
// Scroll back to top when loading new note
if ( loadingNewNote ) {
2019-05-22 16:56:07 +02:00
shared . clearResourceCache ( ) ;
2019-07-29 14:13:23 +02:00
2018-01-09 22:26:20 +02:00
this . editorMaxScrollTop _ = 0 ;
2017-11-10 22:12:38 +02:00
2018-01-09 22:26:20 +02:00
// HACK: To go around a bug in Ace editor, we first set the scroll position to 1
// and then (in the renderer callback) to the value we actually need. The first
// operation helps clear the scroll position cache. See:
// https://github.com/ajaxorg/ace/issues/2195
this . editorSetScrollTop ( 1 ) ;
this . restoreScrollTop _ = 0 ;
2018-01-12 21:58:01 +02:00
2018-07-10 08:35:21 +02:00
// Only force focus on notes when creating a new note/todo
if ( this . props . newNote ) {
2019-07-29 14:13:23 +02:00
const focusSettingName = note . is _todo ? 'newTodoFocus' : 'newNoteFocus' ;
2018-01-30 23:49:22 +02:00
2019-02-14 00:57:43 +02:00
requestAnimationFrame ( ( ) => {
if ( Setting . value ( focusSettingName ) === 'title' ) {
if ( this . titleField _ ) this . titleField _ . focus ( ) ;
} else {
if ( this . editor _ ) this . editor _ . editor . focus ( ) ;
}
} ) ;
2018-01-30 23:49:22 +02:00
}
2018-01-12 21:58:01 +02:00
if ( this . editor _ ) {
this . editor _ . editor . clearSelection ( ) ;
2019-07-29 14:13:23 +02:00
this . editor _ . editor . moveCursorTo ( 0 , 0 ) ;
2019-01-31 00:45:28 +02:00
2019-02-08 10:28:27 +02:00
setTimeout ( ( ) => {
2019-09-09 19:16:00 +02:00
// If we have an anchor hash, jump to that anchor
if ( this . props . selectedNoteHash ) {
this . webviewRef _ . current . wrappedInstance . send ( 'scrollToHash' , this . props . selectedNoteHash ) ;
} else {
// Otherwise restore the normal scroll position
this . setEditorPercentScroll ( scrollPercent ? scrollPercent : 0 ) ;
this . setViewerPercentScroll ( scrollPercent ? scrollPercent : 0 ) ;
}
2019-02-08 10:28:27 +02:00
} , 10 ) ;
2018-01-12 21:58:01 +02:00
}
2019-05-22 16:56:07 +02:00
if ( note && note . body && Setting . value ( 'sync.resourceDownloadMode' ) === 'auto' ) {
const resourceIds = await Note . linkedResourceIds ( note . body ) ;
await ResourceFetcher . instance ( ) . markForDownload ( resourceIds ) ;
}
2018-01-09 22:26:20 +02:00
}
2017-11-10 22:12:38 +02:00
2018-12-09 02:18:10 +02:00
if ( note ) {
2018-06-05 10:27:07 +02:00
parentFolder = Folder . byId ( props . folders , note . parent _id ) ;
}
2018-01-12 21:58:01 +02:00
let newState = {
2017-11-12 20:12:05 +02:00
note : note ,
lastSavedNote : Object . assign ( { } , note ) ,
webviewReady : webviewReady ,
2018-06-05 10:27:07 +02:00
folder : parentFolder ,
2018-06-14 09:52:12 +02:00
lastKeys : [ ] ,
2019-05-06 22:35:29 +02:00
showRevisions : false ,
2018-01-12 21:58:01 +02:00
} ;
if ( ! note ) {
newState . newAndNoTitleChangeNoteId = null ;
} else if ( note . id !== this . state . newAndNoTitleChangeNoteId ) {
newState . newAndNoTitleChangeNoteId = null ;
}
2018-12-09 02:18:10 +02:00
if ( ! note || loadingNewNote ) {
newState . showLocalSearch = false ;
newState . localSearch = Object . assign ( { } , this . localSearchDefaultState ) ;
}
2018-05-10 13:02:39 +02:00
this . lastSetHtml _ = '' ;
2018-12-14 00:57:14 +02:00
this . lastSetMarkers _ = '' ;
2018-12-09 02:18:10 +02:00
this . lastSetMarkersOptions _ = { } ;
2018-05-10 13:02:39 +02:00
2018-01-12 21:58:01 +02:00
this . setState ( newState ) ;
2018-05-10 13:02:39 +02:00
2019-03-08 19:14:17 +02:00
// if (newState.note) await shared.refreshAttachedResources(this, newState.note.body);
2019-07-17 23:42:53 +02:00
await this . updateHtml ( newState . note ? newState . note . markup _language : null , newState . note ? newState . note . body : '' ) ;
defer ( ) ;
2017-11-12 20:12:05 +02:00
}
2019-12-13 03:16:34 +02:00
async UNSAFE _componentWillReceiveProps ( nextProps ) {
2019-02-02 12:47:26 +02:00
if ( this . props . newNote !== nextProps . newNote && nextProps . newNote ) {
2019-01-30 20:06:47 +02:00
await this . scheduleReloadNote ( nextProps ) ;
2019-07-29 14:13:23 +02:00
} else if ( 'noteId' in nextProps && nextProps . noteId !== this . props . noteId ) {
2019-01-30 20:06:47 +02:00
await this . scheduleReloadNote ( nextProps ) ;
2018-11-08 00:16:05 +02:00
} else if ( 'noteTags' in nextProps && this . areNoteTagsModified ( nextProps . noteTags , this . state . noteTags ) ) {
this . setState ( {
2019-07-29 14:13:23 +02:00
noteTags : nextProps . noteTags ,
2018-11-08 00:16:05 +02:00
} ) ;
2017-11-12 20:12:05 +02:00
}
2019-07-29 14:13:23 +02:00
if ( nextProps . syncStarted !== this . props . syncStarted && 'syncStarted' in nextProps && ! nextProps . syncStarted && ! this . isModified ( ) ) {
2019-01-30 20:06:47 +02:00
await this . scheduleReloadNote ( nextProps , { noReloadIfLocalChanges : true } ) ;
2017-11-07 01:56:33 +02:00
}
2018-03-12 20:01:47 +02:00
if ( nextProps . windowCommand ) {
this . doCommand ( nextProps . windowCommand ) ;
}
2017-11-05 20:36:27 +02:00
}
isModified ( ) {
return shared . isModified ( this ) ;
}
2018-11-08 00:16:05 +02:00
areNoteTagsModified ( newTags , oldTags ) {
2018-11-24 13:42:50 +02:00
if ( ! NOTE _TAG _BAR _FEATURE _ENABLED ) return false ;
2018-11-08 00:16:05 +02:00
if ( ! oldTags ) return true ;
if ( newTags . length !== oldTags . length ) return true ;
for ( let i = 0 ; i < newTags . length ; ++ i ) {
2020-01-18 15:55:35 +02:00
let found = false ;
2018-11-08 00:16:05 +02:00
let currNewTag = newTags [ i ] ;
for ( let j = 0 ; j < oldTags . length ; ++ j ) {
let currOldTag = oldTags [ j ] ;
2020-01-18 15:55:35 +02:00
if ( currOldTag . id === currNewTag . id ) {
found = true ;
if ( currOldTag . updated _time !== currNewTag . updated _time ) {
return true ;
}
break ;
2018-11-08 00:16:05 +02:00
}
}
2020-01-18 15:55:35 +02:00
if ( ! found ) {
return true ;
}
2018-11-08 00:16:05 +02:00
}
return false ;
}
2020-01-06 23:23:22 +02:00
canDisplayTagBar ( ) {
if ( ! NOTE _TAG _BAR _FEATURE _ENABLED ) {
return false ;
}
if ( ! this . state . noteTags || this . state . noteTags . length === 0 ) {
return false ;
}
return true ;
}
2019-05-06 22:35:29 +02:00
async noteRevisionViewer _onBack ( ) {
2019-10-11 20:10:25 +02:00
// When coming back from the revision viewer, the webview has been
// unmounted so will need to reload. We set webviewReady to false
// to make sure everything is reloaded as expected.
this . setState ( { showRevisions : false , webviewReady : false } , ( ) => {
this . lastSetHtml _ = '' ;
this . scheduleReloadNote ( this . props ) ;
} ) ;
2019-05-06 22:35:29 +02:00
}
2017-11-10 22:12:38 +02:00
title _changeText ( event ) {
shared . noteComponent _change ( this , 'title' , event . target . value ) ;
2018-01-12 21:58:01 +02:00
this . setState ( { newAndNoTitleChangeNoteId : null } ) ;
2017-11-07 23:46:23 +02:00
this . scheduleSave ( ) ;
2017-11-05 20:36:27 +02:00
}
toggleIsTodo _onPress ( ) {
shared . toggleIsTodo _onPress ( this ) ;
2017-11-07 23:46:23 +02:00
this . scheduleSave ( ) ;
2017-11-05 20:36:27 +02:00
}
2018-02-07 22:23:17 +02:00
async webview _ipcMessage ( event ) {
2017-11-07 23:11:14 +02:00
const msg = event . channel ? event . channel : '' ;
2019-07-29 14:13:23 +02:00
const args = event . args ;
2017-11-07 23:11:14 +02:00
const arg0 = args && args . length >= 1 ? args [ 0 ] : null ;
2019-09-19 23:51:18 +02:00
if ( msg !== 'percentScroll' ) console . info ( ` Got ipc-message: ${ msg } ` , args ) ;
2017-11-07 23:11:14 +02:00
if ( msg . indexOf ( 'checkboxclick:' ) === 0 ) {
2017-11-10 00:44:10 +02:00
// Ugly hack because setting the body here will make the scrollbar
// go to some random position. So we save the scrollTop here and it
// will be restored after the editor ref has been reset, and the
// "afterRender" event has been called.
this . restoreScrollTop _ = this . editorScrollTop ( ) ;
2019-03-08 19:14:17 +02:00
const newBody = shared . toggleCheckbox ( msg , this . state . note . body ) ;
2017-11-07 23:11:14 +02:00
this . saveOneProperty ( 'body' , newBody ) ;
2019-05-24 18:34:18 +02:00
} else if ( msg . indexOf ( 'error:' ) === 0 ) {
const s = msg . split ( ':' ) ;
s . splice ( 0 , 1 ) ;
reg . logger ( ) . error ( s . join ( ':' ) ) ;
2018-12-09 02:18:10 +02:00
} else if ( msg === 'setMarkerCount' ) {
const ls = Object . assign ( { } , this . state . localSearch ) ;
ls . resultCount = arg0 ;
2020-02-05 12:45:24 +02:00
ls . searching = false ;
2018-12-09 02:18:10 +02:00
this . setState ( { localSearch : ls } ) ;
2019-05-22 16:56:07 +02:00
} else if ( msg . indexOf ( 'markForDownload:' ) === 0 ) {
const s = msg . split ( ':' ) ;
2019-09-19 23:51:18 +02:00
if ( s . length < 2 ) throw new Error ( ` Invalid message: ${ msg } ` ) ;
2019-05-22 16:56:07 +02:00
ResourceFetcher . instance ( ) . markForDownload ( s [ 1 ] ) ;
2017-11-07 23:11:14 +02:00
} else if ( msg === 'percentScroll' ) {
this . ignoreNextEditorScroll _ = true ;
this . setEditorPercentScroll ( arg0 ) ;
2018-02-07 22:23:17 +02:00
} else if ( msg === 'contextMenu' ) {
const itemType = arg0 && arg0 . type ;
2019-07-29 14:13:23 +02:00
const menu = new Menu ( ) ;
2018-02-07 22:23:17 +02:00
2019-07-29 14:13:23 +02:00
if ( itemType === 'image' || itemType === 'resource' ) {
2018-02-07 22:23:17 +02:00
const resource = await Resource . load ( arg0 . resourceId ) ;
const resourcePath = Resource . fullPath ( resource ) ;
2019-07-29 14:13:23 +02:00
menu . append (
new MenuItem ( {
label : _ ( 'Open...' ) ,
click : async ( ) => {
2019-09-19 23:51:18 +02:00
const ok = bridge ( ) . openExternal ( ` file:// ${ resourcePath } ` ) ;
2019-07-29 14:13:23 +02:00
if ( ! ok ) bridge ( ) . showErrorMessageBox ( _ ( 'This file could not be opened: %s' , resourcePath ) ) ;
} ,
} )
) ;
menu . append (
new MenuItem ( {
label : _ ( 'Save as...' ) ,
click : async ( ) => {
const filePath = bridge ( ) . showSaveDialog ( {
defaultPath : resource . filename ? resource . filename : resource . title ,
} ) ;
if ( ! filePath ) return ;
await fs . copy ( resourcePath , filePath ) ;
} ,
} )
) ;
menu . append (
new MenuItem ( {
label : _ ( 'Copy path to clipboard' ) ,
click : async ( ) => {
clipboard . writeText ( toSystemSlashes ( resourcePath ) ) ;
} ,
} )
) ;
} else if ( itemType === 'text' ) {
menu . append (
new MenuItem ( {
label : _ ( 'Copy' ) ,
click : async ( ) => {
clipboard . writeText ( arg0 . textToCopy ) ;
} ,
} )
) ;
} else if ( itemType === 'link' ) {
menu . append (
new MenuItem ( {
label : _ ( 'Copy Link Address' ) ,
click : async ( ) => {
clipboard . writeText ( arg0 . textToCopy ) ;
} ,
} )
) ;
2018-02-07 22:23:17 +02:00
} else {
2019-09-19 23:51:18 +02:00
reg . logger ( ) . error ( ` Unhandled item type: ${ itemType } ` ) ;
2018-02-07 22:23:17 +02:00
return ;
}
menu . popup ( bridge ( ) . window ( ) ) ;
2017-11-12 18:33:34 +02:00
} else if ( msg . indexOf ( 'joplin://' ) === 0 ) {
2019-09-09 19:16:00 +02:00
const resourceUrlInfo = urlUtils . parseResourceUrl ( msg ) ;
const itemId = resourceUrlInfo . itemId ;
2018-05-02 16:13:20 +02:00
const item = await BaseItem . loadItemById ( itemId ) ;
2019-09-19 23:51:18 +02:00
if ( ! item ) throw new Error ( ` No item with ID ${ itemId } ` ) ;
2018-05-02 16:13:20 +02:00
if ( item . type _ === BaseModel . TYPE _RESOURCE ) {
2018-11-13 02:45:08 +02:00
const localState = await Resource . localState ( item ) ;
if ( localState . fetch _status !== Resource . FETCH _STATUS _DONE || ! ! item . encryption _blob _encrypted ) {
2019-12-28 21:23:38 +02:00
if ( localState . fetch _status === Resource . FETCH _STATUS _ERROR ) {
bridge ( ) . showErrorMessageBox ( ` ${ _ ( 'There was an error downloading this attachment:' ) } \ n \ n ${ localState . fetch _error } ` ) ;
} else {
bridge ( ) . showErrorMessageBox ( _ ( 'This attachment is not downloaded or not decrypted yet' ) ) ;
}
2018-10-08 20:11:53 +02:00
return ;
}
2018-05-02 16:13:20 +02:00
const filePath = Resource . fullPath ( item ) ;
2017-11-12 18:33:34 +02:00
bridge ( ) . openItem ( filePath ) ;
2018-05-02 16:13:20 +02:00
} else if ( item . type _ === BaseModel . TYPE _NOTE ) {
this . props . dispatch ( {
2019-07-29 14:13:23 +02:00
type : 'FOLDER_AND_NOTE_SELECT' ,
2018-11-08 02:58:06 +02:00
folderId : item . parent _id ,
noteId : item . id ,
2019-09-09 19:16:00 +02:00
hash : resourceUrlInfo . hash ,
2019-02-07 20:17:09 +02:00
historyNoteAction : {
id : this . state . note . id ,
parent _id : this . state . note . parent _id ,
} ,
2018-05-02 16:13:20 +02:00
} ) ;
} else {
2019-09-19 23:51:18 +02:00
throw new Error ( ` Unsupported item type: ${ item . type _ } ` ) ;
2018-05-02 16:13:20 +02:00
}
2018-06-11 01:35:01 +02:00
} else if ( urlUtils . urlProtocol ( msg ) ) {
2018-10-05 20:21:23 +02:00
if ( msg . indexOf ( 'file://' ) === 0 ) {
// When using the file:// protocol, openExternal doesn't work (does nothing) with URL-encoded paths
require ( 'electron' ) . shell . openExternal ( urlDecode ( msg ) ) ;
} else {
require ( 'electron' ) . shell . openExternal ( msg ) ;
}
2018-10-17 09:01:18 +02:00
} else if ( msg . indexOf ( '#' ) === 0 ) {
// This is an internal anchor, which is handled by the WebView so skip this case
2017-11-07 23:11:14 +02:00
} else {
2018-03-02 20:24:02 +02:00
bridge ( ) . showErrorMessageBox ( _ ( 'Unsupported link or message: %s' , msg ) ) ;
2017-11-07 23:11:14 +02:00
}
}
editorMaxScroll ( ) {
2017-11-10 00:44:10 +02:00
return this . editorMaxScrollTop _ ;
}
editorScrollTop ( ) {
return this . editor _ . editor . getSession ( ) . getScrollTop ( ) ;
}
editorSetScrollTop ( v ) {
2017-11-10 22:12:38 +02:00
if ( ! this . editor _ ) return ;
2017-11-10 00:44:10 +02:00
this . editor _ . editor . getSession ( ) . setScrollTop ( v ) ;
2017-11-07 23:11:14 +02:00
}
setEditorPercentScroll ( p ) {
2019-02-08 10:28:27 +02:00
const noteId = this . props . noteId ;
if ( noteId ) {
this . props . dispatch ( {
type : 'EDITOR_SCROLL_PERCENT_SET' ,
noteId : noteId ,
percent : p ,
} ) ;
}
2017-11-10 00:44:10 +02:00
this . editorSetScrollTop ( p * this . editorMaxScroll ( ) ) ;
2017-11-07 23:11:14 +02:00
}
setViewerPercentScroll ( p ) {
2019-02-08 10:28:27 +02:00
const noteId = this . props . noteId ;
if ( noteId ) {
this . props . dispatch ( {
type : 'EDITOR_SCROLL_PERCENT_SET' ,
noteId : noteId ,
percent : p ,
} ) ;
}
2019-02-09 01:07:01 +02:00
if ( this . webviewRef _ . current ) this . webviewRef _ . current . wrappedInstance . send ( 'setPercentScroll' , p ) ;
2017-11-07 23:11:14 +02:00
}
editor _scroll ( ) {
if ( this . ignoreNextEditorScroll _ ) {
this . ignoreNextEditorScroll _ = false ;
return ;
}
2017-11-10 00:44:10 +02:00
2017-11-07 23:11:14 +02:00
const m = this . editorMaxScroll ( ) ;
2019-01-31 00:45:28 +02:00
const percent = m ? this . editorScrollTop ( ) / m : 0 ;
this . setViewerPercentScroll ( percent ) ;
2017-11-07 23:11:14 +02:00
}
2017-11-05 18:51:03 +02:00
webview _domReady ( ) {
2019-12-17 11:44:48 +02:00
console . info ( 'webview_domReady' , this . webviewRef _ . current ) ;
2019-02-09 01:07:01 +02:00
if ( ! this . webviewRef _ . current ) return ;
2017-11-07 23:11:14 +02:00
2017-11-05 18:51:03 +02:00
this . setState ( {
webviewReady : true ,
} ) ;
2019-02-10 19:16:11 +02:00
if ( Setting . value ( 'env' ) === 'dev' ) {
2019-04-01 21:43:13 +02:00
// this.webviewRef_.current.wrappedInstance.openDevTools();
2019-02-10 19:16:11 +02:00
}
2017-11-07 23:11:14 +02:00
}
editor _ref ( element ) {
if ( this . editor _ === element ) return ;
2017-11-10 00:44:10 +02:00
if ( this . editor _ ) {
this . editor _ . editor . renderer . off ( 'afterRender' , this . onAfterEditorRender _ ) ;
2018-05-10 11:45:44 +02:00
document . querySelector ( '#note-editor' ) . removeEventListener ( 'paste' , this . onEditorPaste _ , true ) ;
2018-06-14 09:52:12 +02:00
document . querySelector ( '#note-editor' ) . removeEventListener ( 'keydown' , this . onEditorKeyDown _ ) ;
2018-09-04 19:20:41 +02:00
document . querySelector ( '#note-editor' ) . removeEventListener ( 'contextmenu' , this . onEditorContextMenu _ ) ;
2017-11-10 00:44:10 +02:00
}
2017-11-07 23:11:14 +02:00
this . editor _ = element ;
2017-11-10 00:44:10 +02:00
if ( this . editor _ ) {
this . editor _ . editor . renderer . on ( 'afterRender' , this . onAfterEditorRender _ ) ;
2018-03-20 01:04:48 +02:00
2018-05-09 19:41:32 +02:00
const cancelledKeys = [ ] ;
2019-07-13 15:52:33 +02:00
const letters = [ 'F' , 'T' , 'P' , 'Q' , 'L' , ',' , 'G' , 'K' ] ;
2018-05-09 19:41:32 +02:00
for ( let i = 0 ; i < letters . length ; i ++ ) {
const l = letters [ i ] ;
2019-09-19 23:51:18 +02:00
cancelledKeys . push ( ` Ctrl+ ${ l } ` ) ;
cancelledKeys . push ( ` Command+ ${ l } ` ) ;
2018-05-09 19:41:32 +02:00
}
2018-03-20 01:04:48 +02:00
for ( let i = 0 ; i < cancelledKeys . length ; i ++ ) {
const k = cancelledKeys [ i ] ;
this . editor _ . editor . commands . bindKey ( k , ( ) => {
// HACK: Ace doesn't seem to provide a way to override its shortcuts, but throwing
// an exception from this undocumented function seems to cancel it without any
// side effect.
// https://stackoverflow.com/questions/36075846
2019-09-19 23:51:18 +02:00
throw new Error ( ` HACK: Overriding Ace Editor shortcut: ${ k } ` ) ;
2018-03-20 01:04:48 +02:00
} ) ;
}
2018-05-10 11:45:44 +02:00
document . querySelector ( '#note-editor' ) . addEventListener ( 'paste' , this . onEditorPaste _ , true ) ;
2018-06-14 09:52:12 +02:00
document . querySelector ( '#note-editor' ) . addEventListener ( 'keydown' , this . onEditorKeyDown _ ) ;
2018-09-04 19:20:41 +02:00
document . querySelector ( '#note-editor' ) . addEventListener ( 'contextmenu' , this . onEditorContextMenu _ ) ;
2018-06-14 09:52:12 +02:00
2018-06-21 19:53:42 +02:00
const lineLeftSpaces = function ( line ) {
let output = '' ;
for ( let i = 0 ; i < line . length ; i ++ ) {
if ( [ ' ' , '\t' ] . indexOf ( line [ i ] ) >= 0 ) {
output += line [ i ] ;
} else {
break ;
}
}
return output ;
2019-07-29 14:13:23 +02:00
} ;
2018-06-21 19:53:42 +02:00
2018-06-14 09:52:12 +02:00
// Disable Markdown auto-completion (eg. auto-adding a dash after a line with a dash.
// https://github.com/ajaxorg/ace/issues/2754
const that = this ; // The "this" within the function below refers to something else
this . editor _ . editor . getSession ( ) . getMode ( ) . getNextLineIndent = function ( state , line ) {
const ls = that . state . lastKeys ;
if ( ls . length >= 2 && ls [ ls . length - 1 ] === 'Enter' && ls [ ls . length - 2 ] === 'Enter' ) return this . $getIndent ( line ) ;
2018-06-21 19:53:42 +02:00
const leftSpaces = lineLeftSpaces ( line ) ;
const lineNoLeftSpaces = line . trimLeft ( ) ;
2019-09-19 23:51:18 +02:00
if ( lineNoLeftSpaces . indexOf ( '- [ ] ' ) === 0 || lineNoLeftSpaces . indexOf ( '- [x] ' ) === 0 || lineNoLeftSpaces . indexOf ( '- [X] ' ) === 0 ) return ` ${ leftSpaces } - [ ] ` ;
if ( lineNoLeftSpaces . indexOf ( '- ' ) === 0 ) return ` ${ leftSpaces } - ` ;
if ( lineNoLeftSpaces . indexOf ( '* ' ) === 0 && line . trim ( ) !== '* * *' ) return ` ${ leftSpaces } * ` ;
2018-06-14 09:52:12 +02:00
2018-06-21 19:53:42 +02:00
const bulletNumber = markdownUtils . olLineNumber ( lineNoLeftSpaces ) ;
2019-09-19 23:51:18 +02:00
if ( bulletNumber ) return ` ${ leftSpaces + ( bulletNumber + 1 ) } . ` ;
2018-06-14 09:52:12 +02:00
return this . $getIndent ( line ) ;
} ;
2017-11-10 00:44:10 +02:00
}
2017-11-07 23:11:14 +02:00
}
2017-11-10 00:44:10 +02:00
aceEditor _change ( body ) {
shared . noteComponent _change ( this , 'body' , body ) ;
2018-05-10 13:02:39 +02:00
this . scheduleHtmlUpdate ( ) ;
2017-11-10 00:44:10 +02:00
this . scheduleSave ( ) ;
}
2018-05-10 13:02:39 +02:00
scheduleHtmlUpdate ( timeout = 500 ) {
if ( this . scheduleHtmlUpdateIID _ ) {
clearTimeout ( this . scheduleHtmlUpdateIID _ ) ;
this . scheduleHtmlUpdateIID _ = null ;
}
if ( timeout ) {
this . scheduleHtmlUpdateIID _ = setTimeout ( ( ) => {
this . updateHtml ( ) ;
} , timeout ) ;
} else {
this . updateHtml ( ) ;
}
}
2019-07-16 20:05:47 +02:00
async updateHtml ( markupLanguage = null , body = null , options = null ) {
2019-02-10 18:11:41 +02:00
if ( ! options ) options = { } ;
if ( ! ( 'useCustomCss' in options ) ) options . useCustomCss = true ;
2019-03-08 19:14:17 +02:00
let bodyToRender = body ;
2019-07-16 20:05:47 +02:00
if ( bodyToRender === null ) {
bodyToRender = this . state . note && this . state . note . body ? this . state . note . body : '' ;
2019-12-29 19:58:40 +02:00
markupLanguage = this . state . note ? this . state . note . markup _language : MarkupToHtml . MARKUP _LANGUAGE _MARKDOWN ;
2019-07-16 20:05:47 +02:00
}
2019-12-29 19:58:40 +02:00
if ( ! markupLanguage ) markupLanguage = MarkupToHtml . MARKUP _LANGUAGE _MARKDOWN ;
2019-07-16 20:05:47 +02:00
const resources = await shared . attachedResources ( bodyToRender ) ;
2018-06-29 20:51:50 +02:00
2019-03-08 19:14:17 +02:00
const theme = themeStyle ( this . props . theme ) ;
const mdOptions = {
codeTheme : theme . codeThemeCss ,
2018-09-24 21:14:21 +02:00
postMessageSyntax : 'ipcProxySendToHost' ,
2019-02-18 02:42:52 +02:00
userCss : options . useCustomCss ? this . props . customCss : '' ,
2019-07-16 20:05:47 +02:00
resources : resources ,
2019-06-21 09:28:59 +02:00
codeHighlightCacheKey : this . state . note ? this . state . note . id : null ,
2018-05-10 13:02:39 +02:00
} ;
const visiblePanes = this . props . visiblePanes || [ 'editor' , 'viewer' ] ;
if ( ! bodyToRender . trim ( ) && visiblePanes . indexOf ( 'viewer' ) >= 0 && visiblePanes . indexOf ( 'editor' ) < 0 ) {
// Fixes https://github.com/laurent22/joplin/issues/217
2019-09-19 23:51:18 +02:00
bodyToRender = ` <i> ${ _ ( 'This note has no content. Click on "%s" to toggle the editor and edit the note.' , _ ( 'Layout' ) ) } </i> ` ;
2018-05-10 13:02:39 +02:00
}
2019-12-29 19:58:40 +02:00
const result = await this . markupToHtml ( ) . render ( markupLanguage , bodyToRender , theme , mdOptions ) ;
2018-05-10 13:02:39 +02:00
2019-03-08 19:14:17 +02:00
this . setState ( {
bodyHtml : result . html ,
2019-12-29 19:58:40 +02:00
lastRenderPluginAssets : result . pluginAssets ,
2019-03-08 19:14:17 +02:00
} ) ;
2018-05-10 13:02:39 +02:00
}
2019-01-26 20:04:32 +02:00
titleField _keyDown ( event ) {
const keyCode = event . keyCode ;
2019-07-29 14:13:23 +02:00
if ( keyCode === 9 ) {
// TAB
2019-01-26 20:04:32 +02:00
event . preventDefault ( ) ;
if ( event . shiftKey ) {
this . props . dispatch ( {
type : 'WINDOW_COMMAND' ,
name : 'focusElement' ,
target : 'noteList' ,
} ) ;
} else {
this . props . dispatch ( {
type : 'WINDOW_COMMAND' ,
name : 'focusElement' ,
target : 'noteBody' ,
} ) ;
}
}
}
2018-03-12 20:01:47 +02:00
async doCommand ( command ) {
2019-01-10 20:58:58 +02:00
if ( ! command ) return ;
2018-03-12 20:01:47 +02:00
2018-12-16 03:49:06 +02:00
let fn = null ;
2020-01-18 15:30:15 +02:00
let args = null ;
2018-03-12 20:01:47 +02:00
2019-01-10 20:58:58 +02:00
if ( command . name === 'exportPdf' ) {
2018-12-16 03:49:06 +02:00
fn = this . commandSavePdf ;
2020-02-11 16:14:04 +02:00
args = { noteIds : command . noteIds } ;
2019-01-10 20:58:58 +02:00
} else if ( command . name === 'print' ) {
2018-12-16 03:49:06 +02:00
fn = this . commandPrint ;
2019-01-10 20:58:58 +02:00
}
if ( this . state . note ) {
if ( command . name === 'textBold' ) {
fn = this . commandTextBold ;
} else if ( command . name === 'textItalic' ) {
fn = this . commandTextItalic ;
2019-02-09 21:13:23 +02:00
} else if ( command . name === 'textLink' ) {
fn = this . commandTextLink ;
2019-01-26 20:04:32 +02:00
} else if ( command . name === 'insertDateTime' ) {
2019-01-10 20:58:58 +02:00
fn = this . commandDateTime ;
} else if ( command . name === 'commandStartExternalEditing' ) {
fn = this . commandStartExternalEditing ;
2020-01-06 23:16:39 +02:00
} else if ( command . name === 'commandStopExternalEditing' ) {
fn = this . commandStopExternalEditing ;
2019-01-10 20:58:58 +02:00
} else if ( command . name === 'showLocalSearch' ) {
fn = this . commandShowLocalSearch ;
2019-06-10 09:05:20 +02:00
} else if ( command . name === 'textCode' ) {
fn = this . commandTextCode ;
2019-07-20 23:13:10 +02:00
} else if ( command . name === 'insertTemplate' ) {
2019-07-29 14:13:23 +02:00
fn = ( ) => {
return this . commandTemplate ( command . value ) ;
} ;
2019-01-10 20:58:58 +02:00
}
2018-03-12 20:01:47 +02:00
}
2019-01-26 20:04:32 +02:00
if ( command . name === 'focusElement' && command . target === 'noteTitle' ) {
fn = ( ) => {
if ( ! this . titleField _ ) return ;
this . titleField _ . focus ( ) ;
2019-07-29 14:13:23 +02:00
} ;
2019-01-26 20:04:32 +02:00
}
if ( command . name === 'focusElement' && command . target === 'noteBody' ) {
fn = ( ) => {
if ( ! this . editor _ ) return ;
this . editor _ . editor . focus ( ) ;
2019-07-29 14:13:23 +02:00
} ;
2019-01-26 20:04:32 +02:00
}
2018-12-16 03:49:06 +02:00
if ( ! fn ) return ;
this . props . dispatch ( {
type : 'WINDOW_COMMAND' ,
name : null ,
} ) ;
requestAnimationFrame ( ( ) => {
fn = fn . bind ( this ) ;
2020-01-18 15:30:15 +02:00
fn ( args ) ;
2018-12-16 03:49:06 +02:00
} ) ;
2018-03-12 20:01:47 +02:00
}
2017-12-22 15:48:44 +02:00
2018-12-09 02:18:10 +02:00
commandShowLocalSearch ( ) {
if ( this . state . showLocalSearch ) {
this . noteSearchBar _ . current . wrappedInstance . focus ( ) ;
} else {
2020-02-05 12:45:24 +02:00
this . setState ( {
showLocalSearch : true ,
localSearch : Object . assign ( { } , this . localSearchDefaultState ) } ) ;
2018-12-09 02:18:10 +02:00
}
this . props . dispatch ( {
type : 'NOTE_VISIBLE_PANES_SET' ,
panes : [ 'editor' , 'viewer' ] ,
} ) ;
}
2019-07-29 12:16:47 +02:00
async commandAttachFile ( filePaths = null , createFileURL = false ) {
2018-05-10 11:45:44 +02:00
if ( ! filePaths ) {
filePaths = bridge ( ) . showOpenDialog ( {
properties : [ 'openFile' , 'createDirectory' , 'multiSelections' ] ,
} ) ;
if ( ! filePaths || ! filePaths . length ) return ;
}
2017-11-30 01:03:10 +02:00
2018-02-06 21:31:22 +02:00
await this . saveIfNeeded ( true ) ;
let note = await Note . load ( this . state . note . id ) ;
2017-12-02 01:26:08 +02:00
2018-06-17 03:44:37 +02:00
const position = this . currentTextOffset ( ) ;
2018-05-10 11:45:44 +02:00
2017-12-02 01:26:08 +02:00
for ( let i = 0 ; i < filePaths . length ; i ++ ) {
const filePath = filePaths [ i ] ;
try {
2019-09-19 23:51:18 +02:00
reg . logger ( ) . info ( ` Attaching ${ filePath } ` ) ;
2019-07-29 12:16:47 +02:00
note = await shim . attachFileToNote ( note , filePath , position , createFileURL ) ;
2017-12-02 01:26:08 +02:00
reg . logger ( ) . info ( 'File was attached.' ) ;
this . setState ( {
note : Object . assign ( { } , note ) ,
lastSavedNote : Object . assign ( { } , note ) ,
} ) ;
2018-05-10 13:02:39 +02:00
2019-07-16 20:05:47 +02:00
this . updateHtml ( note . markup _language , note . body ) ;
2017-12-02 01:26:08 +02:00
} catch ( error ) {
reg . logger ( ) . error ( error ) ;
2018-05-03 12:31:07 +02:00
bridge ( ) . showErrorMessageBox ( error . message ) ;
2017-12-02 01:26:08 +02:00
}
2017-11-30 01:03:10 +02:00
}
}
2018-02-06 21:31:22 +02:00
async commandSetAlarm ( ) {
await this . saveIfNeeded ( true ) ;
2017-11-30 01:03:10 +02:00
this . props . dispatch ( {
type : 'WINDOW_COMMAND' ,
name : 'editAlarm' ,
2018-02-06 21:31:22 +02:00
noteId : this . state . note . id ,
2017-11-30 01:03:10 +02:00
} ) ;
}
2019-03-08 19:14:17 +02:00
async printTo _ ( target , options ) {
2019-10-31 10:33:40 +02:00
// Concurrent print calls are disallowed to avoid incorrect settings being restored upon completion
if ( this . isPrinting _ ) {
2020-02-11 16:14:04 +02:00
console . log ( ` Printing ${ options . path } to ${ target } disallowed, already printing. ` ) ;
2019-10-31 10:33:40 +02:00
return ;
}
this . isPrinting _ = true ;
2018-11-08 01:35:14 +02:00
2019-12-17 11:44:48 +02:00
// Need to save because the interop service reloads the note from the database
await this . saveIfNeeded ( ) ;
2020-02-11 16:14:04 +02:00
if ( target === 'pdf' ) {
try {
const pdfData = await InteropServiceHelper . exportNoteToPdf ( options . noteId , {
printBackground : true ,
pageSize : Setting . value ( 'export.pdfPageSize' ) ,
landscape : Setting . value ( 'export.pdfPageOrientation' ) === 'landscape' ,
customCss : this . props . customCss ,
} ) ;
await shim . fsDriver ( ) . writeFile ( options . path , pdfData , 'buffer' ) ;
} catch ( error ) {
console . error ( error ) ;
bridge ( ) . showErrorMessageBox ( error . message ) ;
2018-12-16 03:49:06 +02:00
}
2020-02-11 16:14:04 +02:00
} else if ( target === 'printer' ) {
try {
await InteropServiceHelper . printNote ( options . noteId , {
printBackground : true ,
customCss : this . props . customCss ,
} ) ;
} catch ( error ) {
console . error ( error ) ;
bridge ( ) . showErrorMessageBox ( error . message ) ;
}
}
this . isPrinting _ = false ;
}
pdfFileName _ ( note , folder ) {
return safeFilename ( ` ${ note . title } - ${ folder . title } .pdf ` , 255 , true ) ;
2018-12-16 03:49:06 +02:00
}
2020-01-18 15:30:15 +02:00
async commandSavePdf ( args ) {
2019-01-10 20:58:58 +02:00
try {
2020-02-11 16:14:04 +02:00
if ( ! this . state . note && ! args . noteIds ) throw new Error ( 'No notes selected for pdf export' ) ;
2020-01-18 15:30:15 +02:00
2020-02-11 16:14:04 +02:00
let noteIds = args . noteIds ? args . noteIds : [ this . state . note . id ] ;
2019-01-10 20:58:58 +02:00
2020-02-11 16:14:04 +02:00
let path = null ;
if ( noteIds . length === 1 ) {
const note = await Note . load ( noteIds [ 0 ] ) ;
const folder = Folder . byId ( this . props . folders , note . parent _id ) ;
path = bridge ( ) . showSaveDialog ( {
filters : [ { name : _ ( 'PDF File' ) , extensions : [ 'pdf' ] } ] ,
defaultPath : this . pdfFileName _ ( note , folder ) ,
} ) ;
} else {
path = bridge ( ) . showOpenDialog ( {
properties : [ 'openDirectory' , 'createDirectory' ] ,
} ) ;
}
2019-01-10 20:58:58 +02:00
if ( ! path ) return ;
2018-12-16 03:49:06 +02:00
2020-02-11 16:14:04 +02:00
for ( let i = 0 ; i < noteIds . length ; i ++ ) {
const note = await Note . load ( noteIds [ i ] ) ;
const folder = Folder . byId ( this . props . folders , note . parent _id ) ;
const pdfPath = ( noteIds . length === 1 ) ? path :
await shim . fsDriver ( ) . findUniqueFilename ( ` ${ path } / ${ this . pdfFileName _ ( note , folder ) } ` ) ;
await this . printTo _ ( 'pdf' , { path : pdfPath , noteId : note . id } ) ;
}
2019-01-10 20:58:58 +02:00
} catch ( error ) {
bridge ( ) . showErrorMessageBox ( error . message ) ;
2018-11-08 01:35:14 +02:00
}
}
2019-03-08 19:14:17 +02:00
async commandPrint ( ) {
2019-01-10 20:58:58 +02:00
try {
2020-01-18 15:30:15 +02:00
if ( ! this . state . note ) throw new Error ( _ ( 'Only one note can be printed at a time.' ) ) ;
await this . printTo _ ( 'printer' , { noteId : this . state . note . id } ) ;
2019-01-10 20:58:58 +02:00
} catch ( error ) {
bridge ( ) . showErrorMessageBox ( error . message ) ;
2019-07-29 14:13:23 +02:00
}
2018-11-08 01:35:14 +02:00
}
2018-06-18 20:56:07 +02:00
async commandStartExternalEditing ( ) {
2020-01-06 23:16:39 +02:00
await this . saveIfNeeded ( true , {
autoTitle : false ,
} ) ;
NoteListUtils . startExternalEditing ( this . state . note . id ) ;
2018-06-18 20:56:07 +02:00
}
async commandStopExternalEditing ( ) {
2020-01-06 23:16:39 +02:00
NoteListUtils . stopExternalEditing ( this . state . note . id ) ;
2018-06-18 20:56:07 +02:00
}
2018-02-07 22:35:11 +02:00
async commandSetTags ( ) {
await this . saveIfNeeded ( true ) ;
this . props . dispatch ( {
type : 'WINDOW_COMMAND' ,
name : 'setTags' ,
2020-02-05 23:24:12 +02:00
noteIds : [ this . state . note . id ] ,
2018-02-07 22:35:11 +02:00
} ) ;
}
2018-06-12 00:47:44 +02:00
// Returns the actual Ace Editor instance (not the React wrapper)
rawEditor ( ) {
return this . editor _ && this . editor _ . editor ? this . editor _ . editor : null ;
}
updateEditorWithDelay ( fn ) {
setTimeout ( ( ) => {
if ( ! this . rawEditor ( ) ) return ;
fn ( this . rawEditor ( ) ) ;
} , 10 ) ;
}
2018-06-14 09:52:12 +02:00
lineAtRow ( row ) {
2018-06-13 18:53:41 +02:00
if ( ! this . state . note ) return '' ;
2019-07-29 14:13:23 +02:00
const body = this . state . note . body ;
2018-06-13 18:53:41 +02:00
const lines = body . split ( '\n' ) ;
2018-06-14 09:52:12 +02:00
if ( row < 0 || row >= lines . length ) return '' ;
return lines [ row ] ;
}
2018-09-04 19:20:41 +02:00
selectedText ( ) {
if ( ! this . state . note || ! this . state . note . body ) return '' ;
const selection = this . textOffsetSelection ( ) ;
if ( ! selection || selection . start === selection . end ) return '' ;
return this . state . note . body . substr ( selection . start , selection . end - selection . start ) ;
}
editorCopyText ( ) {
clipboard . writeText ( this . selectedText ( ) ) ;
}
editorCutText ( ) {
const selectedText = this . selectedText ( ) ;
if ( ! selectedText ) return ;
clipboard . writeText ( selectedText ) ;
const s = this . textOffsetSelection ( ) ;
if ( ! s || s . start === s . end ) return '' ;
const s1 = this . state . note . body . substr ( 0 , s . start ) ;
const s2 = this . state . note . body . substr ( s . end ) ;
shared . noteComponent _change ( this , 'body' , s1 + s2 ) ;
2019-07-29 14:13:23 +02:00
this . updateEditorWithDelay ( editor => {
2018-09-04 19:20:41 +02:00
const range = this . selectionRange _ ;
range . setStart ( range . start . row , range . start . column ) ;
range . setEnd ( range . start . row , range . start . column ) ;
2019-07-29 14:13:23 +02:00
editor
. getSession ( )
. getSelection ( )
. setSelectionRange ( range , false ) ;
2018-09-04 19:20:41 +02:00
editor . focus ( ) ;
} , 10 ) ;
}
editorPasteText ( ) {
2020-02-06 08:38:17 +02:00
this . wrapSelectionWithStrings ( clipboard . readText ( ) , '' , '' , '' ) ;
2018-09-04 19:20:41 +02:00
}
2018-06-14 09:52:12 +02:00
selectionRangePreviousLine ( ) {
2018-06-17 03:44:37 +02:00
if ( ! this . selectionRange _ ) return '' ;
const row = this . selectionRange _ . start . row ;
2018-06-14 09:52:12 +02:00
return this . lineAtRow ( row - 1 ) ;
2018-06-13 18:53:41 +02:00
}
2018-06-14 09:52:12 +02:00
selectionRangeCurrentLine ( ) {
2018-06-17 03:44:37 +02:00
if ( ! this . selectionRange _ ) return '' ;
const row = this . selectionRange _ . start . row ;
2018-06-14 09:52:12 +02:00
return this . lineAtRow ( row ) ;
}
2018-09-04 19:20:41 +02:00
textOffsetSelection ( ) {
return this . selectionRange _ ? this . rangeToTextOffsets ( this . selectionRange _ , this . state . note . body ) : null ;
}
2020-02-06 08:38:17 +02:00
wrapSelectionWithStrings ( string1 , string2 = '' , defaultText = '' , replacementText = null , byLine = false ) {
2018-06-12 00:47:44 +02:00
if ( ! this . rawEditor ( ) || ! this . state . note ) return ;
2018-09-04 19:20:41 +02:00
const selection = this . textOffsetSelection ( ) ;
2018-06-13 18:53:41 +02:00
2018-06-12 00:47:44 +02:00
let newBody = this . state . note . body ;
2018-06-13 18:53:41 +02:00
if ( selection && selection . start !== selection . end ) {
2020-02-06 08:38:17 +02:00
const selectedLines = replacementText !== null ? replacementText : this . state . note . body . substr ( selection . start , selection . end - selection . start ) ;
2020-02-05 23:38:55 +02:00
let selectedStrings = byLine ? selectedLines . split ( /\r?\n/ ) : [ selectedLines ] ;
newBody = this . state . note . body . substr ( 0 , selection . start ) ;
for ( let i = 0 ; i < selectedStrings . length ; i ++ ) {
newBody += string1 + selectedStrings [ i ] + string2 ;
}
newBody += this . state . note . body . substr ( selection . end ) ;
2018-06-12 00:47:44 +02:00
2018-06-17 03:44:37 +02:00
const r = this . selectionRange _ ;
2018-06-12 00:47:44 +02:00
2019-10-01 00:24:07 +02:00
// Because some insertion strings will have newlines, we'll need to account for them
const str1Split = string1 . split ( /\r?\n/ ) ;
// Add the number of newlines to the row
// and add the length of the final line to the column (for strings with no newlines this is the string length)
2018-06-12 00:47:44 +02:00
const newRange = {
2019-10-01 00:24:07 +02:00
start : { row : r . start . row + str1Split . length - 1 ,
column : r . start . column + str1Split [ str1Split . length - 1 ] . length } ,
end : { row : r . end . row + str1Split . length - 1 ,
column : r . end . column + str1Split [ str1Split . length - 1 ] . length } ,
2018-06-12 00:47:44 +02:00
} ;
2020-02-06 08:38:17 +02:00
if ( replacementText !== null ) {
2018-09-04 19:20:41 +02:00
const diff = replacementText . length - ( selection . end - selection . start ) ;
newRange . end . column += diff ;
}
2019-07-29 14:13:23 +02:00
this . updateEditorWithDelay ( editor => {
2018-06-17 03:44:37 +02:00
const range = this . selectionRange _ ;
2018-06-12 00:47:44 +02:00
range . setStart ( newRange . start . row , newRange . start . column ) ;
range . setEnd ( newRange . end . row , newRange . end . column ) ;
2019-07-29 14:13:23 +02:00
editor
. getSession ( )
. getSelection ( )
. setSelectionRange ( range , false ) ;
2018-06-12 00:47:44 +02:00
editor . focus ( ) ;
} ) ;
} else {
2020-02-06 08:38:17 +02:00
let middleText = replacementText !== null ? replacementText : defaultText ;
2018-06-17 03:44:37 +02:00
const textOffset = this . currentTextOffset ( ) ;
const s1 = this . state . note . body . substr ( 0 , textOffset ) ;
const s2 = this . state . note . body . substr ( textOffset ) ;
2018-09-04 19:20:41 +02:00
newBody = s1 + string1 + middleText + string2 + s2 ;
2018-06-14 09:52:12 +02:00
2018-06-17 03:44:37 +02:00
const p = this . textOffsetToCursorPosition ( textOffset + string1 . length , newBody ) ;
const newRange = {
start : { row : p . row , column : p . column } ,
2018-09-04 19:20:41 +02:00
end : { row : p . row , column : p . column + middleText . length } ,
2018-06-14 09:52:12 +02:00
} ;
2018-06-12 00:47:44 +02:00
2018-09-30 21:15:30 +02:00
// BUG!! If replacementText contains newline characters, the logic
// to select the new text will not work.
2019-07-29 14:13:23 +02:00
this . updateEditorWithDelay ( editor => {
2018-09-04 19:20:41 +02:00
if ( middleText && newRange ) {
2018-06-17 03:44:37 +02:00
const range = this . selectionRange _ ;
2018-06-14 09:52:12 +02:00
range . setStart ( newRange . start . row , newRange . start . column ) ;
range . setEnd ( newRange . end . row , newRange . end . column ) ;
2019-07-29 14:13:23 +02:00
editor
. getSession ( )
. getSelection ( )
. setSelectionRange ( range , false ) ;
2018-06-14 09:52:12 +02:00
} else {
for ( let i = 0 ; i < string1 . length ; i ++ ) {
2019-07-29 14:13:23 +02:00
editor
. getSession ( )
. getSelection ( )
. moveCursorRight ( ) ;
2018-06-14 09:52:12 +02:00
}
2018-06-12 00:47:44 +02:00
}
editor . focus ( ) ;
} , 10 ) ;
}
shared . noteComponent _change ( this , 'body' , newBody ) ;
this . scheduleHtmlUpdate ( ) ;
2018-06-17 03:44:37 +02:00
this . scheduleSave ( ) ;
2018-06-12 00:47:44 +02:00
}
2018-06-12 01:12:06 +02:00
commandTextBold ( ) {
this . wrapSelectionWithStrings ( '**' , '**' , _ ( 'strong text' ) ) ;
2018-06-12 00:47:44 +02:00
}
2018-06-12 01:12:06 +02:00
commandTextItalic ( ) {
this . wrapSelectionWithStrings ( '*' , '*' , _ ( 'emphasized text' ) ) ;
}
2018-06-16 17:16:27 +02:00
commandDateTime ( ) {
this . wrapSelectionWithStrings ( time . formatMsToLocal ( new Date ( ) . getTime ( ) ) ) ;
}
2018-06-14 09:52:12 +02:00
commandTextCode ( ) {
2019-10-01 00:24:07 +02:00
const selection = this . textOffsetSelection ( ) ;
let string = this . state . note . body . substr ( selection . start , selection . end - selection . start ) ;
// Look for newlines
let match = string . match ( /\r?\n/ ) ;
if ( match && match . length > 0 ) {
// Follow the same newline style
this . wrapSelectionWithStrings ( ` \` \` \` ${ match [ 0 ] } ` , ` ${ match [ 0 ] } \` \` \` ` ) ;
2019-10-01 20:33:46 +02:00
} else {
2019-10-01 00:24:07 +02:00
this . wrapSelectionWithStrings ( '`' , '`' ) ;
}
2018-06-14 09:52:12 +02:00
}
2019-07-20 23:13:10 +02:00
commandTemplate ( value ) {
this . wrapSelectionWithStrings ( TemplateUtils . render ( value ) ) ;
}
2020-02-05 23:38:55 +02:00
addListItem ( string1 , string2 = '' , defaultText = '' , byLine = false ) {
2019-07-29 14:13:23 +02:00
let newLine = '\n' ;
2020-02-05 23:38:55 +02:00
const range = this . selectionRange _ ;
if ( ! range || ( range . start . row === range . end . row && ! this . selectionRangeCurrentLine ( ) ) ) {
newLine = '' ;
}
2020-02-06 08:38:17 +02:00
this . wrapSelectionWithStrings ( newLine + string1 , string2 , defaultText , null , byLine ) ;
2018-06-14 09:52:12 +02:00
}
2018-06-12 01:12:06 +02:00
commandTextCheckbox ( ) {
2020-02-05 23:38:55 +02:00
this . addListItem ( '- [ ] ' , '' , _ ( 'List item' ) , true ) ;
2018-06-12 01:12:06 +02:00
}
2018-06-14 09:52:12 +02:00
commandTextListUl ( ) {
2020-02-05 23:38:55 +02:00
this . addListItem ( '- ' , '' , _ ( 'List item' ) , true ) ;
2018-06-14 09:52:12 +02:00
}
2020-02-05 23:38:55 +02:00
// Converting multiple lines to a numbered list will use the same number on each line
// Not ideal, but the rendered text will still be correct.
2018-06-14 09:52:12 +02:00
commandTextListOl ( ) {
let bulletNumber = markdownUtils . olLineNumber ( this . selectionRangeCurrentLine ( ) ) ;
if ( ! bulletNumber ) bulletNumber = markdownUtils . olLineNumber ( this . selectionRangePreviousLine ( ) ) ;
if ( ! bulletNumber ) bulletNumber = 0 ;
2020-02-05 23:38:55 +02:00
this . addListItem ( ` ${ bulletNumber + 1 } . ` , '' , _ ( 'List item' ) , true ) ;
2018-06-14 09:52:12 +02:00
}
commandTextHeading ( ) {
this . addListItem ( '## ' ) ;
2018-06-12 01:12:06 +02:00
}
2018-06-14 09:52:12 +02:00
commandTextHorizontalRule ( ) {
this . addListItem ( '* * *' ) ;
}
2018-06-12 01:12:06 +02:00
async commandTextLink ( ) {
const url = await dialogs . prompt ( _ ( 'Insert Hyperlink' ) ) ;
2019-09-19 23:51:18 +02:00
this . wrapSelectionWithStrings ( '[' , ` ]( ${ url } ) ` ) ;
2018-06-12 00:47:44 +02:00
}
2019-09-13 00:16:42 +02:00
itemContextMenu ( ) {
2018-02-06 21:31:22 +02:00
const note = this . state . note ;
if ( ! note ) return ;
2017-11-11 00:18:00 +02:00
2019-07-29 14:13:23 +02:00
const menu = new Menu ( ) ;
2017-11-11 00:18:00 +02:00
2019-07-29 14:13:23 +02:00
menu . append (
new MenuItem ( {
label : _ ( 'Attach file' ) ,
click : async ( ) => {
return this . commandAttachFile ( ) ;
} ,
} )
) ;
2017-11-11 00:18:00 +02:00
2019-07-29 14:13:23 +02:00
menu . append (
new MenuItem ( {
label : _ ( 'Tags' ) ,
click : async ( ) => {
return this . commandSetTags ( ) ;
} ,
} )
) ;
2018-02-07 22:35:11 +02:00
2019-07-29 14:13:23 +02:00
if ( note . is _todo ) {
menu . append (
new MenuItem ( {
label : _ ( 'Set alarm' ) ,
click : async ( ) => {
return this . commandSetAlarm ( ) ;
} ,
} )
) ;
2018-02-06 21:31:22 +02:00
}
2017-11-28 00:50:46 +02:00
2017-11-11 00:18:00 +02:00
menu . popup ( bridge ( ) . window ( ) ) ;
}
2019-10-03 01:07:58 +02:00
createToolbarItems ( note , editorIsVisible ) {
2018-06-12 00:47:44 +02:00
const toolbarItems = [ ] ;
if ( note && this . state . folder && [ 'Search' , 'Tag' ] . includes ( this . props . notesParentType ) ) {
toolbarItems . push ( {
2019-03-15 23:57:58 +02:00
title : _ ( 'In: %s' , substrWithEllipsis ( this . state . folder . title , 0 , 16 ) ) ,
2019-02-24 13:10:55 +02:00
iconName : 'fa-book' ,
2019-03-15 23:57:58 +02:00
onClick : ( ) => {
this . props . dispatch ( {
2019-07-29 14:13:23 +02:00
type : 'FOLDER_AND_NOTE_SELECT' ,
2019-03-15 23:57:58 +02:00
folderId : this . state . folder . id ,
noteId : note . id ,
} ) ;
} ,
2018-06-12 00:47:44 +02:00
} ) ;
}
2019-02-07 20:17:09 +02:00
if ( this . props . historyNotes . length ) {
toolbarItems . push ( {
tooltip : _ ( 'Back' ) ,
iconName : 'fa-arrow-left' ,
onClick : ( ) => {
if ( ! this . props . historyNotes . length ) return ;
const lastItem = this . props . historyNotes [ this . props . historyNotes . length - 1 ] ;
this . props . dispatch ( {
2019-07-29 14:13:23 +02:00
type : 'FOLDER_AND_NOTE_SELECT' ,
2019-02-07 20:17:09 +02:00
folderId : lastItem . parent _id ,
noteId : lastItem . id ,
historyNoteAction : 'pop' ,
2019-07-29 14:13:23 +02:00
} ) ;
2019-02-07 20:17:09 +02:00
} ,
} ) ;
}
2019-12-29 19:58:40 +02:00
if ( note . markup _language === MarkupToHtml . MARKUP _LANGUAGE _MARKDOWN && editorIsVisible ) {
2019-07-17 23:42:53 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Bold' ) ,
iconName : 'fa-bold' ,
2019-07-29 14:13:23 +02:00
onClick : ( ) => {
return this . commandTextBold ( ) ;
} ,
2019-07-17 23:42:53 +02:00
} ) ;
2018-06-12 00:47:44 +02:00
2019-07-17 23:42:53 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Italic' ) ,
iconName : 'fa-italic' ,
2019-07-29 14:13:23 +02:00
onClick : ( ) => {
return this . commandTextItalic ( ) ;
} ,
2019-07-17 23:42:53 +02:00
} ) ;
2018-06-12 00:47:44 +02:00
2019-07-17 23:42:53 +02:00
toolbarItems . push ( {
type : 'separator' ,
} ) ;
2018-06-14 09:52:12 +02:00
2019-07-17 23:42:53 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Hyperlink' ) ,
iconName : 'fa-link' ,
2019-07-29 14:13:23 +02:00
onClick : ( ) => {
return this . commandTextLink ( ) ;
} ,
2019-07-17 23:42:53 +02:00
} ) ;
2018-06-12 01:12:06 +02:00
2019-07-17 23:42:53 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Code' ) ,
iconName : 'fa-code' ,
2019-07-29 14:13:23 +02:00
onClick : ( ) => {
return this . commandTextCode ( ) ;
} ,
2019-07-17 23:42:53 +02:00
} ) ;
2018-06-12 01:12:06 +02:00
2019-07-17 23:42:53 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Attach file' ) ,
iconName : 'fa-paperclip' ,
2019-07-29 14:13:23 +02:00
onClick : ( ) => {
return this . commandAttachFile ( ) ;
} ,
2019-07-17 23:42:53 +02:00
} ) ;
2018-06-12 00:47:44 +02:00
2019-07-17 23:42:53 +02:00
toolbarItems . push ( {
type : 'separator' ,
} ) ;
2018-06-14 09:52:12 +02:00
2019-07-17 23:42:53 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Numbered List' ) ,
iconName : 'fa-list-ol' ,
2019-07-29 14:13:23 +02:00
onClick : ( ) => {
return this . commandTextListOl ( ) ;
} ,
2019-07-17 23:42:53 +02:00
} ) ;
2018-06-14 09:52:12 +02:00
2019-07-17 23:42:53 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Bulleted List' ) ,
iconName : 'fa-list-ul' ,
2019-07-29 14:13:23 +02:00
onClick : ( ) => {
return this . commandTextListUl ( ) ;
} ,
2019-07-17 23:42:53 +02:00
} ) ;
2018-06-14 09:52:12 +02:00
2019-07-17 23:42:53 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Checkbox' ) ,
iconName : 'fa-check-square' ,
2019-07-29 14:13:23 +02:00
onClick : ( ) => {
return this . commandTextCheckbox ( ) ;
} ,
2019-07-17 23:42:53 +02:00
} ) ;
2018-06-14 09:52:12 +02:00
2019-07-17 23:42:53 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Heading' ) ,
iconName : 'fa-header' ,
2019-07-29 14:13:23 +02:00
onClick : ( ) => {
return this . commandTextHeading ( ) ;
} ,
2019-07-17 23:42:53 +02:00
} ) ;
2018-06-14 09:52:12 +02:00
2019-07-17 23:42:53 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Horizontal Rule' ) ,
iconName : 'fa-ellipsis-h' ,
2019-07-29 14:13:23 +02:00
onClick : ( ) => {
return this . commandTextHorizontalRule ( ) ;
} ,
2019-07-17 23:42:53 +02:00
} ) ;
2018-06-14 09:52:12 +02:00
2019-07-17 23:42:53 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Insert Date Time' ) ,
iconName : 'fa-calendar-plus-o' ,
2019-07-29 14:13:23 +02:00
onClick : ( ) => {
return this . commandDateTime ( ) ;
} ,
2019-07-17 23:42:53 +02:00
} ) ;
2018-06-16 17:16:27 +02:00
2019-07-17 23:42:53 +02:00
toolbarItems . push ( {
type : 'separator' ,
} ) ;
}
2018-06-14 09:52:12 +02:00
2018-06-18 20:56:07 +02:00
if ( note && this . props . watchedNoteFiles . indexOf ( note . id ) >= 0 ) {
toolbarItems . push ( {
tooltip : _ ( 'Click to stop external editing' ) ,
title : _ ( 'Watching...' ) ,
iconName : 'fa-external-link' ,
2019-07-29 14:13:23 +02:00
onClick : ( ) => {
return this . commandStopExternalEditing ( ) ;
} ,
2018-06-18 20:56:07 +02:00
} ) ;
} else {
toolbarItems . push ( {
tooltip : _ ( 'Edit in external editor' ) ,
iconName : 'fa-external-link' ,
2019-07-29 14:13:23 +02:00
onClick : ( ) => {
return this . commandStartExternalEditing ( ) ;
} ,
2018-06-18 20:56:07 +02:00
} ) ;
}
2018-06-12 00:47:44 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Tags' ) ,
iconName : 'fa-tags' ,
2019-07-29 14:13:23 +02:00
onClick : ( ) => {
return this . commandSetTags ( ) ;
} ,
2018-06-12 00:47:44 +02:00
} ) ;
if ( note . is _todo ) {
const item = {
2018-06-14 10:02:01 +02:00
iconName : 'fa-clock-o' ,
2018-06-12 00:47:44 +02:00
enabled : ! note . todo _completed ,
2019-07-29 14:13:23 +02:00
onClick : ( ) => {
return this . commandSetAlarm ( ) ;
} ,
} ;
2018-06-12 00:47:44 +02:00
if ( Note . needAlarm ( note ) ) {
item . title = time . formatMsToLocal ( note . todo _due ) ;
} else {
item . tooltip = _ ( 'Set alarm' ) ;
}
toolbarItems . push ( item ) ;
}
2019-03-15 23:57:58 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Note properties' ) ,
iconName : 'fa-info-circle' ,
onClick : ( ) => {
const n = this . state . note ;
if ( ! n || ! n . id ) return ;
this . props . dispatch ( {
type : 'WINDOW_COMMAND' ,
name : 'commandNoteProperties' ,
noteId : n . id ,
2019-07-29 14:13:23 +02:00
onRevisionLinkClick : ( ) => {
this . setState ( { showRevisions : true } ) ;
} ,
2019-03-15 23:57:58 +02:00
} ) ;
} ,
} ) ;
2018-06-12 00:47:44 +02:00
return toolbarItems ;
}
2019-01-29 20:02:34 +02:00
renderNoNotes ( rootStyle ) {
2019-07-29 14:13:23 +02:00
const emptyDivStyle = Object . assign (
{
backgroundColor : 'black' ,
opacity : 0.1 ,
} ,
rootStyle
) ;
return < div style = { emptyDivStyle } > < / div > ;
2019-01-29 20:02:34 +02:00
}
renderMultiNotes ( rootStyle ) {
const theme = themeStyle ( this . props . theme ) ;
const multiNotesButton _click = item => {
if ( item . submenu ) {
item . submenu . popup ( bridge ( ) . window ( ) ) ;
} else {
item . click ( ) ;
}
2019-07-29 14:13:23 +02:00
} ;
2019-01-29 20:02:34 +02:00
const menu = NoteListUtils . makeContextMenu ( this . props . selectedNoteIds , {
notes : this . props . notes ,
dispatch : this . props . dispatch ,
2020-01-06 23:16:39 +02:00
watchedNoteFiles : this . props . watchedNoteFiles ,
2019-01-29 20:02:34 +02:00
} ) ;
const buttonStyle = Object . assign ( { } , theme . buttonStyle , {
marginBottom : 10 ,
} ) ;
const itemComps = [ ] ;
const menuItems = menu . items ;
for ( let i = 0 ; i < menuItems . length ; i ++ ) {
const item = menuItems [ i ] ;
if ( ! item . enabled ) continue ;
2019-07-29 14:13:23 +02:00
itemComps . push (
< button key = { item . label } style = { buttonStyle } onClick = { ( ) => multiNotesButton _click ( item ) } >
{ item . label }
< / button >
) ;
2019-01-29 20:02:34 +02:00
}
rootStyle = Object . assign ( { } , rootStyle , {
paddingTop : rootStyle . paddingLeft ,
display : 'inline-flex' ,
justifyContent : 'center' ,
} ) ;
2019-07-29 14:13:23 +02:00
return (
< div style = { rootStyle } >
< div style = { { display : 'flex' , flexDirection : 'column' } } > { itemComps } < / div >
2019-01-29 20:02:34 +02:00
< / div >
2019-07-29 14:13:23 +02:00
) ;
2019-01-29 20:02:34 +02:00
}
2017-11-05 01:27:13 +02:00
render ( ) {
2017-11-07 23:11:14 +02:00
const style = this . props . style ;
2017-11-05 01:27:13 +02:00
const note = this . state . note ;
2018-01-12 21:58:01 +02:00
const body = note && note . body ? note . body : '' ;
2019-12-29 19:58:40 +02:00
const markupLanguage = note ? note . markup _language : MarkupToHtml . MARKUP _LANGUAGE _MARKDOWN ;
2017-11-08 19:51:55 +02:00
const theme = themeStyle ( this . props . theme ) ;
2017-11-10 21:18:19 +02:00
const visiblePanes = this . props . visiblePanes || [ 'editor' , 'viewer' ] ;
2018-02-06 20:12:43 +02:00
const isTodo = note && ! ! note . is _todo ;
2019-11-06 23:51:08 +02:00
var keyboardMode = this . props . keyboardMode ;
if ( keyboardMode === 'default' || ! keyboardMode ) {
keyboardMode = null ;
}
2017-11-07 23:11:14 +02:00
2017-11-10 22:12:38 +02:00
const borderWidth = 1 ;
2019-07-29 14:13:23 +02:00
const rootStyle = Object . assign (
{
2019-09-19 23:51:18 +02:00
borderLeft : ` ${ borderWidth } px solid ${ theme . dividerColor } ` ,
2019-07-29 14:13:23 +02:00
boxSizing : 'border-box' ,
paddingLeft : 10 ,
paddingRight : 0 ,
} ,
style
) ;
2017-11-10 22:12:38 +02:00
const innerWidth = rootStyle . width - rootStyle . paddingLeft - rootStyle . paddingRight - borderWidth ;
2019-05-06 22:35:29 +02:00
if ( this . state . showRevisions && note && note . id ) {
rootStyle . paddingRight = rootStyle . paddingLeft ;
rootStyle . paddingTop = rootStyle . paddingLeft ;
rootStyle . paddingBottom = rootStyle . paddingLeft ;
rootStyle . display = 'inline-flex' ;
return (
< div style = { rootStyle } >
2019-07-29 14:13:23 +02:00
< NoteRevisionViewer noteId = { note . id } customCss = { this . props . customCss } onBack = { this . noteRevisionViewer _onBack } / >
2019-05-06 22:35:29 +02:00
< / div >
) ;
}
2019-01-29 20:02:34 +02:00
if ( this . props . selectedNoteIds . length > 1 ) {
return this . renderMultiNotes ( rootStyle ) ;
2019-07-29 14:13:23 +02:00
} else if ( ! note || ! ! note . encryption _applied ) {
2019-10-09 21:35:13 +02:00
// || (note && !this.props.newNote && this.props.noteId && note.id !== this.props.noteId)) { // note.id !== props.noteId is when the note has not been loaded yet, and the previous one is still in the state
2019-01-29 20:02:34 +02:00
return this . renderNoNotes ( rootStyle ) ;
2017-11-10 19:58:17 +02:00
}
2017-11-11 00:18:00 +02:00
const titleBarStyle = {
2017-11-10 22:12:38 +02:00
width : innerWidth - rootStyle . paddingLeft ,
2017-11-11 00:18:00 +02:00
height : 30 ,
2017-11-10 22:12:38 +02:00
boxSizing : 'border-box' ,
2017-11-11 00:18:00 +02:00
marginTop : 10 ,
2017-11-30 01:03:10 +02:00
marginBottom : 0 ,
2017-11-11 00:18:00 +02:00
display : 'flex' ,
flexDirection : 'row' ,
2018-03-18 01:51:15 +02:00
alignItems : 'center' ,
2017-11-11 00:18:00 +02:00
} ;
const titleEditorStyle = {
flex : 1 ,
display : 'inline-block' ,
2017-11-10 22:12:38 +02:00
paddingTop : 5 ,
paddingBottom : 5 ,
paddingLeft : 8 ,
paddingRight : 8 ,
marginRight : rootStyle . paddingLeft ,
2020-02-05 12:37:26 +02:00
color : theme . textStyle . color ,
fontSize : theme . textStyle . fontSize * 1.25 * 1.5 ,
2018-11-08 00:37:13 +02:00
backgroundColor : theme . backgroundColor ,
border : '1px solid' ,
borderColor : theme . dividerColor ,
2017-11-10 22:12:38 +02:00
} ;
2020-02-05 12:37:26 +02:00
const toolbarStyle = {
marginTop : 3 ,
marginBottom : 0 ,
} ;
2018-11-08 00:16:05 +02:00
const tagStyle = {
2017-11-30 01:03:10 +02:00
marginBottom : 10 ,
2019-07-29 14:13:23 +02:00
height : 30 ,
2017-11-30 01:03:10 +02:00
} ;
2018-12-09 02:18:10 +02:00
const searchBarHeight = this . state . showLocalSearch ? 35 : 0 ;
2018-11-24 13:42:50 +02:00
let bottomRowHeight = 0 ;
2020-01-06 23:23:22 +02:00
if ( this . canDisplayTagBar ( ) ) {
2020-02-05 12:37:26 +02:00
bottomRowHeight = rootStyle . height - titleBarStyle . height - titleBarStyle . marginBottom - titleBarStyle . marginTop - theme . toolbarHeight - toolbarStyle . marginTop - toolbarStyle . marginBottom - tagStyle . height - tagStyle . marginBottom ;
2018-11-24 13:42:50 +02:00
} else {
2020-01-06 23:23:22 +02:00
toolbarStyle . marginBottom = tagStyle . marginBottom ,
2020-02-05 12:37:26 +02:00
bottomRowHeight = rootStyle . height - titleBarStyle . height - titleBarStyle . marginBottom - titleBarStyle . marginTop - theme . toolbarHeight - toolbarStyle . marginTop - toolbarStyle . marginBottom ;
2018-11-24 13:42:50 +02:00
}
2017-11-10 22:12:38 +02:00
2018-12-09 02:18:10 +02:00
bottomRowHeight -= searchBarHeight ;
2019-07-29 14:13:23 +02:00
2017-11-07 23:11:14 +02:00
const viewerStyle = {
2017-11-10 22:12:38 +02:00
width : Math . floor ( innerWidth / 2 ) ,
height : bottomRowHeight ,
2017-11-07 23:11:14 +02:00
overflow : 'hidden' ,
float : 'left' ,
verticalAlign : 'top' ,
2017-11-10 22:43:44 +02:00
boxSizing : 'border-box' ,
2017-11-07 23:11:14 +02:00
} ;
2017-11-08 19:51:55 +02:00
const paddingTop = 14 ;
2017-11-07 23:11:14 +02:00
const editorStyle = {
2017-11-10 22:12:38 +02:00
width : innerWidth - viewerStyle . width ,
height : bottomRowHeight - paddingTop ,
2017-11-10 00:44:10 +02:00
overflowY : 'hidden' ,
2017-11-07 23:11:14 +02:00
float : 'left' ,
verticalAlign : 'top' ,
2019-09-19 23:51:18 +02:00
paddingTop : ` ${ paddingTop } px ` ,
lineHeight : ` ${ theme . textAreaLineHeight } px ` ,
fontSize : ` ${ theme . editorFontSize } px ` ,
2018-11-08 00:37:13 +02:00
color : theme . color ,
backgroundColor : theme . backgroundColor ,
2019-12-03 13:12:47 +02:00
editorTheme : theme . editorTheme , // Defined in theme.js
2017-11-07 23:11:14 +02:00
} ;
2017-11-05 01:27:13 +02:00
2017-11-10 21:18:19 +02:00
if ( visiblePanes . indexOf ( 'viewer' ) < 0 ) {
// Note: setting webview.display to "none" is currently not supported due
// to this bug: https://github.com/electron/electron/issues/8277
// So instead setting the width 0.
viewerStyle . width = 0 ;
2017-11-10 22:12:38 +02:00
editorStyle . width = innerWidth ;
2017-11-10 21:18:19 +02:00
}
if ( visiblePanes . indexOf ( 'editor' ) < 0 ) {
2018-10-04 18:56:39 +02:00
// Note: Ideally we'd set the display to "none" to take the editor out
// of the DOM but if we do that, certain things won't work, in particular
// things related to scroll, which are based on the editor. See
// editorScrollTop_, restoreScrollTop_, etc.
editorStyle . width = 0 ;
2017-11-10 22:12:38 +02:00
viewerStyle . width = innerWidth ;
2017-11-10 21:18:19 +02:00
}
2017-11-10 23:04:53 +02:00
if ( visiblePanes . indexOf ( 'viewer' ) >= 0 && visiblePanes . indexOf ( 'editor' ) >= 0 ) {
2019-09-19 23:51:18 +02:00
viewerStyle . borderLeft = ` 1px solid ${ theme . dividerColor } ` ;
2017-11-10 23:04:53 +02:00
} else {
viewerStyle . borderLeft = 'none' ;
}
2019-05-06 22:35:29 +02:00
if ( this . state . webviewReady && this . webviewRef _ . current ) {
2018-05-10 13:02:39 +02:00
let html = this . state . bodyHtml ;
2018-02-19 20:56:56 +02:00
2018-05-10 13:02:39 +02:00
const htmlHasChanged = this . lastSetHtml _ !== html ;
2019-07-29 14:13:23 +02:00
if ( htmlHasChanged ) {
2019-03-08 19:14:17 +02:00
let options = {
2019-12-29 19:58:40 +02:00
pluginAssets : this . state . lastRenderPluginAssets ,
2019-05-22 16:56:07 +02:00
downloadResources : Setting . value ( 'sync.resourceDownloadMode' ) ,
2019-03-08 19:14:17 +02:00
} ;
2019-02-09 01:07:01 +02:00
this . webviewRef _ . current . wrappedInstance . send ( 'setHtml' , html , options ) ;
2018-05-10 13:02:39 +02:00
this . lastSetHtml _ = html ;
2018-02-19 20:56:56 +02:00
}
2018-12-09 02:18:10 +02:00
let keywords = [ ] ;
const markerOptions = { } ;
if ( this . state . showLocalSearch ) {
2019-07-29 14:13:23 +02:00
keywords = [
{
type : 'text' ,
value : this . state . localSearch . query ,
accuracy : 'partially' ,
} ,
] ;
2018-12-09 02:18:10 +02:00
markerOptions . selectedIndex = this . state . localSearch . selectedIndex ;
2019-06-26 00:09:53 +02:00
markerOptions . separateWordSearch = false ;
2019-09-07 12:57:31 +02:00
markerOptions . searchTimestamp = this . state . localSearch . timestamp ;
2018-12-09 02:18:10 +02:00
} else {
const search = BaseModel . byId ( this . props . searches , this . props . selectedSearchId ) ;
2018-12-14 00:57:14 +02:00
if ( search ) {
const parsedQuery = SearchEngine . instance ( ) . parseQuery ( search . query _pattern ) ;
keywords = SearchEngine . instance ( ) . allParsedQueryTerms ( parsedQuery ) ;
}
2018-12-09 02:18:10 +02:00
}
2018-05-10 13:02:39 +02:00
2018-12-14 00:57:14 +02:00
const keywordHash = JSON . stringify ( keywords ) ;
if ( htmlHasChanged || keywordHash !== this . lastSetMarkers _ || ! ObjectUtils . fieldsEqual ( this . lastSetMarkersOptions _ , markerOptions ) ) {
this . lastSetMarkers _ = keywordHash ;
2018-12-09 02:18:10 +02:00
this . lastSetMarkersOptions _ = Object . assign ( { } , markerOptions ) ;
2019-02-09 01:07:01 +02:00
this . webviewRef _ . current . wrappedInstance . send ( 'setMarkers' , keywords , markerOptions ) ;
2018-05-10 13:02:39 +02:00
}
2017-11-05 18:51:03 +02:00
}
2019-10-03 01:07:58 +02:00
const editorIsVisible = visiblePanes . indexOf ( 'editor' ) >= 0 ;
const toolbarItems = this . createToolbarItems ( note , editorIsVisible ) ;
2017-11-30 01:03:10 +02:00
2019-07-29 14:13:23 +02:00
const toolbar = < Toolbar style = { toolbarStyle } items = { toolbarItems } / > ;
const titleEditor = (
< input
type = "text"
ref = { elem => {
this . titleField _ = elem ;
} }
style = { titleEditorStyle }
value = { note && note . title ? note . title : '' }
onChange = { event => {
this . title _changeText ( event ) ;
} }
onKeyDown = { this . titleField _keyDown }
placeholder = { this . props . newNote ? _ ( 'Creating new %s...' , isTodo ? _ ( 'to-do' ) : _ ( 'note' ) ) : '' }
/ >
) ;
2020-01-06 23:23:22 +02:00
const tagList = this . canDisplayTagBar ( ) ? < TagList style = { tagStyle } items = { this . state . noteTags } / > : null ;
2019-07-29 14:13:23 +02:00
const titleBarMenuButton = (
< IconButton
style = { {
display : 'flex' ,
} }
iconName = "fa-caret-down"
theme = { this . props . theme }
onClick = { ( ) => {
this . itemContextMenu ( ) ;
} }
/ >
) ;
2017-11-11 00:18:00 +02:00
2019-07-29 14:13:23 +02:00
const titleBarDate = < span style = { Object . assign ( { } , theme . textStyle , { color : theme . colorFaded } ) } > { time . formatMsToLocal ( note . user _updated _time ) } < / span > ;
2018-03-18 01:51:15 +02:00
2019-07-29 14:13:23 +02:00
const viewer = < NoteTextViewer ref = { this . webviewRef _ } viewerStyle = { viewerStyle } onDomReady = { this . webview _domReady } onIpcMessage = { this . webview _ipcMessage } / > ;
2017-11-10 00:44:10 +02:00
const editorRootStyle = Object . assign ( { } , editorStyle ) ;
delete editorRootStyle . width ;
delete editorRootStyle . height ;
delete editorRootStyle . fontSize ;
2019-11-06 23:51:08 +02:00
const onBeforeLoad = ( ace ) => {
const save = ( ) => {
this . saveIfNeeded ( ) ;
} ;
const VimApi = ace . acequire ( 'ace/keyboard/vim' ) ;
if ( VimApi . CodeMirror && VimApi . CodeMirror . Vim ) {
VimApi . CodeMirror . Vim . defineEx ( 'write' , 'w' , save ) ;
}
} ;
const onLoad = ( ) => { } ;
2019-07-29 14:13:23 +02:00
const editor = (
< AceEditor
value = { body }
mode = { markupLanguage === Note . MARKUP _LANGUAGE _HTML ? 'text' : 'markdown' }
theme = { editorRootStyle . editorTheme }
style = { editorRootStyle }
2019-09-19 23:51:18 +02:00
width = { ` ${ editorStyle . width } px ` }
height = { ` ${ editorStyle . height } px ` }
2019-07-29 14:13:23 +02:00
fontSize = { editorStyle . fontSize }
showGutter = { false }
name = "note-editor"
wrapEnabled = { true }
2019-09-13 00:16:42 +02:00
onScroll = { ( ) => {
2019-07-29 14:13:23 +02:00
this . editor _scroll ( ) ;
} }
ref = { elem => {
this . editor _ref ( elem ) ;
} }
onChange = { body => {
this . aceEditor _change ( body ) ;
} }
showPrintMargin = { false }
onSelectionChange = { this . aceEditor _selectionChange }
onFocus = { this . aceEditor _focus }
readOnly = { visiblePanes . indexOf ( 'editor' ) < 0 }
2020-01-07 00:27:37 +02:00
// Enable/Disable the autoclosing braces
setOptions = { { behavioursEnabled : Setting . value ( 'editor.autoMatchingBraces' ) } }
2019-07-29 14:13:23 +02:00
// Disable warning: "Automatically scrolling cursor into view after
// selection change this will be disabled in the next version set
// editor.$blockScrolling = Infinity to disable this message"
editorProps = { { $blockScrolling : Infinity } }
// This is buggy (gets outside the container)
highlightActiveLine = { false }
2019-11-06 23:51:08 +02:00
keyboardHandler = { keyboardMode }
onBeforeLoad = { onBeforeLoad }
onLoad = { onLoad }
2018-12-09 02:18:10 +02:00
/ >
) ;
2020-02-05 12:45:24 +02:00
const noteSearchBarComp = ! this . state . showLocalSearch ? null : (
< NoteSearchBar
ref = { this . noteSearchBar _ }
style = { {
display : 'flex' ,
height : searchBarHeight ,
width : innerWidth ,
borderTop : ` 1px solid ${ theme . dividerColor } ` ,
} }
query = { this . state . localSearch . query }
searching = { this . state . localSearch . searching }
resultCount = { this . state . localSearch . resultCount }
selectedIndex = { this . state . localSearch . selectedIndex }
onChange = { this . noteSearchBar _change }
onNext = { this . noteSearchBar _next }
onPrevious = { this . noteSearchBar _previous }
onClose = { this . noteSearchBar _close }
/ >
) ;
2019-07-29 14:13:23 +02:00
2017-11-05 01:27:13 +02:00
return (
2018-05-10 11:45:44 +02:00
< div style = { rootStyle } onDrop = { this . onDrop _ } >
2017-11-11 00:18:00 +02:00
< div style = { titleBarStyle } >
2019-07-29 14:13:23 +02:00
{ titleEditor }
{ titleBarDate }
{ false ? titleBarMenuButton : null }
2017-11-11 00:18:00 +02:00
< / div >
2019-07-29 14:13:23 +02:00
{ toolbar }
{ tagList }
{ editor }
{ viewer }
< div style = { { clear : 'both' } } / >
{ noteSearchBarComp }
2017-11-05 01:27:13 +02:00
< / div >
) ;
}
}
2019-07-29 14:13:23 +02:00
const mapStateToProps = state => {
2017-11-05 01:27:13 +02:00
return {
2017-11-22 20:35:31 +02:00
noteId : state . selectedNoteIds . length === 1 ? state . selectedNoteIds [ 0 ] : null ,
2019-01-29 20:02:34 +02:00
notes : state . notes ,
2019-01-10 20:58:58 +02:00
selectedNoteIds : state . selectedNoteIds ,
2019-09-09 19:16:00 +02:00
selectedNoteHash : state . selectedNoteHash ,
2018-11-08 00:16:05 +02:00
noteTags : state . selectedNoteTags ,
2017-11-05 20:36:27 +02:00
folderId : state . selectedFolderId ,
itemType : state . selectedItemType ,
folders : state . folders ,
theme : state . settings . theme ,
2017-11-12 20:12:05 +02:00
syncStarted : state . syncStarted ,
2018-01-12 21:58:01 +02:00
newNote : state . newNote ,
2018-03-12 20:01:47 +02:00
windowCommand : state . windowCommand ,
2018-03-20 01:04:48 +02:00
notesParentType : state . notesParentType ,
searches : state . searches ,
selectedSearchId : state . selectedSearchId ,
2018-06-18 20:56:07 +02:00
watchedNoteFiles : state . watchedNoteFiles ,
2018-11-08 00:52:31 +02:00
customCss : state . customCss ,
2019-01-31 00:45:28 +02:00
lastEditorScrollPercents : state . lastEditorScrollPercents ,
2019-02-07 20:17:09 +02:00
historyNotes : state . historyNotes ,
2019-07-20 23:13:10 +02:00
templates : state . templates ,
2017-11-05 01:27:13 +02:00
} ;
} ;
const NoteText = connect ( mapStateToProps ) ( NoteTextComponent ) ;
2018-11-08 00:37:13 +02:00
module . exports = { NoteText } ;