7015d4c69aa8fe232b347dca05cf7b17c37ab4c1
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / htmlarea / plugins / FindReplace / find-replace.js
1 /***************************************************************
2 * Copyright notice
3 *
4 * (c) 2004 Cau guanabara <caugb@ibest.com.br>
5 * (c) 2005-2010 Stanislas Rolland <typo3(arobas)sjbr.ca>
6 * All rights reserved
7 *
8 * This script is part of the TYPO3 project. The TYPO3 project is
9 * free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
16 * A copy is found in the textfile GPL.txt and important notices to the license
17 * from the author is found in LICENSE.txt distributed with these scripts.
18 *
19 *
20 * This script is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU General Public License for more details.
24 *
25 * This script is a modified version of a script published under the htmlArea License.
26 * A copy of the htmlArea License may be found in the textfile HTMLAREA_LICENSE.txt.
27 *
28 * This copyright notice MUST APPEAR in all copies of the script!
29 ***************************************************************/
30 /*
31 * Find and Replace Plugin for TYPO3 htmlArea RTE
32 *
33 * TYPO3 SVN ID: $Id$
34 */
35 HTMLArea.FindReplace = HTMLArea.Plugin.extend({
36 constructor: function(editor, pluginName) {
37 this.base(editor, pluginName);
38 },
39 /*
40 * This function gets called by the class constructor
41 */
42 configurePlugin: function(editor) {
43 /*
44 * Registering plugin "About" information
45 */
46 var pluginInformation = {
47 version : '2.0',
48 developer : 'Cau Guanabara & Stanislas Rolland',
49 developerUrl : 'http://www.sjbr.ca',
50 copyrightOwner : 'Cau Guanabara & Stanislas Rolland',
51 sponsor : 'Independent production & SJBR',
52 sponsorUrl : 'http://www.sjbr.ca',
53 license : 'GPL'
54 };
55 this.registerPluginInformation(pluginInformation);
56 /*
57 * Registering the button
58 */
59 var buttonId = 'FindReplace';
60 var buttonConfiguration = {
61 id : buttonId,
62 tooltip : this.localize('Find and Replace'),
63 iconCls : 'htmlarea-action-find-replace',
64 action : 'onButtonPress',
65 dialog : true
66 };
67 this.registerButton(buttonConfiguration);
68 // Compile regular expression to clean up marks
69 this.marksCleaningRE = /(<span\s+[^>]*id="?htmlarea-frmark[^>]*"?>)([^<>]*)(<\/span>)/gi;
70 return true;
71 },
72 /*
73 * This function gets called when the 'Find & Replace' button is pressed.
74 *
75 * @param object editor: the editor instance
76 * @param string id: the button id or the key
77 *
78 * @return boolean false if action is completed
79 */
80 onButtonPress: function (editor, id, target) {
81 // Could be a button or its hotkey
82 var buttonId = this.translateHotKey(id);
83 buttonId = buttonId ? buttonId : id;
84 // Initialize search variables
85 this.buffer = null;
86 this.initVariables();
87 // Disable the toolbar undo/redo buttons and snapshots while this window is opened
88 var plugin = this.getPluginInstance('UndoRedo');
89 if (plugin) {
90 plugin.stop();
91 var undo = this.getButton('Undo');
92 if (undo) {
93 undo.setDisabled(true);
94 }
95 var redo = this.getButton('Redo');
96 if (redo) {
97 redo.setDisabled(true);
98 }
99 }
100 // Open dialogue window
101 this.openDialogue(
102 buttonId,
103 'Find and Replace',
104 this.getWindowDimensions(
105 {
106 width: 410,
107 height:360
108 },
109 buttonId
110 )
111 );
112 return false;
113 },
114 /*
115 * Open the dialogue window
116 *
117 * @param string buttonId: the button id
118 * @param string title: the window title
119 * @param integer dimensions: the opening width of the window
120 *
121 * @return void
122 */
123 openDialogue: function (buttonId, title, dimensions) {
124 this.dialog = new Ext.Window({
125 title: this.localize(title),
126 cls: 'htmlarea-window',
127 border: false,
128 width: dimensions.width,
129 height: 'auto',
130 // As of ExtJS 3.1, JS error with IE when the window is resizable
131 resizable: !Ext.isIE,
132 iconCls: this.getButton(buttonId).iconCls,
133 listeners: {
134 close: {
135 fn: this.onClose,
136 scope: this
137 }
138 },
139 items: [{
140 xtype: 'fieldset',
141 defaultType: 'textfield',
142 labelWidth: 100,
143 defaults: {
144 labelSeparator: '',
145 width: 250,
146 listeners: {
147 change: {
148 fn: this.clearDoc,
149 scope: this
150 }
151 }
152 },
153 listeners: {
154 render: {
155 fn: this.initPattern,
156 scope: this
157 }
158 },
159 items: [{
160 itemId: 'pattern',
161 fieldLabel: this.localize('Search for:')
162 },{
163 itemId: 'replacement',
164 fieldLabel: this.localize('Replace with:')
165 }
166 ]
167 },{
168 xtype: 'fieldset',
169 defaultType: 'checkbox',
170 title: this.localize('Options'),
171 labelWidth: 150,
172 items: [{
173 itemId: 'words',
174 fieldLabel: this.localize('Whole words only'),
175 listeners: {
176 check: {
177 fn: this.clearDoc,
178 scope: this
179 }
180 }
181 },{
182 itemId: 'matchCase',
183 fieldLabel: this.localize('Case sensitive search'),
184 listeners: {
185 check: {
186 fn: this.clearDoc,
187 scope: this
188 }
189 }
190 },{
191 itemId: 'replaceAll',
192 fieldLabel: this.localize('Substitute all occurrences'),
193 listeners: {
194 check: {
195 fn: this.requestReplacement,
196 scope: this
197 }
198 }
199 }
200 ]
201 },{
202 xtype: 'fieldset',
203 defaultType: 'button',
204 title: this.localize('Actions'),
205 defaults: {
206 minWidth: 150,
207 disabled: true,
208 style: {
209 marginBottom: '5px'
210 }
211 },
212 items: [{
213 text: this.localize('Clear'),
214 itemId: 'clear',
215 listeners: {
216 click: {
217 fn: this.clearMarks,
218 scope: this
219 }
220 }
221 },{
222 text: this.localize('Highlight'),
223 itemId: 'hiliteall',
224 listeners: {
225 click: {
226 fn: this.hiliteAll,
227 scope: this
228 }
229 }
230 },{
231 text: this.localize('Undo'),
232 itemId: 'undo',
233 listeners: {
234 click: {
235 fn: this.resetContents,
236 scope: this
237 }
238 }
239 }
240 ]
241 }
242 ],
243 buttons: [
244 this.buildButtonConfig('Next', this.onNext),
245 this.buildButtonConfig('Done', this.onCancel)
246 ]
247 });
248 this.show();
249 },
250 /*
251 * Handler invoked to initialize the pattern to search
252 *
253 * @param object fieldset: the fieldset component
254 *
255 * @return void
256 */
257 initPattern: function (fieldset) {
258 var selection = this.editor.getSelectedHTML();
259 if (/\S/.test(selection)) {
260 selection = selection.replace(/<[^>]*>/g, '');
261 selection = selection.replace(/&nbsp;/g, '');
262 }
263 if (/\S/.test(selection)) {
264 fieldset.getComponent('pattern').setValue(selection);
265 fieldset.getComponent('replacement').focus();
266 } else {
267 fieldset.getComponent('pattern').focus();
268 }
269 },
270 /*
271 * Handler invoked when the replace all checkbox is checked
272 */
273 requestReplacement: function () {
274 if (!this.dialog.find('itemId', 'replacement')[0].getValue() && this.dialog.find('itemId', 'replaceAll')[0].getValue()) {
275 Ext.MessageBox.alert('', this.localize('Inform a replacement word'));
276 }
277 this.clearDoc();
278 },
279 /*
280 * Handler invoked when the 'Next' button is pressed
281 */
282 onNext: function () {
283 if (!this.dialog.find('itemId', 'pattern')[0].getValue()) {
284 Ext.MessageBox.alert('', this.localize('Enter the text you want to find'));
285 this.dialog.find('itemId', 'pattern')[0].focus();
286 return false;
287 }
288 var fields = [
289 'pattern',
290 'replacement',
291 'words',
292 'matchCase',
293 'replaceAll'
294 ];
295 var params = {};
296 Ext.each(fields, function (field) {
297 params[field] = this.dialog.find('itemId', field)[0].getValue();
298 }, this);
299 this.search(params);
300 return false;
301 },
302 /*
303 * Search the pattern and insert span tags
304 *
305 * @param object params: the parameters of the search corresponding to the values of fields:
306 * pattern
307 * replacement
308 * words
309 * matchCase
310 * replaceAll
311 *
312 * @return void
313 */
314 search: function (params) {
315 var html = this.editor.getInnerHTML();
316 if (this.buffer == null) {
317 this.buffer = html;
318 }
319 if (this.matches == 0) {
320 var pattern = new RegExp(params.words ? '(?!<[^>]*)(\\b' + params.pattern + '\\b)(?![^<]*>)' : '(?!<[^>]*)(' + params.pattern + ')(?![^<]*>)', 'g' + (params.matchCase? '' : 'i'));
321 this.editor.setHTML(html.replace(pattern, '<span id="htmlarea-frmark">' + "$1" + '</span>'));
322 Ext.each(this.editor.document.body.getElementsByTagName('span'), function (mark) {
323 if (/^htmlarea-frmark/.test(mark.id)) {
324 this.spans.push(mark);
325 }
326 }, this);
327 }
328 this.spanWalker(params.pattern, params.replacement, params.replaceAll);
329 },
330 /*
331 * Walk the span tags
332 *
333 * @param string pattern: the pattern being searched for
334 * @param string replacement: the replacement string
335 * @param bolean replaceAll: true if all occurrences should be replaced
336 *
337 * @return void
338 */
339 spanWalker: function (pattern, replacement, replaceAll) {
340 this.clearMarks();
341 if (this.spans.length) {
342 Ext.each(this.spans, function (mark, i) {
343 if (i >= this.matches && !/[0-9]$/.test(mark.id)) {
344 this.matches++;
345 this.disableActions('clear', false);
346 mark.id = 'htmlarea-frmark_' + this.matches;
347 mark.style.color = 'white';
348 mark.style.backgroundColor = 'highlight';
349 mark.style.fontWeight = 'bold';
350 mark.scrollIntoView(false);
351 var self = this;
352 function replace(button) {
353 if (button == 'yes') {
354 mark.firstChild.replaceData(0, mark.firstChild.data.length, replacement);
355 self.replaces++;
356 self.disableActions('undo', false);
357 }
358 self.endWalk(pattern, i);
359 }
360 if (replaceAll) {
361 replace('yes');
362 return true;
363 } else {
364 Ext.MessageBox.confirm('', this.localize('Substitute this occurrence?'), replace, this);
365 return false;
366 }
367 }
368 }, this);
369 } else {
370 this.endWalk(pattern, 0);
371 }
372 },
373 /*
374 * End the replacement walk
375 *
376 * @param string pattern: the pattern being searched for
377 * @param integer index: the index reached in the walk
378 *
379 * @return void
380 */
381 endWalk: function (pattern, index) {
382 if (index >= this.spans.length - 1 || !this.spans.length) {
383 var message = this.localize('Done') + ':<br /><br />';
384 if (this.matches > 0) {
385 if (this.matches == 1) {
386 message += this.matches + ' ' + this.localize('found item');
387 } else {
388 message += this.matches + ' ' + this.localize('found items');
389 }
390 if (this.replaces > 0) {
391 if (this.replaces == 1) {
392 message += ',<br />' + this.replaces + ' ' + this.localize('replaced item');
393 } else {
394 message += ',<br />' + this.replaces + ' ' + this.localize('replaced items');
395 }
396 }
397 this.hiliteAll();
398 } else {
399 message += '"' + pattern + '" ' + this.localize('not found');
400 this.disableActions('hiliteall,clear', true);
401 }
402 Ext.MessageBox.minWidth = 300;
403 Ext.MessageBox.alert('', message + '.');
404 }
405 },
406 /*
407 * Remove all marks
408 */
409 clearDoc: function () {
410 this.editor.setHTML(this.editor.getInnerHTML().replace(this.marksCleaningRE, "$2"));
411 this.initVariables();
412 this.disableActions('hiliteall,clear', true);
413 },
414 /*
415 * De-highlight all marks
416 */
417 clearMarks: function () {
418 Ext.each(this.editor.document.body.getElementsByTagName('span'), function (mark) {
419 if (/^htmlarea-frmark/.test(mark.id)) {
420 mark.style.backgroundColor = '';
421 mark.style.color = '';
422 mark.style.fontWeight = '';
423 }
424 }, this);
425 this.disableActions('hiliteall', false);
426 this.disableActions('clear', true);
427 },
428 /*
429 * Highlight all marks
430 */
431 hiliteAll: function () {
432 Ext.each(this.editor.document.body.getElementsByTagName('span'), function (mark) {
433 if (/^htmlarea-frmark/.test(mark.id)) {
434 mark.style.backgroundColor = 'highlight';
435 mark.style.color = 'white';
436 mark.style.fontWeight = 'bold';
437 }
438 }, this);
439 this.disableActions('clear', false);
440 this.disableActions('hiliteall', true);
441 },
442 /*
443 * Undo the replace operation
444 */
445 resetContents: function () {
446 if (this.buffer != null) {
447 var transp = this.editor.getInnerHTML();
448 this.editor.setHTML(this.buffer);
449 this.buffer = transp;
450 this.disableActions('clear', true);
451 }
452 },
453 /*
454 * Disable action buttons
455 *
456 * @param array actions: array of buttonIds to set disabled/enabled
457 * @param boolean disabled: true to set disabled
458 */
459 disableActions: function (actions, disabled) {
460 Ext.each(actions.split(/[,; ]+/), function (action) {
461 this.dialog.find('itemId', action)[0].setDisabled(disabled);
462 }, this);
463 },
464 /*
465 * Initialize find & replace variables
466 */
467 initVariables: function () {
468 this.matches = 0;
469 this.replaces = 0;
470 this.spans = new Array();
471 },
472 /*
473 * Clear the document before leaving on 'Done' button
474 */
475 onCancel: function () {
476 this.clearDoc();
477 var plugin = this.getPluginInstance('UndoRedo');
478 if (plugin) {
479 plugin.start();
480 }
481 this.base();
482 },
483 /*
484 * Clear the document before leaving on window close handle
485 */
486 onClose: function () {
487 this.clearDoc();
488 var plugin = this.getPluginInstance('UndoRedo');
489 if (plugin) {
490 plugin.start();
491 }
492 this.base();
493 }
494 });