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