jodit 3.15.2 → 3.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. package/.idea/workspace.xml +301 -299
  2. package/CHANGELOG.MD +88 -7
  3. package/CONTRIBUTING.md +97 -0
  4. package/README.md +7 -7
  5. package/build/jodit.css +38 -32
  6. package/build/jodit.es2018.css +37 -31
  7. package/build/jodit.es2018.en.css +37 -31
  8. package/build/jodit.es2018.en.js +1981 -1393
  9. package/build/jodit.es2018.en.min.css +1 -1
  10. package/build/jodit.es2018.en.min.js +1 -1
  11. package/build/jodit.es2018.js +2053 -1447
  12. package/build/jodit.es2018.min.css +1 -1
  13. package/build/jodit.es2018.min.js +1 -1
  14. package/build/jodit.js +3475 -2625
  15. package/build/jodit.min.css +2 -2
  16. package/build/jodit.min.js +1 -1
  17. package/build/vdom.css +1 -1
  18. package/build/vdom.js +32 -20
  19. package/package.json +13 -13
  20. package/src/README.md +1 -1
  21. package/src/config.ts +69 -36
  22. package/src/core/async/async.ts +46 -24
  23. package/src/core/constants.ts +1 -0
  24. package/src/core/decorators/README.md +35 -0
  25. package/src/core/decorators/cache/cache.ts +1 -1
  26. package/src/core/decorators/debounce/debounce.ts +20 -9
  27. package/src/core/decorators/idle/README.md +14 -0
  28. package/src/core/decorators/idle/idle.ts +1 -1
  29. package/src/core/decorators/watch/watch.ts +8 -7
  30. package/src/core/dom/README.md +42 -0
  31. package/src/core/dom/dom.ts +37 -23
  32. package/src/core/dom/index.ts +1 -0
  33. package/src/core/dom/lazy-walker.ts +133 -0
  34. package/src/core/event-emitter/event-emitter.ts +8 -8
  35. package/src/core/event-emitter/eventify.ts +73 -0
  36. package/src/core/event-emitter/index.ts +1 -0
  37. package/src/core/helpers/html/apply-styles.ts +1 -1
  38. package/src/core/helpers/html/strip-tags.ts +3 -2
  39. package/src/core/helpers/string/fuzzy-search-index.ts +58 -0
  40. package/src/core/helpers/string/i18n.ts +1 -1
  41. package/src/core/helpers/string/index.ts +3 -2
  42. package/src/core/helpers/utils/append-script.ts +1 -1
  43. package/src/core/helpers/utils/css.ts +1 -1
  44. package/src/core/helpers/utils/selector.ts +1 -1
  45. package/src/core/helpers/utils/utils.ts +3 -3
  46. package/src/core/plugin/plugin-system.ts +14 -8
  47. package/src/core/request/ajax.ts +3 -3
  48. package/src/core/selection/select.ts +10 -10
  49. package/src/core/selection/style/api/toggle/toggle-css.ts +5 -2
  50. package/src/core/selection/style/api/wrap-unwrapped-text.ts +1 -1
  51. package/src/core/selection/style/apply-style.ts +4 -4
  52. package/src/core/storage/engines/local-storage-provider.ts +20 -19
  53. package/src/core/ui/button/button/button.ts +5 -5
  54. package/src/core/ui/element.ts +2 -2
  55. package/src/core/ui/form/inputs/input/input.ts +1 -1
  56. package/src/core/ui/form/inputs/select/select.ts +1 -1
  57. package/src/core/ui/group/list.ts +2 -2
  58. package/src/core/vdom/render/index.ts +12 -8
  59. package/src/core/vdom/v-dom-jodit.ts +1 -1
  60. package/src/core/view/view.ts +1 -1
  61. package/src/index.ts +3 -3
  62. package/src/jodit.ts +72 -55
  63. package/src/langs/README.md +1 -1
  64. package/src/langs/ar.js +2 -1
  65. package/src/langs/cs_cz.js +2 -1
  66. package/src/langs/de.js +2 -1
  67. package/src/langs/es.js +2 -1
  68. package/src/langs/fa.js +2 -1
  69. package/src/langs/fr.js +2 -1
  70. package/src/langs/he.js +2 -1
  71. package/src/langs/hu.js +2 -1
  72. package/src/langs/id.js +2 -1
  73. package/src/langs/index.ts +1 -1
  74. package/src/langs/it.js +2 -1
  75. package/src/langs/ja.js +2 -1
  76. package/src/langs/ko.js +2 -1
  77. package/src/langs/nl.js +2 -1
  78. package/src/langs/pl.js +2 -1
  79. package/src/langs/pt_br.js +2 -1
  80. package/src/langs/ru.js +2 -1
  81. package/src/langs/tr.js +2 -1
  82. package/src/langs/zh_cn.js +2 -1
  83. package/src/langs/zh_tw.js +2 -1
  84. package/src/modules/dialog/dialog.ts +6 -6
  85. package/src/modules/dialog/prompt.ts +1 -1
  86. package/src/modules/file-browser/README.md +2 -2
  87. package/src/modules/file-browser/builders/context-menu.ts +12 -13
  88. package/src/modules/file-browser/fetch/load-tree.ts +1 -1
  89. package/src/modules/file-browser/file-browser.ts +10 -7
  90. package/src/modules/history/README.md +5 -0
  91. package/src/modules/{observer → history}/command.ts +5 -5
  92. package/src/modules/{observer/observer.ts → history/history.ts} +97 -55
  93. package/src/modules/{observer → history}/snapshot.ts +3 -4
  94. package/src/modules/{observer → history}/stack.ts +4 -4
  95. package/src/modules/image-editor/image-editor.ts +8 -8
  96. package/src/modules/image-editor/templates/form.ts +2 -2
  97. package/src/modules/index.ts +3 -3
  98. package/src/modules/status-bar/status-bar.ts +4 -0
  99. package/src/modules/table/table.ts +2 -2
  100. package/src/modules/toolbar/button/button.ts +2 -2
  101. package/src/modules/toolbar/collection/collection.ts +1 -1
  102. package/src/modules/uploader/helpers/process-old-browser-drag.ts +1 -1
  103. package/src/modules/uploader/helpers/send-files.ts +1 -1
  104. package/src/modules/uploader/helpers/send.ts +1 -1
  105. package/src/modules/uploader/uploader.ts +3 -3
  106. package/src/modules/widget/color-picker/color-picker.ts +2 -3
  107. package/src/modules/widget/tabs/tabs.ts +17 -12
  108. package/src/plugins/add-new-line/add-new-line.ts +8 -8
  109. package/src/plugins/class-span/class-span.ts +1 -1
  110. package/src/plugins/clipboard/copy-format.ts +1 -1
  111. package/src/plugins/clipboard/drag-and-drop-element.ts +4 -2
  112. package/src/plugins/clipboard/paste/config.ts +19 -3
  113. package/src/plugins/clipboard/paste/helpers.ts +17 -50
  114. package/src/plugins/clipboard/paste/interface.ts +6 -0
  115. package/src/plugins/clipboard/paste/paste.ts +22 -8
  116. package/src/plugins/clipboard/paste-from-word/config.ts +17 -0
  117. package/src/plugins/clipboard/paste-from-word/paste-from-word.ts +15 -6
  118. package/src/plugins/clipboard/paste-storage/paste-storage.ts +6 -6
  119. package/src/plugins/color/color.ts +2 -2
  120. package/src/plugins/error-messages/error-messages.ts +2 -2
  121. package/src/plugins/fix/clean-html/README.md +26 -0
  122. package/src/plugins/fix/{clean-html.ts → clean-html/clean-html.ts} +59 -142
  123. package/src/plugins/fix/clean-html/config.ts +106 -0
  124. package/src/plugins/fix/index.ts +12 -0
  125. package/src/plugins/fix/wrap-nodes/README.md +27 -0
  126. package/src/plugins/fix/wrap-nodes/config.ts +24 -0
  127. package/src/plugins/fix/{wrap-text-nodes.ts → wrap-nodes/wrap-nodes.ts} +9 -4
  128. package/src/plugins/focus/focus.ts +1 -1
  129. package/src/plugins/format-block/format-block.ts +1 -1
  130. package/src/plugins/fullsize/fullsize.ts +4 -4
  131. package/src/plugins/iframe/iframe.ts +3 -3
  132. package/src/plugins/image/image-properties/image-properties.ts +12 -13
  133. package/src/plugins/indent/indent.ts +1 -1
  134. package/src/plugins/index.ts +2 -2
  135. package/src/plugins/inline-popup/config/items/a.ts +2 -2
  136. package/src/plugins/inline-popup/config/items/cells.ts +11 -11
  137. package/src/plugins/inline-popup/config/items/iframe.ts +1 -1
  138. package/src/plugins/inline-popup/config/items/img.ts +7 -7
  139. package/src/plugins/inline-popup/inline-popup.ts +5 -5
  140. package/src/plugins/keyboard/backspace/backspace.ts +1 -1
  141. package/src/plugins/keyboard/backspace/cases/check-join-neighbors.ts +1 -1
  142. package/src/plugins/keyboard/helpers.ts +1 -1
  143. package/src/plugins/keyboard/hotkeys.ts +1 -1
  144. package/src/plugins/limit/limit.ts +3 -3
  145. package/src/plugins/line-height/line-height.ts +1 -1
  146. package/src/plugins/link/link.ts +8 -8
  147. package/src/plugins/link/template.ts +2 -2
  148. package/src/plugins/media/file.ts +1 -1
  149. package/src/plugins/media/media.ts +1 -1
  150. package/src/plugins/media/video/config.ts +1 -1
  151. package/src/plugins/mobile/config.ts +1 -1
  152. package/src/plugins/mobile/mobile.ts +1 -1
  153. package/src/plugins/ordered-list/config.ts +61 -0
  154. package/src/plugins/ordered-list/ordered-list.ts +3 -153
  155. package/src/plugins/placeholder/placeholder.ts +3 -3
  156. package/src/plugins/print/helpers.ts +14 -7
  157. package/src/plugins/print/index.ts +1 -1
  158. package/src/plugins/print/{preview.less → preview/preview.less} +1 -1
  159. package/src/plugins/print/{preview.ts → preview/preview.ts} +9 -8
  160. package/src/plugins/print/print.ts +19 -10
  161. package/src/plugins/redo-undo/redo-undo.ts +3 -3
  162. package/src/plugins/resizer/resizer.ts +11 -11
  163. package/src/plugins/search/README.md +38 -0
  164. package/src/plugins/search/config.ts +82 -0
  165. package/src/plugins/search/helpers/index.ts +12 -0
  166. package/src/plugins/search/helpers/sentence-finder.ts +103 -0
  167. package/src/plugins/search/helpers/wrap-ranges-texts-in-tmp-span.ts +120 -0
  168. package/src/plugins/search/search.ts +269 -615
  169. package/src/plugins/search/ui/search.less +159 -0
  170. package/src/plugins/search/ui/search.ts +256 -0
  171. package/src/plugins/select/select.ts +1 -1
  172. package/src/plugins/size/config.ts +8 -8
  173. package/src/plugins/size/resize-handler.ts +3 -3
  174. package/src/plugins/size/size.ts +4 -4
  175. package/src/plugins/source/editor/engines/ace.ts +9 -9
  176. package/src/plugins/source/editor/engines/area.ts +3 -3
  177. package/src/plugins/source/source.ts +6 -6
  178. package/src/plugins/spellcheck/README.md +1 -0
  179. package/src/plugins/spellcheck/config.ts +34 -0
  180. package/src/plugins/spellcheck/spellcheck.svg +4 -0
  181. package/src/plugins/spellcheck/spellcheck.ts +48 -0
  182. package/src/plugins/sticky/sticky.ts +3 -3
  183. package/src/plugins/table/resize-cells.ts +11 -11
  184. package/src/plugins/table/select-cells.ts +2 -2
  185. package/src/plugins/tooltip/tooltip.ts +1 -1
  186. package/src/plugins/xpath/xpath.ts +8 -8
  187. package/src/polyfills.ts +5 -4
  188. package/src/styles/icons/README.md +2 -2
  189. package/src/types/async.d.ts +12 -2
  190. package/src/types/core.ts +1 -1
  191. package/src/types/events.d.ts +6 -2
  192. package/src/types/file-browser.d.ts +1 -2
  193. package/{types/types/observer.d.ts → src/types/history.d.ts} +11 -7
  194. package/src/types/index.d.ts +1 -1
  195. package/src/types/jodit.d.ts +12 -4
  196. package/src/types/toolbar.d.ts +5 -5
  197. package/src/types/types.d.ts +11 -4
  198. package/types/config.d.ts +68 -35
  199. package/types/core/async/async.d.ts +11 -4
  200. package/types/core/constants.d.ts +1 -0
  201. package/types/core/dom/dom.d.ts +3 -5
  202. package/types/core/dom/index.d.ts +1 -0
  203. package/types/core/dom/lazy-walker.d.ts +37 -0
  204. package/types/core/event-emitter/eventify.d.ts +39 -0
  205. package/types/core/event-emitter/index.d.ts +1 -0
  206. package/types/core/helpers/string/fuzzy-search-index.d.ts +10 -0
  207. package/types/core/helpers/string/i18n.d.ts +1 -1
  208. package/types/core/helpers/string/index.d.ts +3 -2
  209. package/types/core/helpers/utils/utils.d.ts +1 -1
  210. package/types/core/selection/select.d.ts +1 -1
  211. package/types/core/ui/button/button/button.d.ts +4 -4
  212. package/types/core/view/view.d.ts +1 -1
  213. package/types/jodit.d.ts +19 -6
  214. package/types/modules/{observer → history}/command.d.ts +4 -4
  215. package/types/modules/{observer/observer.d.ts → history/history.d.ts} +17 -9
  216. package/types/modules/{observer → history}/snapshot.d.ts +1 -1
  217. package/types/modules/{observer → history}/stack.d.ts +3 -3
  218. package/types/modules/image-editor/image-editor.d.ts +1 -1
  219. package/types/modules/index.d.ts +3 -3
  220. package/types/modules/toolbar/button/button.d.ts +2 -5
  221. package/types/modules/widget/tabs/tabs.d.ts +1 -1
  222. package/types/plugins/class-span/class-span.d.ts +1 -1
  223. package/types/plugins/clipboard/paste/config.d.ts +8 -0
  224. package/types/plugins/clipboard/paste/helpers.d.ts +2 -2
  225. package/types/plugins/clipboard/paste/interface.d.ts +5 -0
  226. package/types/plugins/clipboard/paste-from-word/config.d.ts +5 -0
  227. package/types/plugins/clipboard/paste-from-word/paste-from-word.d.ts +3 -2
  228. package/types/plugins/fix/clean-html/clean-html.d.ts +70 -0
  229. package/types/plugins/fix/{clean-html.d.ts → clean-html/config.d.ts} +2 -57
  230. package/types/plugins/fix/index.d.ts +10 -0
  231. package/types/plugins/fix/wrap-nodes/config.d.ts +16 -0
  232. package/types/plugins/fix/{wrap-text-nodes.d.ts → wrap-nodes/wrap-nodes.d.ts} +5 -2
  233. package/types/plugins/fullsize/fullsize.d.ts +2 -2
  234. package/types/plugins/index.d.ts +2 -2
  235. package/types/plugins/ordered-list/config.d.ts +6 -0
  236. package/types/plugins/ordered-list/ordered-list.d.ts +1 -1
  237. package/types/plugins/print/helpers.d.ts +2 -2
  238. package/types/plugins/print/index.d.ts +1 -1
  239. package/types/plugins/print/{preview.d.ts → preview/preview.d.ts} +1 -1
  240. package/types/plugins/search/config.d.ts +36 -0
  241. package/types/plugins/search/helpers/index.d.ts +10 -0
  242. package/types/plugins/search/helpers/sentence-finder.d.ts +21 -0
  243. package/types/plugins/search/helpers/wrap-ranges-texts-in-tmp-span.d.ts +14 -0
  244. package/types/plugins/search/search.d.ts +25 -39
  245. package/types/plugins/search/ui/search.d.ts +37 -0
  246. package/types/plugins/spellcheck/config.d.ts +15 -0
  247. package/types/plugins/spellcheck/spellcheck.d.ts +19 -0
  248. package/types/plugins/sticky/sticky.d.ts +2 -2
  249. package/types/types/async.d.ts +12 -2
  250. package/types/types/core.d.ts +1 -1
  251. package/types/types/core.ts +1 -1
  252. package/types/types/events.d.ts +6 -2
  253. package/types/types/file-browser.d.ts +1 -2
  254. package/{src/types/observer.d.ts → types/types/history.d.ts} +11 -7
  255. package/types/types/index.d.ts +1 -1
  256. package/types/types/jodit.d.ts +12 -4
  257. package/types/types/toolbar.d.ts +5 -5
  258. package/types/types/types.d.ts +11 -4
  259. package/src/modules/observer/README.md +0 -0
  260. package/src/plugins/search/search.less +0 -152
