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' ) ;
const Search = require ( 'lib/models/Search.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' ) ;
2017-11-11 00:18:00 +02:00
const { IconButton } = require ( './IconButton.min.js' ) ;
2019-01-15 21:55:58 +02:00
const { urlDecode , escapeHtml , pregQuote , scriptType } = 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' ) ;
2017-11-07 20:39:11 +02:00
const MdToHtml = require ( 'lib/MdToHtml' ) ;
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-05-10 13:02:39 +02:00
const ArrayUtils = require ( 'lib/ArrayUtils' ) ;
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' ) ;
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' ) ;
2018-11-08 00:37:13 +02:00
require ( 'brace/theme/twilight' ) ;
2017-11-05 02:17:48 +02:00
2018-11-24 13:42:50 +02:00
const NOTE _TAG _BAR _FEATURE _ENABLED = false ;
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 ,
} ;
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
noteMetadata : '' ,
showNoteMetadata : false ,
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 : [ ] ,
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 : '' ,
2018-06-14 09:52:12 +02:00
lastKeys : [ ] ,
2018-12-09 02:18:10 +02:00
showLocalSearch : false ,
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 ;
2018-12-09 02:18:10 +02:00
this . noteSearchBar _ = React . createRef ( ) ;
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 ;
}
}
2017-11-30 01:03:10 +02:00
2019-01-30 20:06:47 +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 ( ) ;
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 ) ;
const filePath = Setting . value ( 'tempDir' ) + '/' + md5 ( Date . now ( ) ) + '.' + fileExt ;
await shim . writeImageToFile ( image , format , filePath ) ;
await this . commandAttachFile ( [ filePath ] ) ;
await shim . fsDriver ( ) . remove ( filePath ) ;
}
}
}
2018-06-14 09:52:12 +02:00
this . onEditorKeyDown _ = ( event ) => {
const lastKeys = this . state . lastKeys . slice ( ) ;
lastKeys . push ( event . key ) ;
while ( lastKeys . length > 2 ) lastKeys . splice ( 0 , 1 ) ;
this . setState ( { lastKeys : lastKeys } ) ;
}
2018-09-04 19:20:41 +02:00
this . onEditorContextMenu _ = ( event ) => {
const menu = new Menu ( ) ;
const selectedText = this . selectedText ( ) ;
const clipboardText = clipboard . readText ( ) ;
menu . append ( new MenuItem ( { label : _ ( 'Cut' ) , enabled : ! ! selectedText , click : async ( ) => {
this . editorCutText ( ) ;
} } ) ) ;
menu . append ( new MenuItem ( { label : _ ( 'Copy' ) , enabled : ! ! selectedText , click : async ( ) => {
this . editorCopyText ( ) ;
} } ) ) ;
menu . append ( new MenuItem ( { label : _ ( 'Paste' ) , enabled : true , click : async ( ) => {
if ( clipboardText ) {
this . editorPasteText ( ) ;
} else {
// To handle pasting images
this . onEditorPaste _ ( ) ;
}
} } ) ) ;
menu . popup ( bridge ( ) . window ( ) ) ;
}
2018-05-10 11:45:44 +02:00
this . onDrop _ = async ( event ) => {
2018-09-30 21:15:30 +02:00
const dt = event . dataTransfer ;
if ( dt . types . indexOf ( "text/x-jop-note-ids" ) >= 0 ) {
const noteIds = JSON . parse ( dt . getData ( "text/x-jop-note-ids" ) ) ;
const linkText = [ ] ;
for ( let i = 0 ; i < noteIds . length ; i ++ ) {
const note = await Note . load ( noteIds [ i ] ) ;
linkText . push ( Note . markdownTag ( note ) ) ;
}
this . wrapSelectionWithStrings ( "" , "" , '' , linkText . join ( '\n' ) ) ;
}
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 ) ;
}
await this . commandAttachFile ( filesToAttach ) ;
}
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
2018-06-14 09:52:12 +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 ] ;
2018-06-12 00:47:44 +02:00
}
2018-06-14 09:52:12 +02:00
}
2018-06-12 00:47:44 +02:00
2018-06-14 09:52:12 +02:00
this . aceEditor _selectionChange = ( selection ) => {
updateSelectionRange ( ) ;
}
2018-06-13 18:53:41 +02:00
2018-06-14 09:52:12 +02:00
this . aceEditor _focus = ( event ) => {
updateSelectionRange ( ) ;
2018-06-12 00:47:44 +02:00
}
2018-06-18 20:56:07 +02:00
this . externalEditWatcher _noteChange = ( event ) => {
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
}
}
2018-10-08 20:11:53 +02:00
this . resourceFetcher _downloadComplete = async ( resource ) => {
if ( ! this . state . note || ! this . state . note . body ) return ;
const resourceIds = await Note . linkedResourceIds ( this . state . note . body ) ;
if ( resourceIds . indexOf ( resource . id ) >= 0 ) {
this . mdToHtml ( ) . clearCache ( ) ;
this . lastSetHtml _ = '' ;
this . updateHtml ( this . state . note . body ) ;
}
}
2018-12-09 02:18:10 +02:00
this . noteSearchBar _change = ( query ) => {
this . setState ( { localSearch : {
query : query ,
selectedIndex : 0 ,
} } ) ;
}
const noteSearchBarNextPrevious = ( inc ) => {
const ls = Object . assign ( { } , this . state . localSearch ) ;
ls . selectedIndex += inc ;
if ( ls . selectedIndex < 0 ) ls . selectedIndex = ls . resultCount - 1 ;
if ( ls . selectedIndex >= ls . resultCount ) ls . selectedIndex = 0 ;
this . setState ( { localSearch : ls } ) ;
}
this . noteSearchBar _next = ( ) => {
noteSearchBarNextPrevious ( + 1 ) ;
}
this . noteSearchBar _previous = ( ) => {
noteSearchBarNextPrevious ( - 1 ) ;
}
this . noteSearchBar _close = ( ) => {
this . setState ( {
showLocalSearch : false ,
} ) ;
}
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 ) ;
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 ,
}
}
row ++ ;
2018-11-08 00:16:05 +02:00
currentOffset += line . length + 1 ;
2018-06-17 03:44:37 +02:00
}
}
2017-11-07 23:11:14 +02:00
mdToHtml ( ) {
if ( this . mdToHtml _ ) return this . mdToHtml _ ;
2017-11-21 21:31:21 +02:00
this . mdToHtml _ = new MdToHtml ( {
resourceBaseUrl : 'file://' + Setting . value ( 'resourceDir' ) + '/' ,
} ) ;
2017-11-07 23:11:14 +02:00
return this . mdToHtml _ ;
2017-11-05 01:27:13 +02:00
}
2017-11-07 23:11:14 +02:00
async 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 ,
2018-11-08 00:16:05 +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
2018-05-21 16:29:35 +02:00
this . updateHtml ( note && note . body ? note . body : '' ) ;
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
ResourceFetcher . instance ( ) . on ( 'downloadComplete' , this . resourceFetcher _downloadComplete ) ;
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 ( ) ;
2017-11-05 18:51:03 +02:00
this . mdToHtml _ = 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
2018-10-08 20:11:53 +02:00
ResourceFetcher . instance ( ) . off ( 'downloadComplete' , this . resourceFetcher _downloadComplete ) ;
2018-11-21 21:50:50 +02:00
ExternalEditWatcher . instance ( ) . off ( 'noteChange' , this . externalEditWatcher _noteChange ) ;
2017-11-05 18:51:03 +02:00
}
2018-02-06 21:31:22 +02:00
async saveIfNeeded ( saveIfNewNote = false ) {
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 ;
}
2017-11-07 23:46:23 +02:00
await shared . saveNoteButton _press ( this ) ;
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 ( ) ;
2018-01-12 21:58:01 +02:00
const previousNote = this . state . note ? Object . assign ( { } , this . state . note ) : null ;
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 ;
2018-11-08 00:16:05 +02:00
let noteTags = [ ] ;
2019-01-31 00:45:28 +02:00
let scrollPercent = 0 ;
2018-01-12 21:58:01 +02:00
if ( props . newNote ) {
note = Object . assign ( { } , props . newNote ) ;
this . lastLoadedNoteId _ = null ;
} 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 ;
2018-11-08 00:16:05 +02:00
noteTags = await Tag . tagsByNoteId ( noteId ) ;
2018-01-12 21:58:01 +02:00
this . lastLoadedNoteId _ = noteId ;
note = noteId ? await Note . load ( noteId ) : null ;
if ( noteId !== this . lastLoadedNoteId _ ) return ; // Race condition - current note was changed while this one was loading
if ( options . noReloadIfLocalChanges && this . isModified ( ) ) return ;
// 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 _ ;
if ( ! Object . getOwnPropertyNames ( diff ) . length ) return ;
}
2017-11-28 22:58:07 +02:00
}
2017-11-28 23:15:22 +02:00
this . mdToHtml _ = null ;
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 ) {
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 ) {
2018-01-30 23:49:22 +02:00
const focusSettingName = ! ! note . is _todo ? 'newTodoFocus' : 'newNoteFocus' ;
if ( Setting . value ( focusSettingName ) === 'title' ) {
if ( this . titleField _ ) this . titleField _ . focus ( ) ;
} else {
if ( this . editor _ ) this . editor _ . editor . focus ( ) ;
}
}
2018-01-12 21:58:01 +02:00
if ( this . editor _ ) {
2018-05-01 11:48:15 +02:00
// Calling setValue here does two things:
// 1. It sets the initial value as recorded by the undo manager. If we were to set it instead to "" and wait for the render
// phase to set the value, the initial value would still be "", which means pressing "undo" on a note that has just loaded
// would clear it.
// 2. It resets the undo manager - fixes https://github.com/laurent22/joplin/issues/355
2018-05-01 22:13:17 +02:00
// Note: calling undoManager.reset() doesn't work
try {
this . editor _ . editor . getSession ( ) . setValue ( note ? note . body : '' ) ;
} catch ( error ) {
if ( error . message === "Cannot read property 'match' of undefined" ) {
// The internals of Ace Editor throws an exception when creating a new note,
// but that can be ignored.
} else {
console . error ( error ) ;
}
}
2018-01-12 21:58:01 +02:00
this . editor _ . editor . clearSelection ( ) ;
this . editor _ . editor . moveCursorTo ( 0 , 0 ) ;
2019-01-31 00:45:28 +02:00
2019-02-08 10:28:27 +02:00
setTimeout ( ( ) => {
this . setEditorPercentScroll ( scrollPercent ? scrollPercent : 0 ) ;
this . setViewerPercentScroll ( scrollPercent ? scrollPercent : 0 ) ;
} , 10 ) ;
2018-01-12 21:58:01 +02:00
}
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 : [ ] ,
2018-11-08 00:16:05 +02:00
noteTags : noteTags
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
2018-11-08 00:22:26 +02:00
// https://github.com/laurent22/joplin/pull/893#discussion_r228025210
// @Abijeet: Had to add this check. If not, was going into an infinite loop where state was getting updated repeatedly.
// Since I'm updating the state, the componentWillReceiveProps was getting triggered again, where nextProps.newNote was still true, causing reloadNote to trigger again and again.
// Notes from Laurent: The selected note tags are part of the global Redux state because they need to be updated whenever tags are changed or deleted
// anywhere in the app. Thus it's not possible simple to load the tags here (as we won't have a way to know if they're updated afterwards).
2018-11-17 13:21:57 +02:00
// Perhaps a better way would be to move that code in the middleware, check for TAGS_DELETE, TAGS_UPDATE, etc. actions and update the
2018-11-08 00:22:26 +02:00
// selected note tags accordingly.
2018-11-24 13:42:50 +02:00
if ( NOTE _TAG _BAR _FEATURE _ENABLED ) {
if ( ! this . props . newNote ) {
this . props . dispatch ( {
type : "SET_NOTE_TAGS" ,
items : noteTags ,
} ) ;
}
2018-11-08 00:16:05 +02:00
}
2018-05-10 13:02:39 +02:00
this . updateHtml ( newState . note ? newState . note . body : '' ) ;
2017-11-12 20:12:05 +02:00
}
async 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-02-02 12:47:26 +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 ( {
noteTags : nextProps . noteTags
} ) ;
2017-11-12 20:12:05 +02:00
}
2019-02-02 12:47:26 +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 ) {
let currNewTag = newTags [ i ] ;
for ( let j = 0 ; j < oldTags . length ; ++ j ) {
let currOldTag = oldTags [ j ] ;
if ( currOldTag . id === currNewTag . id && currOldTag . updated _time !== currNewTag . updated _time ) {
return true ;
}
}
}
return false ;
}
2017-11-05 20:36:27 +02:00
refreshNoteMetadata ( force = null ) {
return shared . refreshNoteMetadata ( this , force ) ;
}
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
}
showMetadata _onPress ( ) {
shared . showMetadata _onPress ( this ) ;
2017-11-05 01:27:13 +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 : '' ;
const args = event . args ;
const arg0 = args && args . length >= 1 ? args [ 0 ] : null ;
const arg1 = args && args . length >= 2 ? args [ 1 ] : null ;
2017-11-10 22:12:38 +02:00
reg . logger ( ) . debug ( '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 ( ) ;
2017-11-07 23:11:14 +02:00
const newBody = this . mdToHtml _ . handleCheckboxClick ( msg , this . state . note . body ) ;
this . saveOneProperty ( 'body' , newBody ) ;
2018-12-09 02:18:10 +02:00
} else if ( msg === 'setMarkerCount' ) {
const ls = Object . assign ( { } , this . state . localSearch ) ;
ls . resultCount = arg0 ;
this . setState ( { localSearch : ls } ) ;
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 ;
const menu = new Menu ( )
2018-06-26 01:52:46 +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 ) ;
menu . append ( new MenuItem ( { label : _ ( 'Open...' ) , click : async ( ) => {
2018-06-11 01:08:57 +02:00
const ok = bridge ( ) . openExternal ( 'file://' + resourcePath ) ;
if ( ! ok ) bridge ( ) . showErrorMessageBox ( _ ( 'This file could not be opened: %s' , resourcePath ) ) ;
2018-02-07 22:23:17 +02:00
} } ) ) ;
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 ) ;
} } ) ) ;
2018-05-16 15:16:43 +02:00
menu . append ( new MenuItem ( { label : _ ( 'Copy path to clipboard' ) , click : async ( ) => {
clipboard . writeText ( toSystemSlashes ( resourcePath ) ) ;
} } ) ) ;
2018-06-26 01:52:46 +02:00
} 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 {
reg . logger ( ) . error ( 'Unhandled item type: ' + itemType ) ;
return ;
}
menu . popup ( bridge ( ) . window ( ) ) ;
2017-11-12 18:33:34 +02:00
} else if ( msg . indexOf ( 'joplin://' ) === 0 ) {
2018-05-02 16:13:20 +02:00
const itemId = msg . substr ( 'joplin://' . length ) ;
const item = await BaseItem . loadItemById ( itemId ) ;
if ( ! item ) throw new Error ( 'No item with ID ' + itemId ) ;
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 ) {
2018-10-08 20:11:53 +02:00
bridge ( ) . showErrorMessageBox ( _ ( 'This attachment is not downloaded or not decrypted yet.' ) ) ;
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 ( {
2018-11-08 02:58:06 +02:00
type : "FOLDER_AND_NOTE_SELECT" ,
folderId : item . parent _id ,
noteId : item . id ,
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 {
throw new Error ( 'Unsupported item type: ' + item . type _ ) ;
}
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-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-01-20 18:27:33 +02:00
// if (Setting.value('env') === 'dev') this.webview_.openDevTools();
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 = [ ] ;
const letters = [ 'F' , 'T' , 'P' , 'Q' , 'L' , ',' ] ;
for ( let i = 0 ; i < letters . length ; i ++ ) {
const l = letters [ i ] ;
2018-06-16 17:16:27 +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
throw new Error ( 'HACK: Overriding Ace Editor shortcut: ' + k ) ;
} ) ;
}
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 ;
}
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 ( ) ;
if ( lineNoLeftSpaces . indexOf ( '- [ ] ' ) === 0 || lineNoLeftSpaces . indexOf ( '- [x] ' ) === 0 || lineNoLeftSpaces . indexOf ( '- [X] ' ) === 0 ) return leftSpaces + '- [ ] ' ;
if ( lineNoLeftSpaces . indexOf ( '- ' ) === 0 ) return leftSpaces + '- ' ;
2018-06-30 17:15:44 +02:00
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 ) ;
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-02-10 18:11:41 +02:00
updateHtml ( body = null , options = null ) {
if ( ! options ) options = { } ;
if ( ! ( 'useCustomCss' in options ) ) options . useCustomCss = true ;
2018-05-10 13:02:39 +02:00
const mdOptions = {
onResourceLoaded : ( ) => {
2018-06-29 20:51:50 +02:00
if ( this . resourceLoadedTimeoutId _ ) {
clearTimeout ( this . resourceLoadedTimeoutId _ ) ;
this . resourceLoadedTimeoutId _ = null ;
}
this . resourceLoadedTimeoutId _ = setTimeout ( ( ) => {
this . resourceLoadedTimeoutId _ = null ;
this . updateHtml ( ) ;
this . forceUpdate ( ) ;
2018-06-30 17:12:01 +02:00
} , 100 ) ;
2018-05-10 13:02:39 +02:00
} ,
2018-09-24 21:14:21 +02:00
postMessageSyntax : 'ipcProxySendToHost' ,
2018-05-10 13:02:39 +02:00
} ;
const theme = themeStyle ( this . props . theme ) ;
let bodyToRender = body ;
if ( bodyToRender === null ) bodyToRender = this . state . note && this . state . note . body ? this . state . note . body : '' ;
let bodyHtml = '' ;
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
bodyToRender = '*' + _ ( 'This note has no content. Click on "%s" to toggle the editor and edit the note.' , _ ( 'Layout' ) ) + '*' ;
}
2019-02-10 18:11:41 +02:00
if ( options . useCustomCss ) bodyToRender = '<style>' + this . props . customCss + '</style>\n' + bodyToRender ;
2018-05-10 13:02:39 +02:00
bodyHtml = this . mdToHtml ( ) . render ( bodyToRender , theme , mdOptions ) ;
this . setState ( { bodyHtml : bodyHtml } ) ;
}
2019-01-26 20:04:32 +02:00
titleField _keyDown ( event ) {
const keyCode = event . keyCode ;
if ( keyCode === 9 ) { // TAB
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 ;
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 ;
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 ;
} else if ( command . name === 'showLocalSearch' ) {
fn = this . commandShowLocalSearch ;
}
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 ( ) ;
}
}
if ( command . name === 'focusElement' && command . target === 'noteBody' ) {
fn = ( ) => {
if ( ! this . editor _ ) return ;
this . editor _ . editor . focus ( ) ;
}
}
2018-12-16 03:49:06 +02:00
if ( ! fn ) return ;
this . props . dispatch ( {
type : 'WINDOW_COMMAND' ,
name : null ,
} ) ;
requestAnimationFrame ( ( ) => {
fn = fn . bind ( this ) ;
fn ( ) ;
} ) ;
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 {
this . setState ( { showLocalSearch : true } ) ;
}
this . props . dispatch ( {
type : 'NOTE_VISIBLE_PANES_SET' ,
panes : [ 'editor' , 'viewer' ] ,
} ) ;
}
2018-05-10 11:45:44 +02:00
async commandAttachFile ( filePaths = null ) {
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 {
reg . logger ( ) . info ( 'Attaching ' + filePath ) ;
2018-05-10 11:45:44 +02:00
note = await shim . attachFileToNote ( note , filePath , position ) ;
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
this . updateHtml ( 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
} ) ;
}
2018-12-16 03:49:06 +02:00
printTo _ ( target , options ) {
2019-02-09 01:07:01 +02:00
if ( this . props . selectedNoteIds . length !== 1 || ! this . webviewRef _ . current ) {
2019-01-10 20:58:58 +02:00
throw new Error ( _ ( 'Only one note can be printed or exported to PDF at a time.' ) ) ;
}
2018-12-16 03:49:06 +02:00
const previousBody = this . state . note . body ;
const tempBody = "# " + this . state . note . title + "\n\n" + previousBody ;
2018-11-08 01:35:14 +02:00
2018-12-16 03:49:06 +02:00
const previousTheme = Setting . value ( 'theme' ) ;
Setting . setValue ( 'theme' , Setting . THEME _LIGHT ) ;
this . lastSetHtml _ = '' ;
2019-02-10 18:11:41 +02:00
this . updateHtml ( tempBody , { useCustomCss : false } ) ;
2018-12-16 03:49:06 +02:00
this . forceUpdate ( ) ;
const restoreSettings = ( ) => {
Setting . setValue ( 'theme' , previousTheme ) ;
this . lastSetHtml _ = '' ;
this . updateHtml ( previousBody ) ;
this . forceUpdate ( ) ;
}
2018-11-08 01:35:14 +02:00
2018-12-16 03:49:06 +02:00
setTimeout ( ( ) => {
if ( target === 'pdf' ) {
2019-02-09 01:07:01 +02:00
this . webviewRef _ . current . wrappedInstance . printToPDF ( { } , ( error , data ) => {
2018-12-16 03:49:06 +02:00
restoreSettings ( ) ;
2018-11-08 01:35:14 +02:00
if ( error ) {
bridge ( ) . showErrorMessageBox ( error . message ) ;
} else {
2018-12-16 03:49:06 +02:00
shim . fsDriver ( ) . writeFile ( options . path , data , 'buffer' ) ;
2018-11-08 01:35:14 +02:00
}
} ) ;
2018-12-16 03:49:06 +02:00
} else if ( target === 'printer' ) {
2019-02-09 01:07:01 +02:00
this . webviewRef _ . current . wrappedInstance . print ( ) ;
2018-12-16 03:49:06 +02:00
restoreSettings ( ) ;
}
} , 100 ) ;
}
commandSavePdf ( ) {
2019-01-10 20:58:58 +02:00
try {
if ( ! this . state . note ) throw new Error ( _ ( 'Only one note can be printed or exported to PDF at a time.' ) ) ;
const path = bridge ( ) . showSaveDialog ( {
filters : [ { name : _ ( 'PDF File' ) , extensions : [ 'pdf' ] } ] ,
defaultPath : safeFilename ( this . state . note . title ) ,
} ) ;
if ( ! path ) return ;
2018-12-16 03:49:06 +02:00
this . printTo _ ( 'pdf' , { path : path } ) ;
2019-01-10 20:58:58 +02:00
} catch ( error ) {
bridge ( ) . showErrorMessageBox ( error . message ) ;
2018-11-08 01:35:14 +02:00
}
}
2018-12-16 03:49:06 +02:00
commandPrint ( ) {
2019-01-10 20:58:58 +02:00
try {
this . printTo _ ( 'printer' ) ;
} catch ( error ) {
bridge ( ) . showErrorMessageBox ( error . message ) ;
}
2018-11-08 01:35:14 +02:00
}
2018-06-18 20:56:07 +02:00
async commandStartExternalEditing ( ) {
2018-06-27 22:34:41 +02:00
try {
2018-11-21 21:50:50 +02:00
await ExternalEditWatcher . instance ( ) . openAndWatch ( this . state . note ) ;
2018-06-27 22:34:41 +02:00
} catch ( error ) {
bridge ( ) . showErrorMessageBox ( _ ( 'Error opening note in editor: %s' , error . message ) ) ;
}
2018-06-18 20:56:07 +02:00
}
async commandStopExternalEditing ( ) {
2018-11-21 21:50:50 +02:00
ExternalEditWatcher . instance ( ) . stopWatching ( 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' ,
noteId : this . state . note . id ,
} ) ;
}
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 '' ;
const body = this . state . note . body
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 ) ;
this . updateEditorWithDelay ( ( editor ) => {
const range = this . selectionRange _ ;
range . setStart ( range . start . row , range . start . column ) ;
range . setEnd ( range . start . row , range . start . column ) ;
editor . getSession ( ) . getSelection ( ) . setSelectionRange ( range , false ) ;
editor . focus ( ) ;
} , 10 ) ;
}
editorPasteText ( ) {
const s = this . textOffsetSelection ( ) ;
const s1 = this . state . note . body . substr ( 0 , s . start ) ;
const s2 = this . state . note . body . substr ( s . end ) ;
this . wrapSelectionWithStrings ( "" , "" , '' , clipboard . readText ( ) ) ;
}
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 ;
}
wrapSelectionWithStrings ( string1 , string2 = '' , defaultText = '' , replacementText = '' ) {
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 ) {
2018-06-12 00:47:44 +02:00
const s1 = this . state . note . body . substr ( 0 , selection . start ) ;
2018-09-04 19:20:41 +02:00
const s2 = replacementText ? replacementText : this . state . note . body . substr ( selection . start , selection . end - selection . start ) ;
2018-06-12 00:47:44 +02:00
const s3 = this . state . note . body . substr ( selection . end ) ;
newBody = s1 + string1 + s2 + string2 + s3 ;
2018-06-17 03:44:37 +02:00
const r = this . selectionRange _ ;
2018-06-12 00:47:44 +02:00
const newRange = {
start : { row : r . start . row , column : r . start . column + string1 . length } ,
end : { row : r . end . row , column : r . end . column + string1 . length } ,
} ;
2018-09-04 19:20:41 +02:00
if ( replacementText ) {
const diff = replacementText . length - ( selection . end - selection . start ) ;
newRange . end . column += diff ;
}
2018-06-12 00:47:44 +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 ) ;
editor . getSession ( ) . getSelection ( ) . setSelectionRange ( range , false ) ;
editor . focus ( ) ;
} ) ;
} else {
2018-09-04 19:20:41 +02:00
let middleText = replacementText ? 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.
2018-06-12 00:47:44 +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 ) ;
editor . getSession ( ) . getSelection ( ) . setSelectionRange ( range , false ) ;
} else {
for ( let i = 0 ; i < string1 . length ; i ++ ) {
editor . getSession ( ) . getSelection ( ) . moveCursorRight ( ) ;
}
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 ( ) {
this . wrapSelectionWithStrings ( '`' , '`' ) ;
}
2018-06-17 03:44:37 +02:00
addListItem ( string1 , string2 = '' , defaultText = '' ) {
2018-06-14 09:52:12 +02:00
const currentLine = this . selectionRangeCurrentLine ( ) ;
let newLine = '\n'
if ( ! currentLine ) newLine = '' ;
2018-06-17 03:44:37 +02:00
this . wrapSelectionWithStrings ( newLine + string1 , string2 , defaultText ) ;
2018-06-14 09:52:12 +02:00
}
2018-06-12 01:12:06 +02:00
commandTextCheckbox ( ) {
2018-06-17 03:44:37 +02:00
this . addListItem ( '- [ ] ' , '' , _ ( 'List item' ) ) ;
2018-06-12 01:12:06 +02:00
}
2018-06-14 09:52:12 +02:00
commandTextListUl ( ) {
2018-06-17 03:44:37 +02:00
this . addListItem ( '- ' , '' , _ ( 'List item' ) ) ;
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 ;
2018-06-17 03:44:37 +02:00
this . addListItem ( ( bulletNumber + 1 ) + '. ' , '' , _ ( 'List item' ) ) ;
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' ) ) ;
this . wrapSelectionWithStrings ( '[' , '](' + url + ')' ) ;
2018-06-12 00:47:44 +02:00
}
2017-11-11 00:18:00 +02:00
itemContextMenu ( event ) {
2018-02-06 21:31:22 +02:00
const note = this . state . note ;
if ( ! note ) return ;
2017-11-11 00:18:00 +02:00
const menu = new Menu ( )
menu . append ( new MenuItem ( { label : _ ( 'Attach file' ) , click : async ( ) => {
2017-11-30 01:03:10 +02:00
return this . commandAttachFile ( ) ;
2017-11-11 00:18:00 +02:00
} } ) ) ;
2018-02-07 22:35:11 +02:00
menu . append ( new MenuItem ( { label : _ ( 'Tags' ) , click : async ( ) => {
return this . commandSetTags ( ) ;
} } ) ) ;
2018-02-06 21:31:22 +02:00
if ( ! ! note . is _todo ) {
menu . append ( new MenuItem ( { label : _ ( 'Set alarm' ) , click : async ( ) => {
return this . commandSetAlarm ( ) ;
} } ) ) ;
}
2017-11-28 00:50:46 +02:00
2017-11-11 00:18:00 +02:00
menu . popup ( bridge ( ) . window ( ) ) ;
}
2018-06-12 00:47:44 +02:00
createToolbarItems ( note ) {
const toolbarItems = [ ] ;
if ( note && this . state . folder && [ 'Search' , 'Tag' ] . includes ( this . props . notesParentType ) ) {
toolbarItems . push ( {
title : _ ( 'In: %s' , this . state . folder . title ) ,
iconName : 'fa-folder-o' ,
enabled : false ,
} ) ;
}
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 ( {
type : "FOLDER_AND_NOTE_SELECT" ,
folderId : lastItem . parent _id ,
noteId : lastItem . id ,
historyNoteAction : 'pop' ,
} ) ;
} ,
} ) ;
}
2018-06-12 00:47:44 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Bold' ) ,
iconName : 'fa-bold' ,
onClick : ( ) => { return this . commandTextBold ( ) ; } ,
} ) ;
toolbarItems . push ( {
tooltip : _ ( 'Italic' ) ,
iconName : 'fa-italic' ,
onClick : ( ) => { return this . commandTextItalic ( ) ; } ,
} ) ;
2018-06-14 09:52:12 +02:00
toolbarItems . push ( {
type : 'separator' ,
} ) ;
2018-09-16 20:37:31 +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 ,
} ) ;
} ,
} ) ;
toolbarItems . push ( {
type : 'separator' ,
} ) ;
2018-06-12 01:12:06 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Hyperlink' ) ,
iconName : 'fa-link' ,
onClick : ( ) => { return this . commandTextLink ( ) ; } ,
2018-06-16 17:16:27 +02:00
} ) ;
2018-06-12 01:12:06 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Code' ) ,
iconName : 'fa-code' ,
onClick : ( ) => { return this . commandTextCode ( ) ; } ,
} ) ;
2018-06-12 00:47:44 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Attach file' ) ,
iconName : 'fa-paperclip' ,
onClick : ( ) => { return this . commandAttachFile ( ) ; } ,
} ) ;
2018-06-14 09:52:12 +02:00
toolbarItems . push ( {
type : 'separator' ,
} ) ;
toolbarItems . push ( {
tooltip : _ ( 'Numbered List' ) ,
iconName : 'fa-list-ol' ,
onClick : ( ) => { return this . commandTextListOl ( ) ; } ,
} ) ;
toolbarItems . push ( {
tooltip : _ ( 'Bulleted List' ) ,
iconName : 'fa-list-ul' ,
onClick : ( ) => { return this . commandTextListUl ( ) ; } ,
} ) ;
toolbarItems . push ( {
tooltip : _ ( 'Checkbox' ) ,
iconName : 'fa-check-square' ,
onClick : ( ) => { return this . commandTextCheckbox ( ) ; } ,
} ) ;
toolbarItems . push ( {
tooltip : _ ( 'Heading' ) ,
2018-06-14 10:02:01 +02:00
iconName : 'fa-header' ,
2018-06-14 09:52:12 +02:00
onClick : ( ) => { return this . commandTextHeading ( ) ; } ,
} ) ;
toolbarItems . push ( {
tooltip : _ ( 'Horizontal Rule' ) ,
iconName : 'fa-ellipsis-h' ,
onClick : ( ) => { return this . commandTextHorizontalRule ( ) ; } ,
} ) ;
2018-06-16 17:16:27 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Insert Date Time' ) ,
iconName : 'fa-calendar-plus-o' ,
onClick : ( ) => { return this . commandDateTime ( ) ; } ,
} ) ;
2018-06-14 09:52:12 +02:00
toolbarItems . push ( {
type : 'separator' ,
} ) ;
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' ,
onClick : ( ) => { return this . commandStopExternalEditing ( ) ; } ,
} ) ;
} else {
toolbarItems . push ( {
tooltip : _ ( 'Edit in external editor' ) ,
iconName : 'fa-external-link' ,
onClick : ( ) => { return this . commandStartExternalEditing ( ) ; } ,
} ) ;
}
2018-06-12 00:47:44 +02:00
toolbarItems . push ( {
tooltip : _ ( 'Tags' ) ,
iconName : 'fa-tags' ,
onClick : ( ) => { return this . commandSetTags ( ) ; } ,
} ) ;
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 ,
onClick : ( ) => { return this . commandSetAlarm ( ) ; } ,
}
if ( Note . needAlarm ( note ) ) {
item . title = time . formatMsToLocal ( note . todo _due ) ;
} else {
item . tooltip = _ ( 'Set alarm' ) ;
}
toolbarItems . push ( item ) ;
}
return toolbarItems ;
}
2019-01-29 20:02:34 +02:00
renderNoNotes ( rootStyle ) {
const emptyDivStyle = Object . assign ( {
backgroundColor : 'black' ,
opacity : 0.1 ,
} , rootStyle ) ;
return < div style = { emptyDivStyle } > < / div >
}
renderMultiNotes ( rootStyle ) {
const theme = themeStyle ( this . props . theme ) ;
const multiNotesButton _click = item => {
if ( item . submenu ) {
item . submenu . popup ( bridge ( ) . window ( ) ) ;
} else {
item . click ( ) ;
}
}
const menu = NoteListUtils . makeContextMenu ( this . props . selectedNoteIds , {
notes : this . props . notes ,
dispatch : this . props . dispatch ,
} ) ;
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 ;
itemComps . push ( < button
key = { item . label }
style = { buttonStyle }
onClick = { ( ) => multiNotesButton _click ( item ) }
> { item . label } < / button > ) ;
}
rootStyle = Object . assign ( { } , rootStyle , {
paddingTop : rootStyle . paddingLeft ,
display : 'inline-flex' ,
justifyContent : 'center' ,
} ) ;
return ( < div style = { rootStyle } >
< div style = { { display : 'flex' , flexDirection : 'column' } } >
{ itemComps }
< / div >
< / div > ) ;
}
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 : '' ;
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 ;
2017-11-07 23:11:14 +02:00
2017-11-10 22:12:38 +02:00
const borderWidth = 1 ;
const rootStyle = Object . assign ( {
borderLeft : borderWidth + 'px solid ' + theme . dividerColor ,
boxSizing : 'border-box' ,
paddingLeft : 10 ,
paddingRight : 0 ,
} , style ) ;
const innerWidth = rootStyle . width - rootStyle . paddingLeft - rootStyle . paddingRight - borderWidth ;
2019-01-29 20:02:34 +02:00
if ( this . props . selectedNoteIds . length > 1 ) {
return this . renderMultiNotes ( rootStyle ) ;
2019-02-09 01:07:01 +02:00
} else if ( ! note || ! ! note . encryption _applied ) { //|| (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 = {
display : 'flex' ,
flex : 1 ,
display : 'inline-block' ,
2017-11-10 22:12:38 +02:00
paddingTop : 5 ,
paddingBottom : 5 ,
paddingLeft : 8 ,
paddingRight : 8 ,
marginRight : rootStyle . paddingLeft ,
2018-11-08 00:37:13 +02:00
color : theme . color ,
backgroundColor : theme . backgroundColor ,
border : '1px solid' ,
borderColor : theme . dividerColor ,
2017-11-10 22:12:38 +02:00
} ;
2017-11-30 01:03:10 +02:00
const toolbarStyle = {
2018-11-08 00:16:05 +02:00
} ;
const tagStyle = {
2017-11-30 01:03:10 +02:00
marginBottom : 10 ,
2018-11-17 13:21:57 +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 ;
if ( NOTE _TAG _BAR _FEATURE _ENABLED ) {
bottomRowHeight = rootStyle . height - titleBarStyle . height - titleBarStyle . marginBottom - titleBarStyle . marginTop - theme . toolbarHeight - tagStyle . height - tagStyle . marginBottom ;
} else {
toolbarStyle . marginBottom = 10 ;
bottomRowHeight = rootStyle . height - titleBarStyle . height - titleBarStyle . marginBottom - titleBarStyle . marginTop - theme . toolbarHeight - toolbarStyle . marginBottom ;
}
2017-11-10 22:12:38 +02:00
2018-12-09 02:18:10 +02:00
bottomRowHeight -= searchBarHeight ;
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' ,
2017-11-08 19:51:55 +02:00
paddingTop : paddingTop + 'px' ,
lineHeight : theme . textAreaLineHeight + 'px' ,
2018-12-07 23:26:03 +02:00
fontSize : theme . editorFontSize + 'px' ,
2018-11-08 00:37:13 +02:00
color : theme . color ,
backgroundColor : theme . backgroundColor ,
editorTheme : theme . editorTheme ,
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 ) {
viewerStyle . borderLeft = '1px solid ' + theme . dividerColor ;
} else {
viewerStyle . borderLeft = 'none' ;
}
2017-11-05 18:51:03 +02:00
if ( this . state . webviewReady ) {
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 ;
if ( htmlHasChanged ) {
2018-11-08 00:37:13 +02:00
let options = { codeTheme : theme . codeThemeCss } ;
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-01-15 20:37:44 +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 ;
} 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
}
2018-06-12 00:47:44 +02:00
const toolbarItems = this . createToolbarItems ( note ) ;
2017-11-30 01:03:10 +02:00
const toolbar = < Toolbar
style = { toolbarStyle }
items = { toolbarItems }
/ >
2017-11-10 22:12:38 +02:00
const titleEditor = < input
type = "text"
2018-01-30 23:49:22 +02:00
ref = { ( elem ) => { this . titleField _ = elem ; } }
2017-11-10 22:12:38 +02:00
style = { titleEditorStyle }
2018-01-12 21:58:01 +02:00
value = { note && note . title ? note . title : '' }
2017-11-10 22:12:38 +02:00
onChange = { ( event ) => { this . title _changeText ( event ) ; } }
2019-01-26 20:04:32 +02:00
onKeyDown = { this . titleField _keyDown }
2018-02-06 20:12:43 +02:00
placeholder = { this . props . newNote ? _ ( 'Creating new %s...' , isTodo ? _ ( 'to-do' ) : _ ( 'note' ) ) : '' }
2017-11-10 22:12:38 +02:00
/ >
2018-11-24 13:42:50 +02:00
const tagList = ! NOTE _TAG _BAR _FEATURE _ENABLED ? null : < TagList
2018-11-08 00:16:05 +02:00
style = { tagStyle }
items = { this . state . noteTags }
/ > ;
2017-11-11 00:18:00 +02:00
const titleBarMenuButton = < IconButton style = { {
display : 'flex' ,
} } iconName = "fa-caret-down" theme = { this . props . theme } onClick = { ( ) => { this . itemContextMenu ( ) } } / >
2018-03-18 01:51:15 +02:00
const titleBarDate = < span style = { Object . assign ( { } , theme . textStyle , { color : theme . colorFaded } ) } > { time . formatMsToLocal ( note . user _updated _time ) } < / span >
2019-02-09 01:07:01 +02:00
const viewer = < NoteTextViewer
ref = { this . webviewRef _ }
viewerStyle = { viewerStyle }
onDomReady = { this . webview _domReady }
onIpcMessage = { this . webview _ipcMessage }
2017-11-10 21:18:19 +02:00
/ >
2017-11-10 00:44:10 +02:00
const editorRootStyle = Object . assign ( { } , editorStyle ) ;
delete editorRootStyle . width ;
delete editorRootStyle . height ;
delete editorRootStyle . fontSize ;
const editor = < AceEditor
value = { body }
mode = "markdown"
2018-11-08 00:37:13 +02:00
theme = { editorRootStyle . editorTheme }
2017-11-10 00:44:10 +02:00
style = { editorRootStyle }
width = { editorStyle . width + 'px' }
height = { editorStyle . height + 'px' }
fontSize = { editorStyle . fontSize }
showGutter = { false }
name = "note-editor"
wrapEnabled = { true }
onScroll = { ( event ) => { this . editor _scroll ( ) ; } }
ref = { ( elem ) => { this . editor _ref ( elem ) ; } }
onChange = { ( body ) => { this . aceEditor _change ( body ) } }
2017-11-10 23:04:53 +02:00
showPrintMargin = { false }
2018-06-12 00:47:44 +02:00
onSelectionChange = { this . aceEditor _selectionChange }
2018-06-14 09:52:12 +02:00
onFocus = { this . aceEditor _focus }
2019-02-09 01:07:01 +02:00
readOnly = { visiblePanes . indexOf ( 'editor' ) < 0 }
2017-11-10 00:44:10 +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 : true } }
2017-11-10 01:28:08 +02:00
// This is buggy (gets outside the container)
2018-06-16 17:16:27 +02:00
highlightActiveLine = { false }
2017-11-10 00:44:10 +02:00
/ >
2017-11-05 18:51:03 +02:00
2018-12-09 02:18:10 +02:00
const noteSearchBarComp = ! this . state . showLocalSearch ? null : (
< NoteSearchBar
ref = { this . noteSearchBar _ }
style = { { display : 'flex' , height : searchBarHeight , width : innerWidth , borderTop : '1px solid ' + theme . dividerColor } }
onChange = { this . noteSearchBar _change }
onNext = { this . noteSearchBar _next }
onPrevious = { this . noteSearchBar _previous }
onClose = { this . noteSearchBar _close }
/ >
) ;
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 } >
{ titleEditor }
2018-03-18 01:51:15 +02:00
{ titleBarDate }
{ false ? titleBarMenuButton : null }
2017-11-11 00:18:00 +02:00
< / div >
2017-11-30 01:03:10 +02:00
{ toolbar }
2018-11-08 00:16:05 +02:00
{ tagList }
2017-11-07 23:11:14 +02:00
{ editor }
{ viewer }
2018-12-09 02:18:10 +02:00
< div style = { { clear : 'both' } } / >
{ noteSearchBarComp }
2017-11-05 01:27:13 +02:00
< / div >
) ;
}
}
const mapStateToProps = ( state ) => {
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 ,
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 ,
showAdvancedOptions : state . settings . showAdvancedOptions ,
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 ,
2017-11-05 01:27:13 +02:00
} ;
} ;
const NoteText = connect ( mapStateToProps ) ( NoteTextComponent ) ;
2018-11-08 00:37:13 +02:00
module . exports = { NoteText } ;