UNPKG

jodit

Version:

Jodit is an awesome and useful wysiwyg editor with filebrowser

357 lines (356 loc) 13 kB
/*! * 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 * ![search](https://user-images.githubusercontent.com/794318/34545433-cd0a9220-f10e-11e7-8d26-7e22f66e266d.gif) * * @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);