357 lines
13 KiB
JavaScript
357 lines
13 KiB
JavaScript
/*!
|
|
* Jodit Editor (https://xdsoft.net/jodit/)
|
|
* Released under MIT see LICENSE.txt in the project root for license information.
|
|
* Copyright (c) 2013-2025 Valeriy Chupurnov. All rights reserved. https://xdsoft.net
|
|
*/
|
|
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
|
|
r = Reflect.decorate(decorators, target, key, desc);
|
|
else
|
|
for (var i = decorators.length - 1; i >= 0; i--)
|
|
if (d = decorators[i])
|
|
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
};
|
|
import { IS_PROD } from "../../core/constants.js";
|
|
import { autobind, cache, cached, watch } from "../../core/decorators/index.js";
|
|
import { Dom, LazyWalker } from "../../core/dom/index.js";
|
|
import { pluginSystem } from "../../core/global.js";
|
|
import { scrollIntoViewIfNeeded } from "../../core/helpers/index.js";
|
|
import { Plugin } from "../../core/plugin/index.js";
|
|
import "./config.js";
|
|
import { clearSelectionWrappers, clearSelectionWrappersFromHTML, getSelectionWrappers, highlightTextRanges, SentenceFinder } from "./helpers/index.js";
|
|
import { UISearch } from "./ui/search.js";
|
|
/**
|
|
* Search plugin. it is used for custom search in text
|
|
* 
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const jodit = Jodit.make('#editor', {
|
|
* useSearch: false
|
|
* });
|
|
* // or
|
|
* const jodit = Jodit.make('#editor', {
|
|
* disablePlugins: 'search'
|
|
* });
|
|
* ```
|
|
*/
|
|
export class search extends Plugin {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.buttons = [
|
|
{
|
|
name: 'find',
|
|
group: 'search'
|
|
}
|
|
];
|
|
this.previousQuery = '';
|
|
this.drawPromise = null;
|
|
this.walker = null;
|
|
this.walkerCount = null;
|
|
this.cache = {};
|
|
this.wrapFrameRequest = 0;
|
|
}
|
|
get ui() {
|
|
return new UISearch(this.j);
|
|
}
|
|
async updateCounters() {
|
|
if (!this.ui.isOpened) {
|
|
return;
|
|
}
|
|
this.ui.count = await this.calcCounts(this.ui.query);
|
|
}
|
|
onPressReplaceButton() {
|
|
this.findAndReplace(this.ui.query);
|
|
this.updateCounters();
|
|
}
|
|
tryScrollToElement(startContainer) {
|
|
// find scrollable element
|
|
let parentBox = Dom.closest(startContainer, Dom.isElement, this.j.editor);
|
|
if (!parentBox) {
|
|
parentBox = Dom.prev(startContainer, Dom.isElement, this.j.editor);
|
|
}
|
|
parentBox &&
|
|
parentBox !== this.j.editor &&
|
|
scrollIntoViewIfNeeded(parentBox, this.j.editor, this.j.ed);
|
|
}
|
|
async calcCounts(query) {
|
|
return (await this.findQueryBounds(query, 'walkerCount')).length;
|
|
}
|
|
async findQueryBounds(query, walkerKey) {
|
|
let walker = this[walkerKey];
|
|
if (walker) {
|
|
walker.break();
|
|
}
|
|
walker = new LazyWalker(this.j.async, {
|
|
timeout: this.j.o.search.lazyIdleTimeout
|
|
});
|
|
this[walkerKey] = walker;
|
|
return this.find(walker, query).catch(e => {
|
|
!IS_PROD && console.error(e);
|
|
return [];
|
|
});
|
|
}
|
|
async findAndReplace(query) {
|
|
const bounds = await this.findQueryBounds(query, 'walker');
|
|
if (!bounds.length) {
|
|
return false;
|
|
}
|
|
let currentIndex = this.findCurrentIndexInRanges(bounds, this.j.s.range);
|
|
if (currentIndex === -1) {
|
|
currentIndex = 0;
|
|
}
|
|
const bound = bounds[currentIndex];
|
|
if (bound) {
|
|
try {
|
|
const rng = this.j.ed.createRange();
|
|
rng.setStart(bound.startContainer, bound.startOffset);
|
|
rng.setEnd(bound.endContainer, bound.endOffset);
|
|
rng.deleteContents();
|
|
const textNode = this.j.createInside.text(this.ui.replace);
|
|
Dom.safeInsertNode(rng, textNode);
|
|
clearSelectionWrappers(this.j);
|
|
this.j.s.setCursorAfter(textNode);
|
|
this.tryScrollToElement(textNode);
|
|
this.cache = {};
|
|
this.ui.currentIndex = currentIndex;
|
|
await this.findAndSelect(query, true).catch(e => {
|
|
!IS_PROD && console.error(e);
|
|
return null;
|
|
});
|
|
}
|
|
finally {
|
|
this.j.synchronizeValues();
|
|
}
|
|
this.j.e.fire('afterFindAndReplace');
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
async findAndSelect(query, next) {
|
|
var _a;
|
|
const bounds = await this.findQueryBounds(query, 'walker');
|
|
if (!bounds.length) {
|
|
return false;
|
|
}
|
|
if (this.previousQuery !== query ||
|
|
!getSelectionWrappers(this.j.editor).length) {
|
|
(_a = this.drawPromise) === null || _a === void 0 ? void 0 : _a.rejectCallback();
|
|
this.j.async.cancelAnimationFrame(this.wrapFrameRequest);
|
|
clearSelectionWrappers(this.j);
|
|
this.drawPromise = this.__drawSelectionRanges(bounds);
|
|
}
|
|
this.previousQuery = query;
|
|
let currentIndex = this.ui.currentIndex - 1;
|
|
if (currentIndex === -1) {
|
|
currentIndex = 0;
|
|
}
|
|
else if (next) {
|
|
currentIndex =
|
|
currentIndex === bounds.length - 1 ? 0 : currentIndex + 1;
|
|
}
|
|
else {
|
|
currentIndex =
|
|
currentIndex === 0 ? bounds.length - 1 : currentIndex - 1;
|
|
}
|
|
this.ui.currentIndex = currentIndex + 1;
|
|
const bound = bounds[currentIndex];
|
|
if (bound) {
|
|
const rng = this.j.ed.createRange();
|
|
try {
|
|
rng.setStart(bound.startContainer, bound.startOffset);
|
|
rng.setEnd(bound.endContainer, bound.endOffset);
|
|
this.j.s.selectRange(rng);
|
|
}
|
|
catch (e) {
|
|
!IS_PROD && console.error(e);
|
|
}
|
|
this.tryScrollToElement(bound.startContainer);
|
|
await this.updateCounters();
|
|
await this.drawPromise;
|
|
this.j.e.fire('afterFindAndSelect');
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
findCurrentIndexInRanges(bounds, range) {
|
|
return bounds.findIndex(bound => bound.startContainer === range.startContainer &&
|
|
bound.startOffset === range.startOffset &&
|
|
bound.endContainer === range.startContainer &&
|
|
bound.endOffset === range.endOffset);
|
|
}
|
|
async isValidCache(promise) {
|
|
const res = await promise;
|
|
return res.every(r => {
|
|
var _a, _b, _c, _d;
|
|
return r.startContainer.isConnected &&
|
|
r.startOffset <= ((_b = (_a = r.startContainer.nodeValue) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) &&
|
|
r.endContainer.isConnected &&
|
|
r.endOffset <= ((_d = (_c = r.endContainer.nodeValue) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0);
|
|
});
|
|
}
|
|
async find(walker, query) {
|
|
if (!query.length) {
|
|
return [];
|
|
}
|
|
const cache = this.cache[query];
|
|
if (cache && (await this.isValidCache(cache))) {
|
|
return cache;
|
|
}
|
|
this.cache[query] = this.j.async.promise(resolve => {
|
|
const sentence = new SentenceFinder(this.j.o.search.fuzzySearch);
|
|
walker
|
|
.on('break', () => {
|
|
resolve([]);
|
|
})
|
|
.on('visit', (elm) => {
|
|
if (Dom.isText(elm)) {
|
|
sentence.add(elm);
|
|
}
|
|
return false;
|
|
})
|
|
.on('end', () => {
|
|
var _a;
|
|
resolve((_a = sentence.ranges(query)) !== null && _a !== void 0 ? _a : []);
|
|
})
|
|
.setWork(this.j.editor);
|
|
});
|
|
return this.cache[query];
|
|
}
|
|
__drawSelectionRanges(ranges) {
|
|
const { async, createInside: ci, editor } = this.j;
|
|
async.cancelAnimationFrame(this.wrapFrameRequest);
|
|
const parts = [...ranges];
|
|
let sRange, total = 0;
|
|
return async.promise(resolve => {
|
|
const drawParts = () => {
|
|
do {
|
|
sRange = parts.shift();
|
|
if (sRange) {
|
|
highlightTextRanges(this.j, sRange, parts, ci, editor);
|
|
}
|
|
total += 1;
|
|
} while (sRange && total <= 5);
|
|
if (parts.length) {
|
|
this.wrapFrameRequest =
|
|
async.requestAnimationFrame(drawParts);
|
|
}
|
|
else {
|
|
resolve();
|
|
}
|
|
};
|
|
drawParts();
|
|
});
|
|
}
|
|
onAfterGetValueFromEditor(data) {
|
|
data.value = clearSelectionWrappersFromHTML(data.value);
|
|
}
|
|
/** @override */
|
|
afterInit(editor) {
|
|
if (editor.o.useSearch) {
|
|
const self = this;
|
|
editor.e
|
|
.on('beforeSetMode.search', () => {
|
|
this.ui.close();
|
|
})
|
|
.on(this.ui, 'afterClose', () => {
|
|
clearSelectionWrappers(editor);
|
|
this.ui.currentIndex = 0;
|
|
this.ui.count = 0;
|
|
this.cache = {};
|
|
editor.focus();
|
|
})
|
|
.on('click', () => {
|
|
this.ui.currentIndex = 0;
|
|
clearSelectionWrappers(editor);
|
|
})
|
|
.on('change.search', () => {
|
|
this.cache = {};
|
|
})
|
|
.on('keydown.search mousedown.search', editor.async.debounce(() => {
|
|
if (this.ui.selInfo) {
|
|
editor.s.removeMarkers();
|
|
this.ui.selInfo = null;
|
|
}
|
|
if (this.ui.isOpened) {
|
|
void this.updateCounters();
|
|
}
|
|
}, editor.defaultTimeout))
|
|
.on('searchNext.search searchPrevious.search', () => {
|
|
if (!this.ui.isOpened) {
|
|
this.ui.open();
|
|
}
|
|
return self
|
|
.findAndSelect(self.ui.query, editor.e.current === 'searchNext')
|
|
.catch(e => {
|
|
!IS_PROD && console.error('Search error', e);
|
|
});
|
|
})
|
|
.on('search.search', (value, next = true) => {
|
|
this.ui.currentIndex = 0;
|
|
return self.findAndSelect(value || '', next).catch(e => {
|
|
!IS_PROD && console.error('Search error', e);
|
|
});
|
|
});
|
|
editor
|
|
.registerCommand('search', {
|
|
exec: (command, value, next = true) => {
|
|
value &&
|
|
self.findAndSelect(value, next).catch(e => {
|
|
!IS_PROD && console.error('Search error', e);
|
|
});
|
|
return false;
|
|
}
|
|
})
|
|
.registerCommand('openSearchDialog', {
|
|
exec: (command, value) => {
|
|
self.ui.open(value);
|
|
return false;
|
|
},
|
|
hotkeys: ['ctrl+f', 'cmd+f']
|
|
})
|
|
.registerCommand('openReplaceDialog', {
|
|
exec: (command, query, replace) => {
|
|
if (!editor.o.readonly) {
|
|
self.ui.open(query, replace, true);
|
|
}
|
|
return false;
|
|
},
|
|
hotkeys: ['ctrl+h', 'cmd+h']
|
|
});
|
|
}
|
|
}
|
|
/** @override */
|
|
beforeDestruct(jodit) {
|
|
var _a;
|
|
(_a = cached(this, 'ui')) === null || _a === void 0 ? void 0 : _a.destruct();
|
|
jodit.e.off('.search');
|
|
}
|
|
}
|
|
__decorate([
|
|
cache
|
|
], search.prototype, "ui", null);
|
|
__decorate([
|
|
watch('ui:needUpdateCounters')
|
|
], search.prototype, "updateCounters", null);
|
|
__decorate([
|
|
watch('ui:pressReplaceButton')
|
|
], search.prototype, "onPressReplaceButton", null);
|
|
__decorate([
|
|
autobind
|
|
], search.prototype, "findQueryBounds", null);
|
|
__decorate([
|
|
autobind
|
|
], search.prototype, "findAndReplace", null);
|
|
__decorate([
|
|
autobind
|
|
], search.prototype, "findAndSelect", null);
|
|
__decorate([
|
|
autobind
|
|
], search.prototype, "find", null);
|
|
__decorate([
|
|
watch(':afterGetValueFromEditor')
|
|
], search.prototype, "onAfterGetValueFromEditor", null);
|
|
pluginSystem.add('search', search);
|