View Javadoc
1   // Copyright (C) 2010-2015 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_KEYSTROKES_KEYSTROKE = "keystrokes.keystroke";
50  	public static final String ATTR_KEYSTROKES_COMMAND = "keystrokes.command";
51  	public static final String ATTR_INSERT_HTML = "insert_html";
52  	public static final String ATTR_INSERT_TEXT = "insert_text";
53  	public static final String ATTR_PROTECTED_BODY = "protected_body";
54  	public static final String VAR_TEXT = "text";
55  	public static final String VAR_VAADIN_SAVE_BUTTON_PRESSED = "vaadinsave";
56  	public static final String VAR_VERSION = "version";
57  	
58  	public static final String EVENT_SELECTION_CHANGE = "selectionChange";
59  	
60  	private static String ckeditorVersion;
61  
62  	/** The client side widget identifier */
63  	protected String paintableId;
64  
65  	/** Reference to the server connection object. */
66  	protected ApplicationConnection clientToServer;
67  	
68  	private String dataBeforeEdit = null;
69  	
70  	private boolean immediate;
71  	private boolean readOnly;
72  	private boolean viewWithoutEditor; // Set to true and the editor will not be displayed, just the contents.
73  	private boolean protectedBody;
74  	
75  	private CKEditor ckEditor = null;
76  	private boolean ckEditorIsReady = false;
77  	private boolean resizeListenerInPlace = false;
78  	private boolean notifyBlankSelection = false;
79  	
80  	private LinkedList<String> protectedSourceList = null;
81  	private HashMap<String,String> writerRules = null;
82  	private String writerIndentationChars = null;
83  	private HashMap<Integer,String> keystrokeMappings = null;
84  	
85  	private int tabIndex;
86  	private boolean setFocusAfterReady;
87  	private boolean setTabIndexAfterReady;
88  	
89  	/**
90  	 * The constructor should first call super() to initialize the component and
91  	 * then handle any initialization relevant to Vaadin.
92  	 */
93  	public VCKEditorTextField() {
94  		// CKEditor prefers a textarea, but found too many issues trying to use createTextareaElement() instead of a simple div, 
95  		// which is okay in Vaadin where an HTML form won't be used to send the data back and forth.
96  		DivElement rootDiv = Document.get().createDivElement();
97  		rootDiv.getStyle().setOverflow(Overflow.HIDDEN);
98  		rootDiv.getStyle().setVisibility(Visibility.VISIBLE); // required for FF to show in popup windows repeatedly
99  		setElement(rootDiv);
100 
101 		// This method call of the Paintable interface sets the component
102 		// style name in DOM tree
103 		setStyleName(CLASSNAME);
104 	}
105 	
106 	/**
107 	 * Called whenever an update is received from the server
108 	 */
109 	@Override
110 	public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {
111 		clientToServer = client;
112 		paintableId = uidl.getId();
113 		boolean needsDataUpdate = false;
114 		boolean needsProtectedBodyUpdate = false;
115 		boolean readOnlyModeChanged = false;
116 		
117 		// This call should be made first.
118 		// It handles sizes, captions, tooltips, etc. automatically.
119 		// If clientToServer.updateComponent returns true there have been no changes
120 		// and we do not need to update anything.
121 		if ( clientToServer.updateComponent(this, uidl, true) ) {
122 			return;
123 		}
124 			
125 		if ( ! resizeListenerInPlace ) {
126 			LayoutManager.get(client).addElementResizeListener(getElement(), new ElementResizeListener() {
127 
128 				@Override
129 				public void onElementResize(ElementResizeEvent e) {
130 					doResize();
131 				}
132 				
133 			});
134 			resizeListenerInPlace = true;
135 		}
136 		
137 		if ( uidl.hasAttribute(ATTR_IMMEDIATE) ) {
138 	 		immediate = uidl.getBooleanAttribute(ATTR_IMMEDIATE);
139 		}
140 		if ( uidl.hasAttribute(ATTR_READONLY) ) {
141 			boolean newReadOnly = uidl.getBooleanAttribute(ATTR_READONLY);
142 			readOnlyModeChanged = newReadOnly != readOnly;
143 			readOnly = newReadOnly;
144 		}
145 		if ( uidl.hasAttribute(ATTR_VIEW_WITHOUT_EDITOR) ) {
146 			viewWithoutEditor = uidl.getBooleanAttribute(ATTR_VIEW_WITHOUT_EDITOR);
147 		}
148 		if ( uidl.hasAttribute(ATTR_PROTECTED_BODY) ) {
149 			boolean state = uidl.getBooleanAttribute(ATTR_PROTECTED_BODY);
150 			if (protectedBody != state) {
151 				protectedBody = state ;
152 				needsProtectedBodyUpdate = true;
153 			}
154 		}
155 		if ( uidl.hasVariable(VAR_TEXT) ) {
156 			String data = uidl.getStringVariable(VAR_TEXT);
157 			if ( ckEditor != null )
158 				dataBeforeEdit = ckEditor.getData();
159 			needsDataUpdate = ! data.equals(dataBeforeEdit);
160 			dataBeforeEdit = data;
161 		}
162 		
163 		// Save the client side identifier (paintable id) for the widget
164 		if ( ! paintableId.equals(getElement().getId()) ) {
165 			getElement().setId(paintableId);
166 		}
167 		
168 		if ( viewWithoutEditor ) {
169 			if ( ckEditor != null ) {
170 				// may update the data and change to viewWithoutEditor at the same time 
171 				if ( ! needsDataUpdate ) {
172 					dataBeforeEdit = ckEditor.getData();
173 				}
174 				ckEditor.destroy(true);
175 				ckEditorIsReady = false;
176 				ckEditor = null;
177 			}
178 			getElement().setInnerHTML(dataBeforeEdit);
179 		}
180 		else if ( ckEditor == null ) {
181 			getElement().setInnerHTML(""); // in case we put contents in there while in viewWithoutEditor mode
182 			
183 			final String inPageConfig = uidl.hasAttribute(ATTR_INPAGECONFIG) ? uidl.getStringAttribute(ATTR_INPAGECONFIG) : null;
184 			
185 			writerIndentationChars = uidl.hasAttribute(ATTR_WRITER_INDENTATIONCHARS) ? uidl.getStringAttribute(ATTR_WRITER_INDENTATIONCHARS) : null;
186 			
187 			if ( uidl.hasAttribute(ATTR_FOCUS) ) {
188 				setFocus(uidl.getBooleanAttribute(ATTR_FOCUS));
189 			}
190 			
191 			// See if we have any writer rules
192 			int i = 0;
193 			while( true ) {
194 				if ( ! uidl.hasAttribute(ATTR_WRITERRULES_TAGNAME+i)  ) {
195 					break;
196 				}
197 				// Save the rules until our instance is ready
198 				String tagName = uidl.getStringAttribute(ATTR_WRITERRULES_TAGNAME+i);
199 				String jsRule  = uidl.getStringAttribute(ATTR_WRITERRULES_JSRULE+i);
200 				if ( writerRules == null ) {
201 					writerRules = new HashMap<String,String>();
202 				}
203 				writerRules.put(tagName, jsRule);
204 				++i;
205 			}
206 			
207 			// See if we have any keystrokes
208 			i = 0;
209 			while( true ) {
210 				if ( ! uidl.hasAttribute(ATTR_KEYSTROKES_KEYSTROKE+i)  ) {
211 					break;
212 				}
213 				// Save the keystrokes until our instance is ready
214 				int keystroke = uidl.getIntAttribute(ATTR_KEYSTROKES_KEYSTROKE+i);
215 				String command  = uidl.getStringAttribute(ATTR_KEYSTROKES_COMMAND+i);
216 				if ( keystrokeMappings == null ) {
217 					keystrokeMappings = new HashMap<Integer,String>();
218 				}
219 				keystrokeMappings.put(keystroke, command);
220 				++i;
221 			}
222 			
223 			// See if we have any protected source regexs
224 			i = 0;
225 			while( true ) {
226 				if ( ! uidl.hasAttribute(ATTR_PROTECTED_SOURCE+i)  ) {
227 					break;
228 				}
229 				// Save the regex until our instance is ready
230 				String regex = uidl.getStringAttribute(ATTR_PROTECTED_SOURCE+i);
231 				if ( protectedSourceList == null ) {
232 					protectedSourceList = new LinkedList<String>();
233 				}
234 				protectedSourceList.add(regex);
235 				++i;
236 			}
237 			
238 			ScheduledCommand scE = new ScheduledCommand() {
239 				@Override
240 				public void execute() {
241                     ckEditor = loadEditor(inPageConfig);
242 
243 				}
244 			};
245 			
246 			CKEditorService.loadLibrary(scE);
247 			
248 			// editor data and some options are set when the instance is ready....
249 		} else if ( ckEditorIsReady ) {
250 			if ( needsDataUpdate ) {
251 				ckEditor.setData(dataBeforeEdit);
252 			}
253 			
254 			if ( needsProtectedBodyUpdate ) {
255 				ckEditor.protectBody(protectedBody);
256 			}
257 			
258 			if (uidl.hasAttribute(ATTR_INSERT_HTML)) {
259 				ckEditor.insertHtml(uidl.getStringAttribute(ATTR_INSERT_HTML));
260 			}
261 			
262 			if (uidl.hasAttribute(ATTR_INSERT_TEXT)) {
263 				ckEditor.insertText(uidl.getStringAttribute(ATTR_INSERT_TEXT));
264 			}
265 
266 			if ( uidl.hasAttribute(ATTR_FOCUS) ) {
267 				setFocus(uidl.getBooleanAttribute(ATTR_FOCUS));
268 			}
269 			
270 			if ( readOnlyModeChanged ) {
271 				ckEditor.setReadOnly(readOnly);
272 			}			
273 		}
274 		
275 	}
276 
277     /**
278      * Expose <code>loadEditor</code> command to subclasses, so that they can perform additional logic
279      * before/after creating CKEditor instance on the page, e.g. register external plugins.
280      * <p>
281      * This method is executed as a callback scheduled command when loading the CKEditor library has completed.
282      */
283     protected CKEditor loadEditor(String inPageConfig) {
284         return (CKEditor) CKEditorService.loadEditor(paintableId,
285                 VCKEditorTextField.this,
286                 inPageConfig,
287                 VCKEditorTextField.super.getOffsetWidth(),
288                 VCKEditorTextField.super.getOffsetHeight());
289     }
290 
291 	// Listener callback
292 	@Override
293 	public void onSave() {
294 		if ( ckEditorIsReady && ! readOnly ) {
295 			// Called if the user clicks the Save button. 
296 			String data = ckEditor.getData();
297 			if ( ! data.equals(dataBeforeEdit) ) {
298 				clientToServer.updateVariable(paintableId, VAR_TEXT, data, false);
299 				dataBeforeEdit = data;
300 			}
301 			clientToServer.updateVariable(paintableId, VAR_VAADIN_SAVE_BUTTON_PRESSED,"",false); // inform that the button was pressed too
302 			clientToServer.sendPendingVariableChanges(); // ensure anything queued up goes now on SAVE
303 		}
304 	}
305 
306 	// Listener callback
307 	@Override
308 	public void onBlur() {
309 		if ( ckEditorIsReady ) {
310 			boolean sendToServer = false;
311 			
312 			if ( clientToServer.hasEventListeners(this, EventId.BLUR) ) {
313 				sendToServer = true;
314 	            clientToServer.updateVariable(paintableId, EventId.BLUR, "", false);
315 			}
316 			
317 			// Even though CKEditor 4.2 introduced a change event, it doesn't appear to fire if the user stays in SOURCE mode,
318 			// so while we do use the change event, we still are stuck with the blur listener to detect other such changes.
319 			if (  ! readOnly ) {
320 				String data = ckEditor.getData();
321 				if ( ! data.equals(dataBeforeEdit) ) {
322 					clientToServer.updateVariable(paintableId, VAR_TEXT, data, false);
323 	            	sendToServer = true;
324 	            	dataBeforeEdit = data; 
325 				}
326 			}
327 			
328 	        if (sendToServer) {
329 	            clientToServer.sendPendingVariableChanges();
330 			}
331 		}
332 	}
333 
334 	// Listener callback
335 	@Override
336 	public void onFocus() {
337 		if ( ckEditorIsReady ) {
338 			if ( clientToServer.hasEventListeners(this, EventId.FOCUS) ) {
339 	            clientToServer.updateVariable(paintableId, EventId.FOCUS, "", true);
340 			}
341 		}
342 	}
343 
344 	// Listener callback
345 	@Override
346 	public void onInstanceReady() {
347 		ckEditor.instanceReady(this);
348 		
349 		if ( writerRules != null ) {
350 			Set<String> tagNameSet = writerRules.keySet();
351 			for( String tagName : tagNameSet ) {
352 				ckEditor.setWriterRules(tagName, writerRules.get(tagName));
353 			}
354 			writerRules = null; // don't need them anymore
355 		}
356 		
357 		if ( writerIndentationChars != null ) {
358 			ckEditor.setWriterIndentationChars(writerIndentationChars);
359 			writerIndentationChars = null;
360 		}
361 		
362 		if ( keystrokeMappings != null ) {
363 			Set<Integer> keystrokeSet = keystrokeMappings.keySet();
364 			for( Integer keystroke : keystrokeSet ) {
365 				ckEditor.setKeystroke(keystroke, keystrokeMappings.get(keystroke));
366 			}
367 			keystrokeMappings = null; // don't need them anymore
368 		}
369 		
370 		if ( protectedSourceList != null ) {
371 			for( String regex : protectedSourceList ) {
372 				ckEditor.pushProtectedSource(regex);
373 			}
374 			protectedSourceList = null;
375 		}
376 		
377 		if ( dataBeforeEdit != null ) {
378 			ckEditor.setData(dataBeforeEdit);
379 		}
380 				
381 		ckEditorIsReady = true;
382 		
383 		if (setFocusAfterReady) {
384 			setFocus(true);
385 		}
386 		
387 		if ( setTabIndexAfterReady ) {
388 			setTabIndex(tabIndex);
389 		}
390 		
391 		doResize();
392 		
393 		if (protectedBody) {
394 			ckEditor.protectBody(protectedBody);
395 		}
396 
397 		ckEditor.setReadOnly(readOnly);
398 		
399 		ckeditorVersion = CKEditorService.version();
400 		clientToServer.updateVariable(paintableId, VAR_VERSION, ckeditorVersion, true);
401 	}
402 	
403 	// Listener callback
404 	@Override
405 	public void onChange() {
406 		if ( ckEditor != null && ! readOnly ) {
407 			String data = ckEditor.getData();
408 			if ( ! data.equals(dataBeforeEdit) ) {
409 				clientToServer.updateVariable(paintableId, VAR_TEXT, data, immediate);
410             	dataBeforeEdit = data;
411 			}
412 		}
413 	}
414 	
415 	// Listener callback
416 	@Override
417 	public void onModeChange(String mode) {
418 		if ( ckEditor != null ) {
419 			if ( ! readOnly ) {
420 				String data = ckEditor.getData();
421 				if ( ! data.equals(dataBeforeEdit) ) {
422 					clientToServer.updateVariable(paintableId, VAR_TEXT, data, true);
423 	            	dataBeforeEdit = data; 
424 				}
425 			}
426 			
427 			if ("wysiwyg".equals(mode)) {
428 				ckEditor.protectBody(protectedBody);
429 			}
430 		}
431 	}
432 	
433 	// Listener callback
434 	@Override
435 	public void onSelectionChange() {
436 		if ( ckEditorIsReady ) {
437 			if ( clientToServer.hasEventListeners(this, EVENT_SELECTION_CHANGE) ) {
438 				String html = ckEditor.getSelectedHtml();
439 				if ( html == null )
440 					html = "";
441 				// We'll send an update for nothing selected (unselected) only if we've sent out an event for a prior selected event.
442 				boolean isBlankSelection = "".equals(html);
443 				if ( ! isBlankSelection || notifyBlankSelection ) {
444 		            clientToServer.updateVariable(paintableId, EVENT_SELECTION_CHANGE, html, true);
445 		            notifyBlankSelection = ! isBlankSelection;
446 				}
447 			}
448 		}
449 	}
450 
451 	// Listener callback
452 	@Override
453 	public void onDataReady() {
454 		if ( ckEditor != null ) {
455 			ckEditor.protectBody(protectedBody);
456 		}
457 	}
458 
459 	@Override
460 	public void setWidth(String width) {
461 		super.setWidth(width);
462 		doResize();
463 	}
464 	
465 	@Override
466 	public void setHeight(String height) {
467 		super.setHeight(height);
468 		doResize();
469 	}
470 	
471 	protected void doResize() {
472 		if (ckEditorIsReady) {
473 			Scheduler.get().scheduleDeferred(new ScheduledCommand() {				
474 				@Override
475 				public void execute() {
476 					ckEditor.resize(VCKEditorTextField.super.getOffsetWidth(), VCKEditorTextField.super.getOffsetHeight());
477 				}
478 			});
479 		}
480 	}
481 
482 	@Override
483 	protected void onUnload() {
484 		if ( ckEditor != null ) {
485 			ckEditor.destroy();
486 			ckEditor = null;
487 		}
488 		ckEditorIsReady = false;
489 	}
490 
491 	@Override
492 	public int getTabIndex() {
493 		if (ckEditorIsReady) {
494 			return ckEditor.getTabIndex();
495 		} else {
496 			return tabIndex;
497 		}
498 	}
499 
500 	@Override
501 	public void setTabIndex(int tabIndex) {
502 		if (ckEditorIsReady) {
503 			ckEditor.setTabIndex(tabIndex);
504 		} else {
505 			setTabIndexAfterReady = true;
506 		}
507 		this.tabIndex = tabIndex;
508 	}
509 
510 	@Override
511 	public void setAccessKey(char arg0) {
512 		return;
513 	}
514 
515 	@Override
516 	public void setFocus(boolean arg0) {
517 		if (arg0) {
518 			if (ckEditorIsReady)
519 				ckEditor.focus();
520 			else
521 				setFocusAfterReady = true;
522 		} else {
523 			setFocusAfterReady = false;
524 		}
525 	}
526 
527 }