Why subject’s subscriber keeps consume an error, after the first() operator, if the next emit of this subject was called in the inner tap() operator?

I’ve found a strange rxjs behavior that keeps bothering me, and I can’t even imagine a solution to this problem.

I need to create an Observable from Subject that will either emit an error if the consumed value is null, or emit a value. And then emit from Subject a null value.
Everything works as intended, but the subscriber of this Observable keeps consume the error that was emitted after the first subscription, even with the first() operator.

How is this possible?

import { Subject, throwError, of } from 'rxjs';
import { switchMap, tap, first } from 'rxjs/operators';

var subj$ = new Subject();

var obs$ = subj$.asObservable().pipe(
  switchMap((v) => {
    if (!v) {
      return throwError(() => 'ERROR');
    }

    return of(v);
  })
);

obs$
  .pipe(
    first(),
    tap({
      next(v) {
        console.log('obs$', v);
      },
      error(err) {
        console.error('obs$ error', err);
      },
    }),
    tap(() => subj$.next(null))
  )
  .subscribe();

subj$.next('VALUE');

// obs$ VALUE
// obs$ error ERROR <- ???
// ERROR <- ???

https://stackblitz.com/edit/stackblitz-starters-sdx1oqzx?file=index.js

Real world example:

import { Observable, BehaviorSubject, zip, throwError, of } from 'rxjs';
import { first, switchMap, tap } from 'rxjs/operators';

type User {
  id: number,
  name: string
}

class AuthService {
  private isAuthenticatedSource = new BehaviorSubject<boolean>(false);
  private userSource = new BehaviorSubject<User | null>(null);

  isAuthenticated$: Observable<boolean> = this.isAuthenticatedSource.asObservable();

  user$: Observable<User> = zip(
    this.isAuthenticatedSource.asObservable(),
    this.userSource.asObservable(),
  ).pipe(
    switchMap(([ isAuthenticated, user ]) => {
      if (!isAuthenticated || !user) {
        return throwError(() => 'Unauthenticated.');
      }

      return of(user);
    })
  );

  constructor(private socketService: SocketService) {}

  authenticate(user: User) {
    this.isAuthenticatedSource.next(true);
    this.userSource.next(user);

    return this.socketService.emit('join', user);
  }

  // consumes the error from second emission
  deauthenticate() {
    return this.user$.pipe(
      first(),
      switchMap((user: User) => this.socketService.emit('leave', user)),
      tap(() => {
        this.isAuthenticatedSource.next(false);
        this.userSource.next(null);
      })
    );
  }
}

class SocketService {
  socket = {
    emit(event: string, data: any, ack: (res: any) => any) {
      return { event, data, ack };
    }
  }

  emit<T>(event: string, data: any): Observable<T> {
    return new Observable<T>(subscriber => {
      this.socket.emit(event, data, (res) => {
        subscriber.next(res);
        subscriber.complete();
      })
    });
  }
}

In a prestashop, on mobile, something is overwriting my client-side modifications of the classlist [closed]

I’ve got a react app, running in an Iframe in a prestashop page. For some operations, I want the Iframe to go fullscreen. To implement this, my react frame sends a message to the parent requesting fullscreen, and the parent puts an iframe-fullscreen class on the body. Then I’ve got corresponding styles to position the Iframe and hide the floating header. This works fine on desktop, but on mobile the classlists of every element seem to be managed, actively in the browser (by prestashop?), and any savage attempts to modify classlists are immediately countermanded by the page.

Does anyone know about this? Is it indeed prestashop, or a prestashop theme, or a responsive plugin? I don’t own the prestashop page, and I’m not a prestashop developer, but if someone can help me identify what might be doing this, I can see if the behaviour can be customized or paused.

Can i link another html file which stores info to be used in the other html file? [duplicate]

In my index.html i have images and when i click them they expand into modals (barely did it)

So each image consists of two parts
“Thumbnail”(<img…) + Modal for this image

Modals are quite chunky and so i thought that i can put them all in separate html document to pull them from, into my index.html and other webpages I’m going to have.
Because some of the images are going to appear on both pages and I’d have to copy paste those modals along them otherwise.

So i thought that maybe i can link

<link href="modals.html">

In the “head”

But it didn’t seem to work

Does it work? And if yes, then how?

Cannot get ViewShot to output a quality image using calculated dimensions

Having a strange issue with ViewShot in a react native app, where using a calculated value is causing the output file to be very low quality. Hopefully I can explain this well enough to get some advice on where to look.

I’m trying to use ViewShot to output images with a watermark on them This works as intended provided the line style={[{ height: newHeight, width: newWidth }, styles.cardWrapper]} is set to fixed height and widths. For example if the image output is to be 2000×1500, if I set the values then the resulting file is ok (on my testing device the pixel ratio is 1.96875, so that would mean I need to use the values 1016×762 to achieve the correct image dimensions). If I let the code calculate the values and put them in there, the output file is very low quality, even if the values are exactly the same. I can’t fix the output size as the input images could be any size. This is driving me mad as I can’t figure out why it does this.

{selectedImages.map((image, index) => {
  // Find matching image data by uri
  const imageData = imageSizes.find(item => item.uri === image.uri);

  newWidth = 100;
  newHeight = 100;

  if (imageData?.width) {
    newWidth = Math.round(imageData.width / pixelRatio);
    newHeight = Math.round(imageData.height / pixelRatio);
  }

  return (
    <View
      key={`watermark_${index}`}
      style={[{ height: newHeight, width: newWidth }, styles.cardWrapper]}
      ref={viewRefs.current[index]}
      collapsable={false}
    >
      <Image
        source={{ uri: `${APP_CACHE}${image.uri}` }}
        style={{ width: '100%', height: '100%' }}
      />
      <View style={styles.cardBackground}>
        <Text style={styles.cardInfo}>
          {'text text text'}
          {imageData?.width || 0}{' x '}{imageData?.height || 0}
        </Text>
      </View>
    </View>
  );
})}

function is being called with the following component

<TouchableOpacity activeOpacity={0.6} onPress={() => { processImages(); }} style={styles.button}>
  <Text style={styles.buttonText}>
    {`test watermark`}
  </Text>
</TouchableOpacity>

Processing function here. This works as intended provided the above rule is followed.

const processImages = async () => {
let processedUris = [];

for (let i = 0; i < selectedImages.length; i++) {
  if (!viewRefs.current[i]?.current) {
    console.warn(`Ref for image ${i} is not assigned`);
    continue;
  }

  const watermarkedUri = await addWatermark(selectedImages[i], viewRefs.current[i]);
  if (watermarkedUri) {
    processedUris.push(watermarkedUri);
  }
}

setWatermarkedImages(processedUris);
console.log('All images processed:', processedUris);
};

const addWatermark = async (imageUri, viewRef) => {
return new Promise(async (resolve) => {
  setTimeout(async () => {
    try {
      const snapshot = await captureRef(viewRef.current, {
        format: 'jpg',
        quality: 1,
      });

      const fileName = `watermarked_${Date.now()}.jpg`;
      const newUri = APP_CACHE + fileName;

      await FileSystem.moveAsync({
        from: snapshot,
        to: newUri,
      });

      //   console.log('Saved:', newUri);
      resolve(newUri);
    } catch (error) {
      console.error('Error capturing image:', error);
      resolve(null);
    }
  }, 100);
});
};

Original image (dimensions 2000×1500)

Fixed dimension output (dimensions set at 1016×762, output to 2000×1500)

