plugin.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. (function () {
  2. var searchreplace = (function () {
  3. 'use strict';
  4. var Cell = function (initial) {
  5. var value = initial;
  6. var get = function () {
  7. return value;
  8. };
  9. var set = function (v) {
  10. value = v;
  11. };
  12. var clone = function () {
  13. return Cell(get());
  14. };
  15. return {
  16. get: get,
  17. set: set,
  18. clone: clone
  19. };
  20. };
  21. var global = tinymce.util.Tools.resolve('tinymce.PluginManager');
  22. var global$1 = tinymce.util.Tools.resolve('tinymce.util.Tools');
  23. function isContentEditableFalse(node) {
  24. return node && node.nodeType === 1 && node.contentEditable === 'false';
  25. }
  26. function findAndReplaceDOMText(regex, node, replacementNode, captureGroup, schema) {
  27. var m;
  28. var matches = [];
  29. var text, count = 0, doc;
  30. var blockElementsMap, hiddenTextElementsMap, shortEndedElementsMap;
  31. doc = node.ownerDocument;
  32. blockElementsMap = schema.getBlockElements();
  33. hiddenTextElementsMap = schema.getWhiteSpaceElements();
  34. shortEndedElementsMap = schema.getShortEndedElements();
  35. function getMatchIndexes(m, captureGroup) {
  36. captureGroup = captureGroup || 0;
  37. if (!m[0]) {
  38. throw new Error('findAndReplaceDOMText cannot handle zero-length matches');
  39. }
  40. var index = m.index;
  41. if (captureGroup > 0) {
  42. var cg = m[captureGroup];
  43. if (!cg) {
  44. throw new Error('Invalid capture group');
  45. }
  46. index += m[0].indexOf(cg);
  47. m[0] = cg;
  48. }
  49. return [
  50. index,
  51. index + m[0].length,
  52. [m[0]]
  53. ];
  54. }
  55. function getText(node) {
  56. var txt;
  57. if (node.nodeType === 3) {
  58. return node.data;
  59. }
  60. if (hiddenTextElementsMap[node.nodeName] && !blockElementsMap[node.nodeName]) {
  61. return '';
  62. }
  63. txt = '';
  64. if (isContentEditableFalse(node)) {
  65. return '\n';
  66. }
  67. if (blockElementsMap[node.nodeName] || shortEndedElementsMap[node.nodeName]) {
  68. txt += '\n';
  69. }
  70. if (node = node.firstChild) {
  71. do {
  72. txt += getText(node);
  73. } while (node = node.nextSibling);
  74. }
  75. return txt;
  76. }
  77. function stepThroughMatches(node, matches, replaceFn) {
  78. var startNode, endNode, startNodeIndex, endNodeIndex, innerNodes = [], atIndex = 0, curNode = node, matchLocation = matches.shift(), matchIndex = 0;
  79. out:
  80. while (true) {
  81. if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName] || isContentEditableFalse(curNode)) {
  82. atIndex++;
  83. }
  84. if (curNode.nodeType === 3) {
  85. if (!endNode && curNode.length + atIndex >= matchLocation[1]) {
  86. endNode = curNode;
  87. endNodeIndex = matchLocation[1] - atIndex;
  88. } else if (startNode) {
  89. innerNodes.push(curNode);
  90. }
  91. if (!startNode && curNode.length + atIndex > matchLocation[0]) {
  92. startNode = curNode;
  93. startNodeIndex = matchLocation[0] - atIndex;
  94. }
  95. atIndex += curNode.length;
  96. }
  97. if (startNode && endNode) {
  98. curNode = replaceFn({
  99. startNode: startNode,
  100. startNodeIndex: startNodeIndex,
  101. endNode: endNode,
  102. endNodeIndex: endNodeIndex,
  103. innerNodes: innerNodes,
  104. match: matchLocation[2],
  105. matchIndex: matchIndex
  106. });
  107. atIndex -= endNode.length - endNodeIndex;
  108. startNode = null;
  109. endNode = null;
  110. innerNodes = [];
  111. matchLocation = matches.shift();
  112. matchIndex++;
  113. if (!matchLocation) {
  114. break;
  115. }
  116. } else if ((!hiddenTextElementsMap[curNode.nodeName] || blockElementsMap[curNode.nodeName]) && curNode.firstChild) {
  117. if (!isContentEditableFalse(curNode)) {
  118. curNode = curNode.firstChild;
  119. continue;
  120. }
  121. } else if (curNode.nextSibling) {
  122. curNode = curNode.nextSibling;
  123. continue;
  124. }
  125. while (true) {
  126. if (curNode.nextSibling) {
  127. curNode = curNode.nextSibling;
  128. break;
  129. } else if (curNode.parentNode !== node) {
  130. curNode = curNode.parentNode;
  131. } else {
  132. break out;
  133. }
  134. }
  135. }
  136. }
  137. function genReplacer(nodeName) {
  138. var makeReplacementNode;
  139. if (typeof nodeName !== 'function') {
  140. var stencilNode_1 = nodeName.nodeType ? nodeName : doc.createElement(nodeName);
  141. makeReplacementNode = function (fill, matchIndex) {
  142. var clone = stencilNode_1.cloneNode(false);
  143. clone.setAttribute('data-mce-index', matchIndex);
  144. if (fill) {
  145. clone.appendChild(doc.createTextNode(fill));
  146. }
  147. return clone;
  148. };
  149. } else {
  150. makeReplacementNode = nodeName;
  151. }
  152. return function (range) {
  153. var before;
  154. var after;
  155. var parentNode;
  156. var startNode = range.startNode;
  157. var endNode = range.endNode;
  158. var matchIndex = range.matchIndex;
  159. if (startNode === endNode) {
  160. var node_1 = startNode;
  161. parentNode = node_1.parentNode;
  162. if (range.startNodeIndex > 0) {
  163. before = doc.createTextNode(node_1.data.substring(0, range.startNodeIndex));
  164. parentNode.insertBefore(before, node_1);
  165. }
  166. var el = makeReplacementNode(range.match[0], matchIndex);
  167. parentNode.insertBefore(el, node_1);
  168. if (range.endNodeIndex < node_1.length) {
  169. after = doc.createTextNode(node_1.data.substring(range.endNodeIndex));
  170. parentNode.insertBefore(after, node_1);
  171. }
  172. node_1.parentNode.removeChild(node_1);
  173. return el;
  174. }
  175. before = doc.createTextNode(startNode.data.substring(0, range.startNodeIndex));
  176. after = doc.createTextNode(endNode.data.substring(range.endNodeIndex));
  177. var elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex);
  178. for (var i = 0, l = range.innerNodes.length; i < l; ++i) {
  179. var innerNode = range.innerNodes[i];
  180. var innerEl = makeReplacementNode(innerNode.data, matchIndex);
  181. innerNode.parentNode.replaceChild(innerEl, innerNode);
  182. }
  183. var elB = makeReplacementNode(endNode.data.substring(0, range.endNodeIndex), matchIndex);
  184. parentNode = startNode.parentNode;
  185. parentNode.insertBefore(before, startNode);
  186. parentNode.insertBefore(elA, startNode);
  187. parentNode.removeChild(startNode);
  188. parentNode = endNode.parentNode;
  189. parentNode.insertBefore(elB, endNode);
  190. parentNode.insertBefore(after, endNode);
  191. parentNode.removeChild(endNode);
  192. return elB;
  193. };
  194. }
  195. text = getText(node);
  196. if (!text) {
  197. return;
  198. }
  199. if (regex.global) {
  200. while (m = regex.exec(text)) {
  201. matches.push(getMatchIndexes(m, captureGroup));
  202. }
  203. } else {
  204. m = text.match(regex);
  205. matches.push(getMatchIndexes(m, captureGroup));
  206. }
  207. if (matches.length) {
  208. count = matches.length;
  209. stepThroughMatches(node, matches, genReplacer(replacementNode));
  210. }
  211. return count;
  212. }
  213. var FindReplaceText = { findAndReplaceDOMText: findAndReplaceDOMText };
  214. var getElmIndex = function (elm) {
  215. var value = elm.getAttribute('data-mce-index');
  216. if (typeof value === 'number') {
  217. return '' + value;
  218. }
  219. return value;
  220. };
  221. var markAllMatches = function (editor, currentIndexState, regex) {
  222. var node, marker;
  223. marker = editor.dom.create('span', { 'data-mce-bogus': 1 });
  224. marker.className = 'mce-match-marker';
  225. node = editor.getBody();
  226. done(editor, currentIndexState, false);
  227. return FindReplaceText.findAndReplaceDOMText(regex, node, marker, false, editor.schema);
  228. };
  229. var unwrap = function (node) {
  230. var parentNode = node.parentNode;
  231. if (node.firstChild) {
  232. parentNode.insertBefore(node.firstChild, node);
  233. }
  234. node.parentNode.removeChild(node);
  235. };
  236. var findSpansByIndex = function (editor, index) {
  237. var nodes;
  238. var spans = [];
  239. nodes = global$1.toArray(editor.getBody().getElementsByTagName('span'));
  240. if (nodes.length) {
  241. for (var i = 0; i < nodes.length; i++) {
  242. var nodeIndex = getElmIndex(nodes[i]);
  243. if (nodeIndex === null || !nodeIndex.length) {
  244. continue;
  245. }
  246. if (nodeIndex === index.toString()) {
  247. spans.push(nodes[i]);
  248. }
  249. }
  250. }
  251. return spans;
  252. };
  253. var moveSelection = function (editor, currentIndexState, forward) {
  254. var testIndex = currentIndexState.get();
  255. var dom = editor.dom;
  256. forward = forward !== false;
  257. if (forward) {
  258. testIndex++;
  259. } else {
  260. testIndex--;
  261. }
  262. dom.removeClass(findSpansByIndex(editor, currentIndexState.get()), 'mce-match-marker-selected');
  263. var spans = findSpansByIndex(editor, testIndex);
  264. if (spans.length) {
  265. dom.addClass(findSpansByIndex(editor, testIndex), 'mce-match-marker-selected');
  266. editor.selection.scrollIntoView(spans[0]);
  267. return testIndex;
  268. }
  269. return -1;
  270. };
  271. var removeNode = function (dom, node) {
  272. var parent = node.parentNode;
  273. dom.remove(node);
  274. if (dom.isEmpty(parent)) {
  275. dom.remove(parent);
  276. }
  277. };
  278. var find = function (editor, currentIndexState, text, matchCase, wholeWord) {
  279. text = text.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
  280. text = text.replace(/\s/g, '[^\\S\\r\\n]');
  281. text = wholeWord ? '\\b' + text + '\\b' : text;
  282. var count = markAllMatches(editor, currentIndexState, new RegExp(text, matchCase ? 'g' : 'gi'));
  283. if (count) {
  284. currentIndexState.set(-1);
  285. currentIndexState.set(moveSelection(editor, currentIndexState, true));
  286. }
  287. return count;
  288. };
  289. var next = function (editor, currentIndexState) {
  290. var index = moveSelection(editor, currentIndexState, true);
  291. if (index !== -1) {
  292. currentIndexState.set(index);
  293. }
  294. };
  295. var prev = function (editor, currentIndexState) {
  296. var index = moveSelection(editor, currentIndexState, false);
  297. if (index !== -1) {
  298. currentIndexState.set(index);
  299. }
  300. };
  301. var isMatchSpan = function (node) {
  302. var matchIndex = getElmIndex(node);
  303. return matchIndex !== null && matchIndex.length > 0;
  304. };
  305. var replace = function (editor, currentIndexState, text, forward, all) {
  306. var i, nodes, node, matchIndex, currentMatchIndex, nextIndex = currentIndexState.get(), hasMore;
  307. forward = forward !== false;
  308. node = editor.getBody();
  309. nodes = global$1.grep(global$1.toArray(node.getElementsByTagName('span')), isMatchSpan);
  310. for (i = 0; i < nodes.length; i++) {
  311. var nodeIndex = getElmIndex(nodes[i]);
  312. matchIndex = currentMatchIndex = parseInt(nodeIndex, 10);
  313. if (all || matchIndex === currentIndexState.get()) {
  314. if (text.length) {
  315. nodes[i].firstChild.nodeValue = text;
  316. unwrap(nodes[i]);
  317. } else {
  318. removeNode(editor.dom, nodes[i]);
  319. }
  320. while (nodes[++i]) {
  321. matchIndex = parseInt(getElmIndex(nodes[i]), 10);
  322. if (matchIndex === currentMatchIndex) {
  323. removeNode(editor.dom, nodes[i]);
  324. } else {
  325. i--;
  326. break;
  327. }
  328. }
  329. if (forward) {
  330. nextIndex--;
  331. }
  332. } else if (currentMatchIndex > currentIndexState.get()) {
  333. nodes[i].setAttribute('data-mce-index', currentMatchIndex - 1);
  334. }
  335. }
  336. currentIndexState.set(nextIndex);
  337. if (forward) {
  338. hasMore = hasNext(editor, currentIndexState);
  339. next(editor, currentIndexState);
  340. } else {
  341. hasMore = hasPrev(editor, currentIndexState);
  342. prev(editor, currentIndexState);
  343. }
  344. return !all && hasMore;
  345. };
  346. var done = function (editor, currentIndexState, keepEditorSelection) {
  347. var i, nodes, startContainer, endContainer;
  348. nodes = global$1.toArray(editor.getBody().getElementsByTagName('span'));
  349. for (i = 0; i < nodes.length; i++) {
  350. var nodeIndex = getElmIndex(nodes[i]);
  351. if (nodeIndex !== null && nodeIndex.length) {
  352. if (nodeIndex === currentIndexState.get().toString()) {
  353. if (!startContainer) {
  354. startContainer = nodes[i].firstChild;
  355. }
  356. endContainer = nodes[i].firstChild;
  357. }
  358. unwrap(nodes[i]);
  359. }
  360. }
  361. if (startContainer && endContainer) {
  362. var rng = editor.dom.createRng();
  363. rng.setStart(startContainer, 0);
  364. rng.setEnd(endContainer, endContainer.data.length);
  365. if (keepEditorSelection !== false) {
  366. editor.selection.setRng(rng);
  367. }
  368. return rng;
  369. }
  370. };
  371. var hasNext = function (editor, currentIndexState) {
  372. return findSpansByIndex(editor, currentIndexState.get() + 1).length > 0;
  373. };
  374. var hasPrev = function (editor, currentIndexState) {
  375. return findSpansByIndex(editor, currentIndexState.get() - 1).length > 0;
  376. };
  377. var Actions = {
  378. done: done,
  379. find: find,
  380. next: next,
  381. prev: prev,
  382. replace: replace,
  383. hasNext: hasNext,
  384. hasPrev: hasPrev
  385. };
  386. var get = function (editor, currentIndexState) {
  387. var done = function (keepEditorSelection) {
  388. return Actions.done(editor, currentIndexState, keepEditorSelection);
  389. };
  390. var find = function (text, matchCase, wholeWord) {
  391. return Actions.find(editor, currentIndexState, text, matchCase, wholeWord);
  392. };
  393. var next = function () {
  394. return Actions.next(editor, currentIndexState);
  395. };
  396. var prev = function () {
  397. return Actions.prev(editor, currentIndexState);
  398. };
  399. var replace = function (text, forward, all) {
  400. return Actions.replace(editor, currentIndexState, text, forward, all);
  401. };
  402. return {
  403. done: done,
  404. find: find,
  405. next: next,
  406. prev: prev,
  407. replace: replace
  408. };
  409. };
  410. var Api = { get: get };
  411. var open = function (editor, currentIndexState) {
  412. var last = {}, selectedText;
  413. editor.undoManager.add();
  414. selectedText = global$1.trim(editor.selection.getContent({ format: 'text' }));
  415. function updateButtonStates() {
  416. win.statusbar.find('#next').disabled(Actions.hasNext(editor, currentIndexState) === false);
  417. win.statusbar.find('#prev').disabled(Actions.hasPrev(editor, currentIndexState) === false);
  418. }
  419. function notFoundAlert() {
  420. editor.windowManager.alert('Could not find the specified string.', function () {
  421. win.find('#find')[0].focus();
  422. });
  423. }
  424. var win = editor.windowManager.open({
  425. layout: 'flex',
  426. pack: 'center',
  427. align: 'center',
  428. onClose: function () {
  429. editor.focus();
  430. Actions.done(editor, currentIndexState);
  431. editor.undoManager.add();
  432. },
  433. onSubmit: function (e) {
  434. var count, caseState, text, wholeWord;
  435. e.preventDefault();
  436. caseState = win.find('#case').checked();
  437. wholeWord = win.find('#words').checked();
  438. text = win.find('#find').value();
  439. if (!text.length) {
  440. Actions.done(editor, currentIndexState, false);
  441. win.statusbar.items().slice(1).disabled(true);
  442. return;
  443. }
  444. if (last.text === text && last.caseState === caseState && last.wholeWord === wholeWord) {
  445. if (!Actions.hasNext(editor, currentIndexState)) {
  446. notFoundAlert();
  447. return;
  448. }
  449. Actions.next(editor, currentIndexState);
  450. updateButtonStates();
  451. return;
  452. }
  453. count = Actions.find(editor, currentIndexState, text, caseState, wholeWord);
  454. if (!count) {
  455. notFoundAlert();
  456. }
  457. win.statusbar.items().slice(1).disabled(count === 0);
  458. updateButtonStates();
  459. last = {
  460. text: text,
  461. caseState: caseState,
  462. wholeWord: wholeWord
  463. };
  464. },
  465. buttons: [
  466. {
  467. text: 'Find',
  468. subtype: 'primary',
  469. onclick: function () {
  470. win.submit();
  471. }
  472. },
  473. {
  474. text: 'Replace',
  475. disabled: true,
  476. onclick: function () {
  477. if (!Actions.replace(editor, currentIndexState, win.find('#replace').value())) {
  478. win.statusbar.items().slice(1).disabled(true);
  479. currentIndexState.set(-1);
  480. last = {};
  481. }
  482. }
  483. },
  484. {
  485. text: 'Replace all',
  486. disabled: true,
  487. onclick: function () {
  488. Actions.replace(editor, currentIndexState, win.find('#replace').value(), true, true);
  489. win.statusbar.items().slice(1).disabled(true);
  490. last = {};
  491. }
  492. },
  493. {
  494. type: 'spacer',
  495. flex: 1
  496. },
  497. {
  498. text: 'Prev',
  499. name: 'prev',
  500. disabled: true,
  501. onclick: function () {
  502. Actions.prev(editor, currentIndexState);
  503. updateButtonStates();
  504. }
  505. },
  506. {
  507. text: 'Next',
  508. name: 'next',
  509. disabled: true,
  510. onclick: function () {
  511. Actions.next(editor, currentIndexState);
  512. updateButtonStates();
  513. }
  514. }
  515. ],
  516. title: 'Find and replace',
  517. items: {
  518. type: 'form',
  519. padding: 20,
  520. labelGap: 30,
  521. spacing: 10,
  522. items: [
  523. {
  524. type: 'textbox',
  525. name: 'find',
  526. size: 40,
  527. label: 'Find',
  528. value: selectedText
  529. },
  530. {
  531. type: 'textbox',
  532. name: 'replace',
  533. size: 40,
  534. label: 'Replace with'
  535. },
  536. {
  537. type: 'checkbox',
  538. name: 'case',
  539. text: 'Match case',
  540. label: ' '
  541. },
  542. {
  543. type: 'checkbox',
  544. name: 'words',
  545. text: 'Whole words',
  546. label: ' '
  547. }
  548. ]
  549. }
  550. });
  551. };
  552. var Dialog = { open: open };
  553. var register = function (editor, currentIndexState) {
  554. editor.addCommand('SearchReplace', function () {
  555. Dialog.open(editor, currentIndexState);
  556. });
  557. };
  558. var Commands = { register: register };
  559. var showDialog = function (editor, currentIndexState) {
  560. return function () {
  561. Dialog.open(editor, currentIndexState);
  562. };
  563. };
  564. var register$1 = function (editor, currentIndexState) {
  565. editor.addMenuItem('searchreplace', {
  566. text: 'Find and replace',
  567. shortcut: 'Meta+F',
  568. onclick: showDialog(editor, currentIndexState),
  569. separator: 'before',
  570. context: 'edit'
  571. });
  572. editor.addButton('searchreplace', {
  573. tooltip: 'Find and replace',
  574. onclick: showDialog(editor, currentIndexState)
  575. });
  576. editor.shortcuts.add('Meta+F', '', showDialog(editor, currentIndexState));
  577. };
  578. var Buttons = { register: register$1 };
  579. global.add('searchreplace', function (editor) {
  580. var currentIndexState = Cell(-1);
  581. Commands.register(editor, currentIndexState);
  582. Buttons.register(editor, currentIndexState);
  583. return Api.get(editor, currentIndexState);
  584. });
  585. function Plugin () {
  586. }
  587. return Plugin;
  588. }());
  589. })();