1212import org .jsoup .nodes .Document ;
1313import org .jsoup .nodes .Element ;
1414
15- import java .io .File ;
16- import java .io .IOException ;
15+ import java .io .*;
1716import java .lang .reflect .InvocationTargetException ;
1817import java .net .ServerSocket ;
1918import java .nio .charset .StandardCharsets ;
2019import java .nio .file .Files ;
2120import java .util .*;
21+ import java .util .concurrent .CompletableFuture ;
2222import java .util .concurrent .atomic .AtomicBoolean ;
2323import java .util .function .Consumer ;
2424
@@ -151,8 +151,8 @@ public static void remove(Thread... threads) {
151151 /**
152152 * @see #executeJavaScriptSafely(String, String, int)
153153 */
154- public void executeJavaScriptSafely (String jsCode ) {
155- executeJavaScriptSafely (jsCode , "internal" , 0 );
154+ public CompletableFuture < Void > executeJavaScriptSafely (String jsCode ) {
155+ return executeJavaScriptSafely (jsCode , "internal" , 0 );
156156 }
157157
158158 /**
@@ -161,8 +161,24 @@ public void executeJavaScriptSafely(String jsCode) {
161161 *
162162 * @see #getSnapshot() internal JS dependencies are added here.
163163 */
164- public void executeJavaScriptSafely (String jsCode , String jsCodeSourceName , int jsCodeStartingLineNumber ) {
165- runIfReadyOrLater (() -> executeJavaScript (jsCode , jsCodeSourceName , jsCodeStartingLineNumber ));
164+ public CompletableFuture <Void > executeJavaScriptSafely (String jsCode , String jsCodeSourceName , int jsCodeStartingLineNumber ) {
165+ var f = new CompletableFuture <Void >();
166+ if (App .isInDepthDebugging ) {
167+ Exception e = new Exception ();
168+ var sw = new StringWriter ();
169+ e .printStackTrace (new PrintWriter (sw ));
170+ String finalJsCode = "var javaStackTrace = `" +Value .escapeForJavaScript (sw .toString ())+"`;\n \n \n " + jsCode ;
171+ runIfReadyOrLater (() -> {
172+ executeJavaScript (finalJsCode , jsCodeSourceName , jsCodeStartingLineNumber );
173+ f .complete (null );
174+ });
175+ } else {
176+ runIfReadyOrLater (() -> {
177+ executeJavaScript (jsCode , jsCodeSourceName , jsCodeStartingLineNumber );
178+ f .complete (null );
179+ });
180+ }
181+ return f ;
166182 }
167183
168184 /**
@@ -265,6 +281,9 @@ public UI access(Runnable code) {
265281 for (PendingAppend pendingAppend : pendingAppends ) {
266282 try {
267283 attachToParentSafely (pendingAppend );
284+ } catch (InvalidParentException e ){
285+ // Do nothing / remove pendingAppend from list
286+ // since its component probably was never added to a parent
268287 } catch (Exception e ) {
269288 AL .warn (e );
270289 }
@@ -512,10 +531,10 @@ public void z_internal_load(Class<? extends Route> routeClass) throws IOExceptio
512531 this .route = route ;
513532 this .listenersAndComps .clear ();
514533 this .content = route .loadContent ();
534+ this .content .setAttached (true );
515535 this .content .forEachChildRecursive (child -> {
516536 child .setAttached (true );
517537 });
518- this .content .setAttached (true );
519538 UI .remove (Thread .currentThread ());
520539 }
521540
@@ -680,36 +699,64 @@ public String jsGetComp(String varName, int id) {
680699 }
681700
682701 /**
683- * Ensures all parents are attached before actually
684- * performing the pending append operation.
685- */
686- private void attachToParentSafely (PendingAppend pendingAppend ) {
687- if (pendingAppend .child .isAttached ()) return ;
688-
689- List <MyElement > parents = new ArrayList <>();
690- MyElement parent = pendingAppend .parent .element ;
691- while (parent != null && parent instanceof MyElement ) {
692- parents .add (parent ); // Make sure that the last attached parent is given too
693- if (parent .comp .isAttached ()) break ;
694- Element p = parent .parent ();
695- if (p instanceof MyElement ) parent = (MyElement ) p ;
696- else break ;
697- }
698- if (parents .size () >= 2 ) {
699- MyElement rootParentParent = parents .get (parents .size () - 1 ); // attached
700- MyElement rootParent = parents .get (parents .size () - 2 ); // not attached yet
701- // If this gets appended, there is no need of
702- // performing the pending append operations of all its children.
703- rootParent .comp .updateAll ();
704- attachToParent (rootParentParent .comp , rootParent .comp ,
705- new Component .AddedChildEvent (rootParent .comp , null , false , false ));
706- // Above also sets isAttached = true of all child components recursively,
707- // thus next attachToParentSafely() will return directly without doing nothing,
708- // and thus all pending appends for those children will not be executed,
709- // since otherwise that would cause duplicate components
710- } else {
702+ * Ensures all parents are attached before performing the pending append operation.
703+ */
704+ private CompletableFuture <Void > attachToParentSafely (PendingAppend pendingAppend ) throws InvalidParentException {
705+ if (pendingAppend .child .isAttached ()) {
706+ return CompletableFuture .completedFuture (null );
707+ }
708+
709+ List <Component > parents = getParentChain (pendingAppend .parent );
710+ if (App .isInDepthDebugging ){
711+ AL .info ("Unattached parent chain (" +parents .size ()+") child -> parent: " );
712+ String s = "" ;
713+ for (Component comp : parents ) {
714+ s += comp .toPrintString ()+" isAttached=" +comp .isAttached ()+" ||| " ;
715+ }
716+ AL .debug (this .getClass (), s );
717+ }
718+
719+ if (parents .size () < 2 ) {
711720 pendingAppend .child .updateAll ();
712- attachToParent (pendingAppend .parent , pendingAppend .child , pendingAppend .e );
721+ return attachToParent (pendingAppend .parent , pendingAppend .child , pendingAppend .e );
722+ }
723+
724+ Component rootParentParent = parents .get (parents .size () - 1 ); // attached
725+ Component rootParent = parents .get (parents .size () - 2 ); // not attached yet
726+
727+ rootParent .updateAll ();
728+ return attachToParent (rootParentParent , rootParent ,
729+ new Component .AddedChildEvent (rootParent , null , false , false ));
730+ }
731+
732+ /**
733+ * Returns a list of mainly unattached parents (the last element is the first attached parent that is found), starting from the given comp and moving up the hierarchy.
734+ * The list is in order from child to parent. <br>
735+ * <br>
736+ * @throws InvalidParentException if the last comp is not attached. Meaning the component probably was never added to a parent on the Java side.
737+ */
738+ private List <Component > getParentChain (Component startElement ) throws InvalidParentException {
739+ List <Component > parents = new ArrayList <>();
740+ MyElement current = startElement .element ;
741+
742+ while (current != null && current instanceof MyElement ) {
743+ parents .add (current .comp );
744+ if (current .comp .isAttached ()) break ;
745+
746+ Element parent = current .parent ();
747+ if (parent == null || !(parent instanceof MyElement )){
748+ throw new InvalidParentException ("Parent is invalid/null, possibly meaning that the component was never added to a parent on the Java side." +
749+ "However it can also mean that update() was never called before. comp=" +current .comp .toPrintString ()+" parent=" +parent +" " );
750+ }
751+ current = (MyElement ) parent ;
752+ }
753+
754+ return parents ;
755+ }
756+
757+ public class InvalidParentException extends Exception {
758+ public InvalidParentException (String message ) {
759+ super (message );
713760 }
714761 }
715762
@@ -725,30 +772,35 @@ public void attachWhenAccessEnds(Component<?, ?> parent, Component<?, ?> child,
725772 }
726773 }
727774
728- public <T extends Component <?, ?>> void attachToParent (Component <?, ?> parent , Component <?, ?> child , Component .AddedChildEvent e ) {
775+ public <T extends Component <?, ?>> CompletableFuture < Void > attachToParent (Component <?, ?> parent , Component <?, ?> child , Component .AddedChildEvent e ) {
729776 if (App .isInDepthDebugging ) AL .debug (this .getClass (), "attachToParent() parent = " +parent .toPrintString ()+" attached=" +parent .isAttached ()+" added child = " +
730777 child .toPrintString ()+" child html = \n " + child .element .outerHtml ());
778+ if (!parent .isAttached ())
779+ throw new RuntimeException ("Attempting attach to parent even though parent '" +parent .toPrintString ()+"' is not attached yet!" );
731780
732781 if (e .otherChildComp == null ) { // add
733- executeJavaScript (jsAttachToParent (parent , child ),
734- "internal" , 0 );
735- child .setAttached (true );
736- child .forEachChildRecursive (child2 -> {
737- child2 .setAttached (true );
782+ return executeJavaScriptSafely (jsAttachToParent (parent , child ),
783+ "internal" , 0 ).thenAccept (__ -> {
784+ child .setAttached (true );
785+ child .forEachChildRecursive (child2 -> {
786+ child2 .setAttached (true );
787+ });
738788 });
739- } else if (e .isInsert || e .isReplace ) { // for replace, remove() must be executed after this function returns
789+ } else { // if (e.isInsert || e.isReplace) { // for replace, remove() must be executed after this function returns
740790 // if replace: childComp is the new component to be added and otherChildComp is the one that gets removed/replaced
741791 // "beforebegin" = Before the element. Only valid if the element is in the DOM tree and has a parent element.
742- executeJavaScript (
792+ return executeJavaScriptSafely (
743793 jsGetComp ("otherChildComp" , e .otherChildComp .id ) +
744794 "var child = `" + Value .escapeForJavaScript (Value .escapeForJSON (e .childComp .element .outerHtml ())) + "`;\n " +
745795 "otherChildComp.insertAdjacentHTML(\" beforebegin\" , child);\n " +
746796 (App .isInDepthDebugging ? "console.log('otherChildComp:', otherChildComp); console.log('➡️✅ inserted child:', child); \n " : "" ),
747- "internal" , 0 );
748- e .childComp .setAttached (true );
749- e .childComp .forEachChildRecursive (child2 -> {
750- child2 .setAttached (true );
751- });
797+ "internal" , 0 )
798+ .thenAccept (__ -> {
799+ e .childComp .setAttached (true );
800+ e .childComp .forEachChildRecursive (child2 -> {
801+ child2 .setAttached (true );
802+ });
803+ });
752804 }
753805 }
754806
@@ -789,13 +841,29 @@ public boolean isLoading(){
789841 }
790842
791843 public static class PendingAppend {
844+ /**
845+ * Also removes all children recursively.
846+ */
792847 public static boolean removeFromPendingAppends (UI ui , Component <?, ?> comp ){
793848 if (ui != null ){ // && ui.isLoading()){ // Also remove from pending appends, these can also happen after loading
794849 synchronized (ui .pendingAppends ){
795850 List <UI .PendingAppend > toRemove = new ArrayList <>(0 );
796- for (UI .PendingAppend pendingAppend : ui .pendingAppends ) {
797- if (comp .equals (pendingAppend .child ))
851+ for (UI .PendingAppend pendingAppend : ui .getPendingAppendsCopy () ) {
852+ if (comp .equals (pendingAppend .child )){
798853 toRemove .add (pendingAppend );
854+ // Also remove any children
855+ for (Component child : comp .children ) {
856+ removeFromPendingAppends (ui , child );
857+ }
858+ }
859+ }
860+ if (App .isInDepthDebugging ){
861+ if (!toRemove .isEmpty ()){
862+ AL .info ("Removing " +toRemove .size ()+" components from pending append:" );
863+ for (PendingAppend pa : toRemove ) {
864+ AL .info ("toRemove = " + pa .child .toPrintString ());
865+ }
866+ }
799867 }
800868 return ui .pendingAppends .removeAll (toRemove );
801869 }
0 commit comments