forked from harvardpan/hftinymce-gwt
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathHFRichTextEditor.java
968 lines (878 loc) · 37.9 KB
/
HFRichTextEditor.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
package com.healthfortis.map.shared.client.ui;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import com.google.gwt.core.client.Callback;
import com.google.gwt.core.client.JavaScriptException;
import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.core.client.ScriptInjector;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.BlurEvent;
import com.google.gwt.event.dom.client.BlurHandler;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.DomEvent;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.json.client.JSONBoolean;
import com.google.gwt.json.client.JSONNumber;
import com.google.gwt.json.client.JSONObject;
import com.google.gwt.json.client.JSONString;
import com.google.gwt.json.client.JSONValue;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.safehtml.shared.SafeHtmlUtils;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.TextArea;
public class HFRichTextEditor extends TextArea {
public static final int TINYMCE_VERSION_3 = 3;
public static final int TINYMCE_VERSION_4 = 4;
public static final int DEFAULT_HEIGHT = 72;
public static final int DEFAULT_WIDTH = 640;
public static final int PENDING_COMMAND_UNLOAD = 0;
public static final int PENDING_COMMAND_LOAD = 1;
public static final int PENDING_COMMAND_SELECT_ALL = 2;
public static final int PENDING_COMMAND_SET_HTML = 3;
public static final int PENDING_COMMAND_SET_FOCUS = 4;
public static int libraryVersion = TINYMCE_VERSION_4; // select which version of the library to load at runtime.
private static boolean libraryLoaded = false;
private static final String DEFAULT_ELEMENT_ID = "hfRichTextEditor";
private static HashMap<String, HFRichTextEditor> activeEditors; // stores a mapping of elementId => instances of HFRichTextEditor
static {
activeEditors = new HashMap<String, HFRichTextEditor>();
}
private ArrayList<Integer> pendingCommands = new ArrayList<Integer>(); // handle multiple onLoad and unUnload events in succession
@SuppressWarnings("rawtypes")
private Set fixedOptions = new HashSet(2); // options that can not be overwritten
private JSONObject options = new JSONObject(); // all other TinyMCE options
private boolean initialized = false;
private boolean initializing = false;
private String elementId;
private boolean focused;
private SafeHtml pendingSetHtmlText = null;
private static int numInitialized = 1; // start the count at "1"
private Timer initializationTimeoutTimer = null;
public HFRichTextEditor() {
this(DEFAULT_ELEMENT_ID);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public HFRichTextEditor(String elementId, Map presetOptions)
{
// NOTE: Do NOT use GWT's debug id scheme. This includes UIObject.ensureDebugId and the debugId property of the UI Binder.
// Doing so will prevent the ID from working and this widget from being recognized properly.
this.elementId = generateUniqueName(elementId);
++numInitialized;
activeEditors.put(this.elementId, this);
getElement().setId(this.elementId);
getElement().addClassName(this.elementId);
// fixed attributes
addOption("selector", "textarea." + this.elementId);
fixedOptions.addAll(options.keySet());
// load preset
if (presetOptions == null) {
applyPreset(HFRichTextEditorPreset.getAdvancedOptions());
} else {
applyPreset(presetOptions);
}
//Add handlers to support method isFocused()
addFocusHandler(new FocusHandler() {
@Override
public void onFocus(FocusEvent event) {
focused = true;
}
});
addBlurHandler(new BlurHandler() {
@Override
public void onBlur(BlurEvent event) {
focused = false;
}
});
final HFRichTextEditor me = this;
addMouseUpHandler(new MouseUpHandler() {
@Override
public void onMouseUp(MouseUpEvent event) {
setBookmarkPosition(me.elementId);
}
});
addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
restoreBookmarkPosition(me.elementId);
}
});
addKeyDownHandler(new KeyDownHandler() {
@Override
public void onKeyDown(KeyDownEvent event) {
setBookmarkPosition(me.elementId);
}
});
}
public HFRichTextEditor(String elementId)
{
this(elementId, null);
}
/**
* Returns the HFRichTextEditor instance associated with a particular elementId. Used mainly
* by the handler code to make sure that we end up propagating the handler sources correctly. There's
* probably a better way to do this.
*
* @param elementId The unique ID of the HFRichTextEditor
* @return the HFRichTextEditor instance associated with the input elementId
*/
private static HFRichTextEditor getActiveEditor(String elementId) {
if (elementId == null || elementId.trim().isEmpty() || !activeEditors.containsKey(elementId.toLowerCase().trim())) {
return null;
}
return activeEditors.get(elementId.toLowerCase().trim());
}
/*
* Given a id string, it checks the activeEditors key set to make sure that
* a unique name is generated. This is needed so that you don't instantiate
* multiple editors with the same id (and thus not be able to address them
* correctly in the javascript).
*/
private static String generateUniqueName(String id) {
if (id == null || id.trim().isEmpty()) {
return "";
}
String usedNameKey = id.trim().toLowerCase();
while (true) {
String testKey = usedNameKey + Integer.toString(numInitialized);
testKey = testKey.trim().toLowerCase();
if (!activeEditors.keySet().contains(testKey)) {
return testKey;
}
++numInitialized;
}
}
/**
* Applies a set of options to the TinyMCE init method. This needs to be called prior to
* init being called.
*
* @param optionsMap the list of options, usually generated from HFRichTextEditorPreset class.
*/
@SuppressWarnings("rawtypes")
private void applyPreset(Map optionsMap) {
for (Object optionKey : optionsMap.keySet()) {
addOption((String) optionKey, optionsMap.get(optionKey));
}
}
/**
* Add an option dynamically into the set of options that will be used by the init function
* for TinyMCE.
*
* @param key a string value denoting the configuration parameter (see TinyMCE documentation)
* @param value a value of Boolean, Integer, or String types.
*/
public void addOption(String key, Object value)
{
// do not allow overriding fixed options
if (fixedOptions.contains(key)) {
return;
}
if (value instanceof Boolean) {
options.put(key, JSONBoolean.getInstance((Boolean) value));
} else if (value instanceof Integer) {
options.put(key, new JSONNumber((Integer) value));
} else if (value instanceof String) {
options.put(key, new JSONString((String) value));
} else {
// Shouldn't ever really get to this, but just in case.
options.put(key, new JSONString(value.toString()));
}
}
private void addPendingCommand(int command) {
pendingCommands.add(command);
}
private void addPendingLoad() {
addPendingCommand(PENDING_COMMAND_LOAD);
}
private void addPendingUnload() {
addPendingCommand(PENDING_COMMAND_UNLOAD);
}
private void addPendingSelectAll() {
addPendingCommand(PENDING_COMMAND_SELECT_ALL);
}
private void addPendingSetHtml() {
addPendingCommand(PENDING_COMMAND_SET_HTML);
}
private void addPendingSetFocus() {
addPendingCommand(PENDING_COMMAND_SET_FOCUS);
}
@Override
protected void onLoad()
{
super.onLoad();
// Delay the initialization of the TinyMCE editor until after the current browser loop.
// This will avoid issues where there are multiple loads and unloads.
addPendingLoad();
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
runPendingCommand();
}
});
}
@Override
protected void onUnload() {
boolean deferUnloading = false;
try {
if (!uninitialize()) {
addPendingUnload();
deferUnloading = true;
}
} catch (JavaScriptException e) {
GWT.log("Unable to clean up TinyMCE editor.", e);
} finally {
if (!deferUnloading) {
super.onUnload();
}
}
}
private Timer getInitializationTimeoutTimer() {
if (initializationTimeoutTimer == null) {
initializationTimeoutTimer = new Timer() {
@Override
public void run() {
setInitialized(false);
setInitializing(false);
// Clear the entry in activeEditors so that we don't hold the memory if it needs to be cleaned up.
activeEditors.put(elementId, null);
activeEditors.remove(elementId);
// Run the pending commands to remove all the irrelevant ones
runPendingCommand();
}
};
}
return initializationTimeoutTimer;
}
private boolean initialize() {
try {
if (!isInitialized() && !isInitializing()) {
initTinyMce(elementId, options.getJavaScriptObject());
setInitializing(true); // set this after since the above call is asynchronous. We don't want it to be true and an exception to be thrown
// Even though this entry was set in the constructor, it could have been unset
// in the unload function. We ensure that we have a valid reference while this
// editor is initialized
activeEditors.put(elementId, this);
getInitializationTimeoutTimer().schedule(1000); // give TinyMCE 1 second to initialize the editor
return true;
}
} catch (JavaScriptException e) {
GWT.log("Unable to initialize the TinyMCE editor.", e);
}
return false;
}
private boolean uninitialize() {
if (isInitialized()) {
try {
unloadTinyMce(elementId);
} catch (JavaScriptException e) {
GWT.log("Unable to uninitialize the TinyMCE editor.", e);
} finally {
setInitialized(false);
setInitializing(false);
// Clear the entry in activeEditors so that we don't hold the memory if it needs to be cleaned up.
activeEditors.put(elementId, null);
activeEditors.remove(elementId);
}
return true;
}
return false;
}
private void runPendingCommand()
{
if (!libraryLoaded) {
// Only perform operations specific to TinyMCE if the library was successfully loaded
return;
}
if (isInitializing()) {
return;
}
if (pendingCommands.isEmpty()) {
return;
}
Integer pendingCommand = pendingCommands.get(0);
while (pendingCommand != null) {
try {
if (pendingCommand == PENDING_COMMAND_LOAD) {
if (!initialize()) {
// Need to add the loading command back if it was unsuccessful.
pendingCommands.add(0, PENDING_COMMAND_LOAD);
}
break; // we break because a load is asynchronous. We can't process any of the other commands until later.
}
if (!isInitialized() && !isInitializing()) {
// If we're not initializing or already initialized, then all other commands (other than LOAD) are irrelevant.
// We "continue" (but don't try and execute) to remove them from the pending queue.
continue;
}
if (pendingCommand == PENDING_COMMAND_UNLOAD) {
// This is a deferred onUnload call.
uninitialize();
super.onUnload();
} else if (pendingCommand == PENDING_COMMAND_SELECT_ALL) {
selectAll();
} else if (pendingCommand == PENDING_COMMAND_SET_HTML) {
setHTML(pendingSetHtmlText);
pendingSetHtmlText = null;
} else if (pendingCommand == PENDING_COMMAND_SET_FOCUS) {
if (focused) {
setFocus(true);
}
}
} finally {
pendingCommand = null;
pendingCommands.remove(0);
if (!pendingCommands.isEmpty()) {
pendingCommand = pendingCommands.get(0);
}
}
}
}
private native void initTinyMce(String elementId, JavaScriptObject options)
/*-{
$wnd.tinymce.init(options);
}-*/;
private native void unloadTinyMce(String elementId)
/*-{
var editorsToDelete = [];
// First collect all the editors to delete. Firefox doesn't like it when we delete in place
// since it does end up modifying the editors array.
for (var index = 0; index < $wnd.tinymce.editors.length; ++index) {
var myEditor = $wnd.tinymce.editors[index];
if (!(myEditor.id === elementId)) { // Javascript '===' is the identity operator. both type and value must match to return true.
continue;
}
// Check to see if it's already been added.
var alreadyAdded = false;
for (var deleteIndex = 0; deleteIndex < editorsToDelete.length; ++deleteIndex) {
if (editorsToDelete[deleteIndex].id === myEditor.id) { // Javascript '===' is the identity operator. both type and value must match to return true.
alreadyAdded = true;
break;
}
}
if (alreadyAdded) {
continue;
}
editorsToDelete.push(myEditor);
}
// Have a second loop to actually delete the editors.
for (var index = editorsToDelete.length - 1; index >= 0; index--) {
var myEditor = editorsToDelete[index];
if (myEditor == null) {
continue;
}
delete myEditor.updatedSelectionBookmark;
myEditor.updatedSelectionBookmark = null;
myEditor.remove();
if (myEditor == null) {
continue;
}
myEditor.destroy();
myEditor = null;
}
// Clear the array by setting its length to 0.
editorsToDelete.length = 0;
// Null out the reference altogether
editorsToDelete = null;
}-*/;
public static boolean loadLibrary() {
String scriptUrl = "";
if (libraryVersion == TINYMCE_VERSION_4) {
scriptUrl = GWT.getModuleBaseURL() + "tinymce4/tinymce.min.js";
} else if (libraryVersion == TINYMCE_VERSION_3) {
// scriptUrl = GWT.getModuleBaseURL() + "tinymce3/tiny_mce.js";
return false; // Security Audit found an issue with using eval. Removing from code base for now. 4.20.2015
} else {
// No such version allowed.
return false;
}
JavaScriptObject scriptInstance = ScriptInjector.fromUrl(scriptUrl)
.setWindow(ScriptInjector.TOP_WINDOW)
.setCallback(new Callback<Void, Exception>() {
@Override
public void onFailure(Exception reason) {
Window.alert("TinyMCE failed to load.");
libraryLoaded = false;
}
@Override
public void onSuccess(Void result) {
// We need to register the Javascript method that will handle all of the events.
registerEventHandler();
libraryLoaded = true;
}
}).inject();
return (scriptInstance != null);
}
public static void setupCallback(String elementId) {
// Add actions here to take after the setup of the editor is complete (usually when event handlers are added)
}
public static void initCallback(String elementId) {
// Add actions here to take after the editor has been properly initialized. Usually, calls are made to the widget
// such as setSize, setTabIndex - before the editor is initialized. We make those calls here to ensure that they
// get set properly.
final String elementIdFinal = elementId;
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@Override
public void execute() {
HFRichTextEditor editorInstance = getActiveEditor(elementIdFinal);
if (editorInstance != null) {
editorInstance.getInitializationTimeoutTimer().cancel(); // cancel the timer that will de-initialize this editor since we have initialized properly
editorInstance.setControlTabIndex(elementIdFinal); // set up the tabIndex that comes from the hidden textarea (if it exists)
JSONValue pxWidth = editorInstance.getOptions().get("width");
if (pxWidth == null) {
pxWidth = new JSONNumber(DEFAULT_WIDTH);
}
JSONValue pxHeight = editorInstance.getOptions().get("height");
if (pxHeight == null) {
pxHeight = new JSONNumber(DEFAULT_HEIGHT);
}
editorInstance.setControlSize(elementIdFinal, pxWidth, pxHeight);
editorInstance.setInitialized(true);
editorInstance.setInitializing(false);
// Set it to visible if the parent is visible
if (editorInstance.getParent() != null && editorInstance.getParent().isVisible()) {
editorInstance.showEditor(editorInstance.elementId, true);
}
// If there were any pending unload commands, we run them here.
editorInstance.runPendingCommand();
}
}
});
}
/**
* This gets called by the JSNI code for every event that we want to handle.
* @param elementId the unique ID of the HFRichTextEditor. Used to locate the source of the event.
* @param event the NativeEvent that we will fire into the GWT event handling loop
*/
public static void handleNativeEvent(String elementId, Object event) {
if (event instanceof NativeEvent) {
HFRichTextEditor editorInstance = getActiveEditor(elementId);
if (editorInstance != null) {
DomEvent.fireNativeEvent((NativeEvent) event, editorInstance);
}
}
}
/**
* This is the native JavaScript code that will create the callbacks needed for integration between GWT and
* TinyMCE library. It's called upon loading the library so that each time an editor is initialized, it can
* call the appropriate callback functions to properly register all the handlers for that editor.
*/
private native static void registerEventHandler()
/*-{
$wnd.hfrichtexteditorSetupCallback =
$entry(
function(e) {
@com.healthfortis.map.shared.client.ui.HFRichTextEditor::setupCallback(Ljava/lang/String;)(e.id);
e.on('mousedown', function(ev) {
@com.healthfortis.map.shared.client.ui.HFRichTextEditor::handleNativeEvent(Ljava/lang/String;Ljava/lang/Object;)(e.id, ev);
});
e.on('mouseup', function(ev) {
@com.healthfortis.map.shared.client.ui.HFRichTextEditor::handleNativeEvent(Ljava/lang/String;Ljava/lang/Object;)(e.id, ev);
});
e.on('keyup', function(ev) {
@com.healthfortis.map.shared.client.ui.HFRichTextEditor::handleNativeEvent(Ljava/lang/String;Ljava/lang/Object;)(e.id, ev);
});
e.on('keydown', function(ev) {
@com.healthfortis.map.shared.client.ui.HFRichTextEditor::handleNativeEvent(Ljava/lang/String;Ljava/lang/Object;)(e.id, ev);
// Prevent Ctrl+S from propagating to the TinyMCE editor. We don't want to save to external files.
if (ev.ctrlKey || ev.metaKey) {
switch (String.fromCharCode(ev.which || ev.keyCode).toLowerCase()) {
case 's':
ev.preventDefault();
if (ev.stopPropagation) {
ev.stopPropagation();
} else {
ev.cancelBubble = true;
}
break;
}
}
});
e.on('keypress', function(ev) {
@com.healthfortis.map.shared.client.ui.HFRichTextEditor::handleNativeEvent(Ljava/lang/String;Ljava/lang/Object;)(e.id, ev);
});
e.on('blur', function(ev) {
@com.healthfortis.map.shared.client.ui.HFRichTextEditor::handleNativeEvent(Ljava/lang/String;Ljava/lang/Object;)(e.id, ev);
});
e.on('focus', function(ev) {
@com.healthfortis.map.shared.client.ui.HFRichTextEditor::handleNativeEvent(Ljava/lang/String;Ljava/lang/Object;)(e.id, ev);
});
e.on('click', function(ev) {
@com.healthfortis.map.shared.client.ui.HFRichTextEditor::handleNativeEvent(Ljava/lang/String;Ljava/lang/Object;)(e.id, ev);
});
}
);
$wnd.hfrichtexteditorInitCallback =
$entry(
function(e) {
@com.healthfortis.map.shared.client.ui.HFRichTextEditor::initCallback(Ljava/lang/String;)(e.id);
}
);
}-*/;
public SafeHtml getHTML() {
SafeHtml result = null;
if (libraryLoaded && initialized) {
try {
String contentHtml = getContentHtml(elementId); // TinyMCE takes care of the sanitization.
if (contentHtml == null || contentHtml.trim().isEmpty()) {
return SafeHtmlUtils.fromSafeConstant("");
}
// Remove the root block <p></p> that gets added automatically by TinyMCE
if (contentHtml.startsWith("<p>") && contentHtml.endsWith("</p>")) {
contentHtml = contentHtml.substring(3, contentHtml.length() - 4);
}
result = SafeHtmlUtils.fromTrustedString(contentHtml);
} catch (JavaScriptException e) {
GWT.log("Unable to get the content from the TinyMCE editor.", e);
}
} else {
String text = super.getText();
if (text == null || text.trim().isEmpty()) {
return SafeHtmlUtils.fromSafeConstant("");
} else {
return SafeHtmlUtils.fromString(text);
}
}
return result;
}
public String getText()
{
String result = "";
if (libraryLoaded && initialized) {
try {
String contentText = getContentText(elementId);
if (contentText == null) {
contentText = "";
}
result = SafeHtmlUtils.fromString(contentText).asString(); // requested as text, so we need to escape the string
} catch (JavaScriptException e) {
GWT.log("Unable to get the content from the TinyMCE editor.", e);
}
} else {
result = super.getText();
if (result == null || result.trim().isEmpty()) {
result = "";
} else {
result = SafeHtmlUtils.fromString(result).asString();
}
}
return result;
}
private native String getContentHtml(String elementId)
/*-{
var myEditor = $wnd.tinymce.get(elementId);
if (myEditor == null) {
return "";
}
$wnd.tinyMCE.triggerSave();
return myEditor.getContent();
}-*/;
private native String getContentText(String elementId)
/*-{
var myEditor = $wnd.tinymce.get(elementId);
if (myEditor == null) {
return "";
}
$wnd.tinyMCE.triggerSave();
return myEditor.getContent({ format : "text" });
}-*/;
public void setHTML(SafeHtml html) {
String text = html == null ? null: html.asString();
if (libraryLoaded && (isInitialized() || isInitializing())) {
if (isInitializing()) {
pendingSetHtmlText = html;
addPendingSetHtml();
return;
}
try {
setContent(elementId, text);
} catch (JavaScriptException e) {
// Don't do anything, just allow it to return.
GWT.log("Unable to set the content on the TinyMCE editor.", e);
}
return;
} else {
super.setText(text);
}
}
/*
* Will automatically escape the string and put it into the widget
*/
@Override
public void setText(String text)
{
String htmlText = text == null ? "" : text;
setHTML(SafeHtmlUtils.fromString(htmlText));
}
private native String setContent(String elementId, String text)
/*-{
var myEditor = $wnd.tinymce.get(elementId);
if (myEditor != null) {
myEditor.setContent(text);
}
}-*/;
private native void setBookmarkPosition(String elementId)
/*-{
var myEditor = $wnd.tinymce.get(elementId);
if (myEditor == null) {
return;
}
myEditor.updatedSelectionBookmark = myEditor.selection.getBookmark(1);
}-*/;
private native void restoreBookmarkPosition(String elementId)
/*-{
var myEditor = $wnd.tinymce.get(elementId);
if (myEditor == null) {
return;
}
myEditor.updatedSelectionBookmark && myEditor.selection.moveToBookmark(myEditor.updatedSelectionBookmark);
}-*/;
@Override
public void selectAll() {
if (isInitializing()) {
addPendingSelectAll();
} else if (!isInitialized()) {
// It's not even initialized. We don't do anything.
return;
}
// If it's properly initialized, then we do the select all.
selectAllContent(elementId);
}
private native void selectAllContent(String elementId)
/*-{
var myEditor = $wnd.tinymce.get(elementId);
if (myEditor != null) {
myEditor.selection.select(myEditor.getBody(), true);
}
}-*/;
/**
* Puts the text cursor at the beginning or end of the body text.
*
* @param atBeginning true if the cursor should be set at the beginning, false if at the end.
*/
public void setTextCursor(boolean atBeginning) {
if (!isInitialized()) {
// It's not even initialized. We don't do anything.
return;
}
// If it's properly initialized, we first select all, then collapse the selection
// to either the beginning or the end.
selectAllContent(elementId);
collapseSelection(elementId, atBeginning);
}
private native void collapseSelection(String elementId, boolean atBeginning)
/*-{
var myEditor = $wnd.tinymce.get(elementId);
if (myEditor != null) {
myEditor.selection.collapse(atBeginning);
}
}-*/;
@Override
public String getSelectedText() {
return getSelectedContent(elementId);
}
private native String getSelectedContent(String elementId)
/*-{
var myEditor = $wnd.tinymce.get(elementId);
if (myEditor != null) {
return myEditor.selection.getContent();
}
return "";
}-*/;
private JSONValue parseSizeDimension(String dimension) {
JSONValue pxDimension = new JSONNumber(0);
if (dimension.endsWith("px")) {
dimension = dimension.substring(0, dimension.length() - 2);
pxDimension = new JSONNumber(Integer.parseInt(dimension.trim()));
} else if (dimension.endsWith("em")) {
dimension = dimension.substring(0, dimension.length() - 2);
// 1em == 16px
pxDimension = new JSONNumber((int) Float.parseFloat(dimension.trim()) * 16);
} else if (dimension.endsWith("%")) {
// eg. 100%
pxDimension = new JSONString(dimension);
} else {
// A straight number is equivalent to pixels
pxDimension = new JSONNumber(Integer.parseInt(dimension.trim()));
}
return pxDimension;
}
@Override
public void setSize(String width, String height) {
setWidth(width);
setHeight(height);
}
private native void setControlSize(String elementId, JSONValue pxWidth, JSONValue pxHeight)
/*-{
var myEditor = $wnd.tinymce.get(elementId);
if (myEditor != null) {
// Try multiple methods to set the size. Can't seem to get the size to stick in Chrome and Firefox
myEditor.dom.setStyle(elementId + '_ifr', 'height', pxHeight + 'px');
myEditor.dom.setStyle(elementId + '_ifr', 'width', pxWidth + 'px');
myEditor.theme.resizeTo(pxWidth, pxHeight);
}
}-*/;
@Override
public void setWidth(String width) {
super.setWidth(width);
try {
JSONValue pxWidth = parseSizeDimension(width);
addOption("width", pxWidth);
if (libraryLoaded && initialized) {
JSONValue pxHeight = options.get("height");
if (pxHeight == null) {
pxHeight = new JSONNumber(DEFAULT_HEIGHT);
}
setControlSize(elementId, pxWidth, pxHeight);
}
} catch (JavaScriptException e) {
// Don't do anything, just allow it to return.
GWT.log("Unable to set the width on the TinyMCE editor.", e);
}
}
@Override
public void setHeight(String height) {
super.setHeight(height);
try {
JSONValue pxHeight = parseSizeDimension(height);
addOption("height", pxHeight);
if (libraryLoaded && initialized) {
JSONValue pxWidth = options.get("width");
if (pxWidth == null) {
pxWidth = new JSONNumber(DEFAULT_WIDTH);
}
setControlSize(elementId, pxWidth, pxHeight);
}
} catch (JavaScriptException e) {
// Don't do anything, just allow it to return.
GWT.log("Unable to set the height on the TinyMCE editor.", e);
}
}
/*
* Returns whether the current object is the one that has the focus.
*/
public boolean isFocused() {
return focused;
}
public void setFocus(boolean focused) {
if (!focused) {
super.setFocus(focused); // nothing we can do to unset focus. Let GWT handle it.
return;
}
try {
// Only pass along the focus command
if (libraryLoaded && (isInitialized() || isInitializing())) {
if (isInitializing()) {
// Setting a pending focus actually messes up the final focused element.
// This is because the async call always returns last, so any calls to setFocus
// for any other element essentially get ignored.
// if (focused) {
// addPendingSetFocus();
// }
return;
}
try {
setControlFocus(elementId);
} catch (JavaScriptException e) {
GWT.log("Unable to set the focus on the TinyMCE editor.", e);
}
return;
}
} finally {
super.setFocus(focused);
}
}
private native void setControlFocus(String elementId)
/*-{
var myEditor = $wnd.tinymce.get(elementId);
if (myEditor != null) {
myEditor.focus();
}
}-*/;
public void setTabIndex(int tabIndex) {
super.setTabIndex(tabIndex); // sets the tabIndex for the hidden associated textarea
if (!libraryLoaded || !initialized) {
// If not loaded or initialized, we're done.
return;
}
try {
// This will move the value in the textArea to the iframe
setControlTabIndex(elementId);
} catch (JavaScriptException e) {
GWT.log("Unable to set the tab index on the TinyMCE editor iframe.", e);
}
}
/**
* This will move the tabIndex from the textArea to the iframe.
*
* @param elementId the elementId to uniquely identify the textarea
*/
private native void setControlTabIndex(String elementId)
/*-{
var myEditor = $wnd.tinymce.get(elementId);
if (myEditor != null) {
var textAreaElement = null;
if ($doc.getElementById) {
textAreaElement = $doc.getElementById(elementId); // element replaced by TinyMCE
}
if (textAreaElement == null) {
return;
}
var originalTabIndex = 0; // tabindex of element, or 0
if (textAreaElement.getAttribute && textAreaElement.getAttribute('tabindex')) {
originalTabIndex = textAreaElement.getAttribute('tabindex');
}
var iframeTextEditor = $doc.getElementById(elementId + '_ifr'); // editor iframe element
if (iframeTextEditor == null || !iframeTextEditor.setAttribute) {
return;
}
iframeTextEditor.setAttribute('tabindex', originalTabIndex); // set iframe tabindex
}
}-*/;
private native void showEditor(String elementId, boolean visible)
/*-{
var myEditor = $wnd.tinymce.get(elementId);
if (myEditor != null) {
if (visible) {
myEditor.show();
} else {
myEditor.hide();
}
}
}-*/;
@Override
public void setVisible(boolean visible) {
super.setVisible(visible);
if (visible) {
// Call initialize in case it wasn't previously initialized because the widget was hidden.
initialize();
}
showEditor(elementId, visible);
}
public JSONObject getOptions() {
return options;
}
public boolean isInitialized() {
return initialized;
}
public void setInitialized(boolean initialized) {
this.initialized = initialized;
}
public boolean isInitializing() {
return initializing;
}
public void setInitializing(boolean initializing) {
this.initializing = initializing;
}
}