View Javadoc
1   // Copyright (C) 2010-2013 Yozons, Inc.
2   // CKEditor for Vaadin- Widget linkage for using CKEditor within a Vaadin application.
3   //
4   // This software is released under the Apache License 2.0 <http://www.apache.org/licenses/LICENSE-2.0.html>
5   //
6   // This software was originally based on the Vaadin incubator component TinyMCEEditor written by Matti Tahvonen.
7   //
8   package org.vaadin.openesignforms.ckeditor.widgetset.client.ui;
9   
10  import java.util.HashMap;
11  import java.util.LinkedList;
12  import java.util.Set;
13  
14  import com.google.gwt.core.client.Scheduler;
15  import com.google.gwt.core.client.Scheduler.ScheduledCommand;
16  import com.google.gwt.dom.client.DivElement;
17  import com.google.gwt.dom.client.Document;
18  import com.google.gwt.dom.client.Style.Overflow;
19  import com.google.gwt.dom.client.Style.Visibility;
20  import com.google.gwt.user.client.ui.Focusable;
21  import com.google.gwt.user.client.ui.Widget;
22  import com.vaadin.client.ApplicationConnection;
23  import com.vaadin.client.LayoutManager;
24  import com.vaadin.client.Paintable;
25  import com.vaadin.client.UIDL;
26  import com.vaadin.client.VConsole;
27  import com.vaadin.client.ui.layout.ElementResizeEvent;
28  import com.vaadin.client.ui.layout.ElementResizeListener;
29  import com.vaadin.shared.EventId;
30  
31  /**
32   * Client side CKEditor widget which communicates with the server. Messages from the
33   * server are shown as HTML and mouse clicks are sent to the server.
34   */
35  public class VCKEditorTextField extends Widget implements Paintable, CKEditorService.CKEditorListener, Focusable {
36  	
37  	/** Set the CSS class name to allow styling. */
38  	public static final String CLASSNAME = "v-ckeditortextfield";
39  	
40  	public static final String ATTR_FOCUS = "focus";
41  	public static final String ATTR_IMMEDIATE = "immediate";
42  	public static final String ATTR_READONLY = "readonly";
43  	public static final String ATTR_VIEW_WITHOUT_EDITOR = "viewWithoutEditor";
44  	public static final String ATTR_INPAGECONFIG = "inPageConfig";
45  	public static final String ATTR_PROTECTED_SOURCE = "protectedSource";
46  	public static final String ATTR_WRITERRULES_TAGNAME = "writerRules.tagName";
47  	public static final String ATTR_WRITERRULES_JSRULE = "writerRules.jsRule";
48  	public static final String ATTR_WRITER_INDENTATIONCHARS = "writerIndentationChars";
49  	public static final String ATTR_INSERT_HTML = "insert_html";
50  	public static final String ATTR_INSERT_TEXT = "insert_text";
51  	public static final String ATTR_PROTECTED_BODY = "protected_body";
52  	public static final String VAR_TEXT = "text";
53  	public static final String VAR_VERSION = "version";
54  	
55  	private static String ckeditorVersion;
56  
57  	/** The client side widget identifier */
58  	protected String paintableId;
59  
60  	/** Reference to the server connection object. */
61  	protected ApplicationConnection clientToServer;
62  	
63  	private String dataBeforeEdit = null;
64  	
65  	private boolean immediate;
66  	private boolean readOnly;
67  	private boolean viewWithoutEditor; // 11/19/2012 - New mode to simulate original readOnly before CKEditor had support for being a read-only editor. Set to true and the editor will not be displayed, just the contents.
68  	private boolean protectedBody;
69  	
70  	private CKEditor ckEditor = null;
71  	private boolean ckEditorIsReady = false;
72  	private boolean resizeListenerInPlace = false;
73  	
74  	private LinkedList<String> protectedSourceList = null;
75  	private HashMap<String,String> writerRules = null;
76  	private String writerIndentationChars = null;
77  	
78  	private int tabIndex;
79  	private boolean setFocusAfterReady;
80  	private boolean setTabIndexAfterReady;
81  	
82  	/**
83  	 * The constructor should first call super() to initialize the component and
84  	 * then handle any initialization relevant to Vaadin.
85  	 */
86  	public VCKEditorTextField() {
87  		// CKEditor prefers a textarea, but found too many issues trying to use createTextareaElement() instead of a simple div, 
88  		// which is okay in Vaadin where an HTML form won't be used to send the data back and forth.
89  		DivElement rootDiv = Document.get().createDivElement();
90  		rootDiv.getStyle().setOverflow(Overflow.HIDDEN);
91  		rootDiv.getStyle().setVisibility(Visibility.VISIBLE); // required for FF to show in popup windows repeatedly
92  		setElement(rootDiv);
93  
94  		// This method call of the Paintable interface sets the component
95  		// style name in DOM tree
96  		setStyleName(CLASSNAME);
97  	}
98  	
99  	/**
100 	 * Called whenever an update is received from the server
101 	 */
102 	@Override
103 	public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
104 		clientToServer = client;
105 		paintableId = uidl.getId();
106 		boolean needsDataUpdate = false;
107 		boolean needsProtectedBodyUpdate = false;
108 		boolean readOnlyModeChanged = false;
109 		
110 		// This call should be made first.
111 		// It handles sizes, captions, tooltips, etc. automatically.
112 		// If clientToServer.updateComponent returns true there have been no changes
113 		// and we do not need to update anything.
114 		if ( clientToServer.updateComponent(this, uidl, true) ) {
115 			return;
116 		}
117 			
118 		if ( ! resizeListenerInPlace ) {
119 			LayoutManager.get(client).addElementResizeListener(getElement(), new ElementResizeListener() {
120 
121 				@Override
122 				public void onElementResize(ElementResizeEvent e) {
123 					doResize();
124 				}
125 				
126 			});
127 			resizeListenerInPlace = true;
128 		}
129 		
130 		if ( uidl.hasAttribute(ATTR_IMMEDIATE) ) {
131 	 		immediate = uidl.getBooleanAttribute(ATTR_IMMEDIATE);
132 		}
133 		if ( uidl.hasAttribute(ATTR_READONLY) ) {
134 			boolean newReadOnly = uidl.getBooleanAttribute(ATTR_READONLY);
135 			readOnlyModeChanged = newReadOnly != readOnly;
136 			readOnly = newReadOnly;
137 		}
138 		if ( uidl.hasAttribute(ATTR_VIEW_WITHOUT_EDITOR) ) {
139 			viewWithoutEditor = uidl.getBooleanAttribute(ATTR_VIEW_WITHOUT_EDITOR);
140 		}
141 		if ( uidl.hasAttribute(ATTR_PROTECTED_BODY) ) {
142 			boolean state = uidl.getBooleanAttribute(ATTR_PROTECTED_BODY);
143 			if (protectedBody != state) {
144 				protectedBody = state ;
145 				needsProtectedBodyUpdate = true;
146 			}
147 		}
148 		if ( uidl.hasVariable(VAR_TEXT) ) {
149 			String data = uidl.getStringVariable(VAR_TEXT);
150 			if ( ckEditor != null )
151 				dataBeforeEdit = ckEditor.getData();
152 			needsDataUpdate = ! data.equals(dataBeforeEdit);
153 			dataBeforeEdit = data;
154 		}
155 		
156 		// Save the client side identifier (paintable id) for the widget
157 		if ( ! paintableId.equals(getElement().getId()) ) {
158 			getElement().setId(paintableId);
159 		}
160 		
161 		if ( viewWithoutEditor ) {
162 			if ( ckEditor != null ) {
163 				// may update the data and change to viewWithoutEditor at the same time 
164 				if ( ! needsDataUpdate ) {
165 					dataBeforeEdit = ckEditor.getData();
166 				}
167 				ckEditor.destroy(true);
168 				ckEditorIsReady = false;
169 				ckEditor = null;
170 			}
171 			getElement().setInnerHTML(dataBeforeEdit);
172 		}
173 		else if ( ckEditor == null ) {
174 			getElement().setInnerHTML(""); // in case we put contents in there while in viewWithoutEditor mode
175 			
176 			final String inPageConfig = uidl.hasAttribute(ATTR_INPAGECONFIG) ? uidl.getStringAttribute(ATTR_INPAGECONFIG) : null;
177 			
178 			writerIndentationChars = uidl.hasAttribute(ATTR_WRITER_INDENTATIONCHARS) ? uidl.getStringAttribute(ATTR_WRITER_INDENTATIONCHARS) : null;
179 			
180 			if ( uidl.hasAttribute(ATTR_FOCUS) ) {
181 				setFocus(uidl.getBooleanAttribute(ATTR_FOCUS));
182 			}
183 			
184 			// See if we have any writer rules
185 			int i = 0;
186 			while( true ) {
187 				if ( ! uidl.hasAttribute(ATTR_WRITERRULES_TAGNAME+i)  ) {
188 					break;
189 				}
190 				// Save the rules until our instance is ready
191 				String tagName = uidl.getStringAttribute(ATTR_WRITERRULES_TAGNAME+i);
192 				String jsRule  = uidl.getStringAttribute(ATTR_WRITERRULES_JSRULE+i);
193 				if ( writerRules == null ) {
194 					writerRules = new HashMap<String,String>();
195 				}
196 				writerRules.put(tagName, jsRule);
197 				++i;
198 			}
199 			
200 			// See if we have any protected source regexs
201 			i = 0;
202 			while( true ) {
203 				if ( ! uidl.hasAttribute(ATTR_PROTECTED_SOURCE+i)  ) {
204 					break;
205 				}
206 				// Save the regex until our instance is ready
207 				String regex = uidl.getStringAttribute(ATTR_PROTECTED_SOURCE+i);
208 				if ( protectedSourceList == null ) {
209 					protectedSourceList = new LinkedList<String>();
210 				}
211 				protectedSourceList.add(regex);
212 				++i;
213 			}
214 			
215 			ScheduledCommand scE = new ScheduledCommand() {
216 				@Override
217 				public void execute() {
218                     ckEditor = loadEditor(inPageConfig);
219 				}
220 			};
221 			
222 			CKEditorService.loadLibrary(scE);
223 			
224 			// editor data and some options are set when the instance is ready....
225 		} else if ( ckEditorIsReady ) {
226 			
227 			if ( readOnlyModeChanged ) {
228 				ckEditor.setReadOnly(readOnly);
229 			}
230 			
231 			if ( needsDataUpdate ) {
232 				ckEditor.setData(dataBeforeEdit);
233 			}
234 			
235 			if ( needsProtectedBodyUpdate ) {
236 				ckEditor.protectBody(protectedBody);
237 			}
238 			
239 			if (uidl.hasAttribute(ATTR_INSERT_HTML)) {
240 				ckEditor.insertHtml(uidl.getStringAttribute(ATTR_INSERT_HTML));
241 			}
242 			
243 			if (uidl.hasAttribute(ATTR_INSERT_TEXT)) {
244 				ckEditor.insertText(uidl.getStringAttribute(ATTR_INSERT_TEXT));
245 			}
246 
247 			if ( uidl.hasAttribute(ATTR_FOCUS) ) {
248 				setFocus(uidl.getBooleanAttribute(ATTR_FOCUS));
249 			}
250 		}
251 		
252 	}
253 
254     /**
255      * Expose <code>loadEditor</code> command to subclasses, so that they can perform additional logic
256      * before/after creating CKEditor instance on the page, e.g. register external plugins.
257      * <p>
258      * This method is executed as a callback scheduled command when loading the CKEditor library has completed.
259      */
260     protected CKEditor loadEditor(String inPageConfig) {
261         return (CKEditor) CKEditorService.loadEditor(paintableId,
262                 VCKEditorTextField.this,
263                 inPageConfig,
264                 VCKEditorTextField.super.getOffsetWidth(),
265                 VCKEditorTextField.super.getOffsetHeight());
266     }
267 
268 	// Listener callback
269 	@Override
270 	public void onSave() {
271 		if ( ckEditorIsReady && ! readOnly ) {
272 			// Called if the user clicks the Save button. 
273 			String data = ckEditor.getData();
274 			if ( ! data.equals(dataBeforeEdit) ) {
275 				clientToServer.updateVariable(paintableId, VAR_TEXT, data, false);
276 				dataBeforeEdit = data;
277 			}
278 			clientToServer.sendPendingVariableChanges(); // ensure anything queued up goes now on SAVE
279 		}
280 	}
281 
282 	// Listener callback
283 	@Override
284 	public void onBlur() {
285 		if ( ckEditorIsReady ) {
286 			boolean sendToServer = false;
287 			
288 			if ( clientToServer.hasEventListeners(this, EventId.BLUR) ) {
289 				sendToServer = true;
290 	            clientToServer.updateVariable(paintableId, EventId.BLUR, "", false);
291 			}
292 			
293 			// Even though CKEditor 4.2 introduced a change event, it doesn't appear to fire if the user stays in SOURCE mode,
294 			// so while we do use the change event, we still are stuck with the blur listener to detect other such changes.
295 			if (  ! readOnly ) {
296 				String data = ckEditor.getData();
297 				if ( ! data.equals(dataBeforeEdit) ) {
298 					clientToServer.updateVariable(paintableId, VAR_TEXT, data, false);
299 	            	sendToServer = true;
300 	            	dataBeforeEdit = data; 
301 				}
302 			}
303 			
304 	        if (sendToServer) {
305 	            clientToServer.sendPendingVariableChanges();
306 			}
307 		}
308 	}
309 
310 	// Listener callback
311 	@Override
312 	public void onFocus() {
313 		if ( ckEditorIsReady ) {
314 			if ( clientToServer.hasEventListeners(this, EventId.FOCUS) ) {
315 	            clientToServer.updateVariable(paintableId, EventId.FOCUS, "", true);
316 			}
317 		}
318 	}
319 
320 	// Listener callback
321 	@Override
322 	public void onInstanceReady() {
323 		ckEditor.instanceReady(this);
324 		
325 		if ( writerRules != null ) {
326 			Set<String> tagNameSet = writerRules.keySet();
327 			for( String tagName : tagNameSet ) {
328 				ckEditor.setWriterRules(tagName, writerRules.get(tagName));
329 			}
330 			writerRules = null; // don't need them anymore
331 		}
332 		
333 		if ( writerIndentationChars != null ) {
334 			ckEditor.setWriterIndentationChars(writerIndentationChars);
335 			writerIndentationChars = null;
336 		}
337 		
338 		if ( protectedSourceList != null ) {
339 			for( String regex : protectedSourceList ) {
340 				ckEditor.pushProtectedSource(regex);
341 			}
342 			protectedSourceList = null;
343 		}
344 		
345 		if ( dataBeforeEdit != null ) {
346 			ckEditor.setData(dataBeforeEdit);
347 		}
348 				
349 		ckEditorIsReady = true;
350 		ckEditor.setReadOnly(readOnly);
351 		
352 		if (setFocusAfterReady) {
353 			setFocus(true);
354 		}
355 		
356 		if ( setTabIndexAfterReady ) {
357 			setTabIndex(tabIndex);
358 		}
359 		
360 		doResize();
361 		
362 		if (protectedBody) {
363 			ckEditor.protectBody(protectedBody);
364 		}
365 		ckeditorVersion = CKEditorService.version();
366 		clientToServer.updateVariable(paintableId, VAR_VERSION, ckeditorVersion, true);
367 	}
368 	
369 	@Override
370 	public void setWidth(String width) {
371 		super.setWidth(width);
372 		doResize();
373 	}
374 	
375 	@Override
376 	public void setHeight(String height) {
377 		super.setHeight(height);
378 		doResize();
379 	}
380 	
381 	protected void doResize() {
382 		if (ckEditorIsReady) {
383 			Scheduler.get().scheduleDeferred(new ScheduledCommand() {				
384 				@Override
385 				public void execute() {
386 					ckEditor.resize(VCKEditorTextField.super.getOffsetWidth(), VCKEditorTextField.super.getOffsetHeight());
387 				}
388 			});
389 		}
390 	}
391 
392 	@Override
393 	protected void onUnload() {
394 		if ( ckEditor != null ) {
395 			ckEditor.destroy();
396 			ckEditor = null;
397 		}
398 		ckEditorIsReady = false;
399 	}
400 
401 	@Override
402 	public void onChange() {
403 		if ( ckEditor != null && ! readOnly ) {
404 			String data = ckEditor.getData();
405 			if ( ! data.equals(dataBeforeEdit) ) {
406 				clientToServer.updateVariable(paintableId, VAR_TEXT, data, immediate);
407             	dataBeforeEdit = data;
408 			}
409 		}
410 	}
411 	
412 	@Override
413 	public void onModeChange(String mode) {
414 		if ( ckEditor != null ) {
415 			if ( ! readOnly ) {
416 				String data = ckEditor.getData();
417 				if ( ! data.equals(dataBeforeEdit) ) {
418 					clientToServer.updateVariable(paintableId, VAR_TEXT, data, true);
419 	            	dataBeforeEdit = data; 
420 				}
421 			}
422 			
423 			if ("wysiwyg".equals(mode)) {
424 				ckEditor.protectBody(protectedBody);
425 			}
426 		}
427 	}
428 	
429 	@Override
430 	public void onDataReady() {
431 		if ( ckEditor != null ) {
432 			ckEditor.protectBody(protectedBody);
433 		}
434 	}
435 
436 	@Override
437 	public int getTabIndex() {
438 		if (ckEditorIsReady) {
439 			return ckEditor.getTabIndex();
440 		} else {
441 			return tabIndex;
442 		}
443 	}
444 
445 	@Override
446 	public void setTabIndex(int tabIndex) {
447 		if (ckEditorIsReady) {
448 			ckEditor.setTabIndex(tabIndex);
449 		} else {
450 			setTabIndexAfterReady = true;
451 		}
452 		this.tabIndex = tabIndex;
453 	}
454 
455 	@Override
456 	public void setAccessKey(char arg0) {
457 		return;
458 	}
459 
460 	@Override
461 	public void setFocus(boolean arg0) {
462 		if (arg0) {
463 			if (ckEditorIsReady)
464 				ckEditor.focus();
465 			else
466 				setFocusAfterReady = true;
467 		} else {
468 			setFocusAfterReady = false;
469 		}
470 	}
471 
472 }