plugin.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080
  1. /**
  2. * TinyMCE version 6.0.3 (2022-05-25)
  3. */
  4. (function () {
  5. 'use strict';
  6. const Cell = initial => {
  7. let value = initial;
  8. const get = () => {
  9. return value;
  10. };
  11. const set = v => {
  12. value = v;
  13. };
  14. return {
  15. get,
  16. set
  17. };
  18. };
  19. var global$3 = tinymce.util.Tools.resolve('tinymce.PluginManager');
  20. const hasProto = (v, constructor, predicate) => {
  21. var _a;
  22. if (predicate(v, constructor.prototype)) {
  23. return true;
  24. } else {
  25. return ((_a = v.constructor) === null || _a === void 0 ? void 0 : _a.name) === constructor.name;
  26. }
  27. };
  28. const typeOf = x => {
  29. const t = typeof x;
  30. if (x === null) {
  31. return 'null';
  32. } else if (t === 'object' && Array.isArray(x)) {
  33. return 'array';
  34. } else if (t === 'object' && hasProto(x, String, (o, proto) => proto.isPrototypeOf(o))) {
  35. return 'string';
  36. } else {
  37. return t;
  38. }
  39. };
  40. const isType$1 = type => value => typeOf(value) === type;
  41. const isSimpleType = type => value => typeof value === type;
  42. const isString = isType$1('string');
  43. const isArray = isType$1('array');
  44. const isBoolean = isSimpleType('boolean');
  45. const isNullable = a => a === null || a === undefined;
  46. const isNonNullable = a => !isNullable(a);
  47. const isNumber = isSimpleType('number');
  48. const noop = () => {
  49. };
  50. const constant = value => {
  51. return () => {
  52. return value;
  53. };
  54. };
  55. const always = constant(true);
  56. const punctuationStr = '[!-#%-*,-\\/:;?@\\[-\\]_{}\xA1\xAB\xB7\xBB\xBF;\xB7\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1361-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u3008\u3009\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30\u2E31\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uff3f\uFF5B\uFF5D\uFF5F-\uFF65]';
  57. const punctuation$1 = constant(punctuationStr);
  58. class Optional {
  59. constructor(tag, value) {
  60. this.tag = tag;
  61. this.value = value;
  62. }
  63. static some(value) {
  64. return new Optional(true, value);
  65. }
  66. static none() {
  67. return Optional.singletonNone;
  68. }
  69. fold(onNone, onSome) {
  70. if (this.tag) {
  71. return onSome(this.value);
  72. } else {
  73. return onNone();
  74. }
  75. }
  76. isSome() {
  77. return this.tag;
  78. }
  79. isNone() {
  80. return !this.tag;
  81. }
  82. map(mapper) {
  83. if (this.tag) {
  84. return Optional.some(mapper(this.value));
  85. } else {
  86. return Optional.none();
  87. }
  88. }
  89. bind(binder) {
  90. if (this.tag) {
  91. return binder(this.value);
  92. } else {
  93. return Optional.none();
  94. }
  95. }
  96. exists(predicate) {
  97. return this.tag && predicate(this.value);
  98. }
  99. forall(predicate) {
  100. return !this.tag || predicate(this.value);
  101. }
  102. filter(predicate) {
  103. if (!this.tag || predicate(this.value)) {
  104. return this;
  105. } else {
  106. return Optional.none();
  107. }
  108. }
  109. getOr(replacement) {
  110. return this.tag ? this.value : replacement;
  111. }
  112. or(replacement) {
  113. return this.tag ? this : replacement;
  114. }
  115. getOrThunk(thunk) {
  116. return this.tag ? this.value : thunk();
  117. }
  118. orThunk(thunk) {
  119. return this.tag ? this : thunk();
  120. }
  121. getOrDie(message) {
  122. if (!this.tag) {
  123. throw new Error(message !== null && message !== void 0 ? message : 'Called getOrDie on None');
  124. } else {
  125. return this.value;
  126. }
  127. }
  128. static from(value) {
  129. return isNonNullable(value) ? Optional.some(value) : Optional.none();
  130. }
  131. getOrNull() {
  132. return this.tag ? this.value : null;
  133. }
  134. getOrUndefined() {
  135. return this.value;
  136. }
  137. each(worker) {
  138. if (this.tag) {
  139. worker(this.value);
  140. }
  141. }
  142. toArray() {
  143. return this.tag ? [this.value] : [];
  144. }
  145. toString() {
  146. return this.tag ? `some(${ this.value })` : 'none()';
  147. }
  148. }
  149. Optional.singletonNone = new Optional(false);
  150. const punctuation = punctuation$1;
  151. var global$2 = tinymce.util.Tools.resolve('tinymce.Env');
  152. var global$1 = tinymce.util.Tools.resolve('tinymce.util.Tools');
  153. const nativeSlice = Array.prototype.slice;
  154. const nativePush = Array.prototype.push;
  155. const map = (xs, f) => {
  156. const len = xs.length;
  157. const r = new Array(len);
  158. for (let i = 0; i < len; i++) {
  159. const x = xs[i];
  160. r[i] = f(x, i);
  161. }
  162. return r;
  163. };
  164. const each = (xs, f) => {
  165. for (let i = 0, len = xs.length; i < len; i++) {
  166. const x = xs[i];
  167. f(x, i);
  168. }
  169. };
  170. const eachr = (xs, f) => {
  171. for (let i = xs.length - 1; i >= 0; i--) {
  172. const x = xs[i];
  173. f(x, i);
  174. }
  175. };
  176. const groupBy = (xs, f) => {
  177. if (xs.length === 0) {
  178. return [];
  179. } else {
  180. let wasType = f(xs[0]);
  181. const r = [];
  182. let group = [];
  183. for (let i = 0, len = xs.length; i < len; i++) {
  184. const x = xs[i];
  185. const type = f(x);
  186. if (type !== wasType) {
  187. r.push(group);
  188. group = [];
  189. }
  190. wasType = type;
  191. group.push(x);
  192. }
  193. if (group.length !== 0) {
  194. r.push(group);
  195. }
  196. return r;
  197. }
  198. };
  199. const foldl = (xs, f, acc) => {
  200. each(xs, (x, i) => {
  201. acc = f(acc, x, i);
  202. });
  203. return acc;
  204. };
  205. const flatten = xs => {
  206. const r = [];
  207. for (let i = 0, len = xs.length; i < len; ++i) {
  208. if (!isArray(xs[i])) {
  209. throw new Error('Arr.flatten item ' + i + ' was not an array, input: ' + xs);
  210. }
  211. nativePush.apply(r, xs[i]);
  212. }
  213. return r;
  214. };
  215. const bind = (xs, f) => flatten(map(xs, f));
  216. const sort = (xs, comparator) => {
  217. const copy = nativeSlice.call(xs, 0);
  218. copy.sort(comparator);
  219. return copy;
  220. };
  221. const hasOwnProperty = Object.hasOwnProperty;
  222. const has = (obj, key) => hasOwnProperty.call(obj, key);
  223. typeof window !== 'undefined' ? window : Function('return this;')();
  224. const DOCUMENT = 9;
  225. const DOCUMENT_FRAGMENT = 11;
  226. const ELEMENT = 1;
  227. const TEXT = 3;
  228. const type = element => element.dom.nodeType;
  229. const isType = t => element => type(element) === t;
  230. const isText$1 = isType(TEXT);
  231. const rawSet = (dom, key, value) => {
  232. if (isString(value) || isBoolean(value) || isNumber(value)) {
  233. dom.setAttribute(key, value + '');
  234. } else {
  235. console.error('Invalid call to Attribute.set. Key ', key, ':: Value ', value, ':: Element ', dom);
  236. throw new Error('Attribute value was not simple');
  237. }
  238. };
  239. const set = (element, key, value) => {
  240. rawSet(element.dom, key, value);
  241. };
  242. const fromHtml = (html, scope) => {
  243. const doc = scope || document;
  244. const div = doc.createElement('div');
  245. div.innerHTML = html;
  246. if (!div.hasChildNodes() || div.childNodes.length > 1) {
  247. const message = 'HTML does not have a single root node';
  248. console.error(message, html);
  249. throw new Error(message);
  250. }
  251. return fromDom(div.childNodes[0]);
  252. };
  253. const fromTag = (tag, scope) => {
  254. const doc = scope || document;
  255. const node = doc.createElement(tag);
  256. return fromDom(node);
  257. };
  258. const fromText = (text, scope) => {
  259. const doc = scope || document;
  260. const node = doc.createTextNode(text);
  261. return fromDom(node);
  262. };
  263. const fromDom = node => {
  264. if (node === null || node === undefined) {
  265. throw new Error('Node cannot be null or undefined');
  266. }
  267. return { dom: node };
  268. };
  269. const fromPoint = (docElm, x, y) => Optional.from(docElm.dom.elementFromPoint(x, y)).map(fromDom);
  270. const SugarElement = {
  271. fromHtml,
  272. fromTag,
  273. fromText,
  274. fromDom,
  275. fromPoint
  276. };
  277. const bypassSelector = dom => dom.nodeType !== ELEMENT && dom.nodeType !== DOCUMENT && dom.nodeType !== DOCUMENT_FRAGMENT || dom.childElementCount === 0;
  278. const all = (selector, scope) => {
  279. const base = scope === undefined ? document : scope.dom;
  280. return bypassSelector(base) ? [] : map(base.querySelectorAll(selector), SugarElement.fromDom);
  281. };
  282. const parent = element => Optional.from(element.dom.parentNode).map(SugarElement.fromDom);
  283. const children = element => map(element.dom.childNodes, SugarElement.fromDom);
  284. const spot = (element, offset) => ({
  285. element,
  286. offset
  287. });
  288. const leaf = (element, offset) => {
  289. const cs = children(element);
  290. return cs.length > 0 && offset < cs.length ? spot(cs[offset], 0) : spot(element, offset);
  291. };
  292. const before = (marker, element) => {
  293. const parent$1 = parent(marker);
  294. parent$1.each(v => {
  295. v.dom.insertBefore(element.dom, marker.dom);
  296. });
  297. };
  298. const append = (parent, element) => {
  299. parent.dom.appendChild(element.dom);
  300. };
  301. const wrap = (element, wrapper) => {
  302. before(element, wrapper);
  303. append(wrapper, element);
  304. };
  305. const NodeValue = (is, name) => {
  306. const get = element => {
  307. if (!is(element)) {
  308. throw new Error('Can only get ' + name + ' value of a ' + name + ' node');
  309. }
  310. return getOption(element).getOr('');
  311. };
  312. const getOption = element => is(element) ? Optional.from(element.dom.nodeValue) : Optional.none();
  313. const set = (element, value) => {
  314. if (!is(element)) {
  315. throw new Error('Can only set raw ' + name + ' value of a ' + name + ' node');
  316. }
  317. element.dom.nodeValue = value;
  318. };
  319. return {
  320. get,
  321. getOption,
  322. set
  323. };
  324. };
  325. const api = NodeValue(isText$1, 'text');
  326. const get$1 = element => api.get(element);
  327. const compareDocumentPosition = (a, b, match) => {
  328. return (a.compareDocumentPosition(b) & match) !== 0;
  329. };
  330. const documentPositionPreceding = (a, b) => {
  331. return compareDocumentPosition(a, b, Node.DOCUMENT_POSITION_PRECEDING);
  332. };
  333. const descendants = (scope, selector) => all(selector, scope);
  334. var global = tinymce.util.Tools.resolve('tinymce.dom.TreeWalker');
  335. const isSimpleBoundary = (dom, node) => dom.isBlock(node) || has(dom.schema.getVoidElements(), node.nodeName);
  336. const isContentEditableFalse = (dom, node) => dom.getContentEditable(node) === 'false';
  337. const isContentEditableTrueInCef = (dom, node) => dom.getContentEditable(node) === 'true' && dom.getContentEditableParent(node.parentNode) === 'false';
  338. const isHidden = (dom, node) => !dom.isBlock(node) && has(dom.schema.getWhitespaceElements(), node.nodeName);
  339. const isBoundary = (dom, node) => isSimpleBoundary(dom, node) || isContentEditableFalse(dom, node) || isHidden(dom, node) || isContentEditableTrueInCef(dom, node);
  340. const isText = node => node.nodeType === 3;
  341. const nuSection = () => ({
  342. sOffset: 0,
  343. fOffset: 0,
  344. elements: []
  345. });
  346. const toLeaf = (node, offset) => leaf(SugarElement.fromDom(node), offset);
  347. const walk = (dom, walkerFn, startNode, callbacks, endNode, skipStart = true) => {
  348. let next = skipStart ? walkerFn(false) : startNode;
  349. while (next) {
  350. const isCefNode = isContentEditableFalse(dom, next);
  351. if (isCefNode || isHidden(dom, next)) {
  352. const stopWalking = isCefNode ? callbacks.cef(next) : callbacks.boundary(next);
  353. if (stopWalking) {
  354. break;
  355. } else {
  356. next = walkerFn(true);
  357. continue;
  358. }
  359. } else if (isSimpleBoundary(dom, next)) {
  360. if (callbacks.boundary(next)) {
  361. break;
  362. }
  363. } else if (isText(next)) {
  364. callbacks.text(next);
  365. }
  366. if (next === endNode) {
  367. break;
  368. } else {
  369. next = walkerFn(false);
  370. }
  371. }
  372. };
  373. const collectTextToBoundary = (dom, section, node, rootNode, forwards) => {
  374. if (isBoundary(dom, node)) {
  375. return;
  376. }
  377. const rootBlock = dom.getParent(rootNode, dom.isBlock);
  378. const walker = new global(node, rootBlock);
  379. const walkerFn = forwards ? walker.next.bind(walker) : walker.prev.bind(walker);
  380. walk(dom, walkerFn, node, {
  381. boundary: always,
  382. cef: always,
  383. text: next => {
  384. if (forwards) {
  385. section.fOffset += next.length;
  386. } else {
  387. section.sOffset += next.length;
  388. }
  389. section.elements.push(SugarElement.fromDom(next));
  390. }
  391. });
  392. };
  393. const collect = (dom, rootNode, startNode, endNode, callbacks, skipStart = true) => {
  394. const walker = new global(startNode, rootNode);
  395. const sections = [];
  396. let current = nuSection();
  397. collectTextToBoundary(dom, current, startNode, rootNode, false);
  398. const finishSection = () => {
  399. if (current.elements.length > 0) {
  400. sections.push(current);
  401. current = nuSection();
  402. }
  403. return false;
  404. };
  405. walk(dom, walker.next.bind(walker), startNode, {
  406. boundary: finishSection,
  407. cef: node => {
  408. finishSection();
  409. if (callbacks) {
  410. sections.push(...callbacks.cef(node));
  411. }
  412. return false;
  413. },
  414. text: next => {
  415. current.elements.push(SugarElement.fromDom(next));
  416. if (callbacks) {
  417. callbacks.text(next, current);
  418. }
  419. }
  420. }, endNode, skipStart);
  421. if (endNode) {
  422. collectTextToBoundary(dom, current, endNode, rootNode, true);
  423. }
  424. finishSection();
  425. return sections;
  426. };
  427. const collectRangeSections = (dom, rng) => {
  428. const start = toLeaf(rng.startContainer, rng.startOffset);
  429. const startNode = start.element.dom;
  430. const end = toLeaf(rng.endContainer, rng.endOffset);
  431. const endNode = end.element.dom;
  432. return collect(dom, rng.commonAncestorContainer, startNode, endNode, {
  433. text: (node, section) => {
  434. if (node === endNode) {
  435. section.fOffset += node.length - end.offset;
  436. } else if (node === startNode) {
  437. section.sOffset += start.offset;
  438. }
  439. },
  440. cef: node => {
  441. const sections = bind(descendants(SugarElement.fromDom(node), '*[contenteditable=true]'), e => {
  442. const ceTrueNode = e.dom;
  443. return collect(dom, ceTrueNode, ceTrueNode);
  444. });
  445. return sort(sections, (a, b) => documentPositionPreceding(a.elements[0].dom, b.elements[0].dom) ? 1 : -1);
  446. }
  447. }, false);
  448. };
  449. const fromRng = (dom, rng) => rng.collapsed ? [] : collectRangeSections(dom, rng);
  450. const fromNode = (dom, node) => {
  451. const rng = dom.createRng();
  452. rng.selectNode(node);
  453. return fromRng(dom, rng);
  454. };
  455. const fromNodes = (dom, nodes) => bind(nodes, node => fromNode(dom, node));
  456. const find$2 = (text, pattern, start = 0, finish = text.length) => {
  457. const regex = pattern.regex;
  458. regex.lastIndex = start;
  459. const results = [];
  460. let match;
  461. while (match = regex.exec(text)) {
  462. const matchedText = match[pattern.matchIndex];
  463. const matchStart = match.index + match[0].indexOf(matchedText);
  464. const matchFinish = matchStart + matchedText.length;
  465. if (matchFinish > finish) {
  466. break;
  467. }
  468. results.push({
  469. start: matchStart,
  470. finish: matchFinish
  471. });
  472. regex.lastIndex = matchFinish;
  473. }
  474. return results;
  475. };
  476. const extract = (elements, matches) => {
  477. const nodePositions = foldl(elements, (acc, element) => {
  478. const content = get$1(element);
  479. const start = acc.last;
  480. const finish = start + content.length;
  481. const positions = bind(matches, (match, matchIdx) => {
  482. if (match.start < finish && match.finish > start) {
  483. return [{
  484. element,
  485. start: Math.max(start, match.start) - start,
  486. finish: Math.min(finish, match.finish) - start,
  487. matchId: matchIdx
  488. }];
  489. } else {
  490. return [];
  491. }
  492. });
  493. return {
  494. results: acc.results.concat(positions),
  495. last: finish
  496. };
  497. }, {
  498. results: [],
  499. last: 0
  500. }).results;
  501. return groupBy(nodePositions, position => position.matchId);
  502. };
  503. const find$1 = (pattern, sections) => bind(sections, section => {
  504. const elements = section.elements;
  505. const content = map(elements, get$1).join('');
  506. const positions = find$2(content, pattern, section.sOffset, content.length - section.fOffset);
  507. return extract(elements, positions);
  508. });
  509. const mark = (matches, replacementNode) => {
  510. eachr(matches, (match, idx) => {
  511. eachr(match, pos => {
  512. const wrapper = SugarElement.fromDom(replacementNode.cloneNode(false));
  513. set(wrapper, 'data-mce-index', idx);
  514. const textNode = pos.element.dom;
  515. if (textNode.length === pos.finish && pos.start === 0) {
  516. wrap(pos.element, wrapper);
  517. } else {
  518. if (textNode.length !== pos.finish) {
  519. textNode.splitText(pos.finish);
  520. }
  521. const matchNode = textNode.splitText(pos.start);
  522. wrap(SugarElement.fromDom(matchNode), wrapper);
  523. }
  524. });
  525. });
  526. };
  527. const findAndMark = (dom, pattern, node, replacementNode) => {
  528. const textSections = fromNode(dom, node);
  529. const matches = find$1(pattern, textSections);
  530. mark(matches, replacementNode);
  531. return matches.length;
  532. };
  533. const findAndMarkInSelection = (dom, pattern, selection, replacementNode) => {
  534. const bookmark = selection.getBookmark();
  535. const nodes = dom.select('td[data-mce-selected],th[data-mce-selected]');
  536. const textSections = nodes.length > 0 ? fromNodes(dom, nodes) : fromRng(dom, selection.getRng());
  537. const matches = find$1(pattern, textSections);
  538. mark(matches, replacementNode);
  539. selection.moveToBookmark(bookmark);
  540. return matches.length;
  541. };
  542. const getElmIndex = elm => {
  543. const value = elm.getAttribute('data-mce-index');
  544. if (typeof value === 'number') {
  545. return '' + value;
  546. }
  547. return value;
  548. };
  549. const markAllMatches = (editor, currentSearchState, pattern, inSelection) => {
  550. const marker = editor.dom.create('span', { 'data-mce-bogus': 1 });
  551. marker.className = 'mce-match-marker';
  552. const node = editor.getBody();
  553. done(editor, currentSearchState, false);
  554. if (inSelection) {
  555. return findAndMarkInSelection(editor.dom, pattern, editor.selection, marker);
  556. } else {
  557. return findAndMark(editor.dom, pattern, node, marker);
  558. }
  559. };
  560. const unwrap = node => {
  561. const parentNode = node.parentNode;
  562. if (node.firstChild) {
  563. parentNode.insertBefore(node.firstChild, node);
  564. }
  565. node.parentNode.removeChild(node);
  566. };
  567. const findSpansByIndex = (editor, index) => {
  568. const spans = [];
  569. const nodes = global$1.toArray(editor.getBody().getElementsByTagName('span'));
  570. if (nodes.length) {
  571. for (let i = 0; i < nodes.length; i++) {
  572. const nodeIndex = getElmIndex(nodes[i]);
  573. if (nodeIndex === null || !nodeIndex.length) {
  574. continue;
  575. }
  576. if (nodeIndex === index.toString()) {
  577. spans.push(nodes[i]);
  578. }
  579. }
  580. }
  581. return spans;
  582. };
  583. const moveSelection = (editor, currentSearchState, forward) => {
  584. const searchState = currentSearchState.get();
  585. let testIndex = searchState.index;
  586. const dom = editor.dom;
  587. forward = forward !== false;
  588. if (forward) {
  589. if (testIndex + 1 === searchState.count) {
  590. testIndex = 0;
  591. } else {
  592. testIndex++;
  593. }
  594. } else {
  595. if (testIndex - 1 === -1) {
  596. testIndex = searchState.count - 1;
  597. } else {
  598. testIndex--;
  599. }
  600. }
  601. dom.removeClass(findSpansByIndex(editor, searchState.index), 'mce-match-marker-selected');
  602. const spans = findSpansByIndex(editor, testIndex);
  603. if (spans.length) {
  604. dom.addClass(findSpansByIndex(editor, testIndex), 'mce-match-marker-selected');
  605. editor.selection.scrollIntoView(spans[0]);
  606. return testIndex;
  607. }
  608. return -1;
  609. };
  610. const removeNode = (dom, node) => {
  611. const parent = node.parentNode;
  612. dom.remove(node);
  613. if (dom.isEmpty(parent)) {
  614. dom.remove(parent);
  615. }
  616. };
  617. const escapeSearchText = (text, wholeWord) => {
  618. const escapedText = text.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&').replace(/\s/g, '[^\\S\\r\\n\\uFEFF]');
  619. const wordRegex = '(' + escapedText + ')';
  620. return wholeWord ? `(?:^|\\s|${ punctuation() })` + wordRegex + `(?=$|\\s|${ punctuation() })` : wordRegex;
  621. };
  622. const find = (editor, currentSearchState, text, matchCase, wholeWord, inSelection) => {
  623. const selection = editor.selection;
  624. const escapedText = escapeSearchText(text, wholeWord);
  625. const isForwardSelection = selection.isForward();
  626. const pattern = {
  627. regex: new RegExp(escapedText, matchCase ? 'g' : 'gi'),
  628. matchIndex: 1
  629. };
  630. const count = markAllMatches(editor, currentSearchState, pattern, inSelection);
  631. if (global$2.browser.isSafari()) {
  632. selection.setRng(selection.getRng(), isForwardSelection);
  633. }
  634. if (count) {
  635. const newIndex = moveSelection(editor, currentSearchState, true);
  636. currentSearchState.set({
  637. index: newIndex,
  638. count,
  639. text,
  640. matchCase,
  641. wholeWord,
  642. inSelection
  643. });
  644. }
  645. return count;
  646. };
  647. const next = (editor, currentSearchState) => {
  648. const index = moveSelection(editor, currentSearchState, true);
  649. currentSearchState.set({
  650. ...currentSearchState.get(),
  651. index
  652. });
  653. };
  654. const prev = (editor, currentSearchState) => {
  655. const index = moveSelection(editor, currentSearchState, false);
  656. currentSearchState.set({
  657. ...currentSearchState.get(),
  658. index
  659. });
  660. };
  661. const isMatchSpan = node => {
  662. const matchIndex = getElmIndex(node);
  663. return matchIndex !== null && matchIndex.length > 0;
  664. };
  665. const replace = (editor, currentSearchState, text, forward, all) => {
  666. const searchState = currentSearchState.get();
  667. const currentIndex = searchState.index;
  668. let currentMatchIndex, nextIndex = currentIndex;
  669. forward = forward !== false;
  670. const node = editor.getBody();
  671. const nodes = global$1.grep(global$1.toArray(node.getElementsByTagName('span')), isMatchSpan);
  672. for (let i = 0; i < nodes.length; i++) {
  673. const nodeIndex = getElmIndex(nodes[i]);
  674. let matchIndex = currentMatchIndex = parseInt(nodeIndex, 10);
  675. if (all || matchIndex === searchState.index) {
  676. if (text.length) {
  677. nodes[i].firstChild.nodeValue = text;
  678. unwrap(nodes[i]);
  679. } else {
  680. removeNode(editor.dom, nodes[i]);
  681. }
  682. while (nodes[++i]) {
  683. matchIndex = parseInt(getElmIndex(nodes[i]), 10);
  684. if (matchIndex === currentMatchIndex) {
  685. removeNode(editor.dom, nodes[i]);
  686. } else {
  687. i--;
  688. break;
  689. }
  690. }
  691. if (forward) {
  692. nextIndex--;
  693. }
  694. } else if (currentMatchIndex > currentIndex) {
  695. nodes[i].setAttribute('data-mce-index', String(currentMatchIndex - 1));
  696. }
  697. }
  698. currentSearchState.set({
  699. ...searchState,
  700. count: all ? 0 : searchState.count - 1,
  701. index: nextIndex
  702. });
  703. if (forward) {
  704. next(editor, currentSearchState);
  705. } else {
  706. prev(editor, currentSearchState);
  707. }
  708. return !all && currentSearchState.get().count > 0;
  709. };
  710. const done = (editor, currentSearchState, keepEditorSelection) => {
  711. let startContainer, endContainer;
  712. const searchState = currentSearchState.get();
  713. const nodes = global$1.toArray(editor.getBody().getElementsByTagName('span'));
  714. for (let i = 0; i < nodes.length; i++) {
  715. const nodeIndex = getElmIndex(nodes[i]);
  716. if (nodeIndex !== null && nodeIndex.length) {
  717. if (nodeIndex === searchState.index.toString()) {
  718. if (!startContainer) {
  719. startContainer = nodes[i].firstChild;
  720. }
  721. endContainer = nodes[i].firstChild;
  722. }
  723. unwrap(nodes[i]);
  724. }
  725. }
  726. currentSearchState.set({
  727. ...searchState,
  728. index: -1,
  729. count: 0,
  730. text: ''
  731. });
  732. if (startContainer && endContainer) {
  733. const rng = editor.dom.createRng();
  734. rng.setStart(startContainer, 0);
  735. rng.setEnd(endContainer, endContainer.data.length);
  736. if (keepEditorSelection !== false) {
  737. editor.selection.setRng(rng);
  738. }
  739. return rng;
  740. }
  741. };
  742. const hasNext = (editor, currentSearchState) => currentSearchState.get().count > 1;
  743. const hasPrev = (editor, currentSearchState) => currentSearchState.get().count > 1;
  744. const get = (editor, currentState) => {
  745. const done$1 = keepEditorSelection => {
  746. return done(editor, currentState, keepEditorSelection);
  747. };
  748. const find$1 = (text, matchCase, wholeWord, inSelection = false) => {
  749. return find(editor, currentState, text, matchCase, wholeWord, inSelection);
  750. };
  751. const next$1 = () => {
  752. return next(editor, currentState);
  753. };
  754. const prev$1 = () => {
  755. return prev(editor, currentState);
  756. };
  757. const replace$1 = (text, forward, all) => {
  758. return replace(editor, currentState, text, forward, all);
  759. };
  760. return {
  761. done: done$1,
  762. find: find$1,
  763. next: next$1,
  764. prev: prev$1,
  765. replace: replace$1
  766. };
  767. };
  768. const singleton = doRevoke => {
  769. const subject = Cell(Optional.none());
  770. const revoke = () => subject.get().each(doRevoke);
  771. const clear = () => {
  772. revoke();
  773. subject.set(Optional.none());
  774. };
  775. const isSet = () => subject.get().isSome();
  776. const get = () => subject.get();
  777. const set = s => {
  778. revoke();
  779. subject.set(Optional.some(s));
  780. };
  781. return {
  782. clear,
  783. isSet,
  784. get,
  785. set
  786. };
  787. };
  788. const value = () => {
  789. const subject = singleton(noop);
  790. const on = f => subject.get().each(f);
  791. return {
  792. ...subject,
  793. on
  794. };
  795. };
  796. const open = (editor, currentSearchState) => {
  797. const dialogApi = value();
  798. editor.undoManager.add();
  799. const selectedText = global$1.trim(editor.selection.getContent({ format: 'text' }));
  800. const updateButtonStates = api => {
  801. api.setEnabled('next', hasNext(editor, currentSearchState));
  802. api.setEnabled('prev', hasPrev(editor, currentSearchState));
  803. };
  804. const updateSearchState = api => {
  805. const data = api.getData();
  806. const current = currentSearchState.get();
  807. currentSearchState.set({
  808. ...current,
  809. matchCase: data.matchcase,
  810. wholeWord: data.wholewords,
  811. inSelection: data.inselection
  812. });
  813. };
  814. const disableAll = (api, disable) => {
  815. const buttons = [
  816. 'replace',
  817. 'replaceall',
  818. 'prev',
  819. 'next'
  820. ];
  821. const toggle = name => api.setEnabled(name, !disable);
  822. each(buttons, toggle);
  823. };
  824. const notFoundAlert = api => {
  825. editor.windowManager.alert('Could not find the specified string.', () => {
  826. api.focus('findtext');
  827. });
  828. };
  829. const focusButtonIfRequired = (api, name) => {
  830. if (global$2.browser.isSafari() && global$2.deviceType.isTouch() && (name === 'find' || name === 'replace' || name === 'replaceall')) {
  831. api.focus(name);
  832. }
  833. };
  834. const reset = api => {
  835. done(editor, currentSearchState, false);
  836. disableAll(api, true);
  837. updateButtonStates(api);
  838. };
  839. const doFind = api => {
  840. const data = api.getData();
  841. const last = currentSearchState.get();
  842. if (!data.findtext.length) {
  843. reset(api);
  844. return;
  845. }
  846. if (last.text === data.findtext && last.matchCase === data.matchcase && last.wholeWord === data.wholewords) {
  847. next(editor, currentSearchState);
  848. } else {
  849. const count = find(editor, currentSearchState, data.findtext, data.matchcase, data.wholewords, data.inselection);
  850. if (count <= 0) {
  851. notFoundAlert(api);
  852. }
  853. disableAll(api, count === 0);
  854. }
  855. updateButtonStates(api);
  856. };
  857. const initialState = currentSearchState.get();
  858. const initialData = {
  859. findtext: selectedText,
  860. replacetext: '',
  861. wholewords: initialState.wholeWord,
  862. matchcase: initialState.matchCase,
  863. inselection: initialState.inSelection
  864. };
  865. const spec = {
  866. title: 'Find and Replace',
  867. size: 'normal',
  868. body: {
  869. type: 'panel',
  870. items: [
  871. {
  872. type: 'bar',
  873. items: [
  874. {
  875. type: 'input',
  876. name: 'findtext',
  877. placeholder: 'Find',
  878. maximized: true,
  879. inputMode: 'search'
  880. },
  881. {
  882. type: 'button',
  883. name: 'prev',
  884. text: 'Previous',
  885. icon: 'action-prev',
  886. enabled: false,
  887. borderless: true
  888. },
  889. {
  890. type: 'button',
  891. name: 'next',
  892. text: 'Next',
  893. icon: 'action-next',
  894. enabled: false,
  895. borderless: true
  896. }
  897. ]
  898. },
  899. {
  900. type: 'input',
  901. name: 'replacetext',
  902. placeholder: 'Replace with',
  903. inputMode: 'search'
  904. }
  905. ]
  906. },
  907. buttons: [
  908. {
  909. type: 'menu',
  910. name: 'options',
  911. icon: 'preferences',
  912. tooltip: 'Preferences',
  913. align: 'start',
  914. items: [
  915. {
  916. type: 'togglemenuitem',
  917. name: 'matchcase',
  918. text: 'Match case'
  919. },
  920. {
  921. type: 'togglemenuitem',
  922. name: 'wholewords',
  923. text: 'Find whole words only'
  924. },
  925. {
  926. type: 'togglemenuitem',
  927. name: 'inselection',
  928. text: 'Find in selection'
  929. }
  930. ]
  931. },
  932. {
  933. type: 'custom',
  934. name: 'find',
  935. text: 'Find',
  936. primary: true
  937. },
  938. {
  939. type: 'custom',
  940. name: 'replace',
  941. text: 'Replace',
  942. enabled: false
  943. },
  944. {
  945. type: 'custom',
  946. name: 'replaceall',
  947. text: 'Replace all',
  948. enabled: false
  949. }
  950. ],
  951. initialData,
  952. onChange: (api, details) => {
  953. if (details.name === 'findtext' && currentSearchState.get().count > 0) {
  954. reset(api);
  955. }
  956. },
  957. onAction: (api, details) => {
  958. const data = api.getData();
  959. switch (details.name) {
  960. case 'find':
  961. doFind(api);
  962. break;
  963. case 'replace':
  964. if (!replace(editor, currentSearchState, data.replacetext)) {
  965. reset(api);
  966. } else {
  967. updateButtonStates(api);
  968. }
  969. break;
  970. case 'replaceall':
  971. replace(editor, currentSearchState, data.replacetext, true, true);
  972. reset(api);
  973. break;
  974. case 'prev':
  975. prev(editor, currentSearchState);
  976. updateButtonStates(api);
  977. break;
  978. case 'next':
  979. next(editor, currentSearchState);
  980. updateButtonStates(api);
  981. break;
  982. case 'matchcase':
  983. case 'wholewords':
  984. case 'inselection':
  985. updateSearchState(api);
  986. reset(api);
  987. break;
  988. }
  989. focusButtonIfRequired(api, details.name);
  990. },
  991. onSubmit: api => {
  992. doFind(api);
  993. focusButtonIfRequired(api, 'find');
  994. },
  995. onClose: () => {
  996. editor.focus();
  997. done(editor, currentSearchState);
  998. editor.undoManager.add();
  999. }
  1000. };
  1001. dialogApi.set(editor.windowManager.open(spec, { inline: 'toolbar' }));
  1002. };
  1003. const register$1 = (editor, currentSearchState) => {
  1004. editor.addCommand('SearchReplace', () => {
  1005. open(editor, currentSearchState);
  1006. });
  1007. };
  1008. const showDialog = (editor, currentSearchState) => () => {
  1009. open(editor, currentSearchState);
  1010. };
  1011. const register = (editor, currentSearchState) => {
  1012. editor.ui.registry.addMenuItem('searchreplace', {
  1013. text: 'Find and replace...',
  1014. shortcut: 'Meta+F',
  1015. onAction: showDialog(editor, currentSearchState),
  1016. icon: 'search'
  1017. });
  1018. editor.ui.registry.addButton('searchreplace', {
  1019. tooltip: 'Find and replace',
  1020. onAction: showDialog(editor, currentSearchState),
  1021. icon: 'search'
  1022. });
  1023. editor.shortcuts.add('Meta+F', '', showDialog(editor, currentSearchState));
  1024. };
  1025. var Plugin = () => {
  1026. global$3.add('searchreplace', editor => {
  1027. const currentSearchState = Cell({
  1028. index: -1,
  1029. count: 0,
  1030. text: '',
  1031. matchCase: false,
  1032. wholeWord: false,
  1033. inSelection: false
  1034. });
  1035. register$1(editor, currentSearchState);
  1036. register(editor, currentSearchState);
  1037. return get(editor, currentSearchState);
  1038. });
  1039. };
  1040. Plugin();
  1041. })();