mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-31 21:11:30 +08:00 
			
		
		
		
	Merge pull request #576 from TriliumNext/siriusxt_patch
Add a text replacement feature to the find_widget
This commit is contained in:
		
						commit
						a8b87a1507
					
				| @ -5,6 +5,7 @@ | |||||||
| 
 | 
 | ||||||
| import { t } from "../services/i18n.js"; | import { t } from "../services/i18n.js"; | ||||||
| import NoteContextAwareWidget from "./note_context_aware_widget.js"; | import NoteContextAwareWidget from "./note_context_aware_widget.js"; | ||||||
|  | import attributeService from "../services/attributes.js"; | ||||||
| import FindInText from "./find_in_text.js"; | import FindInText from "./find_in_text.js"; | ||||||
| import FindInCode from "./find_in_code.js"; | import FindInCode from "./find_in_code.js"; | ||||||
| import FindInHtml from "./find_in_html.js"; | import FindInHtml from "./find_in_html.js"; | ||||||
| @ -16,19 +17,18 @@ const waitForEnter = (findWidgetDelayMillis < 0); | |||||||
| // the focusout handler is called with relatedTarget equal to the label instead
 | // the focusout handler is called with relatedTarget equal to the label instead
 | ||||||
| // of undefined. It's -1 instead of > 0, so they don't tabstop
 | // of undefined. It's -1 instead of > 0, so they don't tabstop
 | ||||||
