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