Variable dimension output (calculated at 1016×762, output to 2000×1500

Want a customized logout warning pop up in reactjs

I have a banking website and I am implementing as case where, when a user tries to refresh browser it should show a warning popup which states that you will be sign out and if user click on that button it should logout that user. I am using react and below is my code. But this gives me only option for Reload and Cancel which is predefined and the alert is not working. I want to add my functionality of logout.

 useEffect(() => {
    window.onbeforeunload = function () {
        alert("This will logout.. ")
        return "Leaving this page will reset the wizard";
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
},[]);

How to resolve a React hydration mismatch error so that third-party JavaScript via tags in CKEditor HTML content can be shown in Frontend?

The following third party code is written in the CK editor in drupal in source mode:

<script>
       window.page = 'gidd';
</script><!-- On new release, please update the bundle count so that cache is busted --><!-- Add react-website-component script and styles here -->
<script defer="" src="***/js/runtime.bundle.js?bundle=3"></script>
<script defer="" src="***/js/main.bundle.js?bundle=3"></script>
<link href="***/css/main.css?bundle=3" rel="stylesheet"><!-- end -->

Now, in the following React component, we’re showing the third-party data, that is rendering fine in frontend:

import { graphql } from "gatsby";
import React from "react";
import { Col, Container, Row } from "react-bootstrap";
import ExtractRichText from "../../molecules/ExtractRichText";
import "./database.scss";
import Seo from "../../atoms/seo";

const DatabasePage = ({ data }) => {
  return (
    <>
      <Container className="database-container">
        <Row>
          <Col>
            {/* <ExtractRichText
              richText={data?.nodeDatabasePage?.body?.value}
            ></ExtractRichText> */}
            <div
              dangerouslySetInnerHTML={{
                __html: data.nodeDatabasePage.body.value,
              }}
            />
          </Col>
        </Row>
      </Container>
    </>
  );
};

export default DatabasePage;

export const pageQuery = graphql`
  query ($id: String!) {
    nodeDatabasePage(id: { eq: $id }) {
      title
      body {
        value
      }
      field_meta_tags {
        description
        keywords
      }
      path {
        alias
      }
      relationships {
        field_metatag_image {
          url
        }
      }
    }
  }
`;

export const Head = ({ data }) => (
  <Seo
    title={data?.nodeDatabasePage?.title}
    image={data?.nodeDatabasePage?.relationships?.field_metatag_image?.url}
    description={data?.nodeDatabasePage?.field_meta_tags?.description}
    keywords={data?.nodeDatabasePage?.field_meta_tags?.keywords}
    url={data?.nodeDatabasePage?.path?.alias}
  />
);

Now we’ve modified the above component to the following:

import { graphql } from "gatsby";
import React from "react";
import { Col, Container, Row } from "react-bootstrap";
import ExtractRichText from "../../molecules/ExtractRichText";
import "./database.scss";
import Seo from "../../atoms/seo";

const DatabasePage = ({ data }) => {
  return (
    <>
      <Container className="database-container">
        <Row>
          <Col>
            <ExtractRichText
              richText={data?.nodeDatabasePage?.body?.value}
            ></ExtractRichText>
          </Col>
        </Row>
      </Container>
    </>
  );
};

export default DatabasePage;

export const pageQuery = graphql`
  query ($id: String!) {
    nodeDatabasePage(id: { eq: $id }) {
      title
      body {
        value
      }
      field_meta_tags {
        description
        keywords
      }
      path {
        alias
      }
      relationships {
        field_metatag_image {
          url
        }
      }
    }
  }
`;

export const Head = ({ data }) => (
  <Seo
    title={data?.nodeDatabasePage?.title}
    image={data?.nodeDatabasePage?.relationships?.field_metatag_image?.url}
    description={data?.nodeDatabasePage?.field_meta_tags?.description}
    keywords={data?.nodeDatabasePage?.field_meta_tags?.keywords}
    url={data?.nodeDatabasePage?.path?.alias}
  />
);

We’ve used ExtractRichText component, that’s handing parsing the code and render in the frontend.

Now the ExtractRichText component looks like following:

import * as React from "react";
import parse from "html-react-parser";
import { useFileFile } from "../../hooks/useFileFile";
import { useMediaFileFile } from "../../hooks/useMediaFileFile";
import "./extractRichText.scss"
export const ExtractRichText = ({ richText, extraClasses }) => {
  const drupalIDs = useFileFile().map((e) => e?.node?.drupal_id);
  const allFiles = useFileFile().map((e) => e?.node);
  const allMediafiles = useMediaFileFile().map((e) => e?.node);
  let body = "";
  if (richText) {
    body = extractImage(drupalIDs, richText, extraClasses, allFiles, allMediafiles);
  }
  return <div className={`rich-text ${extraClasses}`}>{body}</div>;
};
function extractImage(drupalIDs, body, extraClasses, allFiles, allMediafiles) {
  return parse(body, {
    transform: (node) => {
      if (node.type === "img" || node.type === "drupal-entity" || node.type === "drupal-media") {
        const currentFile = allFiles.find((file) => node.props["data-entity-uuid"] === file?.drupal_id);
        let imageUrl = currentFile?.publicUrl;
        let mediaUrl;
        let alt;
        if (node.type === "drupal-media") {
          const currentMedia = allMediafiles.find((media) => node.props["data-entity-uuid"] === media?.drupal_id);
          mediaUrl = currentMedia?.relationships?.field_media_image?.publicUrl;
          alt = currentMedia?.field_media_image?.alt;
        }
        const imgAlign = node.props["data-align"] || "";
        const height = node.props["height"];
        const width = node.props["width"];
        let src = imageUrl || mediaUrl || node.props["src"];
        if (src === node.props["src"]) {
          const isRelative = !/^https?:///i.test(src);
          if (isRelative) {
            const base = process.env.GATSBY_DRUPAL_URL.replace(//+$/, '');
            const path = src.replace(/^/+/, '');
            src = `${base}/${path}`;
          }
        }
        const imgStyles = {};
        if (width) {
          imgStyles.width = `${width}px !important`;
        } else {
          // imgStyles.width = `100%`;
        }
        if (height) {
          imgStyles.height = `auto`;
        } else {
          // imgStyles.height = `100%`;
        }
        return (
          <span className={`imgAlign-${imgAlign}`}>
            <img
              style={imgStyles}
              loading="lazy"
              className={`${extraClasses ?? ''} ${node?.type == 'drupal-media' ? 'w-100' : ''}`}
              alt={node?.props?.alt || alt || "image"}
              src={`${src}`}
            />
          </span>
        );
      }
      if (node.type === 'a' && node.props["data-entity-type"] === 'file') {
        const currentFile = allFiles.find((file) => node.props["data-entity-uuid"] === file?.drupal_id);
        const href = node.props["href"] || "";
        const fallbackImage = process.env.GATSBY_DRUPAL_URL + href;
        return (
          <a
            href={`${currentFile?.url || fallbackImage}`}
            target={`${node.props["target"] || ""}`}
            class={`${node.props["className"] || ""}`}
            aria-label={`${node.props['aria-label'] || ""}`}
            id={`${node.props["id"] || ""}`}
            rel={`${node.props["rel"] || ""}`}
          >{`${node.props["children"] || "Link"}`}</a>
        );
      }
      return node;
    },
  });
}
export default ExtractRichText;

But, after using ExtractRichText in DatabasePage component, we’re getting the following runtime errors and third party data are not showing as well:

throw new Error('Text content does not match server-rendered HTML.');
throw new Error('Hydration failed because the initial UI does not match what was ' + 'rendered on the server.');
var recoverableError = createCapturedValueAtFiber(new Error('There was an error while hydrating. Because the error happened outside ' + 'of a Suspense boundary, the entire root will switch to ' + 'client rendering.'), workInProgress); 

How to fix these above errors and ensure that third-party data is rendering fine in frontend?

Full Screen react exam taking app, Controlled full screen exit

1.Can’t able to control full screen mode, if we press escape automatically coming out from the full screen mode.

2.If not possible how other exam taking apps are working, any other ways?

3.Non of the tools providing the controlled exit

4.What is the best approach to implement full screen controlled exit

1.This is the first one
import React, { useState, useEffect, } from 'react';
import Fullscreen from 'react-fullscreen-crossbrowser';
import "./styles.css";

// Dangerous key combinations to block
const DANGEROUS_KEYS = new Set([
  // 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
  'PrintScreen', 'ScrollLock', 'Pause', 'ContextMenu', 'Escape',
]);

const DANGEROUS_COMBOS = [
  { ctrl: true, shift: true, key: 'p' },  // Chrome's print shortcut
  { ctrl: true, shift: true, key: 'i' },  // DevTools
  { ctrl: true, shift: true, key: 'j' },  // DevTools console
  { ctrl: true, shift: true, key: 'c' },  // Force copy
  { alt: true, tab: true },               // Window switching
  { meta: true, tab: true },             // Mac window switching
  { ctrl: true, alt: true, key: 'Delete' } // Windows security screen
];

const SecureExamContainer = () => {
  const [isFullscreen, setIsFullscreen] = useState(false);
  const [showExitWarning, setShowExitWarning] = useState(false);
  const [examStarted, setExamStarted] = useState(false);

  // Handle fullscreen changes
  // const handleFullscreenChange = useCallback((isEnabled) => {
  //   setIsFullscreen(isEnabled);
  //   if (!isEnabled && examStarted) {
  //     setShowExitWarning(true);
  //     // Immediately attempt to re-enter fullscreen
  //     setTimeout(() => setIsFullscreen(true), 100);
  //   }
  // }, [examStarted]);

  // Keyboard and shortcut protection
  useEffect(() => {
    if (!examStarted) return;

    const handleKeyDown = (e) => {
      console.log("e.key ", e.key);
      // Block function keys
      if (DANGEROUS_KEYS.has(e.key)) {
        e.preventDefault();
        e.stopPropogation();
        setShowExitWarning(true);
        // triggerViolation(`Function key pressed: ${e.key}`);
        return;
      }

      // Check for dangerous combinations
      for (const combo of DANGEROUS_COMBOS) {
        const ctrlMatch = !combo.ctrl || e.ctrlKey || e.metaKey;
        const shiftMatch = !combo.shift || e.shiftKey;
        const altMatch = !combo.alt || e.altKey;
        const keyMatch = !combo.key || e.key === combo.key;
        const tabMatch = combo.tab === undefined || e.key === 'Tab';

        if (ctrlMatch && shiftMatch && altMatch && keyMatch && tabMatch) {
          e.preventDefault();
          setShowExitWarning(true);
          // triggerViolation(`Dangerous combination detected: ${e.key}`);
          return;
        }
      }

      // Block Ctrl/Alt/Meta combinations
      if (e.ctrlKey || e.altKey || e.metaKey) {
        const allowedCombos = [
          { ctrl: true, key: 'c' },  // Allow copy
          { ctrl: true, key: 'v' },  // Allow paste
          { ctrl: true, key: 'x' }   // Allow cut
        ];

        const isAllowed = allowedCombos.some(combo =>
          (combo.ctrl ? e.ctrlKey || e.metaKey : true) &&
          e.key === combo.key
        );

        if (!isAllowed) {
          e.preventDefault();
          setShowExitWarning(true);
          // triggerViolation(`Modifier key combination detected: ${e.key}`);
        }
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [examStarted]);

  // Block keyboard shortcuts
  useEffect(() => {
    const handleKeyDown = (e) => {
      if (isFullscreen && (e.key === 'Escape' || e.key === 'F11')) {
        e.preventDefault();
        setShowExitWarning(true);
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    return () => window.removeEventListener('keydown', handleKeyDown);
  }, [isFullscreen]);

  // Block right-click and text selection
  useEffect(() => {
    if (!isFullscreen) return;

    const blockContextMenu = (e) => e.preventDefault();
    const blockSelection = (e) => e.preventDefault();

    document.addEventListener('contextmenu', blockContextMenu);
    document.addEventListener('selectstart', blockSelection);

    return () => {
      document.removeEventListener('contextmenu', blockContextMenu);
      document.removeEventListener('selectstart', blockSelection);
    };
  }, [isFullscreen]);

  // Fullscreen enforcement interval
  useEffect(() => {
    if (!examStarted) return;

    const interval = setInterval(() => {
      if (!isFullscreen) {
        setIsFullscreen(true);
      }
    }, 1000);

    return () => clearInterval(interval);
  }, [examStarted, isFullscreen]);

  const startExam = () => {
    setExamStarted(true);
    setIsFullscreen(true);
  };

  const confirmExit = () => {
    setExamStarted(false);
    // setIsFullscreen(false);
    setShowExitWarning(false);
    // Additional cleanup or submission logic here
  };

  const cancelExit = () => {
    setShowExitWarning(false);
    setIsFullscreen(true);
  };
  console.log("showExitWarning ", showExitWarning)
  console.log("isFullscreen ", isFullscreen)
  return (
    <div className="exam-container">
      {!examStarted && (
        <div className="exam-start-screen">
          <h1>Exam Instructions</h1>
          <p>You must complete this exam in fullscreen mode.</p>
          <button onClick={startExam}>Start Exam</button>
        </div>
      )}
      <Fullscreen
        enabled={isFullscreen}
        onChange={(isEnabled) => {
          if (!isEnabled && examStarted) {
            setShowExitWarning(true);
            setIsFullscreen(true); // Immediately re-enable fullscreen
          }
        }}
      >
        <div className="exam-content">
          <h2>Exam in Progress</h2>
          {/* Your exam questions/components here */}

          <button
            className="submit-button"
            onClick={confirmExit}
          >
            Submit Exam
          </button>
        </div>
        {showExitWarning && (
          <div className="exit-warning-modal">
            <div className="modal-content">
              <h3>Warning!</h3>
              <p>You are not allowed to exit fullscreen during the exam.</p>
              <div className="modal-buttons">
                <button onClick={confirmExit}>Submit & Exit</button>
                <button onClick={cancelExit}>Continue Exam</button>
              </div>
            </div>
          </div>
        )}
      </Fullscreen>
    </div>
  );
};

export default SecureExamContainer;


2. This is the second one
import React, { useCallback, useEffect } from 'react'

// Dangerous key combinations to block
const DANGEROUS_KEYS = new Set([
    'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
    'PrintScreen', 'ScrollLock', 'Pause', 'ContextMenu', 'Escape',
]);

const DANGEROUS_COMBOS = [
    { ctrl: true, shift: true, key: 'p' },  // Chrome's print shortcut
    { ctrl: true, shift: true, key: 'i' },  // DevTools
    { ctrl: true, shift: true, key: 'j' },  // DevTools console
    { ctrl: true, shift: true, key: 'c' },  // Force copy
    { alt: true, tab: true },               // Window switching
    { meta: true, tab: true },             // Mac window switching
    { ctrl: true, alt: true, key: 'Delete' } // Windows security screen
];

const StrictFullScreenMdn = () => {

    const handleOpenFullScreen = useCallback(() => {
        document.documentElement.requestFullscreen();
    }, []);

    useEffect(() => {
        const handleKeyDown = (e) => {
            handleOpenFullScreen();
            console.log("e.key ", e.key);
            // Block function keys
            if (DANGEROUS_KEYS.has(e.key)) {
                e.preventDefault();
                e.stopPropogation();
                // setShowExitWarning(true);
                // triggerViolation(`Function key pressed: ${e.key}`);
                return;
            }

            // Check for dangerous combinations
            for (const combo of DANGEROUS_COMBOS) {
                const ctrlMatch = !combo.ctrl || e.ctrlKey || e.metaKey;
                const shiftMatch = !combo.shift || e.shiftKey;
                const altMatch = !combo.alt || e.altKey;
                const keyMatch = !combo.key || e.key === combo.key;
                const tabMatch = combo.tab === undefined || e.key === 'Tab';

                if (ctrlMatch && shiftMatch && altMatch && keyMatch && tabMatch) {
                    e.preventDefault();
                    // setShowExitWarning(true);
                    // triggerViolation(`Dangerous combination detected: ${e.key}`);
                    return;
                }
            }

            // Block Ctrl/Alt/Meta combinations
            if (e.ctrlKey || e.altKey || e.metaKey) {
                const allowedCombos = [
                    { ctrl: true, key: 'c' },  // Allow copy
                    { ctrl: true, key: 'v' },  // Allow paste
                    { ctrl: true, key: 'x' }   // Allow cut
                ];

                const isAllowed = allowedCombos.some(combo =>
                    (combo.ctrl ? e.ctrlKey || e.metaKey : true) &&
                    e.key === combo.key
                );

                if (!isAllowed) {
                    e.preventDefault();
                    // setShowExitWarning(true);
                    // triggerViolation(`Modifier key combination detected: ${e.key}`);
                }
            }
        };

        window.addEventListener('keydown', handleKeyDown);
        return () => window.removeEventListener('keydown', handleKeyDown);
    }, []);

    const handleCloseFullScreen = useCallback(() => {
        document.exitFullscreen();
    }, []);

    return (
        <div>StrictFullScreenMdn

            <button onClick={handleOpenFullScreen}>Open Mode</button>
            <button onClick={handleCloseFullScreen}>close Mode</button>
        </div>
    )
}

export default StrictFullScreenMdn

Need help reproducing a nodered dashboard 2.0 animation

I’m trying to recreate the Dashboard 2.0 ui-gauge-tank with a custom unit (i need somthing else than %). I used the Vue component i found on Dashboard 2.0’s github, and adapted it to change the unit. It works well but I can’t get the wave animation working correctly…

I’m using node-red v4.0.9, NodeJS v22.14.0 and Dashboard 2.0 V1.22.1

Here’s my flow :

[
{
    "id": "7c0a6281fad8692c",
    "type": "tab",
    "label": "Flux 1",
    "disabled": false,
    "info": "",
    "env": []
},
{
    "id": "c26cf52ff92bf8dc",
    "type": "inject",
    "z": "7c0a6281fad8692c",
    "name": "",
    "props": [
        {
            "p": "payload"
        },
        {
            "p": "topic",
            "vt": "str"
        }
    ],
    "repeat": "",
    "crontab": "",
    "once": false,
    "onceDelay": 0.1,
    "topic": "",
    "payload": "15",
    "payloadType": "num",
    "x": 170,
    "y": 200,
    "wires": [
        [
            "77736be83ff7aa82"
        ]
    ]
},
{
    "id": "77736be83ff7aa82",
    "type": "function",
    "z": "7c0a6281fad8692c",
    "name": "function 2",
    "func": "msg.min = 4nreturn msg;",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 380,
    "y": 200,
    "wires": [
        [
            "442aaa386a504831"
        ]
    ]
},
{
    "id": "f2bc295826c5e605",
    "type": "inject",
    "z": "7c0a6281fad8692c",
    "name": "",
    "props": [
        {
            "p": "payload"
        },
        {
            "p": "topic",
            "vt": "str"
        }
    ],
    "repeat": "",
    "crontab": "",
    "once": false,
    "onceDelay": 0.1,
    "topic": "",
    "payload": "15",
    "payloadType": "num",
    "x": 150,
    "y": 260,
    "wires": [
        [
            "d0ccf216830a748d"
        ]
    ]
},
{
    "id": "442aaa386a504831",
    "type": "ui-template",
    "z": "7c0a6281fad8692c",
    "group": "fb6501fa7d8b6b7b",
    "page": "",
    "ui": "",
    "name": "custom-tank",
    "order": 1,
    "width": "3",
    "height": "3",
    "head": "",
    "format": "<template>n  <div v-resize="onResize" class="nrdb-ui-gauge-tank--container">n    <label class="nrdb-ui-gauge-title"> CUSTOM-TANK</label>nn    <divn        class="nrdb-ui-gauge-tank"n        :style="{'--gauge-fill': color, '--gauge-fill-pc': pc + '%', 'color': color}"n    >n      <div class="nrdb-ui-gauge-tank--center">n        <div ref="fill" class="nrdb-ui-gauge-tank--fill"></div>n        <svg class="WaveBG" :style="`bottom: 0; height: ${svgBottom}px`" :viewBox="`0 ${amplitude_setting} 1000 ${Math.min(100, svgScaleRatio * svgBottom)}`" preserveAspectRatio="xMinYMin meet">n          <path :d="waves[0]">n            <animaten                dur="5s"n                repeatCount="indefinite"n                attributeName="d"n                :values="`${waves[0]}; ${waves[1]}; ${waves[0]};`"n            />n          </path>n        </svg>n        <svg class="Wave" :style="`bottom: 0; height: ${svgBottom}px;`" :viewBox="`0 ${amplitude_setting} 1000 ${Math.min(100, svgScaleRatio * svgBottom)}`" preserveAspectRatio="xMinYMin meet">n          <path :d="waves[0]">n            <animaten                dur="5s"n                repeatCount="indefinite"n                attributeName="d"n                :values="`${waves[0]}; ${waves[1]}; ${waves[0]};`"n            />n          </path>n        </svg>nn      <div ref="labels" class="nrdb-ui-gauge-tank-labels">n        <label class="nrdb-ui-gauge-tank--label" >{{ this.value }} T</label>n      </div>n    </div>n    </div>nn  </div>n</template>n<script>nexport default {nn  data() {n    return {n      labelLineHeight: 0,n      svgBottom: 0,n      amplitude_setting: 15,n      svgScaleRatio: 1,n      minivalue : 0,n      maxivalue : 50n    }n  },n  watch: {n    value: function () {n      this.$nextTick(() => {n        // react to the fill element being updatedn        this.updateMask()n      })n    }n  },n  computed: {nn    color: function ()n    {n      if (this.value <= this.min) {return "#FF0000"}n      return "#00FF00"n    }n    ,n    pc: function () {n      if (typeof this.value !== 'undefined') {n        return Math.max(0, Math.min(Math.round((this.value - this.minivalue) / (this.maxivalue - this.minivalue) * 100), 100))n      } else {nn        return 0n      }n    },n    clipId: function () {n      return `clip-${this.id}`n    },n    waves: function () {n      const amplitude = this.amplitude_setting * this.svgScaleRation      const svgBottom = this.svgBottomn      const svgScaleRatio = this.svgScaleRationn      return [n        `M750,${amplitude} c -125,0 -125,-${amplitude} -250,-${amplitude} s -125,${amplitude} -250,${amplitude} S 125,0, 0,0 v${svgScaleRatio * (svgBottom + amplitude)}h1000 V0 c-125,0 -125,${amplitude} -250,${amplitude}Z`,n        `M750,0 c -125,0 -125,${amplitude} -250,${amplitude} S 375,0 250,0, S 125,${amplitude}, 0,${amplitude} v${svgScaleRatio * svgBottom}h1000 V${amplitude} c-125,0 -125-${amplitude} -250-${amplitude}Z`n      ]n    }n  },n  methods: {n    updateMask () {n      const h = this.$refs.fill?.clientHeight || 0n      const w = this.$refs.fill?.clientWidth || 0nn      this.labelLineHeight =`${this.$refs.labels.clientHeight}px`nn      if (h >= 0 && this.pc !== 0) {n        this.svgBottom = hn        this.svgScaleRatio = w !== 0 ? 1000 / w : 1n      } else {n        this.svgBottom = 0n        this.svgScaleRatio = 1n      }n    },n    onResize () {n      this.updateMask()n    }n  },n  mounted () {nn    this.$nextTick(() => {n      this.updateMask()n    })nn    this.$socket.on('msg-input:' + this.id, (msg) => {n      this.value = msg.payloadn      this.min = msg.minn    })n  },n  unmounted() {nn  }n}nn</script>n<style scoped>n.Wave,n.WaveBG {n  width: 200%;n  position: absolute;n  overflow: visible;n  animation-name: swell;n  animation-fill-mode: forwards;n  animation-iteration-count: infinite;n  animation-timing-function: linear;n  fill: var(--gauge-fill);n  bottom: var(--gauge-fill-pc);n}nn.WaveBG {n  opacity: 0.4;n  /* offset the animation so that the wave's standing nodes never overlap */n  animation-duration: 1.5s;n  animation-delay: 1.2s;n}n.Wave {n  animation-duration: 2s;n}nn@keyframes swell {n  0% {n    transform: scaleX(2) translateX(25%) translateY(1px);n  }n  100% {n    transform: scaleX(2) translateX(-25%) translateY(1px);n  }n}nn.nrdb-ui-gauge-tank--container {n  display: flex;n  flex-direction: column;n  container: tank-container / size;n}n.nrdb-ui-gauge-tank {n  --tank-margin: 6px;n  --tank-padding: 3px;n  --tank-radius: 6px;n  --tank-border: 4px;n}nn@container tank-container (min-width: 75px) and (min-height: 75px) {n  .nrdb-ui-gauge-tank {n    --tank-margin: 12px;n    --tank-radius: 12px;n    --tank-border: 8px;n    --tank-padding: 6px;n  }n}nn.nrdb-ui-gauge-tank {n  border-radius: var(--tank-radius);n  border-width: var(--tank-border);n  padding: var(--tank-padding);n  border-color: var(--gauge-fill);n  border-style: solid;n  flex-grow: 1;n  position: relative;n}n.nrdb-ui-gauge-tank--fill {n  background-color: transparent;n}n.nrdb-ui-gauge-tank-labels {n  position: relative;n  width: 100%;n  height: 100%;n  display: flex;n  container-type: size;n}n.nrdb-ui-gauge-tank label {n  font-weight: bold;n  display:flex;n  resize: both;n  font-size: min(2.5rem, 50cqmin);n  position: absolute;n  z-index: 2;n  width: 100%;n  height: 100%;n  text-align: center;n  justify-content: center;n  align-items: center;n  color: black;n  --text-border: 1px;n  --text-border-inv: calc(-1 * 1px);n  --text-border-color: white;n  text-shadow: var(--text-border) var(--text-border-inv) var(--text-border-color),n  var(--text-border-inv) var(--text-border-inv) var(--text-border-color),n  var(--text-border-inv) var(--text-border) var(--text-border-color),n  var(--text-border) var(--text-border) var(--text-border-color);n}nnnrdb-ui-gauge-title {n  font-weight: bold;n  display:flex;n  resize: both;n  font-size: min(2.5rem, 50cqmin);n  position: absolute;n  z-index: 2;n  width: 100%;n  height: 100%;n  text-align: center;n  justify-content: center;n  align-items: center;n  color: black;nn}nn.nrdb-ui-gauge-tank--center {n  display: flex;n  justify-content: center;n  align-items: center;n  height: 100%;n  position: relative;n  overflow: hidden;n}nn/* text mask */n.nrdb-ui-gauge-tank svg.mask {n  top: 0;n}n.nrdb-ui-gauge-tank svg.mask,n.nrdb-ui-gauge-tank--fill {n  position: absolute;n  left: 0;n}n.nrdb-ui-gauge-tank--fill {n  bottom: 0;n  height: var(--gauge-fill-pc);n  width: 100%;n}n</style>",
    "storeOutMessages": true,
    "passthru": false,
    "resendOnRefresh": true,
    "templateScope": "local",
    "className": "",
    "x": 610,
    "y": 200,
    "wires": [
        []
    ]
},
{
    "id": "c6cc4137fbb3cdda",
    "type": "ui-template",
    "z": "7c0a6281fad8692c",
    "group": "",
    "page": "d30cf1a71bcee0b6",
    "ui": "",
    "name": "Dasboard 2.0 CSS",
    "order": 0,
    "width": 0,
    "height": 0,
    "head": "",
    "format": ".nrdb-ui-text {n    background-color: #eeeeee !important;n}nn.nrdb-ui-gauge {n        background-color: #eeeeee !important;n}nn.nrdb-ui-gauge-title {n        font-size: larger;n        background-color: aqua;n}nn.valid.valid-label label {n    color: black;n}nn.valid.dark-text span {n    color: black;n}nn.valid.valid-text span {n    color: green;n}nn.invalid.invalid-label label {n    color: black;n}nn.invalid.invalid-text span {n    color: red;n}nn",
    "storeOutMessages": true,
    "passthru": false,
    "resendOnRefresh": true,
    "templateScope": "page:style",
    "className": "",
    "x": 190,
    "y": 100,
    "wires": [
        []
    ]
},
{
    "id": "d0ccf216830a748d",
    "type": "ui-gauge",
    "z": "7c0a6281fad8692c",
    "name": "tank",
    "group": "fb6501fa7d8b6b7b",
    "order": 2,
    "width": "3",
    "height": "3",
    "gtype": "gauge-tank",
    "gstyle": "needle",
    "title": "TANK",
    "units": "Bar",
    "icon": "",
    "prefix": "",
    "suffix": "",
    "segments": [
        {
            "from": "0",
            "color": "#ff0000"
        },
        {
            "from": "4",
            "color": "#00ff40"
        }
    ],
    "min": 0,
    "max": "50",
    "sizeThickness": 16,
    "sizeGap": 4,
    "sizeKeyThickness": 8,
    "styleRounded": true,
    "styleGlow": false,
    "className": "custom-tank",
    "x": 590,
    "y": 260,
    "wires": []
},
{
    "id": "fb6501fa7d8b6b7b",
    "type": "ui-group",
    "name": "Group Name",
    "page": "fd51264148764b4f",
    "width": "12",
    "height": "1",
    "order": 1,
    "showTitle": true,
    "className": "",
    "visible": "true",
    "disabled": "false",
    "groupType": "default"
},
{
    "id": "fd51264148764b4f",
    "type": "ui-page",
    "name": "Page Name",
    "ui": "955d24bee66399fa",
    "path": "/exemple",
    "icon": "home",
    "layout": "grid",
    "theme": "4cbed8b158df0175",
    "breakpoints": [
        {
            "name": "Default",
            "px": "0",
            "cols": "3"
        },
        {
            "name": "Tablet",
            "px": "576",
            "cols": "6"
        },
        {
            "name": "Small Desktop",
            "px": "768",
            "cols": "9"
        },
        {
            "name": "Desktop",
            "px": "1024",
            "cols": "12"
        }
    ],
    "order": 1,
    "className": "",
    "visible": "true",
    "disabled": "false"
},
{
    "id": "955d24bee66399fa",
    "type": "ui-base",
    "name": "UI Name",
    "path": "/dashboard",
    "appIcon": "",
    "includeClientData": true,
    "acceptsClientConfig": [
        "ui-notification",
        "ui-control"
    ],
    "showPathInSidebar": false,
    "headerContent": "page",
    "navigationStyle": "default",
    "titleBarStyle": "default",
    "showReconnectNotification": true,
    "notificationDisplayTime": "1",
    "showDisconnectNotification": true
},
{
    "id": "4cbed8b158df0175",
    "type": "ui-theme",
    "name": "Default Theme",
    "colors": {
        "surface": "#ffffff",
        "primary": "#0094CE",
        "bgPage": "#eeeeee",
        "groupBg": "#ffffff",
        "groupOutline": "#cccccc"
    },
    "sizes": {
        "density": "default",
        "pagePadding": "12px",
        "groupGap": "12px",
        "groupBorderRadius": "4px",
        "widgetGap": "12px"
    }
}
]

Thanks in advance for your help!

Argument ‘….’ is not a function, got undefined

Getting this persistent error: “Error: [ng:areq] Argument ‘myController’ is not a function, got undefined” in my console, trying to set up an email registration form within a Piano.io iframe. The angular script is loaded natively by Piano so I don’t have to add it to my code. Ultimately, I’m trying to get the success message to show up after the email is submitted but it’s not showing up (ng-hide is being applied by angular) and given that I have no other errors and the email is being successfully retrieved per my console.log, I suspect this error has to first be resolved. I haven’t coded the logic for POST and GET etc yet, just trying to get this simulation to work. Been trying to work this out for a few days now so any insight at all would be much appreciated!

HTML

<div id="my-app" ng-app="myApp" ng-controller="myController">
<div class="my-content" id="my-content">
<div class="inner-container">
  <div class="some-info">
    <h2>Header</h2>
    <p>Text</p>
  </div> 
  
  <form class="my-form" id="registration-form" ng-submit="registerUser($event)">
    <div class="form">
      <label for="email">Email address:</label>
      <input type="email" id="user-email" ng-model="email" placeholder="Enter email" required>
      <button id="my-btn" type="submit" class="button">Continue</button>
    </div>
  </form>

  <div class="success-message" ng-show="showSuccessMessage">
    <p class="success-text">Thanks for registering! Your email has been submitted.</p>
  </div>
</div>

SCRIPT

<script>
tp = window.tp || [];
tp.push(["init", function() {
tp.pianoId.init({
  displayMode: 'inline',
  containerSelector: '#my-app'
});
}]);

var app = angular.module('myApp', []);

app.controller('myController', function($scope) {

$scope.showSuccessMessage = false;
$scope.successMessage = "";
 
$scope.registerUser = function($event) {
  $event.preventDefault();
  var email = $scope.email;

  if (!email || email.trim() === "") {
    console.error('Error: Email is missing or invalid.');
    alert('Please provide a valid email address to proceed.');
    return;
  }

  console.log('Email successfully retrieved:', email);

  // Register user with Piano
  tp.pianoId.show({
    screen: 'register',
    registrationSuccess: function(data) {
      console.log('Registration successful:', data);

      // Update AngularJS scope to show the success message
      $scope.$apply(function() {
        $scope.showSuccessMessage = true;
        $scope.successMessage = "Thank you for registering! A confirmation email has been sent to " + email + ".";
      });
    },
    registrationFailed: function(error) {
      console.error('Error during registration:', error);

      // Update AngularJS scope to handle registration failure
      $scope.$apply(function() {
        $scope.showSuccessMessage = false;
        alert('There was an issue creating your account. Please try again.');
      });
    }
 });
 };
 });
 </script>

Recursion problem traversing deeply nested function constructors

I’ve already failed to my exam for this task 🙁
I can’t iterate through all nested objects to get only ‘.js’ files. The property check for the function is not working in all of the cases and can not pass: test 3.
Just can not jump in a new Folder([“7.txt”, “9.js”])] to get “9.js” and my
expectedResult: [“1.js”, “2.js”, “4.js”, “6.js”, “7.js”, “8.js” ] must include “9.js” too. I will be appreciated for any help to fix the problem, thanks

mocha.setup("bdd");
let expect = chai.expect;
let assert = chai.assert; 

function findAllJavascriptFiles(folder) {
  return new Promise((resolve) => {
    let jsFiles = [];  
    function processFolder(f) {
      f.read().then((files) => {  
        const promises = files.map((file) => {  
           if (typeof file.read === "function") {  
            return processFolder(file); 
          } 
          
          if (typeof file === "string" ) {  
            if(file.endsWith(".js")) {
              jsFiles.push(file); 
            } 
          }   
        
        }); 
        
        Promise.all(promises).then(() => { 
          resolve(jsFiles);
        });
      });
    } 
    processFolder(folder);
  });
}

describe("let the test started", () => { 
  it("flat file system", function (done) { 
    const tests = [
      {
        root: new Folder(["1.js", "2.js", "3.js", "4.html"]),
        expectedResult: ["1.js", "2.js", "3.js"]
      },
      {
        root: new Folder([
          "1.js",
          "2.js",
          new Folder([new Folder(["3.txt"]), "4.js"]),
          new Folder(["5.png", "6.js", new Folder(["7.txt"])]),
          "8.html"
        ]),
        expectedResult: ["1.js", "2.js", "4.js", "6.js"]
      },
      {
        root: new Folder([
          "1.js",
          "2.js",
          new Folder([new Folder(["3.txt"]), "4.js"]),
          new Folder(["5.png", "6.js", new Folder(["7.txt"])]),
          new Folder([
            "5.png",
            "7.js",
            new Folder([
              "7.txt",
              new Folder(["5.png", "8.js", new Folder(["7.txt", "9.js"])])
            ])
          ]),
          "8.html"
        ]),
        expectedResult: ["1.js", "2.js", "4.js", "6.js", "7.js", "8.js", "9.js"]
      },
      {
        root: new Folder([
          "1.js",
          "2.js",
          new Folder([new Folder(["3.txt"]), "4.js"]),
          new Folder(["5.png", "6.js", new Folder(["7.txt"])]),
          new Folder([
            "5.png",
            "7.js",
            new Folder([
              "7.txt",
              new Folder([
                "5.png",
                "8.js",
                new Folder([
                  "7.txt",
                  "9.js",
                  new Folder([
                    "11.js",
                    "12.js",
                    new Folder([new Folder(["3.txt"]), "14.js"]),
                    new Folder(["5.png", "16.js", new Folder(["7.txt"])]),
                    new Folder([
                      "5.png",
                      "17.js",
                      new Folder([
                        "7.txt",
                        new Folder([
                          "5.png",
                          "18.js",
                          new Folder(["7.txt", "19.js"])
                        ])
                      ])
                    ]),
                    "8.html",
                    new Folder([
                      "21.js",
                      "22.js",
                      new Folder([new Folder(["3.txt"]), "24.js"]),
                      new Folder(["5.png", "26.js", new Folder(["7.txt"])]),
                      new Folder([
                        "5.png",
                        "27.js",
                        new Folder([
                          "7.txt",
                          new Folder([
                            "5.png",
                            "28.js",
                            new Folder(["7.txt", "29.js"])
                          ])
                        ])
                      ]),
                      "8.html"
                    ])
                  ])
                ])
              ])
            ])
          ]),
          "8.html"
        ]),
        expectedResult: [
          "1.js",
          "11.js",
          "12.js",
          "14.js",
          "16.js",
          "17.js",
          "18.js",
          "19.js",
          "2.js",
          "21.js",
          "22.js",
          "24.js",
          "26.js",
          "27.js",
          "28.js",
          "29.js",
          "4.js",
          "6.js",
          "7.js",
          "8.js",
          "9.js"
        ]
      }
    ];

    const testPromises = tests.map(({ root, expectedResult }) => { 
      return findAllJavascriptFiles(root).then((actualResult) => {
        console.log(`RESULT:`, actualResult);
        assert.deepEqual(actualResult.sort(), expectedResult.sort());
      });
    });

    Promise.all(testPromises)
      .then(() => {
        done();
      })
      .catch((err) => {
        console.log("err", err);
        done(err);
      });
  });
});

function Folder(files) {
  return {
    size: () => {
      return new Promise((res) => {
        res(files.length);
      });
    },
    read: () => {
      return new Promise((res) => {
        res(files);
      });
    }
  };
}

mocha.run();

Where is the iOS WKWebView Camera integration enabled?

I have tried the code and options in the answers here iOS WKWebview: Always allow camera permission
to make a webcam integration in Javascript as usual.

I still can’t get it to work – console log showing “getUserMedia is not implemented in this browser”.

The app delegate class:

import Foundation
import WebKit
class MyDelegate: NSObject, WKUIDelegate {

    // Handle media capture permissions
    func webView(_ webView: WKWebView,
                 requestMediaCapturePermissionFor origin: WKSecurityOrigin,
                 initiatedByFrame frame: WKFrameInfo,
                 type: WKMediaCaptureType,
                 decisionHandler: @escaping (WKPermissionDecision) -> Void) {
        // Always grant permission for demonstration purposes
        decisionHandler(.grant)
        
    }
    
    func webView(_ webView: WKWebView, runJavaScriptAlertPanelWithMessage message: String, initiatedByFrame frame: WKFrameInfo, completionHandler: @escaping () -> Void) {
            print("JavaScript alert with message: (message)")
            completionHandler()
        }

}

The ContentView Swift for main swiftUI:

import SwiftUI
import WebKit

struct WebView: UIViewRepresentable {
    let htmlFileName: String
    let delegate = MyDelegate()

    func makeUIView(context: Context) -> WKWebView {
        let webViewConfiguration = WKWebViewConfiguration();
                webViewConfiguration.allowsInlineMediaPlayback = true;
        let webView = WKWebView(frame:.zero, configuration:webViewConfiguration)
        webView.configuration.preferences.javaScriptEnabled = true;
        webView.uiDelegate = delegate
        webView.isInspectable = true   //////////////// DEBUGGING purpose
        webView.configuration.preferences.setValue(true, forKey: "allowFileAccessFromFileURLs")
        return webView
    }

    func updateUIView(_ webView: WKWebView, context: Context) {
        if let url = Bundle.main.url(forResource: htmlFileName, withExtension: "html") {
            
            webView.loadFileURL(url, allowingReadAccessTo: url)
            
        }
    }
    
}

struct ContentView: View {
    var body: some View {
        WebView(htmlFileName: "index") // Replace "index" with your HTML file name
            .edgesIgnoringSafeArea(.all)
    }
}

Output on the map leaflet js big data from .shp-file

I have a shp file with big data-polygon that needs to be displayed on a leaflet map. First, I tried to convert .shp to geoJson. The data was displayed, but the load on the client device was very high – everything was freezes very much. After that, I tried to convert shp to mbiles using qgis, the map stopped freezes, but loading the mbiles file takes a long time. Perhaps there is some general approach to displaying big data in leaflet?

Loading a model with GLTFLoader through Web Worker causes mesh deformation on SkinnedMesh

My page is taking a long time to load because of the loading of the models that I do as soon as the page loads

During loading in the main thread, page interactions, such as hover effects, etc., stop working

One of the ideas I had to try to solve this was to load the models through the web worker

The page freezing decreased a lot, to almost 0!

However, when I added the models to the scene, they were all deformed

Example of a working model (loaded from the main thread) and the same models, loaded by the web worker

I’ve tried to debug this a lot but I have no idea what it could be or what it might have to do with it

This is the minimal reproducible example

index.html

<body>
    <canvas id="canvas-container"></canvas>

    <button id="default-intantiating-button" class="
        h-8 w-40
        bg-green-600
        rounded-md

        hover:bg-yellow-600
        ">
        <span> default loading </span>
    </button>

    <button id="web-worker-intantiating-button" class="
        h-8 w-40
        bg-green-600
        rounded-md

        hover:bg-yellow-600
        ">
        <span> Web Worker loading </span>
    </button>

</body>

<script type="importmap">
    {
        "imports": {
            "three": "https://esm.sh/[email protected]/build/three.module.js",
            "three/addons/": "https://esm.sh/[email protected]/examples/jsm/"
        }
    }
</script>

<script src="https://cdn.tailwindcss.com"></script>

<script type="module">
    // base libs
    import * as THREE from 'three';
    import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
    import * as SkeletonUtils from 'three/addons/utils/SkeletonUtils.js';

    // dependencies
    let _glbLoader = new GLTFLoader();
    // state
    let _isSceneInstantiated = false;
    // models
    let _itemsContainer = undefined;
    let _baseSkeleton = undefined;
    // animation
    let _clock = new THREE.Clock();
    let _skeletonMixer = undefined;
    // parts
    let _scene = undefined;
    let _camera = undefined;
    let _renderer = undefined;


    let InstantiateScene = (hasToLoadWithWebWorker = true) =>
    {
        if (!_isSceneInstantiated)
        {
            _isSceneInstantiated = true;

            let canvas = document.getElementById('canvas-container');
            canvas.classList.remove('hidden');
            let rect = canvas.getBoundingClientRect();

            _scene = new THREE.Scene();
            _camera = new THREE.PerspectiveCamera(30, rect.width / rect.height, 0.1, 1000);
            _renderer = new THREE.WebGLRenderer({ canvas: canvas, });
            _renderer.setSize(rect.width, rect.height);
            _renderer.setAnimationLoop(animate);
            const geometry = new THREE.BoxGeometry(0.4, 0.4, 0.4);
            const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
            const cube = new THREE.Mesh(geometry, material);
            _scene.add(cube);
            _camera.position.z = 5;

            function animate()
            {
                cube.rotation.x += 0.01;
                cube.rotation.y += 0.01;

                let deltaTime = (60.0 / 1000.0);

                if (_clock)
                    deltaTime = _clock.getDelta();

                if (_skeletonMixer)
                    _skeletonMixer.update(deltaTime);

                if (_renderer)
                    _renderer.render(_scene, _camera);
            }

            _scene.background = new THREE.Color("rgb(10, 80, 10)");

            SetupInitialState(hasToLoadWithWebWorker);
        }
    }

    let DestroyScene = () =>
    {
        if (_isSceneInstantiated)
        {
            _isSceneInstantiated = false;

            _scene = undefined;
            _camera = undefined;
            _renderer = undefined;

            let canvas = document.getElementById('canvas-container');
            canvas.classList.add('hidden');
        }
    }

    let SetupInitialState = (hasToLoadWithWebWorker = true) =>
    {
        _itemsContainer = new THREE.Group();
        _scene.add(_itemsContainer);

        _itemsContainer.position.set(0, -1, 0);

        InstantiateLights();

        let onFinishCallback = () =>
        {
            let blueMaterial = new THREE.MeshStandardMaterial({ color: 0x0000ff });
            let redMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 });

            InstantiateDynamicBodyPart('default-male-body-01-model-01.glb', blueMaterial, hasToLoadWithWebWorker);
            InstantiateDynamicBodyPart('default-male-shirt-01-model-01.glb', redMaterial, hasToLoadWithWebWorker);
        }

        InstantiateBaseSkeleton(onFinishCallback, hasToLoadWithWebWorker)
    }

    let InstantiateLights = () =>
    {
        const mainUpLight = new THREE.DirectionalLight(0xffffff, 2);
        const secondaryUpLight = new THREE.DirectionalLight(0xffffff, 1);

        mainUpLight.position.set(20, 30, 10);
        mainUpLight.target.position.set(0, 0, 0);
        mainUpLight.castShadow = true;

        secondaryUpLight.position.set(-20, 30, -10);
        secondaryUpLight.target.position.set(0, 0, 0);
        secondaryUpLight.castShadow = true;

        const reverseLight1 = new THREE.DirectionalLight(0xffffff, 1);
        const reverseLight2 = new THREE.DirectionalLight(0xffffff, 1);

        reverseLight1.position.set(20, -30, -10);
        reverseLight1.target.position.set(0, 0, 0);
        reverseLight1.castShadow = false;
        reverseLight2.position.set(-20, -30, 10);
        reverseLight2.target.position.set(0, 0, 0);
        reverseLight2.castShadow = false;

        const ambientLight = new THREE.AmbientLight(0xaaaaaa); // soft white light

        _scene.add(ambientLight);
        _scene.add(mainUpLight);
    }

    let InstantiateBaseSkeleton = async (onFinishCallback, hasToLoadWithWebWorker = true) =>
    {
        let onFinishCallback2 = (baseModel) =>
        {
            if (baseModel)
            {
                _itemsContainer.add(baseModel);

                let skeletonHelper = new THREE.SkeletonHelper(_scene);
                _scene.add(skeletonHelper)

                let baseBones = [];

                baseModel.traverse(
                    (object) =>
                    {
                        switch (object.type)
                        {
                            case 'Bone':
                                baseBones.push(object);
                                break;
                        }
                    });

                _baseSkeleton = new THREE.Skeleton(baseBones);

                let charAnimationGroup = new THREE.AnimationObjectGroup(baseModel);
                _skeletonMixer = new THREE.AnimationMixer(charAnimationGroup);

                if (baseModel.animations.length > 0)
                {
                    let currentAnimation = _skeletonMixer.clipAction(baseModel.animations[1]);
                    currentAnimation.play();
                }

                let finalScale = 1;
                let finalPositionX = 0;
                let finalPositionY = 0;

                baseModel.scale.set(finalScale, finalScale, finalScale);
                baseModel.position.set(finalPositionX, 0, finalPositionY);
            }

            onFinishCallback();
        };

        let baseModel = await LoadObject3D('default-skeleton-01.glb', onFinishCallback2, hasToLoadWithWebWorker);
    }

    let InstantiateDynamicBodyPart = (
        modelFileName,
        baseMaterial,
        hasToLoadWithWebWorker = true) =>
    {
        let onFinishCallback = (baseModel) =>
        {
            if (baseModel)
            {
                let skinnedMeshesList = [];

                baseModel.traverse(
                    (object) =>
                    {
                        switch (object.type)
                        {
                            case 'SkinnedMesh':
                                skinnedMeshesList.push(object);
                                break;
                        }
                    });

                InjectMaterial(baseModel, baseMaterial);

                skinnedMeshesList.map(
                    (skinnedMesh) =>
                    {
                        _itemsContainer.add(skinnedMesh);
                        skinnedMesh.skeleton = _baseSkeleton;
                    });
            }
        }

        LoadObject3D(modelFileName, onFinishCallback, hasToLoadWithWebWorker);
    }

    let InjectMaterial = (baseModel, material) =>
    {
        if (baseModel)
        {
            baseModel.traverse(
                (object) =>
                {
                    if (object.isMesh)
                        object.material = material;
                });
        }
    }

    let GetRandomId = (length) =>
    {
        let result = '';

        const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
        const charactersLength = characters.length;
        let counter = 0;
        while (counter < length)
        {
            result += characters.charAt(Math.floor(Math.random() * charactersLength));
            counter += 1;
        }

        return result;
    }

    let LoadObject3D = (assetSubPath, onFinishCallback, hasToLoadWithWebWorker = true) =>
    {
        let value = undefined;

        let assetsRootPath = 'https://y87vj9.csb.app/public/';
        let assetPath = assetsRootPath + assetSubPath;

        if (!hasToLoadWithWebWorker)
        {
            if (!value)
            {
                console.log('[default loading] > loading model... | assetPath =', assetPath);

                let onSuccessCallback =
                    (modelGLTF) =>
                    {
                        console.log('[default loading] >>> model loaded! | assetPath =', assetPath);

                        let moodelObject3D = SkeletonUtils.clone(modelGLTF.scene.clone());
                        moodelObject3D.animations = modelGLTF.animations;

                        onFinishCallback(moodelObject3D);
                    };

                _glbLoader.load(assetPath, onSuccessCallback);
            };
        }
        else
        {
            if (typeof Worker !== 'undefined')
            {
                let requestId = GetRandomId(16);
                _webworkersCallbacksById[requestId] = onFinishCallback;

                let webworkerRequest =
                {
                    RequestId: requestId,
                    ProcessName: 'LoadModel',
                    Params: [assetPath],
                }

                _worker.postMessage(JSON.stringify(webworkerRequest));
            }
            else
            {
                // Web workers are not supported in this environment.
                // You should add a fallback so that your program still executes correctly.
                console.error('$$q> AssetsService.GetAsset | Web workers are not supported in this environment!');
            }
        }
    }




    // region Worker
    // state
    let _webworkersCallbacksById = {};

    let HandleWebworkerMessage = (data) =>
    {
        // let webworkerResponse = JSON.parse(data);
        let webworkerResponse = JSON.parse(data.data);
        let requestId = webworkerResponse.RequestId;
        let baseModel = undefined;

        if (webworkerResponse.RawResponse)
        {
            let object3DJSON = webworkerResponse.RawResponse;

            const loader = new THREE.ObjectLoader();
            baseModel = loader.parse(object3DJSON);
        }
        else
            console.error('$> webworkerResponse.RawResponse is undefined! ');

        let isCallbackRegistered = requestId in _webworkersCallbacksById;

        if (isCallbackRegistered)
        {
            let callback = _webworkersCallbacksById[requestId];

            callback(baseModel);
            delete _webworkersCallbacksById[requestId];
        }
        else
            console.error('$> Unexpected requestId! requestId = ', requestId);
    }

    const _worker = new Worker("/model-loading-worker.js", { type: 'module' });
    _worker.onmessage = HandleWebworkerMessage;
    //end region Worker

    let DefaultIntantiatingButton = () =>
    {
        DestroyScene();
        InstantiateScene(false);
    }
    
    let WebWorkerIntantiatingButton = () =>
    {
        DestroyScene();
        InstantiateScene(true);
    }

    let SetupButtons = () =>
    {
        let defaultIntantiatingButton = document.querySelector('#default-intantiating-button');
        let webWorkerIntantiatingButton = document.querySelector('#web-worker-intantiating-button');

        defaultIntantiatingButton.onclick = DefaultIntantiatingButton;
        webWorkerIntantiatingButton.onclick = WebWorkerIntantiatingButton;
    }

    SetupButtons();
    DefaultIntantiatingButton();

</script>

model-loading-worker.js

import { GLTFLoader } from 'https://esm.sh/[email protected]/examples/jsm/loaders/GLTFLoader.js';


// dependencies
let _glbLoader = new GLTFLoader();

let HandleMessage = async (request) =>
{
    let response = {};
    response.RequestId = request.RequestId;

    switch (request.ProcessName)
    {
        case 'LoadModel':
            {
                let assetPath = request.Params[0];
                console.log('[web worker] > loading model... | assetPath =', assetPath);

                let onSuccessCallback =
                    (modelGLTF) =>
                    {
                        console.log('[web worker] >>> model loaded! | assetPath =', assetPath);

                        let finalAssetValue = modelGLTF.scene;

                        finalAssetValue.animations = modelGLTF.animations;

                        let finalAssetValueObject3DJSON = finalAssetValue.toJSON()
                        response.RawResponse = finalAssetValueObject3DJSON;
                    }

                await _glbLoader.loadAsync(assetPath).then(onSuccessCallback);

                break
            }

        default:
            {
                console.error('Unexpected request.ProcessName! | request.ProcessName =', request.ProcessName);
                break;
            }
    }

    return response;
}

addEventListener(
    'message',
    async ({ data }) =>
    {
        let webworkerRequest = JSON.parse(data, function (key, value)
        {
            return value;
        });

        let response = await HandleMessage(webworkerRequest);
        data = JSON.stringify(response);

        postMessage(data);
    });

I’ll leave a link below to a practical example on codesandbox with reproduction (change between loading modes by changing the state of the checkbox in the lower right corner and then press “Instatiate Scene”. The buttons have a yellow hover to test with the mouse if the page is really stuck during loading, as well as the cube animation)

https://codesandbox.io/p/sandbox/y87vj9

Any help or guidance will be very welcome, I’ve been trying to find a way to improve the optimization of this page for about 2 weeks, and this was the only thing that had a good result in performance, however, it’s not working.

Auto hide animation after x seconds

I have a a react project where i am importing a third party lib which has some reusable components like input etc. I want to update the styles of the component and a animation to the border. I was able to make it work and here is the demo

Demo

Now my requirement is, i want to hide this animation after 10 sec. Basically stop animation after 10 sec and remove the border-image animation. Is there a way to do this just by using css ? I tried making border-image: none after 100% of keyframe but nothing worked. Please help.

I am not able run vite project in which I am using react 19 and tailwind css version 4 [closed]

Error I get when I run npm run dev or yarn dev:

failed to load config from C:fekey-nest-FEvite.config.ts
error when starting dev server:
Error: Cannot find module ‘../lightningcss.win32-x64-msvc.node’
Require stack:

  • C:fekey-nest-FEnode_moduleslightningcssnodeindex.js
    at Function._resolveFilename (node:internal/modules/cjs/loader:1225:15)
    at Function._load (node:internal/modules/cjs/loader:1055:27)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:220:24)
    at Module.require (node:internal/modules/cjs/loader:1311:12)
    at require (node:internal/modules/helpers:136:16)
    at Object.<anonymous> (C:fekey-nest-FEnode_moduleslightningcssnodeindex.js:22:22)
    at Module._compile (node:internal/modules/cjs/loader:1554:14)
    at Object..js (node:internal/modules/cjs/loader:1706:10)
    at Module.load (node:internal/modules/cjs/loader:1289:32)

I have tried unistalling node modules and reinstall and tried addin lightningcss

i have node version 22.14.0

Can anyone help me??