| const TPL = ` | const TPL = ` | ||||||
| <div style="contain: none;"> | <div class='find-replace-widget' style="contain: none; border-top: 1px solid var(--main-border-color);"> | ||||||
|     <style> |     <style> | ||||||
|         .find-widget-box { |         .find-widget-box, .replace-widget-box { | ||||||
|             padding: 10px; |             padding: 2px 10px 2px 10px; | ||||||
|             border-top: 1px solid var(--main-border-color);  |  | ||||||
|             align-items: center; |             align-items: center; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         .find-widget-box > * { |         .find-widget-box > *, .replace-widget-box > *{ | ||||||
|             margin-right: 15px; |             margin-right: 15px; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         .find-widget-box { |         .find-widget-box, .replace-widget-box { | ||||||
|             display: flex; |             display: flex; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -36,7 +36,7 @@ const TPL = ` | |||||||
|             font-weight: bold; |             font-weight: bold; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         .find-widget-search-term-input-group { |         .find-widget-search-term-input-group, .replace-widget-replacetext-input { | ||||||
|             max-width: 300px; |             max-width: 300px; | ||||||
|         } |         } | ||||||
|          |          | ||||||
| @ -47,19 +47,23 @@ const TPL = ` | |||||||
| 
 | 
 | ||||||
|     <div class="find-widget-box"> |     <div class="find-widget-box"> | ||||||
|         <div class="input-group find-widget-search-term-input-group"> |         <div class="input-group find-widget-search-term-input-group"> | ||||||
|             <input type="text" class="form-control find-widget-search-term-input"> |             <input type="text" class="form-control find-widget-search-term-input" placeholder="${t('find.find_placeholder')}"> | ||||||
|             <button class="btn btn-outline-secondary bx bxs-chevron-up find-widget-previous-button" type="button"></button> |             <button class="btn btn-outline-secondary bx bxs-chevron-up find-widget-previous-button" type="button"></button> | ||||||
|             <button class="btn btn-outline-secondary bx bxs-chevron-down find-widget-next-button" type="button"></button> |             <button class="btn btn-outline-secondary bx bxs-chevron-down find-widget-next-button" type="button"></button> | ||||||
|         </div> |         </div> | ||||||
|          |          | ||||||
|         <div class="form-check"> |         <div class="form-check"> | ||||||
|  |             <label tabIndex="-1" class="form-check-label"> | ||||||
|                 <input type="checkbox" class="form-check-input find-widget-case-sensitive-checkbox">  |                 <input type="checkbox" class="form-check-input find-widget-case-sensitive-checkbox">  | ||||||
|             <label tabIndex="-1" class="form-check-label">${t('find.case_sensitive')}</label> |                 ${t('find.case_sensitive')} | ||||||
|  |             </label> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <div class="form-check"> |         <div class="form-check"> | ||||||
|  |             <label tabIndex="-1" class="form-check-label"> | ||||||
|                 <input type="checkbox" class="form-check-input find-widget-match-words-checkbox"> |                 <input type="checkbox" class="form-check-input find-widget-match-words-checkbox"> | ||||||
|             <label tabIndex="-1" class="form-check-label">${t('find.match_words')}</label> |                 ${t('find.match_words')} | ||||||
|  |             </label> | ||||||
|         </div> |         </div> | ||||||
|          |          | ||||||
|         <div class="find-widget-found-wrapper"> |         <div class="find-widget-found-wrapper"> | ||||||
| @ -72,6 +76,12 @@ const TPL = ` | |||||||
|          |          | ||||||
|         <div class="find-widget-close-button"><button class="btn icon-action bx bx-x"></button></div> |         <div class="find-widget-close-button"><button class="btn icon-action bx bx-x"></button></div> | ||||||
|     </div> |     </div> | ||||||
|  | 
 | ||||||
|  |     <div class="replace-widget-box" style='display: none'> | ||||||
|  |         <input type="text" class="form-control replace-widget-replacetext-input" placeholder="${t('find.replace_placeholder')}"> | ||||||
|  |         <button class="btn btn-sm replace-widget-replaceall-button" type="button">${t('find.replace_all')}</button> | ||||||
|  |         <button class="btn btn-sm  replace-widget-replace-button" type="button">${t('find.replace')}</button> | ||||||
|  |     </div> | ||||||
| </div>`; | </div>`; | ||||||
| 
 | 
 | ||||||
| export default class FindWidget extends NoteContextAwareWidget { | export default class FindWidget extends NoteContextAwareWidget { | ||||||
| @ -93,8 +103,7 @@ export default class FindWidget extends NoteContextAwareWidget { | |||||||
| 
 | 
 | ||||||
|     doRender() { |     doRender() { | ||||||
|         this.$widget = $(TPL); |         this.$widget = $(TPL); | ||||||
|         this.$findBox = this.$widget.find('.find-widget-box'); |         this.$widget.hide(); | ||||||
|         this.$findBox.hide(); |  | ||||||
|         this.$input = this.$widget.find('.find-widget-search-term-input'); |         this.$input = this.$widget.find('.find-widget-search-term-input'); | ||||||
|         this.$currentFound = this.$widget.find('.find-widget-current-found'); |         this.$currentFound = this.$widget.find('.find-widget-current-found'); | ||||||
|         this.$totalFound = this.$widget.find('.find-widget-total-found'); |         this.$totalFound = this.$widget.find('.find-widget-total-found'); | ||||||
| @ -109,6 +118,13 @@ export default class FindWidget extends NoteContextAwareWidget { | |||||||
|         this.$closeButton = this.$widget.find(".find-widget-close-button"); |         this.$closeButton = this.$widget.find(".find-widget-close-button"); | ||||||
|         this.$closeButton.on("click", () => this.closeSearch()); |         this.$closeButton.on("click", () => this.closeSearch()); | ||||||
| 
 | 
 | ||||||
|  |         this.$replaceWidgetBox = this.$widget.find(".replace-widget-box"); | ||||||
|  |         this.$replaceTextInput = this.$widget.find(".replace-widget-replacetext-input"); | ||||||
|  |         this.$replaceAllButton = this.$widget.find(".replace-widget-replaceall-button"); | ||||||
|  |         this.$replaceAllButton.on("click", () => this.replaceAll()); | ||||||
|  |         this.$replaceButton = this.$widget.find(".replace-widget-replace-button"); | ||||||
|  |         this.$replaceButton.on("click", () => this.replace()); | ||||||
|  | 
 | ||||||
|         this.$input.keydown(async e => { |         this.$input.keydown(async e => { | ||||||
|             if ((e.metaKey || e.ctrlKey) && (e.key === 'F' || e.key === 'f')) { |             if ((e.metaKey || e.ctrlKey) && (e.key === 'F' || e.key === 'f')) { | ||||||
|                 // If ctrl+f is pressed when the findbox is shown, select the
 |                 // If ctrl+f is pressed when the findbox is shown, select the
 | ||||||
| @ -121,7 +137,7 @@ export default class FindWidget extends NoteContextAwareWidget { | |||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         this.$findBox.keydown(async e => { |         this.$widget.keydown(async e => { | ||||||
|             if (e.key === 'Escape') { |             if (e.key === 'Escape') { | ||||||
|                 await this.closeSearch(); |                 await this.closeSearch(); | ||||||
|             } |             } | ||||||
| @ -143,12 +159,24 @@ export default class FindWidget extends NoteContextAwareWidget { | |||||||
| 
 | 
 | ||||||
|         this.handler = await this.getHandler(); |         this.handler = await this.getHandler(); | ||||||
|          |          | ||||||
|         const selectedText = window.getSelection().toString() || ""; |         const isReadOnly = await this.noteContext.isReadOnly(); | ||||||
| 
 | 
 | ||||||
|         this.$findBox.show(); |         let selectedText = ''; | ||||||
|  |         if (this.note.type === 'code' && !isReadOnly){ | ||||||
|  |             const codeEditor = await this.noteContext.getCodeEditor(); | ||||||
|  |             selectedText = codeEditor.getSelection(); | ||||||
|  |         }else{ | ||||||
|  |             selectedText = window.getSelection().toString() || ""; | ||||||
|  |         } | ||||||
|  |         this.$widget.show(); | ||||||
|         this.$input.focus(); |         this.$input.focus(); | ||||||
|  |         if (['text', 'code'].includes(this.note.type) && !isReadOnly) { | ||||||
|  |             this.$replaceWidgetBox.show(); | ||||||
|  |         }else{ | ||||||
|  |             this.$replaceWidgetBox.hide(); | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         const isAlreadyVisible = this.$findBox.is(":visible"); |         const isAlreadyVisible = this.$widget.is(":visible"); | ||||||
| 
 | 
 | ||||||
|         if (isAlreadyVisible) { |         if (isAlreadyVisible) { | ||||||
|             if (selectedText) { |             if (selectedText) { | ||||||
| @ -254,8 +282,8 @@ export default class FindWidget extends NoteContextAwareWidget { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async closeSearch() { |     async closeSearch() { | ||||||
|         if (this.$findBox.is(":visible")) { |         if (this.$widget.is(":visible")) { | ||||||
|             this.$findBox.hide(); |             this.$widget.hide(); | ||||||
| 
 | 
 | ||||||
|             // Restore any state, if there's a current occurrence clear markers
 |             // Restore any state, if there's a current occurrence clear markers
 | ||||||
|             // and scroll to and select the last occurrence
 |             // and scroll to and select the last occurrence
 | ||||||
| @ -268,6 +296,16 @@ export default class FindWidget extends NoteContextAwareWidget { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     async replace() { | ||||||
|  |         const replaceText = this.$replaceTextInput.val(); | ||||||
|  |         await this.handler.replace(replaceText); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async replaceAll() { | ||||||
|  |         const replaceText = this.$replaceTextInput.val(); | ||||||
|  |         await this.handler.replaceAll(replaceText); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     isEnabled() { |     isEnabled() { | ||||||
|         return super.isEnabled() && ['text', 'code', 'render'].includes(this.note.type); |         return super.isEnabled() && ['text', 'code', 'render'].includes(this.note.type); | ||||||
|     } |     } | ||||||
| @ -275,6 +313,10 @@ export default class FindWidget extends NoteContextAwareWidget { | |||||||
|     async entitiesReloadedEvent({ loadResults }) { |     async entitiesReloadedEvent({ loadResults }) { | ||||||
|         if (loadResults.isNoteContentReloaded(this.noteId)) { |         if (loadResults.isNoteContentReloaded(this.noteId)) { | ||||||
|             this.$totalFound.text("?") |             this.$totalFound.text("?") | ||||||
|  |         } else if (loadResults.getAttributeRows().find(attr => attr.type === 'label' | ||||||
|  |             && (attr.name.toLowerCase().includes('readonly')) | ||||||
|  |             && attributeService.isAffecting(attr, this.note))) { | ||||||
|  |             this.closeSearch(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -170,4 +170,55 @@ export default class FindInCode { | |||||||
| 
 | 
 | ||||||
|         codeEditor.focus(); |         codeEditor.focus(); | ||||||
|     } |     } | ||||||
|  |     async replace(replaceText) { | ||||||
|  |         // this.findResult may be undefined and null
 | ||||||
|  |         if (!this.findResult || this.findResult.length===0){ | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         let currentFound = -1; | ||||||
|  |         this.findResult.forEach((marker, index) => { | ||||||
|  |             const pos = marker.find(); | ||||||
|  |             if (pos) { | ||||||
|  |                 if (marker.className === FIND_RESULT_SELECTED_CSS_CLASSNAME) { | ||||||
|  |                     currentFound = index; | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         if (currentFound >= 0) { | ||||||
|  |             let marker = this.findResult[currentFound]; | ||||||
|  |             let pos = marker.find(); | ||||||
|  |             const codeEditor = await this.getCodeEditor(); | ||||||
|  |             const doc = codeEditor.doc; | ||||||
|  |             doc.replaceRange(replaceText, pos.from, pos.to); | ||||||
|  |             marker.clear(); | ||||||
|  | 
 | ||||||
|  |             let nextFound; | ||||||
|  |             if (currentFound === this.findResult.length - 1) { | ||||||
|  |                 nextFound = 0; | ||||||
|  |             } else { | ||||||
|  |                 nextFound = currentFound; | ||||||
|  |             } | ||||||
|  |             this.findResult.splice(currentFound, 1); | ||||||
|  |             if (this.findResult.length > 0) { | ||||||
|  |                 this.findNext(0, nextFound, nextFound); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     async replaceAll(replaceText) { | ||||||
|  |         if (!this.findResult || this.findResult.length===0){ | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         const codeEditor = await this.getCodeEditor(); | ||||||
|  |         const doc = codeEditor.doc; | ||||||
|  |         codeEditor.operation(() => { | ||||||
|  |             for (let currentFound = 0; currentFound < this.findResult.length; currentFound++) { | ||||||
|  |                 let marker = this.findResult[currentFound]; | ||||||
|  |                 let pos = marker.find(); | ||||||
|  |                 doc.replaceRange(replaceText, pos.from, pos.to); | ||||||
|  |                 marker.clear(); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         this.findResult = []; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -21,6 +21,7 @@ export default class FindInText { | |||||||
|         const findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); |         const findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); | ||||||
|         findAndReplaceEditing.state.clear(model); |         findAndReplaceEditing.state.clear(model); | ||||||
|         findAndReplaceEditing.stop(); |         findAndReplaceEditing.stop(); | ||||||
|  |         this.editingState = findAndReplaceEditing.state; | ||||||
|         if (searchTerm !== "") { |         if (searchTerm !== "") { | ||||||
|             // Parameters are callback/text, options.matchCase=false, options.wholeWords=false
 |             // Parameters are callback/text, options.matchCase=false, options.wholeWords=false
 | ||||||
|             // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findcommand.js#L44
 |             // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findcommand.js#L44
 | ||||||
| @ -102,4 +103,18 @@ export default class FindInText { | |||||||
| 
 | 
 | ||||||
|         textEditor.focus(); |         textEditor.focus(); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     async replace(replaceText) { | ||||||
|  |         if (this.editingState !== undefined && this.editingState.highlightedResult !== null) { | ||||||
|  |             const textEditor = await this.getTextEditor(); | ||||||
|  |             textEditor.execute('replace', replaceText, this.editingState.highlightedResult); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     async replaceAll(replaceText) { | ||||||
|  |         if (this.editingState !== undefined  && this.editingState.results.length > 0) { | ||||||
|  |             const textEditor = await this.getTextEditor(); | ||||||
|  |             textEditor.execute('replaceAll', replaceText, this.editingState.results); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -51,7 +51,7 @@ export default class ReadOnlyCodeTypeWidget extends AbstractCodeTypeWidget { | |||||||
| 
 | 
 | ||||||
|         await this.initialized; |         await this.initialized; | ||||||
| 
 | 
 | ||||||
|         resolve(this.$content); |         resolve(this.$editor); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     format(html) { |     format(html) { | ||||||
|  | |||||||
| @ -1378,8 +1378,12 @@ | |||||||
|   }, |   }, | ||||||
|   "open-help-page": "Open help page", |   "open-help-page": "Open help page", | ||||||
|   "find": { |   "find": { | ||||||
|     "case_sensitive": "case sensitive", |     "case_sensitive": "Case sensitive", | ||||||
|     "match_words": "match words" |     "match_words": "Match words", | ||||||
|  |     "find_placeholder":"Find in text...", | ||||||
|  |     "replace_placeholder":"Replace with...", | ||||||
|  |     "replace": "Replace", | ||||||
|  |     "replace_all": "Replace all" | ||||||
|   }, |   }, | ||||||
|   "highlights_list_2": { |   "highlights_list_2": { | ||||||
|     "title": "Highlights List", |     "title": "Highlights List", | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Elian Doran
						Elian Doran