@@ -5,74 +5,34 @@
5
5
  */
6
6
 
7
7
  /**
8
+ * [[include:plugins/search/README.md]]
9
+ * @packageDocumentation
8
10
  * @module plugins/search
9
11
  */
10
12
 
11
- import './search.less';
12
-
13
13
  import type {
14
14
  ISelectionRange,
15
- MarkerInfo,
16
15
  IJodit,
17
16
  Nullable,
18
- IControlType,
19
- IPlugin
17
+ IPlugin,
18
+ IDictionary,
19
+ CanUndef,
20
+ RejectablePromise
20
21
  } from 'jodit/types';
21
- import { Config } from 'jodit/config';
22
- import * as consts from 'jodit/core/constants';
23
- import { MODE_WYSIWYG } from 'jodit/core/constants';
24
- import { Dom } from 'jodit/core/dom';
22
+ import { Dom, LazyWalker } from 'jodit/core/dom';
25
23
  import { Plugin } from 'jodit/core/plugin';
26
- import { Icon } from 'jodit/core/ui';
27
- import { css, position, refs, trim } from 'jodit/core/helpers';
28
- import { autobind } from 'jodit/core/decorators';
29
-
30
- declare module 'jodit/config' {
31
- interface Config {
32
- /**
33
- * Enable custom search plugin
34
- * ![search](https://user-images.githubusercontent.com/794318/34545433-cd0a9220-f10e-11e7-8d26-7e22f66e266d.gif)
35
- */
36
- useSearch: boolean;
37
- // searchByInput: boolean,
38
- }
39
- }
40
-
41
- Config.prototype.useSearch = true;
42
-
43
- Config.prototype.controls.find = {
44
- tooltip: 'Find',
45
- icon: 'search',
46
- exec(jodit: IJodit, _, { control }) {
47
- const value = control.args && control.args[0];
48
-
49
- switch (value) {
50
- case 'findPrevious':
51
- jodit.e.fire('searchPrevious');
52
- break;
53
-
54
- case 'findNext':
55
- jodit.e.fire('searchNext');
56
- break;
57
-
58
- case 'replace':
59
- jodit.execCommand('openReplaceDialog');
60
- break;
61
-
62
- default:
63
- jodit.execCommand('openSearchDialog');
64
- }
65
- },
66
-
67
- list: {
68
- search: 'Find',
69
- findNext: 'Find Next',
70
- findPrevious: 'Find Previous',
71
- replace: 'Replace'
72
- },
73
-
74
- childTemplate: (_, k, v) => v
75
- } as IControlType;
24
+ import { autobind, cache, watch } from 'jodit/core/decorators';
25
+ import {
26
+ clearSelectionWrappers,
27
+ clearSelectionWrappersFromHTML,
28
+ getSelectionWrappers,
29
+ SentenceFinder,
30
+ wrapRangesTextsInTmpSpan
31
+ } from 'jodit/plugins/search/helpers';
32
+ import { UISearch } from 'jodit/plugins/search/ui/search';
33
+
34
+ import './config';
35
+ import { scrollIntoViewIfNeeded } from 'jodit/core/helpers';
76
36
 
