f277c8480ce06c7709e6f246459c3159df949b39
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / htmlarea / plugins / UndoRedo / undo-redo.js
1 /***************************************************************
2 * Copyright notice
3 *
4 * (c) 2008-2012 Stanislas Rolland <typo3(arobas)sjbr.ca>
5 * All rights reserved
6 *
7 * This script is part of the TYPO3 project. The TYPO3 project is
8 * free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * The GNU General Public License can be found at
14 * http://www.gnu.org/copyleft/gpl.html.
15 * A copy is found in the textfile GPL.txt and important notices to the license
16 * from the author is found in LICENSE.txt distributed with these scripts.
17 *
18 *
19 * This script is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * This script is a modified version of a script published under the htmlArea License.
25 * A copy of the htmlArea License may be found in the textfile HTMLAREA_LICENSE.txt.
26 *
27 * This copyright notice MUST APPEAR in all copies of the script!
28 ***************************************************************/
29 /*
30 * Undo Redo Plugin for TYPO3 htmlArea RTE
31 */
32 HTMLArea.UndoRedo = Ext.extend(HTMLArea.Plugin, {
33 /*
34 * This function gets called by the class constructor
35 */
36 configurePlugin: function (editor) {
37 this.pageTSconfiguration = this.editorConfiguration.buttons.undo;
38 this.customUndo = true;
39 this.undoQueue = new Array();
40 this.undoPosition = -1;
41 // Maximum size of the undo queue
42 this.undoSteps = 25;
43 // The time interval at which undo samples are taken: 1/2 sec.
44 this.undoTimeout = 500;
45 /*
46 * Registering plugin "About" information
47 */
48 var pluginInformation = {
49 version : '2.2',
50 developer : 'Stanislas Rolland',
51 developerUrl : 'http://www.sjbr.ca',
52 copyrightOwner : 'Stanislas Rolland',
53 sponsor : 'SJBR',
54 sponsorUrl : 'http://www.sjbr.ca',
55 license : 'GPL'
56 };
57 this.registerPluginInformation(pluginInformation);
58 /*
59 * Registering the buttons
60 */
61 var buttonList = this.buttonList, buttonId;
62 for (var i = 0; i < buttonList.length; ++i) {
63 var button = buttonList[i];
64 buttonId = button[0];
65 var buttonConfiguration = {
66 id : buttonId,
67 tooltip : this.localize(buttonId.toLowerCase()),
68 iconCls : 'htmlarea-action-' + button[3],
69 action : 'onButtonPress',
70 hotKey : ((this.editorConfiguration.buttons[buttonId.toLowerCase()] && this.editorConfiguration.buttons[buttonId.toLowerCase()].hotKey) ? this.editorConfiguration.buttons[buttonId.toLowerCase()].hotKey : button[2]),
71 noAutoUpdate : true
72 };
73 this.registerButton(buttonConfiguration);
74 }
75 return true;
76 },
77 /*
78 * The list of buttons added by this plugin
79 */
80 buttonList: [
81 ['Undo', null, 'z', 'undo'],
82 ['Redo', null, 'y', 'redo']
83 ],
84 /*
85 * This function gets called when the editor is generated
86 */
87 onGenerate: function () {
88 // Start undo snapshots
89 if (this.customUndo) {
90 this.task = {
91 run: this.takeSnapshot,
92 scope: this,
93 interval: this.undoTimeout
94 };
95 this.start();
96 }
97 },
98 /*
99 * Start the undo/redo snapshot task
100 */
101 start: function () {
102 if (this.customUndo) {
103 Ext.TaskMgr.start(this.task);
104 }
105 },
106 /*
107 * Start the undo/redo snapshot task
108 */
109 stop: function () {
110 if (this.customUndo) {
111 Ext.TaskMgr.stop(this.task);
112 }
113 },
114 /*
115 * Take a snapshot of the current contents for undo
116 */
117 takeSnapshot: function () {
118 var currentTime = (new Date()).getTime();
119 var newSnapshot = false;
120 if (this.undoPosition >= this.undoSteps) {
121 // Remove the first element
122 this.undoQueue.shift();
123 --this.undoPosition;
124 }
125 // New undo slot should be used if this is first takeSnapshot call or if undoTimeout is elapsed
126 if (this.undoPosition < 0 || this.undoQueue[this.undoPosition].time < currentTime - this.undoTimeout) {
127 ++this.undoPosition;
128 newSnapshot = true;
129 }
130 // Get the html text
131 var text = this.editor.getInnerHTML();
132
133 if (newSnapshot) {
134 // If previous slot contains the same text, a new one should not be used
135 if (this.undoPosition == 0 || this.undoQueue[this.undoPosition - 1].text != text) {
136 this.undoQueue[this.undoPosition] = this.buildSnapshot();
137 this.undoQueue[this.undoPosition].time = currentTime;
138 this.undoQueue.length = this.undoPosition + 1;
139 this.updateButtonsState();
140 } else {
141 --this.undoPosition;
142 }
143 } else {
144 if (this.undoQueue[this.undoPosition].text != text){
145 var snapshot = this.buildSnapshot();
146 this.undoQueue[this.undoPosition].text = snapshot.text;
147 this.undoQueue[this.undoPosition].bookmark = snapshot.bookmark;
148 this.undoQueue[this.undoPosition].bookmarkedText = snapshot.bookmarkedText;
149 this.undoQueue.length = this.undoPosition + 1;
150 }
151 }
152 },
153 /*
154 * Build the snapshot entry
155 *
156 * @return object a snapshot entry with three components:
157 * - text (the content of the RTE without any bookmark),
158 * - bookmark (the bookmark),
159 * - bookmarkedText (the content of the RTE including the bookmark)
160 */
161 buildSnapshot: function () {
162 var bookmark = null, bookmarkedText = null;
163 // Insert a bookmark
164 if (this.getEditorMode() === 'wysiwyg' && this.editor.isEditable()) {
165 if ((!Ext.isIE && !(Ext.isOpera && navigator.userAgent.toLowerCase().indexOf('presto/2.1') != -1)) || (Ext.isIE && this.editor.getSelection().getType() !== 'Control')) {
166 // Catch error in FF when the selection contains no usable range
167 try {
168 bookmark = this.editor.getBookMark().get(this.editor.getSelection().createRange());
169 } catch (e) {
170 bookmark = null;
171 }
172 }
173 // Get the bookmarked html text and remove the bookmark
174 if (bookmark) {
175 bookmarkedText = this.editor.getInnerHTML();
176 var range = this.editor.getBookMark().moveTo(bookmark);
177 // Restore Firefox selection
178 if (Ext.isGecko) {
179 this.editor.getSelection().selectRange(range);
180 }
181 }
182 }
183 return {
184 text : this.editor.getInnerHTML(),
185 bookmark : bookmark,
186 bookmarkedText : bookmarkedText
187 };
188 },
189 /*
190 * Execute the undo request
191 */
192 undo: function () {
193 if (this.undoPosition > 0) {
194 // Make sure we would not loose any changes
195 this.takeSnapshot();
196 this.setContent(--this.undoPosition);
197 this.updateButtonsState();
198 }
199 },
200 /*
201 * Execute the redo request
202 */
203 redo: function () {
204 if (this.undoPosition < this.undoQueue.length - 1) {
205 // Make sure we would not loose any changes
206 this.takeSnapshot();
207 // Previous call could make undo queue shorter
208 if (this.undoPosition < this.undoQueue.length - 1) {
209 this.setContent(++this.undoPosition);
210 this.updateButtonsState();
211 }
212 }
213 },
214 /*
215 * Set content using undo queue position
216 */
217 setContent: function (undoPosition) {
218 var bookmark = this.undoQueue[undoPosition].bookmark;
219 if (bookmark) {
220 this.editor.setHTML(this.undoQueue[undoPosition].bookmarkedText);
221 this.editor.getSelection().selectRange(this.editor.getBookMark().moveTo(bookmark));
222 this.editor.scrollToCaret();
223 } else {
224 this.editor.setHTML(this.undoQueue[undoPosition].text);
225 }
226 },
227 /*
228 * This function gets called when the toolbar is updated
229 */
230 onUpdateToolbar: function (button, mode, selectionEmpty, ancestors) {
231 if (mode == 'wysiwyg' && this.editor.isEditable()) {
232 if (this.customUndo) {
233 switch (button.itemId) {
234 case 'Undo':
235 button.setDisabled(this.undoPosition == 0);
236 break;
237 case 'Redo':
238 button.setDisabled(this.undoPosition >= this.undoQueue.length-1);
239 break;
240 }
241 } else {
242 try {
243 button.setDisabled(!this.editor.document.queryCommandEnabled(button.itemId));
244 } catch (e) {
245 button.setDisabled(true);
246 }
247 }
248 } else {
249 button.setDisabled(!button.textMode);
250 }
251 },
252 /*
253 * Update the state of the undo/redo buttons
254 */
255 updateButtonsState: function () {
256 var mode = this.getEditorMode(),
257 selectionEmpty = true,
258 ancestors = null;
259 if (mode === 'wysiwyg') {
260 selectionEmpty = this.editor.getSelection().isEmpty();
261 ancestors = this.editor.getSelection().getAllAncestors();
262 }
263 var button = this.getButton('Undo');
264 if (button) {
265 this.onUpdateToolbar(button, mode, selectionEmpty, ancestors)
266 }
267 var button = this.getButton('Redo');
268 if (button) {
269 this.onUpdateToolbar(button, mode, selectionEmpty, ancestors)
270 }
271 },
272 /*
273 * This function gets called when the button was pressed.
274 *
275 * @param object editor: the editor instance
276 * @param string id: the button id or the key
277 *
278 * @return boolean false if action is completed
279 */
280 onButtonPress: function (editor, id) {
281 // Could be a button or its hotkey
282 var buttonId = this.translateHotKey(id);
283 buttonId = buttonId ? buttonId : id;
284 if (this.getButton(buttonId) && !this.getButton(buttonId).disabled) {
285 if (this.customUndo) {
286 this[buttonId.toLowerCase()]();
287 } else {
288 this.editor.getSelection().execCommand(buttonId, false, null);
289 }
290 }
291 return false;
292 }
293 });