mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-31 21:11:30 +08:00 
			
		
		
		
	Merge branch 'custom-search-dialog'
This commit is contained in:
		
						commit
						82fcc97ed2
					
				| @ -367,25 +367,61 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain | ||||
|     /** | ||||
|      * Adds given text to the editor cursor | ||||
|      * | ||||
|      * @deprecated use addTextToActiveContextEditor() instead | ||||
|      * @param {string} text - this must be clear text, HTML is not supported. | ||||
|      * @method | ||||
|      */ | ||||
|     this.addTextToActiveTabEditor = text => appContext.triggerCommand('addTextToActiveEditor', {text}); | ||||
|     this.addTextToActiveTabEditor = text => { | ||||
|         console.warn("api.addTextToActiveTabEditor() is deprecated, use addTextToActiveContextEditor() instead."); | ||||
| 
 | ||||
|         return appContext.triggerCommand('addTextToActiveEditor', {text}); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Adds given text to the editor cursor | ||||
|      * | ||||
|      * @param {string} text - this must be clear text, HTML is not supported. | ||||
|      * @method | ||||
|      */ | ||||
|     this.addTextToActiveContextEditor = text => appContext.triggerCommand('addTextToActiveEditor', {text}); | ||||
| 
 | ||||
|     /** | ||||
|      * @method | ||||
|      * @deprecated use getActiveContextNote() instead | ||||
|      * @returns {NoteShort} active note (loaded into right pane) | ||||
|      */ | ||||
|     this.getActiveTabNote = () => { | ||||
|         console.warn("api.getActiveTabNote() is deprecated, use getActiveContextNote() instead."); | ||||
| 
 | ||||
|         return appContext.tabManager.getActiveContextNote(); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * @method | ||||
|      * @returns {NoteShort} active note (loaded into right pane) | ||||
|      */ | ||||
|     this.getActiveTabNote = () => appContext.tabManager.getActiveContextNote(); | ||||
|     this.getActiveContextNote = () => appContext.tabManager.getActiveContextNote(); | ||||
| 
 | ||||
|     /** | ||||
|      * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance. | ||||
|      * | ||||
|      * @deprecated use getActiveContextTextEditor() | ||||
|      * @method | ||||
|      * @param [callback] - callback receiving "textEditor" instance | ||||
|      */ | ||||
|     this.getActiveTabTextEditor = callback => { | ||||
|         console.warn("api.getActiveTabTextEditor() is deprecated, use getActiveContextTextEditor() instead."); | ||||
| 
 | ||||
|         return appContext.tabManager.getActiveContextTextEditor(callback); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance. | ||||
|      * | ||||
|      * @method | ||||
|      * @param [callback] - deprecated (use returned promise): callback receiving "textEditor" instance | ||||
|      * @returns {Promise<CKEditor>} instance of CKEditor | ||||
|      */ | ||||
|     this.getActiveTabTextEditor = callback => new Promise(resolve => appContext.triggerCommand('executeInActiveTextEditor', {callback, resolve})); | ||||
|     this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContextTextEditor(); | ||||
| 
 | ||||
|     /** | ||||
|      * See https://codemirror.net/doc/manual.html#api | ||||
| @ -393,7 +429,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain | ||||
|      * @method | ||||
|      * @returns {Promise<CodeMirror>} instance of CodeMirror | ||||
|      */ | ||||
|     this.getActiveTabCodeEditor = () => new Promise(resolve => appContext.triggerCommand('executeInActiveCodeEditor', {callback: resolve})); | ||||
|     this.getActiveContextCodeEditor = () => appContext.tabManager.getActiveContextCodeEditor(); | ||||
| 
 | ||||
|     /** | ||||
|      * Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the | ||||
| @ -406,9 +442,20 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain | ||||
| 
 | ||||
|     /** | ||||
|      * @method | ||||
|      * @deprecated use getActiveContextNotePath() instead | ||||
|      * @returns {Promise<string|null>} returns note path of active note or null if there isn't active note | ||||
|      */ | ||||
|     this.getActiveTabNotePath = () => appContext.tabManager.getActiveContextNotePath(); | ||||
|     this.getActiveTabNotePath = () => { | ||||
|         console.warn("api.getActiveTabNotePath() is deprecated, use getActiveContextNotePath() instead."); | ||||
| 
 | ||||
|         return appContext.tabManager.getActiveContextNotePath(); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * @method | ||||
|      * @returns {Promise<string|null>} returns note path of active note or null if there isn't active note | ||||
|      */ | ||||
|     this.getActiveContextNotePath = () => appContext.tabManager.getActiveContextNotePath(); | ||||
| 
 | ||||
|     /** | ||||
|      * Returns component which owns given DOM element (the nearest parent component in DOM tree) | ||||
|  | ||||
							
								
								
									
										11
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										11
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -25,7 +25,6 @@ | ||||
|         "ejs": "3.1.8", | ||||
|         "electron-debug": "3.2.0", | ||||
|         "electron-dl": "3.3.1", | ||||
|         "electron-find": "1.0.7", | ||||
|         "electron-window-state": "5.0.3", | ||||
|         "express": "4.18.1", | ||||
|         "express-partial-content": "1.0.2", | ||||
| @ -3575,11 +3574,6 @@ | ||||
|         "url": "https://github.com/sponsors/sindresorhus" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/electron-find": { | ||||
|       "version": "1.0.7", | ||||
|       "resolved": "https://registry.npmjs.org/electron-find/-/electron-find-1.0.7.tgz", | ||||
|       "integrity": "sha512-C2FQJuk8567P2a2loBNwl5c8kwOTQVMB0capgHtPI7zKwZG16X0UxG+sNYZExQfnJ0PA+ecECA/4LcXxQa2TCA==" | ||||
|     }, | ||||
|     "node_modules/electron-installer-common": { | ||||
|       "version": "0.10.3", | ||||
|       "resolved": "https://registry.npmjs.org/electron-installer-common/-/electron-installer-common-0.10.3.tgz", | ||||
| @ -13742,11 +13736,6 @@ | ||||
|         "unused-filename": "^2.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "electron-find": { | ||||
|       "version": "1.0.7", | ||||
|       "resolved": "https://registry.npmjs.org/electron-find/-/electron-find-1.0.7.tgz", | ||||
|       "integrity": "sha512-C2FQJuk8567P2a2loBNwl5c8kwOTQVMB0capgHtPI7zKwZG16X0UxG+sNYZExQfnJ0PA+ecECA/4LcXxQa2TCA==" | ||||
|     }, | ||||
|     "electron-installer-common": { | ||||
|       "version": "0.10.3", | ||||
|       "resolved": "https://registry.npmjs.org/electron-installer-common/-/electron-installer-common-0.10.3.tgz", | ||||
|  | ||||
| @ -40,7 +40,6 @@ | ||||
|     "ejs": "3.1.8", | ||||
|     "electron-debug": "3.2.0", | ||||
|     "electron-dl": "3.3.1", | ||||
|     "electron-find": "1.0.7", | ||||
|     "electron-window-state": "5.0.3", | ||||
|     "express": "4.18.1", | ||||
|     "express-partial-content": "1.0.2", | ||||
|  | ||||
| @ -16,7 +16,7 @@ async function convertMarkdownToHtml(text) { | ||||
| 
 | ||||
|     const result = writer.render(parsed); | ||||
| 
 | ||||
|     appContext.triggerCommand('executeInActiveTextEditor', { | ||||
|     appContext.triggerCommand('executeInTextEditor', { | ||||
|         callback: textEditor => { | ||||
|             const viewFragment = textEditor.data.processor.toView(result); | ||||
|             const modelFragment = textEditor.data.toModel(viewFragment); | ||||
| @ -24,7 +24,8 @@ async function convertMarkdownToHtml(text) { | ||||
|             textEditor.model.insertContent(modelFragment, textEditor.model.document.selection); | ||||
| 
 | ||||
|             toastService.showMessage("Markdown content has been imported into the document."); | ||||
|         } | ||||
|         }, | ||||
|         ntxId: this.ntxId | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -48,6 +48,7 @@ import BookmarkButtons from "../widgets/bookmark_buttons.js"; | ||||
| import NoteWrapperWidget from "../widgets/note_wrapper.js"; | ||||
| import BacklinksWidget from "../widgets/backlinks.js"; | ||||
| import SharedInfoWidget from "../widgets/shared_info.js"; | ||||
| import FindWidget from "../widgets/find.js"; | ||||
| 
 | ||||
| export default class DesktopLayout { | ||||
|     constructor(customWidgets) { | ||||
| @ -161,6 +162,7 @@ export default class DesktopLayout { | ||||
|                                         .child(new SearchResultWidget()) | ||||
|                                         .child(new SqlResultWidget()) | ||||
|                                 ) | ||||
|                                 .child(new FindWidget()) | ||||
|                                 .child(...this.customWidgets.get('node-detail-pane')) | ||||
|                             ) | ||||
|                         ) | ||||
|  | ||||
| @ -39,29 +39,6 @@ export default class Entrypoints extends Component { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     findInTextCommand() { | ||||
|         if (!utils.isElectron()) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const remote = utils.dynamicRequire('@electron/remote'); | ||||
|         const {FindInPage} = utils.dynamicRequire('electron-find'); | ||||
|         const findInPage = new FindInPage(remote.getCurrentWebContents(), { | ||||
|             offsetTop: 10, | ||||
|             offsetRight: 10, | ||||
|             boxBgColor: 'var(--main-background-color)', | ||||
|             boxShadowColor: '#000', | ||||
|             inputColor: 'var(--input-text-color)', | ||||
|             inputBgColor: 'var(--input-background-color)', | ||||
|             inputFocusColor: '#555', | ||||
|             textColor: 'var(--main-text-color)', | ||||
|             textHoverBgColor: '#555', | ||||
|             caseSelectedColor: 'var(--main-border-color)' | ||||
|         }); | ||||
| 
 | ||||
|         findInPage.openFindWindow(); | ||||
|     } | ||||
| 
 | ||||
|     async createNoteIntoInboxCommand() { | ||||
|         const inboxNote = await dateNoteService.getInboxNote(); | ||||
| 
 | ||||
|  | ||||
| @ -339,25 +339,61 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain | ||||
|     /** | ||||
|      * Adds given text to the editor cursor | ||||
|      * | ||||
|      * @deprecated use addTextToActiveContextEditor() instead | ||||
|      * @param {string} text - this must be clear text, HTML is not supported. | ||||
|      * @method | ||||
|      */ | ||||
|     this.addTextToActiveTabEditor = text => appContext.triggerCommand('addTextToActiveEditor', {text}); | ||||
|     this.addTextToActiveTabEditor = text => { | ||||
|         console.warn("api.addTextToActiveTabEditor() is deprecated, use addTextToActiveContextEditor() instead."); | ||||
| 
 | ||||
|         return appContext.triggerCommand('addTextToActiveEditor', {text}); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * Adds given text to the editor cursor | ||||
|      * | ||||
|      * @param {string} text - this must be clear text, HTML is not supported. | ||||
|      * @method | ||||
|      */ | ||||
|     this.addTextToActiveContextEditor = text => appContext.triggerCommand('addTextToActiveEditor', {text}); | ||||
| 
 | ||||
|     /** | ||||
|      * @method | ||||
|      * @deprecated use getActiveContextNote() instead | ||||
|      * @returns {NoteShort} active note (loaded into right pane) | ||||
|      */ | ||||
|     this.getActiveTabNote = () => { | ||||
|         console.warn("api.getActiveTabNote() is deprecated, use getActiveContextNote() instead."); | ||||
| 
 | ||||
|         return appContext.tabManager.getActiveContextNote(); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * @method | ||||
|      * @returns {NoteShort} active note (loaded into right pane) | ||||
|      */ | ||||
|     this.getActiveTabNote = () => appContext.tabManager.getActiveContextNote(); | ||||
|     this.getActiveContextNote = () => appContext.tabManager.getActiveContextNote(); | ||||
| 
 | ||||
|     /** | ||||
|      * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance.
 | ||||
|      * | ||||
|      * @deprecated use getActiveContextTextEditor() | ||||
|      * @method | ||||
|      * @param [callback] - callback receiving "textEditor" instance | ||||
|      */ | ||||
|     this.getActiveTabTextEditor = callback => { | ||||
|         console.warn("api.getActiveTabTextEditor() is deprecated, use getActiveContextTextEditor() instead."); | ||||
| 
 | ||||
|         return appContext.tabManager.getActiveContext()?.getTextEditor(callback); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html for a documentation on the returned instance.
 | ||||
|      * | ||||
|      * @method | ||||
|      * @param [callback] - deprecated (use returned promise): callback receiving "textEditor" instance | ||||
|      * @returns {Promise<CKEditor>} instance of CKEditor | ||||
|      */ | ||||
|     this.getActiveTabTextEditor = callback => new Promise(resolve => appContext.triggerCommand('executeInActiveTextEditor', {callback, resolve})); | ||||
|     this.getActiveContextTextEditor = () => appContext.tabManager.getActiveContext()?.getTextEditor(); | ||||
| 
 | ||||
|     /** | ||||
|      * See https://codemirror.net/doc/manual.html#api
 | ||||
| @ -365,7 +401,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain | ||||
|      * @method | ||||
|      * @returns {Promise<CodeMirror>} instance of CodeMirror | ||||
|      */ | ||||
|     this.getActiveTabCodeEditor = () => new Promise(resolve => appContext.triggerCommand('executeInActiveCodeEditor', {callback: resolve})); | ||||
|     this.getActiveContextCodeEditor = () => appContext.tabManager.getActiveContext()?.getCodeEditor(); | ||||
| 
 | ||||
|     /** | ||||
|      * Get access to the widget handling note detail. Methods like `getWidgetType()` and `getTypeWidget()` to get to the | ||||
| @ -378,9 +414,20 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain | ||||
| 
 | ||||
|     /** | ||||
|      * @method | ||||
|      * @deprecated use getActiveContextNotePath() instead | ||||
|      * @returns {Promise<string|null>} returns note path of active note or null if there isn't active note | ||||
|      */ | ||||
|     this.getActiveTabNotePath = () => appContext.tabManager.getActiveContextNotePath(); | ||||
|     this.getActiveTabNotePath = () => { | ||||
|         console.warn("api.getActiveTabNotePath() is deprecated, use getActiveContextNotePath() instead."); | ||||
| 
 | ||||
|         return appContext.tabManager.getActiveContextNotePath(); | ||||
|     }; | ||||
| 
 | ||||
|     /** | ||||
|      * @method | ||||
|      * @returns {Promise<string|null>} returns note path of active note or null if there isn't active note | ||||
|      */ | ||||
|     this.getActiveContextNotePath = () => appContext.tabManager.getActiveContextNotePath(); | ||||
| 
 | ||||
|     /** | ||||
|      * Returns component which owns given DOM element (the nearest parent component in DOM tree) | ||||
|  | ||||
| @ -226,6 +226,21 @@ class NoteContext extends Component { | ||||
|             && this.note.mime !== 'text/x-sqlite;schema=trilium' | ||||
|             && !this.note.hasLabel('hideChildrenOverview'); | ||||
|     } | ||||
| 
 | ||||
|     async getTextEditor(callback) { | ||||
|         return new Promise(resolve => appContext.triggerCommand('executeInTextEditor', { | ||||
|             callback, | ||||
|             resolve, | ||||
|             ntxId: this.ntxId | ||||
|         })); | ||||
|     } | ||||
| 
 | ||||
|     async getCodeEditor() { | ||||
|         return new Promise(resolve => appContext.triggerCommand('executeInCodeEditor', { | ||||
|             resolve, | ||||
|             ntxId: this.ntxId | ||||
|         })); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export default NoteContext; | ||||
|  | ||||
							
								
								
									
										235
									
								
								src/public/app/widgets/find.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								src/public/app/widgets/find.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,235 @@ | ||||
| /** | ||||
|  * (c) Antonio Tejada 2022 | ||||
|  * https://github.com/antoniotejada/Trilium-FindWidget
 | ||||
|  */ | ||||
| 
 | ||||
| import NoteContextAwareWidget from "./note_context_aware_widget.js"; | ||||
| import FindInText from "./find_in_text.js"; | ||||
| import FindInCode from "./find_in_code.js"; | ||||
| 
 | ||||
| const findWidgetDelayMillis = 200; | ||||
| const waitForEnter = (findWidgetDelayMillis < 0); | ||||
| 
 | ||||
| // tabIndex=-1 on the checkbox labels is necessary so when clicking on the label
 | ||||
| // 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
 | ||||
| const TPL = ` | ||||
| <div style="contain: none;"> | ||||
|     <style> | ||||
|         .find-widget-box { | ||||
|             padding: 10px; | ||||
|             border-top: 1px solid var(--main-border-color);  | ||||
|             align-items: center; | ||||
|         } | ||||
|          | ||||
|         .find-widget-box > * { | ||||
|             margin-right: 15px; | ||||
|         } | ||||
|          | ||||
|         .find-widget-box { | ||||
|             display: flex; | ||||
|         } | ||||
|          | ||||
|         .find-widget-found-wrapper { | ||||
|             font-weight: bold; | ||||
|         } | ||||
|          | ||||
|         .find-widget-search-term-input { | ||||
|             max-width: 250px; | ||||
|         } | ||||
|          | ||||
|         .find-widget-spacer { | ||||
|             flex-grow: 1; | ||||
|         } | ||||
|     </style> | ||||
| 
 | ||||
|     <div class="find-widget-box"> | ||||
|         <input type="text" class="form-control find-widget-search-term-input"> | ||||
|          | ||||
|         <div class="form-check"> | ||||
|             <label tabIndex="-1" class="form-check-label"> | ||||
|                 <input type="checkbox" class="form-check-input find-widget-case-sensitive-checkbox">  | ||||
|                 case sensitive | ||||
|             </label> | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="form-check"> | ||||
|             <label tabIndex="-1" class="form-check-label"> | ||||
|                 <input type="checkbox" class="form-check-input find-widget-match-words-checkbox">  | ||||
|                 match words | ||||
|             </label> | ||||
|         </div> | ||||
|          | ||||
|         <div class="find-widget-found-wrapper"> | ||||
|             <span class="find-widget-current-found">0</span> | ||||
|             / | ||||
|             <span class="find-widget-total-found">0</span> | ||||
|         </div> | ||||
|          | ||||
|         <div class="find-widget-spacer"></div> | ||||
|          | ||||
|         <div class="find-widget-close-button"><button class="btn icon-action bx bx-x"></button></div> | ||||
|     </div> | ||||
| </div>`; | ||||
| 
 | ||||
| export default class FindWidget extends NoteContextAwareWidget { | ||||
|     constructor() { | ||||
|         super(); | ||||
| 
 | ||||
|         this.searchTerm = null; | ||||
| 
 | ||||
|         this.textHandler = new FindInText(this); | ||||
|         this.codeHandler = new FindInCode(this); | ||||
|     } | ||||
| 
 | ||||
|     doRender() { | ||||
|         this.$widget = $(TPL); | ||||
|         this.$findBox = this.$widget.find('.find-widget-box'); | ||||
|         this.$findBox.hide(); | ||||
|         this.$input = this.$widget.find('.find-widget-search-term-input'); | ||||
|         this.$currentFound = this.$widget.find('.find-widget-current-found'); | ||||
|         this.$totalFound = this.$widget.find('.find-widget-total-found'); | ||||
|         this.$caseSensitiveCheckbox = this.$widget.find(".find-widget-case-sensitive-checkbox"); | ||||
|         this.$caseSensitiveCheckbox.change(() => this.performFind()); | ||||
|         this.$matchWordsCheckbox = this.$widget.find(".find-widget-match-words-checkbox"); | ||||
|         this.$matchWordsCheckbox.change(() => this.performFind()); | ||||
|         this.$closeButton = this.$widget.find(".find-widget-close-button"); | ||||
|         this.$closeButton.on("click", () => this.closeSearch()); | ||||
| 
 | ||||
|         this.$input.keydown(async e => { | ||||
|             if ((e.metaKey || e.ctrlKey) && (e.key === 'F' || e.key === 'f')) { | ||||
|                 // If ctrl+f is pressed when the findbox is shown, select the
 | ||||
|                 // whole input to find
 | ||||
|                 this.$input.select(); | ||||
|             } else if (e.key === 'Enter' || e.key === 'F3') { | ||||
|                 await this.findNext(e); | ||||
|                 e.preventDefault(); | ||||
|                 return false; | ||||
|             } else if (e.key === 'Escape') { | ||||
|                 await this.closeSearch(); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         this.$input.on('input', () => this.startSearch()); | ||||
| 
 | ||||
|         return this.$widget; | ||||
|     } | ||||
| 
 | ||||
|     startSearch() { | ||||
|         // XXX This should clear the previous search immediately in all cases
 | ||||
|         //     (the search is stale when waitforenter but also while the
 | ||||
|         //     delay is running for non waitforenter case)
 | ||||
|         if (!waitForEnter) { | ||||
|             // Clear the previous timeout if any, it's ok if timeoutId is
 | ||||
|             // null or undefined
 | ||||
|             clearTimeout(this.timeoutId); | ||||
| 
 | ||||
|             // Defer the search a few millis so the search doesn't start
 | ||||
|             // immediately, as this can cause search word typing lag with
 | ||||
|             // one or two-char searchwords and long notes
 | ||||
|             // See https://github.com/antoniotejada/Trilium-FindWidget/issues/1
 | ||||
|             this.timeoutId = setTimeout(async () => { | ||||
|                 this.timeoutId = null; | ||||
|                 await this.performFind(); | ||||
|             }, findWidgetDelayMillis); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async findNext(e) { | ||||
|         const searchTerm = this.$input.val(); | ||||
|         if (waitForEnter && this.searchTerm !== searchTerm) { | ||||
|             await this.performFind(); | ||||
|         } | ||||
|         const totalFound = parseInt(this.$totalFound.text()); | ||||
|         const currentFound = parseInt(this.$currentFound.text()) - 1; | ||||
| 
 | ||||
|         if (totalFound > 0) { | ||||
|             const direction = e.shiftKey ? -1 : 1; | ||||
|             let nextFound = currentFound + direction; | ||||
|             // Wrap around
 | ||||
|             if (nextFound > totalFound - 1) { | ||||
|                 nextFound = 0; | ||||
|             } else if (nextFound < 0) { | ||||
|                 nextFound = totalFound - 1; | ||||
|             } | ||||
| 
 | ||||
|             this.$currentFound.text(nextFound + 1); | ||||
| 
 | ||||
|             await this.handler.findNext(direction, currentFound, nextFound); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async findInTextEvent() { | ||||
|         if (!this.isActiveNoteContext()) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         // Only writeable text and code supported
 | ||||
|         const readOnly = await this.noteContext.isReadOnly(); | ||||
| 
 | ||||
|         if (readOnly || !['text', 'code'].includes(this.note.type) || !this.$findBox.is(":hidden")) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.$findBox.show(); | ||||
|         this.$input.focus(); | ||||
|         this.$totalFound.text(0); | ||||
|         this.$currentFound.text(0); | ||||
| 
 | ||||
|         const searchTerm = await this.handler.getInitialSearchTerm(); | ||||
| 
 | ||||
|         this.$input.val(searchTerm || ""); | ||||
| 
 | ||||
|         // Directly perform the search if there's some text to
 | ||||
|         // find, without delaying or waiting for enter
 | ||||
|         if (searchTerm !== "") { | ||||
|             this.$input.select(); | ||||
|             await this.performFind(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** Perform the find and highlight the find results. */ | ||||
|     async performFind() { | ||||
|         const searchTerm = this.$input.val(); | ||||
|         const matchCase = this.$caseSensitiveCheckbox.prop("checked"); | ||||
|         const wholeWord = this.$matchWordsCheckbox.prop("checked"); | ||||
| 
 | ||||
|         const {totalFound, currentFound} = await this.handler.performFind(searchTerm, matchCase, wholeWord); | ||||
| 
 | ||||
|         this.$totalFound.text(totalFound); | ||||
|         this.$currentFound.text(currentFound); | ||||
| 
 | ||||
|         this.searchTerm = searchTerm; | ||||
|     } | ||||
| 
 | ||||
|     async closeSearch() { | ||||
|         this.$findBox.hide(); | ||||
| 
 | ||||
|         // Restore any state, if there's a current occurrence clear markers
 | ||||
|         // and scroll to and select the last occurrence
 | ||||
|         const totalFound = parseInt(this.$totalFound.text()); | ||||
|         const currentFound = parseInt(this.$currentFound.text()) - 1; | ||||
| 
 | ||||
|         if (totalFound > 0) { | ||||
|             await this.handler.cleanup(totalFound, currentFound); | ||||
|         } | ||||
| 
 | ||||
|         this.searchTerm = null; | ||||
|     } | ||||
| 
 | ||||
|     async entitiesReloadedEvent({loadResults}) { | ||||
|         if (loadResults.isNoteContentReloaded(this.noteId)) { | ||||
|             this.refresh(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     isEnabled() { | ||||
|         return super.isEnabled() && ['text', 'code'].includes(this.note.type); | ||||
|     } | ||||
| 
 | ||||
|     get handler() { | ||||
|         return this.note.type === "code" | ||||
|             ? this.codeHandler | ||||
|             : this.textHandler; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										198
									
								
								src/public/app/widgets/find_in_code.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								src/public/app/widgets/find_in_code.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,198 @@ | ||||
| // ck-find-result and ck-find-result_selected are the styles ck-editor
 | ||||
| // uses for highlighting matches, use the same one on CodeMirror
 | ||||
| // for consistency
 | ||||
| const FIND_RESULT_SELECTED_CSS_CLASSNAME = "ck-find-result_selected"; | ||||
| const FIND_RESULT_CSS_CLASSNAME = "ck-find-result"; | ||||
| 
 | ||||
| const escapeRegExp = str => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | ||||
| 
 | ||||
| export default class FindInCode { | ||||
|     constructor(parent) { | ||||
|         this.parent = parent; | ||||
|     } | ||||
| 
 | ||||
|     async getCodeEditor() { | ||||
|         return this.parent.noteContext.getCodeEditor(); | ||||
|     } | ||||
| 
 | ||||
|     async getInitialSearchTerm() { | ||||
|         const codeEditor = await this.getCodeEditor(); | ||||
| 
 | ||||
|         // highlightSelectionMatches is the overlay that highlights
 | ||||
|         // the words under the cursor. This occludes the search
 | ||||
|         // markers style, save it, disable it. Will be restored when
 | ||||
|         // the focus is back into the note
 | ||||
|         this.oldHighlightSelectionMatches = codeEditor.getOption("highlightSelectionMatches"); | ||||
|         codeEditor.setOption("highlightSelectionMatches", false); | ||||
| 
 | ||||
|         // Fill in the findbox with the current selection if any
 | ||||
|         const selectedText = codeEditor.getSelection() | ||||
|         if (selectedText !== "") { | ||||
|             return selectedText; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async performFind(searchTerm, matchCase, wholeWord) { | ||||
|         let findResult = null; | ||||
|         let totalFound = 0; | ||||
|         let currentFound = -1; | ||||
| 
 | ||||
|         // See https://codemirror.net/addon/search/searchcursor.js for tips
 | ||||
|         const codeEditor = await this.getCodeEditor(); | ||||
|         const doc = codeEditor.doc; | ||||
|         const text = doc.getValue(); | ||||
| 
 | ||||
|         // Clear all markers
 | ||||
|         if (this.findResult != null) { | ||||
|             codeEditor.operation(() => { | ||||
|                 for (let i = 0; i < this.findResult.length; ++i) { | ||||
|                     const marker = this.findResult[i]; | ||||
|                     marker.clear(); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         if (searchTerm !== "") { | ||||
|             searchTerm = escapeRegExp(searchTerm); | ||||
| 
 | ||||
|             // Find and highlight matches
 | ||||
|             // Find and highlight matches
 | ||||
|             // XXX Using \\b and not using the unicode flag probably doesn't
 | ||||
|             //     work with non ascii alphabets, findAndReplace uses a more
 | ||||
|             //     complicated regexp, see
 | ||||
|             //     https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/utils.js#L145
 | ||||
|             const wholeWordChar = wholeWord ? "\\b" : ""; | ||||
|             const re = new RegExp(wholeWordChar + searchTerm + wholeWordChar, | ||||
|                 'g' + (matchCase ? '' : 'i')); | ||||
|             let curLine = 0; | ||||
|             let curChar = 0; | ||||
|             let curMatch = null; | ||||
|             findResult = []; | ||||
|             // All those markText take several seconds on eg this ~500-line
 | ||||
|             // script, batch them inside an operation so they become
 | ||||
|             // unnoticeable. Alternatively, an overlay could be used, see
 | ||||
|             // https://codemirror.net/addon/search/match-highlighter.js ?
 | ||||
|             codeEditor.operation(() => { | ||||
|                 for (let i = 0; i < text.length; ++i) { | ||||
|                     // Fetch next match if it's the first time or
 | ||||
|                     // if past the current match start
 | ||||
|                     if ((curMatch == null) || (curMatch.index < i)) { | ||||
|                         curMatch = re.exec(text); | ||||
|                         if (curMatch == null) { | ||||
|                             // No more matches
 | ||||
|                             break; | ||||
|                         } | ||||
|                     } | ||||
|                     // Create a non-selected highlight marker for the match, the
 | ||||
|                     // selected marker highlight will be done later
 | ||||
|                     if (i === curMatch.index) { | ||||
|                         let fromPos = { "line" : curLine, "ch" : curChar }; | ||||
|                         // XXX If multiline is supported, this needs to
 | ||||
|                         //     recalculate curLine since the match may span
 | ||||
|                         //     lines
 | ||||
|                         let toPos = { "line" : curLine, "ch" : curChar + curMatch[0].length}; | ||||
|                         // XXX or css = "color: #f3"
 | ||||
|                         let marker = doc.markText( fromPos, toPos, { "className" : FIND_RESULT_CSS_CLASSNAME }); | ||||
|                         findResult.push(marker); | ||||
| 
 | ||||
|                         // Set the first match beyond the cursor as current
 | ||||
|                         // match
 | ||||
|                         if (currentFound === -1) { | ||||
|                             const cursorPos = codeEditor.getCursor(); | ||||
|                             if ((fromPos.line > cursorPos.line) || | ||||
|                                 ((fromPos.line === cursorPos.line) && | ||||
|                                     (fromPos.ch >= cursorPos.ch))){ | ||||
|                                 currentFound = totalFound; | ||||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                         totalFound++; | ||||
|                     } | ||||
|                     // Do line and char position tracking
 | ||||
|                     if (text[i] === "\n") { | ||||
|                         curLine++; | ||||
|                         curChar = 0; | ||||
|                     } else { | ||||
|                         curChar++; | ||||
|                     } | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
| 
 | ||||
|         this.findResult = findResult; | ||||
| 
 | ||||
|         // Calculate curfound if not already, highlight it as selected
 | ||||
|         if (totalFound > 0) { | ||||
|             currentFound = Math.max(0, currentFound) | ||||
|             let marker = findResult[currentFound]; | ||||
|             let pos = marker.find(); | ||||
|             codeEditor.scrollIntoView(pos.to); | ||||
|             marker.clear(); | ||||
|             findResult[currentFound] = doc.markText( pos.from, pos.to, | ||||
|                 { "className" : FIND_RESULT_SELECTED_CSS_CLASSNAME } | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             totalFound, | ||||
|             currentFound: currentFound + 1 | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     async findNext(direction, currentFound, nextFound) { | ||||
|         const codeEditor = await this.getCodeEditor(); | ||||
|         const doc = codeEditor.doc; | ||||
| 
 | ||||
|         //
 | ||||
|         // Dehighlight current, highlight & scrollIntoView next
 | ||||
|         //
 | ||||
| 
 | ||||
|         let marker = this.findResult[currentFound]; | ||||
|         let pos = marker.find(); | ||||
|         marker.clear(); | ||||
|         marker = doc.markText( | ||||
|             pos.from, pos.to, | ||||
|             { "className" : FIND_RESULT_CSS_CLASSNAME } | ||||
|         ); | ||||
|         this.findResult[currentFound] = marker; | ||||
| 
 | ||||
|         marker = this.findResult[nextFound]; | ||||
|         pos = marker.find(); | ||||
|         marker.clear(); | ||||
|         marker = doc.markText( | ||||
|             pos.from, pos.to, | ||||
|             { "className" : FIND_RESULT_SELECTED_CSS_CLASSNAME } | ||||
|         ); | ||||
|         this.findResult[nextFound] = marker; | ||||
| 
 | ||||
|         codeEditor.scrollIntoView(pos.from); | ||||
|     } | ||||
| 
 | ||||
|     async cleanup(totalFound, currentFound) { | ||||
|         const codeEditor = await this.getCodeEditor(); | ||||
| 
 | ||||
|         if (totalFound > 0) { | ||||
|             const doc = codeEditor.doc; | ||||
|             const pos = this.findResult[currentFound].find(); | ||||
|             // Note setting the selection sets the cursor to
 | ||||
|             // the end of the selection and scrolls it into
 | ||||
|             // view
 | ||||
|             doc.setSelection(pos.from, pos.to); | ||||
|             // Clear all markers
 | ||||
|             codeEditor.operation(() => { | ||||
|                 for (let i = 0; i < this.findResult.length; ++i) { | ||||
|                     let marker = this.findResult[i]; | ||||
|                     marker.clear(); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|         // Restore the highlightSelectionMatches setting
 | ||||
|         codeEditor.setOption("highlightSelectionMatches", this.oldHighlightSelectionMatches); | ||||
|         this.findResult = null; | ||||
|     } | ||||
| 
 | ||||
|     async close() { | ||||
|         const codeEditor = await this.getCodeEditor(); | ||||
|         codeEditor.focus(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										120
									
								
								src/public/app/widgets/find_in_text.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/public/app/widgets/find_in_text.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,120 @@ | ||||
| export default class FindInText { | ||||
|     constructor(parent) { | ||||
|         this.parent = parent; | ||||
|     } | ||||
| 
 | ||||
|     async getTextEditor() { | ||||
|         return this.parent.noteContext.getTextEditor(); | ||||
|     } | ||||
| 
 | ||||
|     async getInitialSearchTerm() { | ||||
|         const textEditor = await this.getTextEditor(); | ||||
| 
 | ||||
|         const selection = textEditor.model.document.selection; | ||||
|         const range = selection.getFirstRange(); | ||||
| 
 | ||||
|         for (const item of range.getItems()) { | ||||
|             // Fill in the findbox with the current selection if
 | ||||
|             // any
 | ||||
|             return item.data; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async performFind(searchTerm, matchCase, wholeWord) { | ||||
|         // Do this even if the searchTerm is empty so the markers are cleared and
 | ||||
|         // the counters updated
 | ||||
|         const textEditor = await this.getTextEditor(); | ||||
|         const model = textEditor.model; | ||||
|         let findResult = null; | ||||
|         let totalFound = 0; | ||||
|         let currentFound = -1; | ||||
| 
 | ||||
|         // Clear
 | ||||
|         const findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); | ||||
|         findAndReplaceEditing.state.clear(model); | ||||
|         findAndReplaceEditing.stop(); | ||||
|         if (searchTerm !== "") { | ||||
|             // 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
 | ||||
|             // XXX Need to use the callback version for regexp
 | ||||
|             // searchTerm = escapeRegExp(searchTerm);
 | ||||
|             // let re = new RegExp(searchTerm, 'gi');
 | ||||
|             // let m = text.match(re);
 | ||||
|             // totalFound = m ? m.length : 0;
 | ||||
|             const options = { "matchCase" : matchCase, "wholeWords" : wholeWord }; | ||||
|             findResult = textEditor.execute('find', searchTerm, options); | ||||
|             totalFound = findResult.results.length; | ||||
|             // Find the result beyond the cursor
 | ||||
|             const cursorPos = model.document.selection.getLastPosition(); | ||||
|             for (let i = 0; i < findResult.results.length; ++i) { | ||||
|                 const marker = findResult.results.get(i).marker; | ||||
|                 const fromPos = marker.getStart(); | ||||
|                 if (fromPos.compareWith(cursorPos) !== "before") { | ||||
|                     currentFound = i; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         this.findResult = findResult; | ||||
| 
 | ||||
|         // Calculate curfound if not already, highlight it as
 | ||||
|         // selected
 | ||||
|         if (totalFound > 0) { | ||||
|             currentFound = Math.max(0, currentFound); | ||||
|             // XXX Do this accessing the private data?
 | ||||
|             // See
 | ||||
|             // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js
 | ||||
|             for (let i = 0 ; i < currentFound; ++i) { | ||||
|                 textEditor.execute('findNext', searchTerm); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             totalFound, | ||||
|             currentFound: currentFound + 1 | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     async findNext(direction, currentFound, nextFound) { | ||||
|         const textEditor = await this.getTextEditor(); | ||||
| 
 | ||||
|         // There are no parameters for findNext/findPrev
 | ||||
|         // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js#L57
 | ||||
|         // curFound wrap around above assumes findNext and
 | ||||
|         // findPrevious wraparound, which is what they do
 | ||||
|         if (direction > 0) { | ||||
|             textEditor.execute('findNext'); | ||||
|         } else { | ||||
|             textEditor.execute('findPrevious'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async cleanup(totalFound, currentFound) { | ||||
|         if (totalFound > 0) { | ||||
|             const textEditor = await this.getTextEditor(); | ||||
|             // Clear the markers and set the caret to the
 | ||||
|             // current occurrence
 | ||||
|             const model = textEditor.model; | ||||
|             const range = this.findResult.results.get(currentFound).marker.getRange(); | ||||
|             // From
 | ||||
|             // https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findandreplace.js#L92
 | ||||
|             // XXX Roll our own since already done for codeEditor and
 | ||||
|             //     will probably allow more refactoring?
 | ||||
|             let findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing'); | ||||
|             findAndReplaceEditing.state.clear(model); | ||||
|             findAndReplaceEditing.stop(); | ||||
|             model.change(writer => { | ||||
|                 writer.setSelection(range, 0); | ||||
|             }); | ||||
|             textEditor.editing.view.scrollToTheSelection(); | ||||
|         } | ||||
| 
 | ||||
|         this.findResult = null; | ||||
|     } | ||||
| 
 | ||||
|     async close() { | ||||
|         const textEditor = await this.getTextEditor(); | ||||
|         textEditor.focus(); | ||||
|     } | ||||
| } | ||||
| @ -171,13 +171,13 @@ export default class EditableCodeTypeWidget extends TypeWidget { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async executeInActiveCodeEditorEvent({callback}) { | ||||
|         if (!this.isActive()) { | ||||
|     async executeInCodeEditorEvent({resolve, ntxId}) { | ||||
|         if (!this.isNoteContext(ntxId)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         await this.initialized; | ||||
| 
 | ||||
|         callback(this.codeEditor); | ||||
|         resolve(this.codeEditor); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -229,8 +229,8 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { | ||||
|         return !selection.isCollapsed; | ||||
|     } | ||||
| 
 | ||||
|     async executeInActiveTextEditorEvent({callback, resolve}) { | ||||
|         if (!this.isActive()) { | ||||
|     async executeInTextEditorEvent({callback, resolve, ntxId}) { | ||||
|         if (!this.isNoteContext(ntxId)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 zadam
						zadam