diff --git a/ElectronClient/app/gui/NoteText.jsx b/ElectronClient/app/gui/NoteText.jsx index e56c3580b..d45963ded 100644 --- a/ElectronClient/app/gui/NoteText.jsx +++ b/ElectronClient/app/gui/NoteText.jsx @@ -66,6 +66,7 @@ class NoteTextComponent extends React.Component { this.restoreScrollTop_ = null; this.lastSetHtml_ = ''; this.lastSetMarkers_ = []; + this.selectionRange_ = null; // Complicated but reliable method to get editor content height // https://github.com/ajaxorg/ace/issues/2046 @@ -127,11 +128,12 @@ class NoteTextComponent extends React.Component { } const updateSelectionRange = () => { + const ranges = this.rawEditor().getSelection().getAllRanges(); if (!ranges || !ranges.length || !this.state.note) { - this.setState({ selectionRange: null }); + this.selectionRange_ = null; } else { - this.setState({ selectionRange: ranges[0] }); + this.selectionRange_ = ranges[0]; } } @@ -144,18 +146,23 @@ class NoteTextComponent extends React.Component { } } + // Note: + // - What's called "cursor position" is expressed as { row: x, column: y } and is how Ace Editor get/set the cursor position + // - A "range" defines a selection with a start and end cusor position, expressed as { start: , end: } + // - 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. rangeToTextOffsets(range, body) { return { - start: this.cursorPositionToTextOffsets(range.start, body), - end: this.cursorPositionToTextOffsets(range.end, body), + start: this.cursorPositionToTextOffset(range.start, body), + end: this.cursorPositionToTextOffset(range.end, body), }; } - cursorPosition() { - return this.cursorPositionToTextOffsets(this.editor_.editor.getCursorPosition(), this.state.note.body); + currentTextOffset() { + return this.cursorPositionToTextOffset(this.editor_.editor.getCursorPosition(), this.state.note.body); } - cursorPositionToTextOffsets(cursorPos, body) { + cursorPositionToTextOffset(cursorPos, body) { if (!this.editor_ || !this.editor_.editor || !this.state.note || !this.state.note.body) return 0; const noteLines = body.split('\n'); @@ -175,6 +182,24 @@ class NoteTextComponent extends React.Component { return pos; } + 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++; + currentOffset += line.length + 1; + } + } + mdToHtml() { if (this.mdToHtml_) return this.mdToHtml_; this.mdToHtml_ = new MdToHtml({ @@ -719,7 +744,7 @@ class NoteTextComponent extends React.Component { await this.saveIfNeeded(true); let note = await Note.load(this.state.note.id); - const position = this.cursorPosition(); + const position = this.currentTextOffset(); for (let i = 0; i < filePaths.length; i++) { const filePath = filePaths[i]; @@ -781,21 +806,21 @@ class NoteTextComponent extends React.Component { } selectionRangePreviousLine() { - if (!this.state.selectionRange) return ''; - const row = this.state.selectionRange.start.row; + if (!this.selectionRange_) return ''; + const row = this.selectionRange_.start.row; return this.lineAtRow(row - 1); } selectionRangeCurrentLine() { - if (!this.state.selectionRange) return ''; - const row = this.state.selectionRange.start.row; + if (!this.selectionRange_) return ''; + const row = this.selectionRange_.start.row; return this.lineAtRow(row); } wrapSelectionWithStrings(string1, string2 = '', defaultText = '') { if (!this.rawEditor() || !this.state.note) return; - const selection = this.state.selectionRange ? this.rangeToTextOffsets(this.state.selectionRange, this.state.note.body) : null; + const selection = this.selectionRange_ ? this.rangeToTextOffsets(this.selectionRange_, this.state.note.body) : null; let newBody = this.state.note.body; @@ -805,7 +830,7 @@ class NoteTextComponent extends React.Component { const s3 = this.state.note.body.substr(selection.end); newBody = s1 + string1 + s2 + string2 + s3; - const r = this.state.selectionRange; + const r = this.selectionRange_; const newRange = { start: { row: r.start.row, column: r.start.column + string1.length}, @@ -813,27 +838,29 @@ class NoteTextComponent extends React.Component { }; this.updateEditorWithDelay((editor) => { - const range = this.state.selectionRange; + const range = this.selectionRange_; 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 { - const cursorPos = this.cursorPosition(); - const s1 = this.state.note.body.substr(0, cursorPos); - const s2 = this.state.note.body.substr(cursorPos); + const textOffset = this.currentTextOffset(); + const s1 = this.state.note.body.substr(0, textOffset); + const s2 = this.state.note.body.substr(textOffset); newBody = s1 + string1 + defaultText + string2 + s2; - const r = this.state.selectionRange; - const newRange = !r ? null : { - start: { row: r.start.row, column: r.start.column + string1.length }, - end: { row: r.end.row, column: r.start.column + string1.length + defaultText.length }, + const p = this.textOffsetToCursorPosition(textOffset + string1.length, newBody); + const newRange = { + start: { row: p.row, column: p.column }, + end: { row: p.row, column: p.column + defaultText.length }, }; + console.info('DDDDD', defaultText, newRange); + this.updateEditorWithDelay((editor) => { if (defaultText && newRange) { - const range = this.state.selectionRange; + const range = this.selectionRange_; range.setStart(newRange.start.row, newRange.start.column); range.setEnd(newRange.end.row, newRange.end.column); editor.getSession().getSelection().setSelectionRange(range, false); @@ -848,6 +875,7 @@ class NoteTextComponent extends React.Component { shared.noteComponent_change(this, 'body', newBody); this.scheduleHtmlUpdate(); + this.scheduleSave(); } commandTextBold() { @@ -862,26 +890,26 @@ class NoteTextComponent extends React.Component { this.wrapSelectionWithStrings('`', '`'); } - addListItem(item) { + addListItem(string1, string2 = '', defaultText = '') { const currentLine = this.selectionRangeCurrentLine(); let newLine = '\n' if (!currentLine) newLine = ''; - this.wrapSelectionWithStrings(newLine + item); + this.wrapSelectionWithStrings(newLine + string1, string2, defaultText); } commandTextCheckbox() { - this.addListItem('- [ ] '); + this.addListItem('- [ ] ', '', _('List item')); } commandTextListUl() { - this.addListItem('- '); + this.addListItem('- ', '', _('List item')); } commandTextListOl() { let bulletNumber = markdownUtils.olLineNumber(this.selectionRangeCurrentLine()); if (!bulletNumber) bulletNumber = markdownUtils.olLineNumber(this.selectionRangePreviousLine()); if (!bulletNumber) bulletNumber = 0; - this.addListItem((bulletNumber + 1) + '. '); + this.addListItem((bulletNumber + 1) + '. ', '', _('List item')); } commandTextHeading() { diff --git a/ReactNativeClient/lib/SyncTargetWebDAV.js b/ReactNativeClient/lib/SyncTargetWebDAV.js index e5ebf99a8..032205c40 100644 --- a/ReactNativeClient/lib/SyncTargetWebDAV.js +++ b/ReactNativeClient/lib/SyncTargetWebDAV.js @@ -53,7 +53,7 @@ class SyncTargetWebDAV extends BaseSyncTarget { try { const result = await fileApi.stat(''); - if (!result) throw new Error('WebDAV directory not found: ' + options.path); + if (!result) throw new Error('WebDAV directory not found: ' + options.path()); output.ok = true; } catch (error) { output.errorMessage = error.message; diff --git a/docs/donate/index.html b/docs/donate/index.html index 5b81d2e2f..12ef35d5d 100644 --- a/docs/donate/index.html +++ b/docs/donate/index.html @@ -256,7 +256,7 @@

Patreon

Or you may support the project on Patreon:

-

+

Bitcoin

Bitcoins are also accepted:

1AnbeRd5NZT1ssG93jXzaDoHwzgjQAHX3R

diff --git a/readme/donate.md b/readme/donate.md index 471755d8a..b46aa4ef5 100644 --- a/readme/donate.md +++ b/readme/donate.md @@ -14,7 +14,7 @@ To donate via Paypal, please follow this link: Or you may support the project on Patreon: - + ## Bitcoin