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 = (CKEditor)CKEditorService.loadEditor(paintableId,
219 							VCKEditorTextField.this,
220 							inPageConfig,
221 							VCKEditorTextField.super.getOffsetWidth(),
222 							VCKEditorTextField.super.getOffsetHeight());
223 					
224 				}
225 			};
226 			
227 			CKEditorService.loadLibrary(scE);
228 			
229 			// editor data and some options are set when the instance is ready....
230 		} else if ( ckEditorIsReady ) {
231 			
232 			if ( readOnlyModeChanged ) {
233 				ckEditor.setReadOnly(readOnly);
234 			}
235 			
236 			if ( needsDataUpdate ) {
237 				ckEditor.setData(dataBeforeEdit);
238 			}
239 			
240 			if ( needsProtectedBodyUpdate ) {
241 				ckEditor.protectBody(protectedBody);
242 			}
243 			
244 			if (uidl.hasAttribute(ATTR_INSERT_HTML)) {
245 				ckEditor.insertHtml(uidl.getStringAttribute(ATTR_INSERT_HTML));
246 			}
247 			
248 			if (uidl.hasAttribute(ATTR_INSERT_TEXT)) {
249 				ckEditor.insertText(uidl.getStringAttribute(ATTR_INSERT_TEXT));
250 			}
251 
252 			if ( uidl.hasAttribute(ATTR_FOCUS) ) {
253 				setFocus(uidl.getBooleanAttribute(ATTR_FOCUS));
254 			}
255 		}
256 		
257 	}
258 
259 	// Listener callback
260 	@Override
261 	public void onSave() {
262 		if ( ckEditorIsReady && ! readOnly ) {
263 			// Called if the user clicks the Save button. 
264 			String data = ckEditor.getData();
265 			if ( ! data.equals(dataBeforeEdit) ) {
266 				clientToServer.updateVariable(paintableId, VAR_TEXT, data, false);
267 				dataBeforeEdit = data;
268 			}
269 			clientToServer.sendPendingVariableChanges(); // ensure anything queued up goes now on SAVE
270 		}
271 	}
272 
273 	// Listener callback
274 	@Override
275 	public void onBlur() {
276 		if ( ckEditorIsReady ) {
277 			boolean sendToServer = false;
278 			
279 			if ( clientToServer.hasEventListeners(this, EventId.BLUR) ) {
280 				sendToServer = true;
281 	            clientToServer.updateVariable(paintableId, EventId.BLUR, "", false);
282 			}
283 			
284 			// Even though CKEditor 4.2 introduced a change event, it doesn't appear to fire if the user stays in SOURCE mode,
285 			// so while we do use the change event, we still are stuck with the blur listener to detect other such changes.
286 			if (  ! readOnly ) {
287 				String data = ckEditor.getData();
288 				if ( ! data.equals(dataBeforeEdit) ) {
289 					clientToServer.updateVariable(paintableId, VAR_TEXT, data, false);
290 	            	sendToServer = true;
291 	            	dataBeforeEdit = data; 
292 				}
293 			}
294 			
295 	        if (sendToServer) {
296 	            clientToServer.sendPendingVariableChanges();
297 			}
298 		}
299 	}
300 
301 	// Listener callback
302 	@Override
303 	public void onFocus() {
304 		if ( ckEditorIsReady ) {
305 			if ( clientToServer.hasEventListeners(this, EventId.FOCUS) ) {
306 	            clientToServer.updateVariable(paintableId, EventId.FOCUS, "", true);
307 			}
308 		}
309 	}
310 
311 	// Listener callback
312 	@Override
313 	public void onInstanceReady() {
314 		ckEditor.instanceReady(this);
315 		
316 		if ( writerRules != null ) {
317 			Set<String> tagNameSet = writerRules.keySet();
318 			for( String tagName : tagNameSet ) {
319 				ckEditor.setWriterRules(tagName, writerRules.get(tagName));
320 			}
321 			writerRules = null; // don't need them anymore
322 		}
323 		
324 		if ( writerIndentationChars != null ) {
325 			ckEditor.setWriterIndentationChars(writerIndentationChars);
326 			writerIndentationChars = null;
327 		}
328 		
329 		if ( protectedSourceList != null ) {
330 			for( String regex : protectedSourceList ) {
331 				ckEditor.pushProtectedSource(regex);
332 			}
333 			protectedSourceList = null;
334 		}
335 		
336 		if ( dataBeforeEdit != null ) {
337 			ckEditor.setData(dataBeforeEdit);
338 		}
339 				
340 		ckEditorIsReady = true;
341 		ckEditor.setReadOnly(readOnly);
342 		
343 		if (setFocusAfterReady) {
344 			setFocus(true);
345 		}
346 		
347 		if ( setTabIndexAfterReady ) {
348 			setTabIndex(tabIndex);
349 		}
350 		
351 		doResize();
352 		
353 		if (protectedBody) {
354 			ckEditor.protectBody(protectedBody);
355 		}
356 		ckeditorVersion = CKEditorService.version();
357 		clientToServer.updateVariable(paintableId, VAR_VERSION, ckeditorVersion, true);
358 	}
359 	
360 	@Override
361 	public void setWidth(String width) {
362 		super.setWidth(width);
363 		doResize();
364 	}
365 	
366 	@Override
367 	public void setHeight(String height) {
368 		super.setHeight(height);
369 		doResize();
370 	}
371 	
372 	protected void doResize() {
373 		if (ckEditorIsReady) {
374 			Scheduler.get().scheduleDeferred(new ScheduledCommand() {				
375 				@Override
376 				public void execute() {
377 					ckEditor.resize(VCKEditorTextField.super.getOffsetWidth(), VCKEditorTextField.super.getOffsetHeight());
378 				}
379 			});
380 		}
381 	}
382 
383 	@Override
384 	protected void onUnload() {
385 		if ( ckEditor != null ) {
386 			ckEditor.destroy();
387 			ckEditor = null;
388 		}
389 		ckEditorIsReady = false;
390 	}
391 
392 	@Override
393 	public void onChange() {
394 		if ( ckEditor != null && ! readOnly ) {
395 			String data = ckEditor.getData();
396 			if ( ! data.equals(dataBeforeEdit) ) {
397 				clientToServer.updateVariable(paintableId, VAR_TEXT, data, immediate);
398             	dataBeforeEdit = data;
399 			}
400 		}
401 	}
402 	
403 	@Override
404 	public void onModeChange(String mode) {
405 		if ( ckEditor != null ) {
406 			if ( ! readOnly ) {
407 				String data = ckEditor.getData();
408 				if ( ! data.equals(dataBeforeEdit) ) {
409 					clientToServer.updateVariable(paintableId, VAR_TEXT, data, true);
410 	            	dataBeforeEdit = data; 
411 				}
412 			}
413 			
414 			if ("wysiwyg".equals(mode)) {
415 				ckEditor.protectBody(protectedBody);
416 			}
417 		}
418 	}
419 	
420 	@Override
421 	public void onDataReady() {
422 		if ( ckEditor != null ) {
423 			ckEditor.protectBody(protectedBody);
424 		}
425 	}
426 
427 	@Override
428 	public int getTabIndex() {
429 		if (ckEditorIsReady) {
430 			return ckEditor.getTabIndex();
431 		} else {
432 			return tabIndex;
433 		}
434 	}
435 
436 	@Override
437 	public void setTabIndex(int tabIndex) {
438 		if (ckEditorIsReady) {
439 			ckEditor.setTabIndex(tabIndex);
440 		} else {
441 			setTabIndexAfterReady = true;
442 		}
443 		this.tabIndex = tabIndex;
444 	}
445 
446 	@Override
447 	public void setAccessKey(char arg0) {
448 		return;
449 	}
450 
451 	@Override
452 	public void setFocus(boolean arg0) {
453 		if (arg0) {
454 			if (ckEditorIsReady)
455 				ckEditor.focus();
456 			else
457 				setFocusAfterReady = true;
458 		} else {
459 			setFocusAfterReady = false;
460 		}
461 	}
462 
463 }