Skip to content

Commit 99e1026

Browse files
committed
Backport Dialog fixes and eventing improvements from V24 branch to V23 branch. Improved debounce mode.
1 parent 4004f7e commit 99e1026

File tree

7 files changed

+2588
-7119
lines changed

7 files changed

+2588
-7119
lines changed

package-lock.json

+2,390-7,071
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pom.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
<artifactId>vaadin-bom</artifactId>
5757
<type>pom</type>
5858
<scope>import</scope>
59-
<version>23.3.6</version>
59+
<version>23.3.33</version>
6060
</dependency>
6161
</dependencies>
6262
</dependencyManagement>
@@ -142,7 +142,7 @@
142142
<plugin>
143143
<groupId>com.vaadin</groupId>
144144
<artifactId>vaadin-maven-plugin</artifactId>
145-
<version>23.3.6</version>
145+
<version>23.3.33</version>
146146
<executions>
147147
<execution>
148148
<goals>

src/main/java/org/vaadin/tinymce/TinyMce.java

+119-25
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,23 @@
1818
import com.vaadin.flow.component.AbstractCompositeField;
1919
import com.vaadin.flow.component.AttachEvent;
2020
import com.vaadin.flow.component.ClientCallable;
21+
import com.vaadin.flow.component.ComponentEventListener;
2122
import com.vaadin.flow.component.DetachEvent;
23+
import com.vaadin.flow.component.Focusable;
2224
import com.vaadin.flow.component.HasSize;
2325
import com.vaadin.flow.component.Tag;
2426
import com.vaadin.flow.component.UI;
2527
import com.vaadin.flow.component.dependency.CssImport;
2628
import com.vaadin.flow.component.dependency.JavaScript;
2729
import com.vaadin.flow.component.dependency.StyleSheet;
2830
import com.vaadin.flow.component.html.Div;
31+
import com.vaadin.flow.dom.DomEventListener;
32+
import com.vaadin.flow.dom.DomListenerRegistration;
2933
import com.vaadin.flow.dom.Element;
3034
import com.vaadin.flow.dom.ShadowRoot;
3135
import com.vaadin.flow.function.SerializableConsumer;
36+
import com.vaadin.flow.shared.Registration;
37+
3238
import elemental.json.Json;
3339
import elemental.json.JsonArray;
3440
import elemental.json.JsonObject;
@@ -50,15 +56,19 @@
5056
@JavaScript("context://frontend/tinymce_addon/tinymce/tinymce.js")
5157
@CssImport("./tinymceLumo.css")
5258
public class TinyMce extends AbstractCompositeField<Div, TinyMce, String>
53-
implements HasSize {
59+
implements HasSize, Focusable<TinyMce> {
5460

61+
private final DomListenerRegistration domListenerRegistration;
5562
private String id;
5663
private boolean initialContentSent;
5764
private String currentValue = "";
5865
private String rawConfig;
5966
JsonObject config = Json.createObject();
6067
private Element ta = new Element("div");
6168
private boolean basicTinyMCECreated;
69+
private boolean enabled = true;
70+
private boolean readOnly = false;
71+
private int debounceTimeout = 0;
6272

6373
/**
6474
* Creates a new TinyMce editor with shadowroot set or disabled. The shadow
@@ -67,6 +77,8 @@ public class TinyMce extends AbstractCompositeField<Div, TinyMce, String>
6777
* hand, the shadow root must not be on when for example used in inline
6878
* mode.
6979
*
80+
* @deprecated No longer needed since version x.x
81+
*
7082
* @param shadowRoot
7183
* true of shadow root hack should be used
7284
*/
@@ -81,6 +93,40 @@ public TinyMce(boolean shadowRoot) {
8193
getElement().appendChild(ta);
8294
}
8395
getElement().getClassList().add("tinymce-flow");
96+
domListenerRegistration = getElement().addEventListener("tchange",
97+
(DomEventListener) event -> {
98+
boolean value = event.getEventData()
99+
.hasKey("event.htmlString");
100+
String htmlString = event.getEventData()
101+
.getString("event.htmlString");
102+
currentValue = htmlString;
103+
setModelValue(htmlString, true);
104+
});
105+
domListenerRegistration.addEventData("event.htmlString");
106+
domListenerRegistration.debounce(debounceTimeout);
107+
}
108+
109+
/**
110+
* Sets the debounce timeout for the value change event. The default is 0,
111+
* when value change is triggered on blur and enter key presses. When value
112+
* is more than 0 the value change is emitted with delay of given timeout
113+
* milliseconds after last keystroke.
114+
*
115+
* @param debounceTimeout
116+
* the debounce timeout in milliseconds
117+
*/
118+
public void setDebounceTimeout(int debounceTimeout) {
119+
if (debounceTimeout > 0) {
120+
runBeforeClientResponse(ui -> {
121+
getElement().callJsFunction("$connector.setEager", true);
122+
});
123+
} else {
124+
runBeforeClientResponse(ui -> {
125+
getElement().callJsFunction("$connector.setEager", false);
126+
});
127+
}
128+
this.debounceTimeout = debounceTimeout;
129+
domListenerRegistration.debounce(debounceTimeout);
84130
}
85131

86132
public TinyMce() {
@@ -101,8 +147,16 @@ public void setEditorContent(String html) {
101147

102148
@Override
103149
protected void onAttach(AttachEvent attachEvent) {
104-
id = UUID.randomUUID().toString();
105-
ta.setAttribute("id", id);
150+
if (id == null) {
151+
id = UUID.randomUUID().toString();
152+
ta.setAttribute("id", id);
153+
}
154+
if (!getEventBus().hasListener(BlurEvent.class)) {
155+
// adding fake blur listener so throttled value
156+
// change events happen by latest at blur
157+
addBlurListener(e -> {
158+
});
159+
}
106160
if (!attachEvent.isInitialAttach()) {
107161
// Value after initial attach should be set via TinyMCE JavaScript
108162
// API, otherwise value is not updated upon reattach
@@ -115,6 +169,8 @@ protected void onAttach(AttachEvent attachEvent) {
115169

116170
@Override
117171
protected void onDetach(DetachEvent detachEvent) {
172+
detachEvent.getUI().getPage().executeJs("tinymce.get($0).remove();",
173+
id);
118174
super.onDetach(detachEvent);
119175
initialContentSent = false;
120176
// save the current value to the dom element in case the component gets
@@ -126,8 +182,9 @@ private void initConnector() {
126182

127183
runBeforeClientResponse(ui -> {
128184
ui.getPage().executeJs(
129-
"window.Vaadin.Flow.tinymceConnector.initLazy($0, $1, $2, $3)",
130-
rawConfig, getElement(), ta, config).then(res -> {
185+
"window.Vaadin.Flow.tinymceConnector.initLazy($0, $1, $2, $3, $4, $5)",
186+
rawConfig, getElement(), ta, config, currentValue,
187+
(enabled && !readOnly)).then(res -> {
131188
// Delay setting flag on first attach, otherwise setting
132189
// initial value on attach does not work
133190
initialContentSent = true;
@@ -140,12 +197,6 @@ void runBeforeClientResponse(SerializableConsumer<UI> command) {
140197
.beforeClientResponse(this, context -> command.accept(ui)));
141198
}
142199

143-
@ClientCallable
144-
private void updateValue(String htmlString) {
145-
this.currentValue = htmlString;
146-
setModelValue(htmlString, true);
147-
}
148-
149200
public String getCurrentValue() {
150201
return currentValue;
151202
}
@@ -189,33 +240,76 @@ public void replaceSelectionContent(String htmlString) {
189240
"$connector.replaceSelectionContent", htmlString));
190241
}
191242

243+
@Override
192244
public void focus() {
193-
runBeforeClientResponse(
194-
ui -> getElement().callJsFunction("$connector.focus"));
245+
runBeforeClientResponse(ui -> {
246+
// Dialog has timing issues...
247+
getElement().executeJs(
248+
//@formatter:off
249+
"const el = this;"+
250+
"if($connector.isInDialog()) {"+
251+
" setTimeout(() => {"+
252+
" $connector.focus()"+
253+
" }, 150);"+
254+
"} else {"+
255+
" $connetor.focus();"+
256+
"}"
257+
//@formatter:on
258+
);
259+
});
260+
}
261+
262+
@Override
263+
public Registration addFocusListener(
264+
ComponentEventListener<FocusEvent<TinyMce>> listener) {
265+
DomListenerRegistration domListenerRegistration = getElement()
266+
.addEventListener("tfocus", event -> listener
267+
.onComponentEvent(new FocusEvent<>(this, false)));
268+
return domListenerRegistration;
269+
}
270+
271+
@Override
272+
public Registration addBlurListener(
273+
ComponentEventListener<BlurEvent<TinyMce>> listener) {
274+
DomListenerRegistration domListenerRegistration = getElement()
275+
.addEventListener("tblur", event -> listener
276+
.onComponentEvent(new BlurEvent<>(this, false)));
277+
return domListenerRegistration;
278+
}
279+
280+
@Override
281+
public void blur() {
282+
throw new RuntimeException(
283+
"Not implemented, TinyMce does not support programmatic blur.");
195284
}
196285

197286
@Override
198287
public void setEnabled(boolean enabled) {
199-
super.setEnabled(enabled);
200-
runBeforeClientResponse(ui -> {
201-
getElement().callJsFunction("$connector.setEnabled", enabled);
202-
});
288+
this.enabled = enabled;
289+
adjustEnabledState();
290+
}
291+
292+
private void adjustEnabledState() {
293+
boolean reallyEnabled = this.enabled && !this.readOnly;
294+
super.setEnabled(reallyEnabled);
295+
runBeforeClientResponse(ui -> getElement()
296+
.callJsFunction("$connector.setEnabled", reallyEnabled));
203297
}
204298

205299
@Override
206300
public void setReadOnly(boolean readOnly) {
301+
this.readOnly = readOnly;
207302
super.setReadOnly(readOnly);
208-
setEnabled(!readOnly);
209-
if (readOnly) {
210-
getElement().setAttribute("readonly", "");
211-
} else {
212-
getElement().removeAttribute("readonly");
213-
}
303+
adjustEnabledState();
214304
}
215305

216306
@Override
217-
protected void setPresentationValue(String t) {
218-
setEditorContent(t);
307+
protected void setPresentationValue(String html) {
308+
this.currentValue = html;
309+
if (initialContentSent) {
310+
runBeforeClientResponse(ui -> getElement()
311+
.callJsFunction("$connector.setEditorContent", html));
312+
}
219313
}
220314

221315
private TinyMce createBasicTinyMce() {

src/main/resources/META-INF/resources/frontend/tinymceConnector.js

+74-19
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
window.Vaadin.Flow.tinymceConnector = {
2-
initLazy: function (customConfig, c, ta, options) {
2+
initLazy: function (customConfig, c, ta, options, initialContent, enabled) {
33
// Check whether the connector was already initialized for the editor
4-
var currentValue = ta.innerHTML;
54
var readonlyTimeout;
5+
var currentValue = ta.innerHTML;
6+
var eager = false;
67

8+
var readonlyTimeout;
79
if (c.$connector) {
810
// If connector was already set, this is re-attach, remove editor
911
// and re-init
10-
tinymce.remove();
12+
c.$connector.editor.remove();
1113
} else {
1214
// Init connector at first visit
1315
c.$connector = {
@@ -18,54 +20,107 @@ window.Vaadin.Flow.tinymceConnector = {
1820
currentValue = this.editor.setContent(html, {format : 'html'});
1921
}, 50);
2022
},
21-
23+
2224
replaceSelectionContent : function(html) {
2325
this.editor.selection.setContent(html);
2426
},
25-
27+
2628
focus : function() {
27-
this.editor.focus();
29+
this.editor.focus();
2830
},
29-
31+
3032
setEnabled : function(enabled) {
3133
// Debounce is needed if mode is attempted to be changed more than once
3234
// during the attach
3335
clearTimeout(readonlyTimeout);
3436
readonlyTimeout = setTimeout(() => {
3537
this.editor.mode.set(enabled ? 'design' : 'readonly');
3638
}, 20);
37-
}
39+
},
40+
41+
setEager : function(changeMode) {
42+
eager = changeMode;
43+
},
3844

45+
isInDialog: function() {
46+
let inDialog = false;
47+
let parent = c.parentElement;
48+
while(parent != null) {
49+
if(parent.tagName.toLowerCase().indexOf("vaadin-dialog") == 0) {
50+
inDialog = true;
51+
break;
52+
}
53+
parent = parent.parentElement;
54+
}
55+
return inDialog;
56+
}
3957
};
4058

4159
}
4260

43-
const pushChanges = function() {
44-
c.$server.updateValue(currentValue)
45-
}
61+
var baseconfig = JSON.parse(customConfig) || {};
4662

47-
var baseconfig = JSON.parse(customConfig) || {}
48-
4963
Object.assign(baseconfig, options);
5064
// Height defined in Java component, always just adapt to that
5165
baseconfig['height'] = "100%";
5266

5367
baseconfig['target'] = ta;
54-
68+
69+
if(!enabled) {
70+
baseconfig['readonly'] = true;
71+
}
72+
5573
baseconfig['setup'] = function(ed) {
5674
c.$connector.editor = ed;
57-
ed.on('setContent', function(e) {
58-
currentValue = ed.getContent();
75+
76+
ed.on('init', e => {
77+
if(c.$connector.isInDialog()) {
78+
// This is inside a shadowroot (Dialog in Vaadin)
79+
// and needs some hacks to make this nagigateable with keyboard
80+
if(c.tabIndex < 0) {
81+
// make the wrapping element also focusable
82+
c.setAttribute("tabindex", 0);
83+
}
84+
// on focus to wrapping element, pass forward to editor
85+
c.addEventListener("focus", e => {
86+
ed.focus();
87+
});
88+
// Move aux element as child from body to element to fix menus in modal Dialog
89+
const aux = document.getElementsByClassName('tox-tinymce-aux')[0];
90+
aux.parentElement.removeChild(aux);
91+
// Fix to allow menu grow outside Dialog
92+
aux.style.position = 'absolute';
93+
c.appendChild(aux);
94+
}
5995
});
96+
6097
ed.on('change', function(e) {
61-
currentValue = ed.getContent();
98+
const event = new Event("tchange");
99+
event.htmlString = ed.getContent();
100+
c.dispatchEvent(event);
62101
});
63102
ed.on('blur', function(e) {
64-
currentValue = ed.getContent();
65-
pushChanges();
103+
const blurEvent = new Event("tblur");
104+
c.dispatchEvent(blurEvent);
105+
const changeEvent = new Event("tchange");
106+
changeEvent.htmlString = ed.getContent();
107+
c.dispatchEvent(changeEvent);
108+
});
109+
ed.on('focus', function(e) {
110+
const event = new Event("tfocus");
111+
c.dispatchEvent(event);
112+
});
113+
ed.on('input', function(e) {
114+
if (eager) {
115+
const event = new Event("tchange");
116+
event.htmlString = ed.getContent();
117+
c.dispatchEvent(event);
118+
}
66119
});
67120
};
68121

122+
ta.innerHTML = initialContent;
123+
69124
// Allways re-init editor
70125
tinymce.init(baseconfig);
71126
}

0 commit comments

Comments
 (0)