77
37
  /**
78
38
  * Search plugin. it is used for custom search in text
@@ -80,11 +40,11 @@ Config.prototype.controls.find = {
80
40
  *
81
41
  * @example
82
42
  * ```typescript
83
- * var jodit = new Jodit('#editor', {
43
+ * var jodit = Jodit.make('#editor', {
84
44
  * useSearch: false
85
45
  * });
86
46
  * // or
87
- * var jodit = new Jodit('#editor', {
47
+ * var jodit = Jodit.make('#editor', {
88
48
  * disablePlugins: 'search'
89
49
  * });
90
50
  * ```
@@ -97,159 +57,27 @@ export class search extends Plugin {
97
57
  }
98
58
  ];
99
59
 
100
- static getSomePartOfStringIndex(
101
- needle: string,
102
- haystack: string,
103
- start: boolean = true
104
- ): number | false {
105
- return this.findSomePartOfString(needle, haystack, start, true) as
106
- | number
107
- | false;
108
- }
109
-
110
- static findSomePartOfString(
111
- needle: string,
112
- haystack: string,
113
- start: boolean = true,
114
- getIndex: boolean = false
115
- ): boolean | string | number {
116
- needle = trim(
117
- needle.toLowerCase().replace(consts.SPACE_REG_EXP(), ' ')
118
- );
119
- haystack = haystack.toLowerCase();
120
-
121
- let i: number = start ? 0 : haystack.length - 1,
122
- needleStart: number = start ? 0 : needle.length - 1,
123
- tmpEqualLength: number = 0,
124
- startAtIndex: number | null = null;
125
-
126
- const inc = start ? 1 : -1,
127
- tmp: string[] = [];
128
-
129
- for (; haystack[i] !== undefined; i += inc) {
130
- const some: boolean = needle[needleStart] === haystack[i];
131
- if (
132
- some ||
133
- (startAtIndex != null &&
134
- consts.SPACE_REG_EXP().test(haystack[i]))
135
- ) {
136
- if (startAtIndex == null || !start) {
137
- startAtIndex = i;
138
- }
139
-
140
- tmp.push(haystack[i]);
141
-
142
- if (some) {
143
- tmpEqualLength += 1;
144
- needleStart += inc;
145
- }
146
- } else {
147
- startAtIndex = null;
148
- tmp.length = 0;
149
- tmpEqualLength = 0;
150
- needleStart = start ? 0 : needle.length - 1;
151
- }
152
-
153
- if (tmpEqualLength === needle.length) {
154
- return getIndex ? (startAtIndex as number) : true;
155
- }
156
- }
157
-
158
- if (getIndex) {
159
- return startAtIndex ?? false;
160
- }
161
-
162
- if (tmp.length) {
163
- return start ? tmp.join('') : tmp.reverse().join('');
164
- }
165
-
166
- return false;
60
+ @cache
61
+ private get ui(): UISearch {
62
+ return new UISearch(this.j);
167
63
  }
168
64
 
169
- private template = `<div class="jodit-search">
170
- <div class="jodit-search__box">
171
- <div class="jodit-search__inputs">
172
- <input data-ref="query" tabindex="0" placeholder="${this.j.i18n(
173
- 'Search for'
174
- )}" type="text"/>
175
- <input data-ref="replace" tabindex="0" placeholder="${this.j.i18n(
176
- 'Replace with'
177
- )}" type="text"/>
178
- </div>
179
- <div class="jodit-search__counts">
180
- <span data-ref="counter-box">0/0</span>
181
- </div>
182
- <div class="jodit-search__buttons">
183
- <button data-ref="next" tabindex="0" type="button">${Icon.get(
184
- 'angle-down'
185
- )}</button>
186
- <button data-ref="prev" tabindex="0" type="button">${Icon.get(
187
- 'angle-up'
188
- )}</button>
189
- <button data-ref="cancel" tabindex="0" type="button">${Icon.get(
190
- 'cancel'
191
- )}</button>
192
- <button data-ref="replace-btn" tabindex="0" type="button" class="jodit-ui-button">${this.j.i18n(
193
- 'Replace'
194
- )}</button>
195
- </div>
196
- </div>
197
- </div>`;
198
-
199
- private isOpened: boolean = false;
200
-
201
- private selInfo: Nullable<MarkerInfo[]> = null;
202
- private current: Nullable<Node> = null;
203
-
204
- private eachMap = (
205
- node: Node,
206
- callback: (elm: Node) => boolean,
207
- next: boolean
208
- ) => {
209
- Dom.findWithCurrent(
210
- node,
211
- (child: Node | null): boolean => {
212
- return Boolean(child && callback(child));
213
- },
214
- this.j.editor,
215
- next ? 'nextSibling' : 'previousSibling',
216
- next ? 'firstChild' : 'lastChild'
217
- );
218
- };
219
-
220
- private updateCounters = () => {
221
- if (!this.isOpened) {
65
+ @watch('ui:needUpdateCounters')
66
+ private async updateCounters(): Promise<void> {
67
+ if (!this.ui.isOpened) {
222
68
  return;
223
69
  }
224
70
 
225
- this.counterBox.style.display = this.queryInput.value.length
226
- ? 'inline-block'
227
- : 'none';
71
+ this.ui.count = await this.calcCounts(this.ui.query);
72
+ }
228
73
 
229
- const range = this.j.s.range,
230
- counts: [number, number] = this.calcCounts(
231
- this.queryInput.value,
232
- range
233
- );
234
-
235
- this.counterBox.textContent = counts.join('/');
236
- };
237
-
238
- private boundAlreadyWas(
239
- current: ISelectionRange,
240
- bounds: ISelectionRange[]
241
- ): boolean {
242
- return bounds.some((bound: ISelectionRange) => {
243
- return (
244
- bound.startContainer === current.startContainer &&
245
- bound.endContainer === current.endContainer &&
246
- bound.startOffset === current.startOffset &&
247
- bound.endOffset === current.endOffset
248
- );
249
- }, false);
74
+ @watch('ui:pressReplaceButton')
75
+ protected onPressReplaceButton(): void {
76
+ this.findAndReplace(this.ui.query);
77
+ this.updateCounters();
250
78
  }
251
79
 
252
- private tryScrollToElement(startContainer: Node) {
80
+ private tryScrollToElement(startContainer: Node): void {
253
81
  // find scrollable element
254
82
  let parentBox: HTMLElement | false = Dom.closest(
255
83
  startContainer,
@@ -265,449 +93,302 @@ export class search extends Plugin {
265
93
  ) as HTMLElement | false;
266
94
  }
267
95
 
268
- parentBox && parentBox !== this.j.editor && parentBox.scrollIntoView();
96
+ parentBox &&
97
+ parentBox !== this.j.editor &&
98
+ scrollIntoViewIfNeeded(parentBox, this.j.editor, this.j.ed);
269
99
  }
270
100
 
271
- searchBox!: HTMLDivElement;
272
- queryInput!: HTMLInputElement;
273
- replaceInput!: HTMLInputElement;
274
- closeButton!: HTMLButtonElement;
275
- nextButton!: HTMLButtonElement;
276
- prevButton!: HTMLButtonElement;
277
- replaceButton!: HTMLButtonElement;
278
- counterBox!: HTMLSpanElement;
279
-
280
- calcCounts = (
281
- query: string,
282
- current: ISelectionRange | false = false
283
- ): [number, number] => {
284
- const bounds: ISelectionRange[] = [];
285
-
286
- let currentIndex: number = 0,
287
- count: number = 0,
288
- bound: ISelectionRange | false = false,
289
- start: Node | null = this.j.editor.firstChild;
290
-
291
- while (start && query.length) {
292
- bound = this.find(
293
- start,
294
- query,
295
- true,
296
- 0,
297
- (bound as Range) || this.j.ed.createRange()
298
- );
299
- if (bound) {
300
- if (this.boundAlreadyWas(bound, bounds)) {
301
- break;
302
- }
303
- bounds.push(bound);
304
- start = bound.startContainer;
305
- count += 1;
306
- if (current && this.boundAlreadyWas(current, [bound])) {
307
- currentIndex = count;
308
- }
309
- } else {
310
- start = null;
311
- }
101
+ protected async calcCounts(query: string): Promise<number> {
102
+ if (this.walkerCount) {
103
+ this.walkerCount.break();
312
104
  }
313
105
 
314
- return [currentIndex, count];
315
- };
316
-
317
- findAndReplace = (start: Node | null, query: string): boolean => {
318
- const range = this.j.s.range,
319
- bound: ISelectionRange | false = this.find(
320
- start,
321
- query,
322
- true,
323
- 0,
324
- range
325
- );
326
-
327
- if (bound && bound.startContainer && bound.endContainer) {
328
- const rng = this.j.ed.createRange();
106
+ this.walkerCount = new LazyWalker(this.j.async, {
107
+ timeout: this.j.o.search.lazyIdleTimeout
108
+ });
329
109
 
330
- try {
331
- if (bound && bound.startContainer && bound.endContainer) {
332
- rng.setStart(
333
- bound.startContainer,
334
- bound.startOffset as number
335
- );
336
-
337
- rng.setEnd(bound.endContainer, bound.endOffset as number);
338
- rng.deleteContents();
339
-
340
- const textNode: Node = this.j.createInside.text(
341
- this.replaceInput.value
342
- );
343
-
344
- rng.insertNode(textNode);
345
- this.j.s.select(textNode);
346
- this.tryScrollToElement(textNode);
347
- }
348
- } catch {}
110
+ const result = await this.find(this.walkerCount, query);
111
+ return result.length;
112
+ }
349
113
 
350
- return true;
114
+ @autobind
115
+ async findAndReplace(query: string): Promise<boolean> {
116
+ if (this.walker) {
117
+ this.walker.break();
351
118
  }
352
119
 
353
- return false;
354
- };
120
+ this.walker = new LazyWalker(this.j.async, {
121
+ timeout: this.j.o.search.lazyIdleTimeout
122
+ });
355
123
 
356
- findAndSelect = (
357
- start: Node | null,
358
- query: string,
359
- next: boolean
360
- ): boolean => {
361
124
  const range = this.j.s.range,
362
- bound: ISelectionRange | false = this.find(
363
- start,
364
- query,
365
- next,
366
- 0,
367
- range
368
- );
125
+ bounds = await this.find(this.walker, query);
369
126
 
370
- if (bound && bound.startContainer && bound.endContainer) {
371
- const rng: Range = this.j.ed.createRange();
127
+ let currentIndex = this.findCurrentIndexInRanges(bounds, range);
372
128
 
129
+ if (currentIndex === -1) {
130
+ currentIndex = 0;
131
+ }
132
+
133
+ const bound = bounds[currentIndex];
134
+
135
+ if (bound) {
373
136
  try {
374
- rng.setStart(bound.startContainer, bound.startOffset as number);
375
- rng.setEnd(bound.endContainer, bound.endOffset as number);
376
- this.j.s.selectRange(rng);
377
- } catch (e) {}
137
+ const rng = this.j.ed.createRange();
378
138
 
379
- this.tryScrollToElement(bound.startContainer);
139
+ rng.setStart(bound.startContainer, bound.startOffset);
140
+ rng.setEnd(bound.endContainer, bound.endOffset);
141
+ rng.deleteContents();
142
+
143
+ const textNode = this.j.createInside.text(this.ui.replace);
144
+
145
+ rng.insertNode(textNode);
146
+ this.j.s.select(textNode);
147
+ this.tryScrollToElement(textNode);
148
+ this.cache = {};
149
+ this.j.synchronizeValues();
150
+ } catch {}
380
151
 
381
- this.current = bound.startContainer;
382
- this.updateCounters();
152
+ this.j.e.fire('afterFindAndReplace');
383
153
 
384
154
  return true;
385
155
  }
386
156
 
387
157
  return false;
388
- };
158
+ }
389
159
 
390
- find = (
391
- start: Node | null,
392
- query: string,
393
- next: boolean,
394
- deep: number,
395
- range: Range
396
- ): false | ISelectionRange => {
397
- if (start && query.length) {
398
- let sentence: string = '',
399
- bound: ISelectionRange = {
400
- startContainer: null,
401
- startOffset: null,
402
- endContainer: null,
403
- endOffset: null
404
- };
405
-
406
- this.eachMap(
407
- start,
408
- (elm: Node): boolean => {
409
- if (
410
- Dom.isText(elm) &&
411
- elm.nodeValue != null &&
412
- elm.nodeValue.length
413
- ) {
414
- let value: string = elm.nodeValue;
415
-
416
- if (!next && elm === range.startContainer) {
417
- value = !deep
418
- ? value.substr(0, range.startOffset)
419
- : value.substr(range.endOffset);
420
- } else if (next && elm === range.endContainer) {
421
- value = !deep
422
- ? value.substr(range.endOffset)
423
- : value.substr(0, range.startOffset);
424
- }
160
+ private previousQuery: string = '';
161
+ private drawPromise: RejectablePromise<void> | null = null;
425
162
 
426
- const tmpSentence: string = next
427
- ? sentence + value
428
- : value + sentence;
429
-
430
- const part: boolean | string =
431
- search.findSomePartOfString(
432
- query,
433
- tmpSentence,
434
- next
435
- ) as boolean | string;
436
-
437
- if (part !== false) {
438
- let currentPart: string | boolean =
439
- search.findSomePartOfString(
440
- query,
441
- value,
442
- next
443
- ) as string | boolean;
444
-
445
- if (currentPart === true) {
446
- currentPart = trim(query);
447
- } else if (currentPart === false) {
448
- currentPart = search.findSomePartOfString(
449
- value,
450
- query,
451
- next
452
- ) as string | true;
453
- if (currentPart === true) {
454
- currentPart = trim(value);
455
- }
456
- }
457
-
458
- let currentPartIndex: number =
459
- search.getSomePartOfStringIndex(
460
- query,
461
- value,
462
- next
463
- ) || 0;
464
-
465
- if (
466
- ((next && !deep) || (!next && deep)) &&
467
- elm.nodeValue.length - value.length > 0
468
- ) {
469
- currentPartIndex +=
470
- elm.nodeValue.length - value.length;
471
- }
472
-
473
- if (bound.startContainer == null) {
474
- bound.startContainer = elm;
475
- bound.startOffset = currentPartIndex;
476
- }
477
- if (part !== true) {
478
- sentence = tmpSentence;
479
- } else {
480
- bound.endContainer = elm;
481
- bound.endOffset = currentPartIndex;
482
- bound.endOffset += (
483
- currentPart as string
484
- ).length;
485
-
486
- return true;
487
- }
488
- } else {
489
- sentence = '';
490
- bound = {
491
- startContainer: null,
492
- startOffset: null,
493
- endContainer: null,
494
- endOffset: null
495
- };
496
- }
497
- } else if (Dom.isBlock(elm) && sentence !== '') {
498
- sentence = next ? sentence + ' ' : ' ' + sentence;
499
- }
163
+ @autobind
164
+ async findAndSelect(query: string, next: boolean): Promise<boolean> {
165
+ if (this.walker) {
166
+ this.walker.break();
167
+ }
500
168
 
501
- return false;
502
- },
503
- next
504
- );
505
-
506
- if (bound.startContainer && bound.endContainer) {
507
- return bound;
508
- }
509
-
510
- if (!deep) {
511
- this.current = next
512
- ? (this.j.editor.firstChild as Node)
513
- : (this.j.editor.lastChild as Node);
514
- return this.find(this.current, query, next, deep + 1, range);
515
- }
169
+ this.walker = new LazyWalker(this.j.async, {
170
+ timeout: this.j.defaultTimeout
171
+ });
172
+
173
+ const bounds = await this.find(this.walker, query);
174
+
175
+ if (!bounds.length) {
176
+ return false;
516
177
  }
517
178
 
518
- return false;
519
- };
179
+ if (
180
+ this.previousQuery !== query ||
181
+ !getSelectionWrappers(this.j.editor).length
182
+ ) {
183
+ this.drawPromise?.rejectCallback();
184
+ this.j.async.cancelAnimationFrame(this.wrapFrameRequest);
185
+ clearSelectionWrappers(this.j.editor);
186
+ this.drawPromise = this.drawSelectionRanges(bounds);
187
+ }
520
188
 
521
- open = (searchAndReplace: boolean = false): void => {
522
- if (!this.isOpened) {
523
- this.searchBox.classList.add('jodit-search_active');
524
- this.isOpened = true;
189
+ this.previousQuery = query;
190
+
191
+ let currentIndex = this.ui.currentIndex - 1;
192
+
193
+ if (currentIndex === -1) {
194
+ currentIndex = 0;
195
+ } else if (next) {
196
+ currentIndex =
197
+ currentIndex === bounds.length - 1 ? 0 : currentIndex + 1;
198
+ } else {
199
+ currentIndex =
200
+ currentIndex === 0 ? bounds.length - 1 : currentIndex - 1;
525
201
  }
526
202
 
527
- this.calcSticky(this.j.e.fire('getStickyState.sticky') || false);
203
+ this.ui.currentIndex = currentIndex + 1;
528
204
 
529
- this.j.e.fire('hidePopup');
205
+ const bound = bounds[currentIndex];
530
206
 
531
- this.searchBox.classList.toggle(
532
- 'jodit-search_replace',
533
- searchAndReplace
534
- );
207
+ if (bound) {
208
+ const rng = this.j.ed.createRange();
535
209
 
536
- this.current = this.j.s.current();
210
+ try {
211
+ rng.setStart(bound.startContainer, bound.startOffset);
212
+ rng.setEnd(bound.endContainer, bound.endOffset);
213
+ this.j.s.selectRange(rng);
214
+ } catch (e) {}
537
215
 
538
- const selStr: string = (this.j.s.sel || '').toString();
216
+ this.tryScrollToElement(bound.startContainer);
539
217
 
540
- if (selStr) {
541
- this.queryInput.value = selStr;
218
+ await this.updateCounters();
219
+ await this.drawPromise;
220
+ this.j.e.fire('afterFindAndSelect');
221
+
222
+ return true;
542
223
  }
543
224
 
544
- this.updateCounters();
225
+ return false;
226
+ }
545
227
 
546
- if (selStr) {
547
- this.queryInput.select();
548
- } else {
549
- this.queryInput.focus();
550
- }
551
- };
228
+ private findCurrentIndexInRanges(
229
+ bounds: ISelectionRange[],
230
+ range: Range
231
+ ): number {
232
+ return bounds.findIndex(
233
+ bound =>
234
+ bound.startContainer === range.startContainer &&
235
+ bound.startOffset === range.startOffset &&
236
+ bound.endContainer === range.startContainer &&
237
+ bound.endOffset === range.endOffset
238
+ );
239
+ }
552
240
 
553
- close = (): void => {
554
- if (!this.isOpened) {
555
- return;
241
+ walker: Nullable<LazyWalker> = null;
242
+ walkerCount: Nullable<LazyWalker> = null;
243
+
244
+ private cache: IDictionary<CanUndef<Promise<ISelectionRange[]>>> = {};
245
+
246
+ private async isValidCache(
247
+ promise: Promise<ISelectionRange[]>
248
+ ): Promise<boolean> {
249
+ const res = await promise;
250
+ return res.every(
251
+ r =>
252
+ r.startContainer.isConnected &&
253
+ r.startOffset <= (r.startContainer.nodeValue?.length ?? 0) &&
254
+ r.endContainer.isConnected &&
255
+ r.endOffset <= (r.endContainer.nodeValue?.length ?? 0)
256
+ );
257
+ }
258
+
259
+ @autobind
260
+ private async find(
261
+ walker: LazyWalker,
262
+ query: string
263
+ ): Promise<ISelectionRange[]> {
264
+ if (!query.length) {
265
+ return [];
556
266
  }
557
267
 
558
- this.j.s.restore();
268
+ const cache = this.cache[query];
269
+ if (cache && (await this.isValidCache(cache))) {
270
+ return cache;
271
+ }
559
272
 
560
- this.searchBox.classList.remove('jodit-search_active');
561
- this.isOpened = false;
562
- };
273
+ const sentence = new SentenceFinder(this.j.o.search.fuzzySearch);
563
274
 
564
- /** @override */
565
- afterInit(editor: IJodit): void {
566
- if (editor.o.useSearch) {
567
- const self: search = this;
275
+ this.cache[query] = this.j.async.promise(resolve => {
276
+ walker
277
+ .on('break', (): void => {
278
+ resolve([]);
279
+ })
280
+ .on('visit', (elm: Node): boolean => {
281
+ if (Dom.isText(elm)) {
282
+ sentence.add(elm);
283
+ }
568
284
 
569
- self.searchBox = editor.c.fromHTML(self.template) as HTMLDivElement;
285
+ return false;
286
+ })
287
+ .on('end', (): void => {
288
+ resolve(sentence.ranges(query) ?? []);
289
+ })
290
+ .setWork(this.j.editor);
291
+ });
570
292
 
571
- const {
572
- query,
573
- replace,
574
- cancel,
575
- next,
576
- prev,
577
- replaceBtn,
578
- counterBox
579
- } = refs(self.searchBox);
293
+ return this.cache[query] as Promise<ISelectionRange[]>;
294
+ }
580
295
 
581
- self.queryInput = query as HTMLInputElement;
296
+ private wrapFrameRequest: number = 0;
582
297
 
583
- self.replaceInput = replace as HTMLInputElement;
298
+ private drawSelectionRanges(
299
+ ranges: ISelectionRange[]
300
+ ): RejectablePromise<void> {
301
+ const { async, createInside: ci, editor } = this.j;
584
302
 
585
- self.closeButton = cancel as HTMLButtonElement;
303
+ async.cancelAnimationFrame(this.wrapFrameRequest);
586
304
 
587
- self.nextButton = next as HTMLButtonElement;
305
+ const parts = [...ranges];
588
306
 
589
- self.prevButton = prev as HTMLButtonElement;
307
+ let sRange: CanUndef<ISelectionRange>,
308
+ total = 0;
590
309
 
591
- self.replaceButton = replaceBtn as HTMLButtonElement;
310
+ return async.promise(resolve => {
311
+ const drawParts = (): void => {
312
+ do {
313
+ sRange = parts.shift();
592
314
 
593
- self.counterBox = counterBox as HTMLButtonElement;
315
+ if (sRange) {
316
+ wrapRangesTextsInTmpSpan(sRange, parts, ci, editor);
317
+ }
594
318
 
595
- const onInit = () => {
596
- editor.workplace.appendChild(this.searchBox);
319
+ total += 1;
320
+ } while (sRange && total <= 5);
597
321
 
598
- editor.e
599
- .off(this.j.container, 'keydown.search')
600
- .on(
601
- this.j.container,
602
- 'keydown.search',
603
- (e: KeyboardEvent) => {
604
- if (editor.getRealMode() !== MODE_WYSIWYG) {
605
- return;
606
- }
322
+ if (parts.length) {
323
+ this.wrapFrameRequest =
324
+ async.requestAnimationFrame(drawParts);
325
+ } else {
326
+ resolve();
327
+ }
328
+ };
607
329
 
608
- switch (e.key) {
609
- case consts.KEY_ESC:
610
- this.close();
611
- break;
330
+ drawParts();
331
+ });
332
+ }
612
333
 
613
- case consts.KEY_F3:
614
- if (self.queryInput.value) {
615
- editor.e.fire(
616
- !e.shiftKey
617
- ? 'searchNext'
618
- : 'searchPrevious'
619
- );
334
+ @watch(':afterGetValueFromEditor')
335
+ protected onAfterGetValueFromEditor(data: { value: string }): void {
336
+ data.value = clearSelectionWrappersFromHTML(data.value);
337
+ }
620
338
 
621
- e.preventDefault();
622
- }
623
- break;
624
- }
625
- }
626
- );
627
- };
628
- onInit();
339
+ /** @override */
340
+ afterInit(editor: IJodit): void {
341
+ if (editor.o.useSearch) {
342
+ const self: search = this;
629
343
 
630
344
  editor.e
631
- .on('changePlace', onInit)
632
- .on(self.closeButton, 'click', this.close)
633
- .on(self.queryInput, 'mousedown', () => {
634
- if (editor.s.isFocused()) {
635
- editor.s.removeMarkers();
636
- self.selInfo = editor.s.save();
637
- }
345
+ .on('beforeSetMode.search', () => {
346
+ this.ui.close();
638
347
  })
639
- .on(self.replaceButton, 'click', (e: MouseEvent) => {
640
- self.findAndReplace(
641
- editor.s.current() || editor.editor.firstChild,
642
- self.queryInput.value
643
- );
644
-
645
- this.updateCounters();
646
-
647
- e.preventDefault();
648
- e.stopImmediatePropagation();
348
+ .on(this.ui, 'afterClose', () => {
349
+ clearSelectionWrappers(editor.editor);
350
+ this.ui.currentIndex = 0;
351
+ this.ui.count = 0;
352
+ this.cache = {};
353
+ })
354
+ .on('click', () => {
355
+ this.ui.currentIndex = 0;
356
+ clearSelectionWrappers(editor.editor);
357
+ })
358
+ .on('change.search', () => {
359
+ this.cache = {};
649
360
  })
650
361
  .on(
651
- [self.nextButton, self.prevButton],
652
- 'click',
653
- function (this: HTMLButtonElement, e: MouseEvent) {
654
- editor.e.fire(
655
- self.nextButton === this
656
- ? 'searchNext'
657
- : 'searchPrevious'
658
- );
659
- e.preventDefault();
660
- e.stopImmediatePropagation();
661
- }
662
- )
663
- .on(
664
- this.queryInput,
665
- 'keydown',
666
- this.j.async.debounce((e: KeyboardEvent) => {
667
- switch (e.key) {
668
- case consts.KEY_ENTER:
669
- e.preventDefault();
670
- e.stopImmediatePropagation();
671
- if (editor.e.fire('searchNext')) {
672
- this.close();
673
- }
674
-
675
- break;
676
-
677
- default:
678
- this.updateCounters();
679
- break;
362
+ 'keydown.search mousedown.search',
363
+ editor.async.debounce(() => {
364
+ if (this.ui.selInfo) {
365
+ editor.s.removeMarkers();
366
+ this.ui.selInfo = null;
680
367
  }
681
- }, this.j.defaultTimeout)
368
+
369
+ if (this.ui.isOpened) {
370
+ this.updateCounters();
371
+ }
372
+ }, editor.defaultTimeout)
682
373
  )
683
- .on('beforeSetMode.search', () => {
684
- this.close();
685
- })
686
- .on('keydown.search mousedown.search', () => {
687
- if (this.selInfo) {
688
- editor.s.removeMarkers();
689
- this.selInfo = null;
690
- }
691
- if (this.isOpened) {
692
- this.current = this.j.s.current();
693
- this.updateCounters();
694
- }
695
- })
696
374
  .on('searchNext.search searchPrevious.search', () => {
697
- if (!self.isOpened) {
698
- return self.open();
375
+ if (!this.ui.isOpened) {
376
+ this.ui.open();
699
377
  }
700
378
 
701
- return self.findAndSelect(
702
- editor.s.current() || editor.editor.firstChild,
703
- self.queryInput.value,
704
- editor.e.current === 'searchNext'
705
- );
379
+ return self
380
+ .findAndSelect(
381
+ self.ui.query,
382
+ editor.e.current === 'searchNext'
383
+ )
384
+ .catch(() => {});
706
385
  })
707
386
  .on('search.search', (value: string, next: boolean = true) => {
708
- editor.execCommand('search', value, next);
709
- })
710
- .on('toggleSticky.search', this.calcSticky);
387
+ this.ui.currentIndex = 0;
388
+ return self
389
+ .findAndSelect(value || '', next)
390
+ .catch(() => {});
391
+ });
711
392
 
712
393
  editor
713
394
  .registerCommand('search', {
@@ -716,18 +397,15 @@ export class search extends Plugin {
716
397
  value?: string,
717
398
  next: boolean = true
718
399
  ) => {
719
- self.findAndSelect(
720
- editor.s.current() || editor.editor.firstChild,
721
- value || '',
722
- next
723
- );
400
+ value &&
401
+ self.findAndSelect(value, next).catch(() => {});
724
402
 
725
403
  return false;
726
404
  }
727
405
  })
728
406
  .registerCommand('openSearchDialog', {
729
407
  exec: () => {
730
- self.open();
408
+ self.ui.open();
731
409
  return false;
732
410
  },
733
411
  hotkeys: ['ctrl+f', 'cmd+f']
@@ -735,7 +413,7 @@ export class search extends Plugin {
735
413
  .registerCommand('openReplaceDialog', {
736
414
  exec: () => {
737
415
  if (!editor.o.readonly) {
738
- self.open(true);
416
+ self.ui.open(true);
739
417
  }
740
418
  return false;
741
419
  },
@@ -746,31 +424,7 @@ export class search extends Plugin {
746
424
 
747
425
  /** @override */
748
426
  beforeDestruct(jodit: IJodit): void {
749
- Dom.safeRemove(this.searchBox);
750
- jodit.events?.off('.search');
751
- }
752
-
753
- /**
754
- * Calculate position if sticky is enabled
755
- */
756
- @autobind
757
- private calcSticky(enabled: boolean): void {
758
- if (this.isOpened) {
759
- this.searchBox.classList.toggle('jodit-search_sticky', enabled);
760
-
761
- if (enabled) {
762
- const pos = position(this.j.toolbarContainer);
763
-
764
- css(this.searchBox, {
765
- top: pos.top + pos.height,
766
- left: pos.left + pos.width
767
- });
768
- } else {
769
- css(this.searchBox, {
770
- top: null,
771
- left: null
772
- });
773
- }
774
- }
427
+ this.ui.destruct();
428
+ jodit.e.off('.search');
775
429
  }
776
430
  }