PluginProbe ʕ •ᴥ•ʔ
Advanced Editor Tools / 4.6.7
Advanced Editor Tools v4.6.7
trunk 1.0 1.0.1 2.0 2.2 3.0 3.0.1 3.1 3.2 3.2.4 3.2.7 3.3.9 3.3.9.1 3.3.9.2 3.4.2 3.4.2.1 3.4.5 3.4.5.1 3.4.9 3.5.8 3.5.9 3.5.9.1 4.0 4.0.1 4.0.2 4.1 4.1.1 4.1.7 4.1.9 4.2.3 4.2.3.1 4.2.5 4.2.8 4.3.10 4.3.10.1 4.3.8 4.4.1 4.4.3 4.5.6 4.6.3 4.6.7 4.7.11 4.8.0 4.8.1 4.8.2 5.0.0 5.0.1 5.1.0 5.2 5.2.1 5.3 5.4.0 5.5.0 5.5.1 5.6.0 5.9.0 5.9.1 5.9.2
tinymce-advanced / mce / searchreplace / plugin.js
tinymce-advanced / mce / searchreplace Last commit date
plugin.js 8 years ago plugin.min.js 8 years ago
plugin.js
787 lines
1 (function () {
2
3 var defs = {}; // id -> {dependencies, definition, instance (possibly undefined)}
4
5 // Used when there is no 'main' module.
6 // The name is probably (hopefully) unique so minification removes for releases.
7 var register_3795 = function (id) {
8 var module = dem(id);
9 var fragments = id.split('.');
10 var target = Function('return this;')();
11 for (var i = 0; i < fragments.length - 1; ++i) {
12 if (target[fragments[i]] === undefined)
13 target[fragments[i]] = {};
14 target = target[fragments[i]];
15 }
16 target[fragments[fragments.length - 1]] = module;
17 };
18
19 var instantiate = function (id) {
20 var actual = defs[id];
21 var dependencies = actual.deps;
22 var definition = actual.defn;
23 var len = dependencies.length;
24 var instances = new Array(len);
25 for (var i = 0; i < len; ++i)
26 instances[i] = dem(dependencies[i]);
27 var defResult = definition.apply(null, instances);
28 if (defResult === undefined)
29 throw 'module [' + id + '] returned undefined';
30 actual.instance = defResult;
31 };
32
33 var def = function (id, dependencies, definition) {
34 if (typeof id !== 'string')
35 throw 'module id must be a string';
36 else if (dependencies === undefined)
37 throw 'no dependencies for ' + id;
38 else if (definition === undefined)
39 throw 'no definition function for ' + id;
40 defs[id] = {
41 deps: dependencies,
42 defn: definition,
43 instance: undefined
44 };
45 };
46
47 var dem = function (id) {
48 var actual = defs[id];
49 if (actual === undefined)
50 throw 'module [' + id + '] was undefined';
51 else if (actual.instance === undefined)
52 instantiate(id);
53 return actual.instance;
54 };
55
56 var req = function (ids, callback) {
57 var len = ids.length;
58 var instances = new Array(len);
59 for (var i = 0; i < len; ++i)
60 instances.push(dem(ids[i]));
61 callback.apply(null, callback);
62 };
63
64 var ephox = {};
65
66 ephox.bolt = {
67 module: {
68 api: {
69 define: def,
70 require: req,
71 demand: dem
72 }
73 }
74 };
75
76 var define = def;
77 var require = req;
78 var demand = dem;
79 // this helps with minificiation when using a lot of global references
80 var defineGlobal = function (id, ref) {
81 define(id, [], function () { return ref; });
82 };
83 /*jsc
84 ["tinymce.plugins.searchreplace.Plugin","tinymce.core.PluginManager","tinymce.core.util.Tools","tinymce.plugins.searchreplace.core.FindReplaceText","global!tinymce.util.Tools.resolve"]
85 jsc*/
86 defineGlobal("global!tinymce.util.Tools.resolve", tinymce.util.Tools.resolve);
87 /**
88 * ResolveGlobal.js
89 *
90 * Released under LGPL License.
91 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
92 *
93 * License: http://www.tinymce.com/license
94 * Contributing: http://www.tinymce.com/contributing
95 */
96
97 define(
98 'tinymce.core.PluginManager',
99 [
100 'global!tinymce.util.Tools.resolve'
101 ],
102 function (resolve) {
103 return resolve('tinymce.PluginManager');
104 }
105 );
106
107 /**
108 * ResolveGlobal.js
109 *
110 * Released under LGPL License.
111 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
112 *
113 * License: http://www.tinymce.com/license
114 * Contributing: http://www.tinymce.com/contributing
115 */
116
117 define(
118 'tinymce.core.util.Tools',
119 [
120 'global!tinymce.util.Tools.resolve'
121 ],
122 function (resolve) {
123 return resolve('tinymce.util.Tools');
124 }
125 );
126
127 /**
128 * FindReplaceText.js
129 *
130 * Released under LGPL License.
131 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
132 *
133 * License: http://www.tinymce.com/license
134 * Contributing: http://www.tinymce.com/contributing
135 */
136
137 /*jshint smarttabs:true, undef:true, unused:true, latedef:true, curly:true, bitwise:true */
138 /*eslint no-labels:0, no-constant-condition: 0 */
139
140 define(
141 'tinymce.plugins.searchreplace.core.FindReplaceText',
142 [
143 ],
144 function () {
145 function isContentEditableFalse(node) {
146 return node && node.nodeType == 1 && node.contentEditable === "false";
147 }
148
149 // Based on work developed by: James Padolsey http://james.padolsey.com
150 // released under UNLICENSE that is compatible with LGPL
151 // TODO: Handle contentEditable edgecase:
152 // <p>text<span contentEditable="false">text<span contentEditable="true">text</span>text</span>text</p>
153 function findAndReplaceDOMText(regex, node, replacementNode, captureGroup, schema) {
154 var m, matches = [], text, count = 0, doc;
155 var blockElementsMap, hiddenTextElementsMap, shortEndedElementsMap;
156
157 doc = node.ownerDocument;
158 blockElementsMap = schema.getBlockElements(); // H1-H6, P, TD etc
159 hiddenTextElementsMap = schema.getWhiteSpaceElements(); // TEXTAREA, PRE, STYLE, SCRIPT
160 shortEndedElementsMap = schema.getShortEndedElements(); // BR, IMG, INPUT
161
162 function getMatchIndexes(m, captureGroup) {
163 captureGroup = captureGroup || 0;
164
165 if (!m[0]) {
166 throw 'findAndReplaceDOMText cannot handle zero-length matches';
167 }
168
169 var index = m.index;
170
171 if (captureGroup > 0) {
172 var cg = m[captureGroup];
173
174 if (!cg) {
175 throw 'Invalid capture group';
176 }
177
178 index += m[0].indexOf(cg);
179 m[0] = cg;
180 }
181
182 return [index, index + m[0].length, [m[0]]];
183 }
184
185 function getText(node) {
186 var txt;
187
188 if (node.nodeType === 3) {
189 return node.data;
190 }
191
192 if (hiddenTextElementsMap[node.nodeName] && !blockElementsMap[node.nodeName]) {
193 return '';
194 }
195
196 txt = '';
197
198 if (isContentEditableFalse(node)) {
199 return '\n';
200 }
201
202 if (blockElementsMap[node.nodeName] || shortEndedElementsMap[node.nodeName]) {
203 txt += '\n';
204 }
205
206 if ((node = node.firstChild)) {
207 do {
208 txt += getText(node);
209 } while ((node = node.nextSibling));
210 }
211
212 return txt;
213 }
214
215 function stepThroughMatches(node, matches, replaceFn) {
216 var startNode, endNode, startNodeIndex,
217 endNodeIndex, innerNodes = [], atIndex = 0, curNode = node,
218 matchLocation = matches.shift(), matchIndex = 0;
219
220 out: while (true) {
221 if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName] || isContentEditableFalse(curNode)) {
222 atIndex++;
223 }
224
225 if (curNode.nodeType === 3) {
226 if (!endNode && curNode.length + atIndex >= matchLocation[1]) {
227 // We've found the ending
228 endNode = curNode;
229 endNodeIndex = matchLocation[1] - atIndex;
230 } else if (startNode) {
231 // Intersecting node
232 innerNodes.push(curNode);
233 }
234
235 if (!startNode && curNode.length + atIndex > matchLocation[0]) {
236 // We've found the match start
237 startNode = curNode;
238 startNodeIndex = matchLocation[0] - atIndex;
239 }
240
241 atIndex += curNode.length;
242 }
243
244 if (startNode && endNode) {
245 curNode = replaceFn({
246 startNode: startNode,
247 startNodeIndex: startNodeIndex,
248 endNode: endNode,
249 endNodeIndex: endNodeIndex,
250 innerNodes: innerNodes,
251 match: matchLocation[2],
252 matchIndex: matchIndex
253 });
254
255 // replaceFn has to return the node that replaced the endNode
256 // and then we step back so we can continue from the end of the
257 // match:
258 atIndex -= (endNode.length - endNodeIndex);
259 startNode = null;
260 endNode = null;
261 innerNodes = [];
262 matchLocation = matches.shift();
263 matchIndex++;
264
265 if (!matchLocation) {
266 break; // no more matches
267 }
268 } else if ((!hiddenTextElementsMap[curNode.nodeName] || blockElementsMap[curNode.nodeName]) && curNode.firstChild) {
269 if (!isContentEditableFalse(curNode)) {
270 // Move down
271 curNode = curNode.firstChild;
272 continue;
273 }
274 } else if (curNode.nextSibling) {
275 // Move forward:
276 curNode = curNode.nextSibling;
277 continue;
278 }
279
280 // Move forward or up:
281 while (true) {
282 if (curNode.nextSibling) {
283 curNode = curNode.nextSibling;
284 break;
285 } else if (curNode.parentNode !== node) {
286 curNode = curNode.parentNode;
287 } else {
288 break out;
289 }
290 }
291 }
292 }
293
294 /**
295 * Generates the actual replaceFn which splits up text nodes
296 * and inserts the replacement element.
297 */
298 function genReplacer(nodeName) {
299 var makeReplacementNode;
300
301 if (typeof nodeName != 'function') {
302 var stencilNode = nodeName.nodeType ? nodeName : doc.createElement(nodeName);
303
304 makeReplacementNode = function (fill, matchIndex) {
305 var clone = stencilNode.cloneNode(false);
306
307 clone.setAttribute('data-mce-index', matchIndex);
308
309 if (fill) {
310 clone.appendChild(doc.createTextNode(fill));
311 }
312
313 return clone;
314 };
315 } else {
316 makeReplacementNode = nodeName;
317 }
318
319 return function (range) {
320 var before, after, parentNode, startNode = range.startNode,
321 endNode = range.endNode, matchIndex = range.matchIndex;
322
323 if (startNode === endNode) {
324 var node = startNode;
325
326 parentNode = node.parentNode;
327 if (range.startNodeIndex > 0) {
328 // Add `before` text node (before the match)
329 before = doc.createTextNode(node.data.substring(0, range.startNodeIndex));
330 parentNode.insertBefore(before, node);
331 }
332
333 // Create the replacement node:
334 var el = makeReplacementNode(range.match[0], matchIndex);
335 parentNode.insertBefore(el, node);
336 if (range.endNodeIndex < node.length) {
337 // Add `after` text node (after the match)
338 after = doc.createTextNode(node.data.substring(range.endNodeIndex));
339 parentNode.insertBefore(after, node);
340 }
341
342 node.parentNode.removeChild(node);
343
344 return el;
345 }
346
347 // Replace startNode -> [innerNodes...] -> endNode (in that order)
348 before = doc.createTextNode(startNode.data.substring(0, range.startNodeIndex));
349 after = doc.createTextNode(endNode.data.substring(range.endNodeIndex));
350 var elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex);
351 var innerEls = [];
352
353 for (var i = 0, l = range.innerNodes.length; i < l; ++i) {
354 var innerNode = range.innerNodes[i];
355 var innerEl = makeReplacementNode(innerNode.data, matchIndex);
356 innerNode.parentNode.replaceChild(innerEl, innerNode);
357 innerEls.push(innerEl);
358 }
359
360 var elB = makeReplacementNode(endNode.data.substring(0, range.endNodeIndex), matchIndex);
361
362 parentNode = startNode.parentNode;
363 parentNode.insertBefore(before, startNode);
364 parentNode.insertBefore(elA, startNode);
365 parentNode.removeChild(startNode);
366
367 parentNode = endNode.parentNode;
368 parentNode.insertBefore(elB, endNode);
369 parentNode.insertBefore(after, endNode);
370 parentNode.removeChild(endNode);
371
372 return elB;
373 };
374 }
375
376 text = getText(node);
377 if (!text) {
378 return;
379 }
380
381 if (regex.global) {
382 while ((m = regex.exec(text))) {
383 matches.push(getMatchIndexes(m, captureGroup));
384 }
385 } else {
386 m = text.match(regex);
387 matches.push(getMatchIndexes(m, captureGroup));
388 }
389
390 if (matches.length) {
391 count = matches.length;
392 stepThroughMatches(node, matches, genReplacer(replacementNode));
393 }
394
395 return count;
396 }
397
398 return {
399 findAndReplaceDOMText: findAndReplaceDOMText
400 };
401 }
402 );
403 /**
404 * Plugin.js
405 *
406 * Released under LGPL License.
407 * Copyright (c) 1999-2017 Ephox Corp. All rights reserved
408 *
409 * License: http://www.tinymce.com/license
410 * Contributing: http://www.tinymce.com/contributing
411 */
412
413 /*jshint smarttabs:true, undef:true, unused:true, latedef:true, curly:true, bitwise:true */
414 /*eslint no-labels:0, no-constant-condition: 0 */
415
416 /**
417 * This class contains all core logic for the searchreplace plugin.
418 *
419 * @class tinymce.searchreplace.Plugin
420 * @private
421 */
422 define(
423 'tinymce.plugins.searchreplace.Plugin',
424 [
425 'tinymce.core.PluginManager',
426 'tinymce.core.util.Tools',
427 'tinymce.plugins.searchreplace.core.FindReplaceText'
428 ],
429 function (PluginManager, Tools, FindReplaceText) {
430 function Plugin(editor) {
431 var self = this, currentIndex = -1;
432
433 function showDialog() {
434 var last = {}, selectedText;
435
436 selectedText = Tools.trim(editor.selection.getContent({ format: 'text' }));
437
438 function updateButtonStates() {
439 win.statusbar.find('#next').disabled(!findSpansByIndex(currentIndex + 1).length);
440 win.statusbar.find('#prev').disabled(!findSpansByIndex(currentIndex - 1).length);
441 }
442
443 function notFoundAlert() {
444 editor.windowManager.alert('Could not find the specified string.', function () {
445 win.find('#find')[0].focus();
446 });
447 }
448
449 var win = editor.windowManager.open({
450 layout: "flex",
451 pack: "center",
452 align: "center",
453 onClose: function () {
454 editor.focus();
455 self.done();
456 },
457 onSubmit: function (e) {
458 var count, caseState, text, wholeWord;
459
460 e.preventDefault();
461
462 caseState = win.find('#case').checked();
463 wholeWord = win.find('#words').checked();
464
465 text = win.find('#find').value();
466 if (!text.length) {
467 self.done(false);
468 win.statusbar.items().slice(1).disabled(true);
469 return;
470 }
471
472 if (last.text == text && last.caseState == caseState && last.wholeWord == wholeWord) {
473 if (findSpansByIndex(currentIndex + 1).length === 0) {
474 notFoundAlert();
475 return;
476 }
477
478 self.next();
479 updateButtonStates();
480 return;
481 }
482
483 count = self.find(text, caseState, wholeWord);
484 if (!count) {
485 notFoundAlert();
486 }
487
488 win.statusbar.items().slice(1).disabled(count === 0);
489 updateButtonStates();
490
491 last = {
492 text: text,
493 caseState: caseState,
494 wholeWord: wholeWord
495 };
496 },
497 buttons: [
498 {
499 text: "Find", subtype: 'primary', onclick: function () {
500 win.submit();
501 }
502 },
503 {
504 text: "Replace", disabled: true, onclick: function () {
505 if (!self.replace(win.find('#replace').value())) {
506 win.statusbar.items().slice(1).disabled(true);
507 currentIndex = -1;
508 last = {};
509 }
510 }
511 },
512 {
513 text: "Replace all", disabled: true, onclick: function () {
514 self.replace(win.find('#replace').value(), true, true);
515 win.statusbar.items().slice(1).disabled(true);
516 last = {};
517 }
518 },
519 { type: "spacer", flex: 1 },
520 {
521 text: "Prev", name: 'prev', disabled: true, onclick: function () {
522 self.prev();
523 updateButtonStates();
524 }
525 },
526 {
527 text: "Next", name: 'next', disabled: true, onclick: function () {
528 self.next();
529 updateButtonStates();
530 }
531 }
532 ],
533 title: "Find and replace",
534 items: {
535 type: "form",
536 padding: 20,
537 labelGap: 30,
538 spacing: 10,
539 items: [
540 { type: 'textbox', name: 'find', size: 40, label: 'Find', value: selectedText },
541 { type: 'textbox', name: 'replace', size: 40, label: 'Replace with' },
542 { type: 'checkbox', name: 'case', text: 'Match case', label: ' ' },
543 { type: 'checkbox', name: 'words', text: 'Whole words', label: ' ' }
544 ]
545 }
546 });
547 }
548
549 self.init = function (ed) {
550 ed.addMenuItem('searchreplace', {
551 text: 'Find and replace',
552 shortcut: 'Meta+F',
553 onclick: showDialog,
554 separator: 'before',
555 context: 'edit'
556 });
557
558 ed.addButton('searchreplace', {
559 tooltip: 'Find and replace',
560 shortcut: 'Meta+F',
561 onclick: showDialog
562 });
563
564 ed.addCommand("SearchReplace", showDialog);
565 ed.shortcuts.add('Meta+F', '', showDialog);
566 };
567
568 function getElmIndex(elm) {
569 var value = elm.getAttribute('data-mce-index');
570
571 if (typeof value == "number") {
572 return "" + value;
573 }
574
575 return value;
576 }
577
578 function markAllMatches(regex) {
579 var node, marker;
580
581 marker = editor.dom.create('span', {
582 "data-mce-bogus": 1
583 });
584
585 marker.className = 'mce-match-marker'; // IE 7 adds class="mce-match-marker" and class=mce-match-marker
586 node = editor.getBody();
587
588 self.done(false);
589
590 return FindReplaceText.findAndReplaceDOMText(regex, node, marker, false, editor.schema);
591 }
592
593 function unwrap(node) {
594 var parentNode = node.parentNode;
595
596 if (node.firstChild) {
597 parentNode.insertBefore(node.firstChild, node);
598 }
599
600 node.parentNode.removeChild(node);
601 }
602
603 function findSpansByIndex(index) {
604 var nodes, spans = [];
605
606 nodes = Tools.toArray(editor.getBody().getElementsByTagName('span'));
607 if (nodes.length) {
608 for (var i = 0; i < nodes.length; i++) {
609 var nodeIndex = getElmIndex(nodes[i]);
610
611 if (nodeIndex === null || !nodeIndex.length) {
612 continue;
613 }
614
615 if (nodeIndex === index.toString()) {
616 spans.push(nodes[i]);
617 }
618 }
619 }
620
621 return spans;
622 }
623
624 function moveSelection(forward) {
625 var testIndex = currentIndex, dom = editor.dom;
626
627 forward = forward !== false;
628
629 if (forward) {
630 testIndex++;
631 } else {
632 testIndex--;
633 }
634
635 dom.removeClass(findSpansByIndex(currentIndex), 'mce-match-marker-selected');
636
637 var spans = findSpansByIndex(testIndex);
638 if (spans.length) {
639 dom.addClass(findSpansByIndex(testIndex), 'mce-match-marker-selected');
640 editor.selection.scrollIntoView(spans[0]);
641 return testIndex;
642 }
643
644 return -1;
645 }
646
647 function removeNode(node) {
648 var dom = editor.dom, parent = node.parentNode;
649
650 dom.remove(node);
651
652 if (dom.isEmpty(parent)) {
653 dom.remove(parent);
654 }
655 }
656
657 self.find = function (text, matchCase, wholeWord) {
658 text = text.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
659 text = text.replace(/\s/g, '\\s');
660 text = wholeWord ? '\\b' + text + '\\b' : text;
661
662 var count = markAllMatches(new RegExp(text, matchCase ? 'g' : 'gi'));
663
664 if (count) {
665 currentIndex = -1;
666 currentIndex = moveSelection(true);
667 }
668
669 return count;
670 };
671
672 self.next = function () {
673 var index = moveSelection(true);
674
675 if (index !== -1) {
676 currentIndex = index;
677 }
678 };
679
680 self.prev = function () {
681 var index = moveSelection(false);
682
683 if (index !== -1) {
684 currentIndex = index;
685 }
686 };
687
688 function isMatchSpan(node) {
689 var matchIndex = getElmIndex(node);
690
691 return matchIndex !== null && matchIndex.length > 0;
692 }
693
694 self.replace = function (text, forward, all) {
695 var i, nodes, node, matchIndex, currentMatchIndex, nextIndex = currentIndex, hasMore;
696
697 forward = forward !== false;
698
699 node = editor.getBody();
700 nodes = Tools.grep(Tools.toArray(node.getElementsByTagName('span')), isMatchSpan);
701 for (i = 0; i < nodes.length; i++) {
702 var nodeIndex = getElmIndex(nodes[i]);
703
704 matchIndex = currentMatchIndex = parseInt(nodeIndex, 10);
705 if (all || matchIndex === currentIndex) {
706 if (text.length) {
707 nodes[i].firstChild.nodeValue = text;
708 unwrap(nodes[i]);
709 } else {
710 removeNode(nodes[i]);
711 }
712
713 while (nodes[++i]) {
714 matchIndex = parseInt(getElmIndex(nodes[i]), 10);
715
716 if (matchIndex === currentMatchIndex) {
717 removeNode(nodes[i]);
718 } else {
719 i--;
720 break;
721 }
722 }
723
724 if (forward) {
725 nextIndex--;
726 }
727 } else if (currentMatchIndex > currentIndex) {
728 nodes[i].setAttribute('data-mce-index', currentMatchIndex - 1);
729 }
730 }
731
732 editor.undoManager.add();
733 currentIndex = nextIndex;
734
735 if (forward) {
736 hasMore = findSpansByIndex(nextIndex + 1).length > 0;
737 self.next();
738 } else {
739 hasMore = findSpansByIndex(nextIndex - 1).length > 0;
740 self.prev();
741 }
742
743 return !all && hasMore;
744 };
745
746 self.done = function (keepEditorSelection) {
747 var i, nodes, startContainer, endContainer;
748
749 nodes = Tools.toArray(editor.getBody().getElementsByTagName('span'));
750 for (i = 0; i < nodes.length; i++) {
751 var nodeIndex = getElmIndex(nodes[i]);
752
753 if (nodeIndex !== null && nodeIndex.length) {
754 if (nodeIndex === currentIndex.toString()) {
755 if (!startContainer) {
756 startContainer = nodes[i].firstChild;
757 }
758
759 endContainer = nodes[i].firstChild;
760 }
761
762 unwrap(nodes[i]);
763 }
764 }
765
766 if (startContainer && endContainer) {
767 var rng = editor.dom.createRng();
768 rng.setStart(startContainer, 0);
769 rng.setEnd(endContainer, endContainer.data.length);
770
771 if (keepEditorSelection !== false) {
772 editor.selection.setRng(rng);
773 }
774
775 return rng;
776 }
777 };
778 }
779
780 PluginManager.add('searchreplace', Plugin);
781
782 return function () { };
783 }
784 );
785 dem('tinymce.plugins.searchreplace.Plugin')();
786 })();
787