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