Swiper JS playbutton disappear after observer

It only happens on mobile devices.

Situation on mobile:

  1. The user plays a video.
  2. The user scrolls to another carousel video, and the intersection observer triggers, playing the second video.
  3. The user navigates back to the first video via intersection, but the
    play button is missing.
  4. The user touches the mobile screen and
    swipes slightly forward or backward, and the play button appears.
    The user releases their finger, and the play button disappears.

Expected: The play button should always be visible if the video is stopped. This behavior works on desktop but not on touchscreens.

window.addEventListener("DOMContentLoaded", () => {
  const iframesWrappers = document.getElementsByClassName("video-wrapper");
  let currentPlayingIndex = null;
  const players = [];
  const videoStates = {};

  const isMultiplePlayback = document.getElementById("dataBlock").getAttribute("data-allow-multiple-playback");

  const observerOptions = {
    root: document.getElementById("swiperCarousel"),
    rootMargin: "0px",
    threshold: 0.3,
  };

  const intersectionCallback = (entries, observer) => {
    entries.forEach((entry) => {
      const index = entry.target.dataset.index;
      const videoElement = document.getElementById(`video-${index}`);
      const playButton = document.getElementById(`playButton-${index}`);
      const overlay = videoElement.querySelector(".overlay");
      const iframe = videoElement.querySelector("iframe");

      if (!entry.isIntersecting) {
        if (iframe.src.includes("autoplay=true")) {
          playButton.style.display = "none";
          overlay.style.display = "block";
        }
      } else {
        if (videoStates[index] === "stopped") {
          let src = iframe.src;
          if (src.includes("autoplay=true")) {
            src = src.replace("autoplay=true", "");
            iframe.src = src;
          }

          playButton.style.display = "block";
          overlay.style.display = "none";
          Stream(iframe).pause();
        } else if (iframe.src.includes("autoplay=true")) {
          playButton.style.display = "none";
          overlay.style.display = "block";
          Stream(iframe).pause();
        } else if (videoStates[index] === "playing") {
          playButton.style.display = "none";
          overlay.style.display = "block";
          Stream(iframe).play();
        } else {
          playButton.style.display = "block";
          overlay.style.display = "none";
          Stream(iframe).pause();
          console.log("pause");
        }
      }
    });
  };

  const observer = new IntersectionObserver(intersectionCallback, observerOptions);

  Array.from(iframesWrappers).forEach((videoElement, index) => {
    videoElement.dataset.index = index;
    const videoUrl = videoElement.dataset.url;

    const iframe = document.createElement("iframe");
    iframe.src = videoUrl;
    iframe.style.transform = "scale(1.02)";
    iframe.style.width = "100%";
    iframe.style.height = "100%";
    iframe.style.border = "none";
    iframe.style.pointerEvents = "none";
    iframe.style.position = "absolute";
    iframe.style.top = "0";
    iframe.allow =
      "accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture; web-share";

    videoElement.style.position = "relative";
    videoElement.style.borderRadius = "10px";
    videoElement.style.overflow = "hidden";
    videoElement.appendChild(iframe);

    const overlay = document.createElement("div");
    overlay.classList.add("overlay");
    overlay.style.position = "absolute";
    overlay.style.top = "0";
    overlay.style.left = "0";
    overlay.style.width = "100%";
    overlay.style.height = "100%";
    overlay.style.cursor = "pointer";
    overlay.style.zIndex = "1";

    const playButton = document.getElementById(`playButton-${index}`);

    if (videoUrl.includes("autoplay=true")) {
      overlay.style.display = "block";
      playButton.style.display = "none";
      currentPlayingIndex = index;
      videoStates[index] = "playing";
    } else {
      overlay.style.display = "none";
      playButton.style.display = "block";
      videoStates[index] = "stopped";
    }

    videoElement.appendChild(overlay);

    Stream(iframe).addEventListener("ended", () => {
      playButton.style.display = "block";
      overlay.style.display = "none";
      videoStates[index] = "stopped";
    });

    const player = Stream(iframe);
    players[index] = player;

    observer.observe(videoElement);

    playButton.addEventListener("click", () => {
      if (isMultiplePlayback === "false") {
        if (currentPlayingIndex !== index && currentPlayingIndex !== null) {
          const prevPlayer = players[currentPlayingIndex];
          const prevVideoElement = document.getElementById(`video-${currentPlayingIndex}`);
          const prevPlayButton = document.getElementById(`playButton-${currentPlayingIndex}`);
          const prevOverlay = prevVideoElement.querySelector(".overlay");

          prevPlayButton.style.display = "block";
          prevOverlay.style.display = "none";

          if (prevPlayer) {
            prevPlayer.pause();
            videoStates[currentPlayingIndex] = "stopped";
          }
        }
      }

      currentPlayingIndex = index;
      player.play();
      iframe.style.display = "block";
      overlay.style.display = "block";
      playButton.style.display = "none";
      videoStates[index] = "playing";
    });

    overlay.addEventListener("click", () => {
      player.pause();
      playButton.style.display = "block";
      overlay.style.display = "none";
      videoStates[index] = "stopped";
      currentPlayingIndex = null;
    });
  });
});

I also have swiper settings:

const swiperElement = document.querySelector("#swiperCarousel");

if (swiperElement) {
  const totalSlides = swiperElement.querySelectorAll(".swiper-slide").length;

  let slidesPerView = null;
  let loop = true;

  if (totalSlides == 3) {
    slidesPerView = 2.3;
  } else if (totalSlides == 2) {
    slidesPerView = 1.9;
    loop = false;
  }

  let breakpoints = {
    481: { slidesPerView: totalSlides >= 4 ? 3.125 : slidesPerView },
    320: { slidesPerView: 1.1 },
  };

  new Swiper("#swiperCarousel", {
    loop: loop,
    slidesPerView: slidesPerView,
    centeredSlides: false,
    spaceBetween: 8,
    observer: true,
    observeParents: true,
    pagination: {
      el: ".swiper-pagination",
      clickable: true,
    },
    breakpoints: breakpoints,
  });
}

I’m using HTML:

import { useBlockProps } from "@wordpress/block-editor";
import PlayButtonSvg from "../assets/PlayButtonSvg";
import PlayButton from "../assets/Vector.png";

const Save = ({ attributes }) => {
  const blockProps = useBlockProps.save();
  return (
    <>
      <script src="https://embed.cloudflarestream.com/embed/sdk.latest.js"></script>

      <div {...blockProps} id="dataBlock" data-allow-multiple-playback={attributes.allowMultiplePlayback}>
        <div id="swiperCarousel" className="swiper">
          <div className="swiper-wrapper">
            {attributes.videos.map((video, index) => (
              <div className="swiper-slide" key={index}>
                <div className="video-wrapper" id={`video-${index}`} data-url={video.videoUrl}>
                  <img className="playButtonFull" id={`playButton-${index}`} src={PlayButton} alt="play button" />
                </div>
              </div>
            ))}
          </div>
          <div className="swiper-pagination"></div>
        </div>
      </div>
    </>
  );
};

export default Save;

I checked on the dev tools it has display: block, but not visible.

Custom sort order according to cell value (GAS)

Currently I sort my sheet via this function

var ss = SpreadsheetApp.getActiveSpreadsheet();
var sheet = ss.getSheetByName(a);
var dataRange = sheet.getDataRange();
dataRange.sort([
  {column: role, ascending: true},
  {column: cases, ascending: false}
]);

This sorts the by role first in alphabetical order. Is it possible to customise this sort ?
For example, ideally the sort is by role level and not aplhabetical

Eg. if the roles are [intern, junior, senior, lead, manager] can the sheet be sorted in that order ?

Do all international browsers display the exception STACK in English or in the browser’s native language?

This question is NOT about translating the content of a web page. It is about the Exceptions thrown in the JavaScript engine.

I am wondering if all international installations of browsers, installed on international OSs, produce the exception STACK in english or in the native language.

I am NOT asking about any of the following:

  • Translating a web page.
  • The runtime language choice of a web page.

I am asking about a different language install of the browser where all of the UI in the browser is in the installed language including the menus, etc.

For example in the English Chrome browser I get an exception stack like this:

myState.js:10 Uncaught TypeError: Cannot read properties of undefined (reading 'stack')
    at new MyStateError (myState.js:10:17)
    at createState (myState.js:22:11)
    at statetest.html:16:23

Will it be the same for a French or Spanish installed browser? Or will the French installed version look like this:

myState.js:10 Non capturé TypeError : Impossible de lire les propriétés d'une valeur indéfinie (lecture de 'stack')
    à new MyStateError (myState.js:10:17)
    à créerÉtat (myState.js:22:11)
    à statetest.html:16:23

I don’t have a second machine on which I can install a different language OS and browser. Can someone that has a non-English install of a browser let me know what they get. (Again I am not concerned with page translations, I am looking for someone that has a full Non-English language version installed on a non-English OS)

You can run these lines of code in Dev Tools to find out:

let a = new Error('Testing');
console.log(a.stack)

How to add an auto increment field to an existing table with TypeORM

This SQL satatement works, and is what I’m trying to generate:

ALTER TABLE `some_entity`
ADD `generatedNumber` INT NOT NULL AUTO_INCREMENT,
ADD UNIQUE (`generatedNumber`);

However I need to do this via TypeORM and this code:

@Entity()
@Unique(['generatedNumber'])
export class SomeEntity extends BaseEntity { //existing entity
  @PrimaryGeneratedColumn('uuid') //existing column
  id: string;

  @Column() //new column trying to create
  @Generated('increment')
  generatedNumber: number;

  @Column({ type: 'text' }) //existing column
  otherColumn: string;

  ...
}

Results in this error:

Error: ER_WRONG_AUTO_KEY: Incorrect table definition; there can be only one auto column and it must be defined as a key

I can see this is because the generated SQL is:

ALTER TABLE some_entity ADD generatedNumber int NOT NULL AUTO_INCREMENT;

It is not adding a unique constraint to the generated sql query.

I’ve Tried:

  • Adding @Unique(['generatedNumber'])
  • Adding @Index()
  • Disabling synchronize: true and using migrations – would work but as workaround where synchronize is required (CI)

How can I add this AUTO_INCREMENT with TypeORM?

Problème clé de non valide 403 Requête API [closed]

I hope you are all well

I need your help concerning the use of API requests, I use Avensys requests to create an automatic mail sending functionality, I first test the request on POSTMAN with the necessary API keys and tokens, I put CORS headers to prevent the browser from blocking the request, I put the right insertions in the request body, I did everything right. On Postman, the request works perfectly, but as soon as I program all this in php, and javascript and html for display, it doesn’t work and keeps returning the 403 error. I can provide you with photos of the files.

I’m waiting for the Avensys application, after executing the request, to send me the email. Thank you all if you have any leads to give me.

Have a nice day

React/JS async operations within onChange callback changes event data

I just ran into this curious behavior that I can’t explain. In my React app I have the following onChange callback for when value changes within an input.

const onInputChange = useCallback(async (e: ChangeEvent<HTMLInputElement>) => {
    console.log('before: ' + e.target.value);

    await doAsyncThing();

    console.log('after: ' + e.target.value);
}, [])

And I noticed that after calling doAsyncThing() the contents of e.target.value are gone; this value is now just an empty string whereas before it was something else. It will show something like:

before: a
after:

Is there some nuance I am missing with doing asynchronous operations within onChange callbacks?

Echarts sankey diagram rendering

I’m trying to create a dynamic Sankey diagram with echarts. I have a case where I have a lot of data and my diagram is not really readable as you can see in the image (I have hidden the node names for privacy reasons).

sankey_diagram

Here are my options :

var option = {
            tooltip: {
                trigger: 'item',
                triggerOn: 'mousemove',
                formatter: function (params) {
                    if (params.dataType === 'edge') {
                        return `${params.data.color}<br>${params.data.source} -->  ${params.data.target} : <b>${formatNumber(params.data.trueValue, formatLocale(lang))}</b>`;
                    }
                    else {
                        return `${params.data.tooltip_text}`;
                    }
                }
            },
            legend: {},
            backgroundColor: '{{backgroundColor}}',
            dataZoom: [
                {
                type: 'slider',
                show: true,
                }
            ],
            series: {
                type: 'sankey',
                layout: 'none',
                layoutIterations: 0,
                nodeGap: 8,
                nodeWidth: 20,
                labelLayout: {
                    hideOverlap: true
                },
                emphasis: {
                focus: 'adjacency'
                },
                lineStyle: {
                    color: 'gradient',
                    curveness: 0.4
                },
                label: {
                    show: false,
                    position: 'right',
                    distance: 10,
                },
                data : {{{convertJS data}}},
                links: {{{convertJS links}}},
            }
        };

Here are examples of nodes formatting :

[
  {
    color: 'Inconsistent node',
    name: "Electricity meter TD",
    parents: [
      "Electricity meter 1 D",
      "Electricity meter 2 D",
      "Photovoltaic electricity meter D"
    ],
    tooltip: 'Inconsistent node<br>Sum of datapoints : 2.636<br>Sum of children : 2,599',
    type: 'LT_ELEC',
    itemStyle: { color: '#ffb77f' },
    tooltip_text: 'Inconsistent node<br>Sum of datapoints : 2.636<br>Sum of children : 2,599'
  },
  {
    color: 'Inconsistent node',
    name: "Electricity meter -2 (D)",
    parents: [
      "Electricity meter 1 D"
    ],
    tooltip: 'Inconsistent node<br>Sum of datapoints : 30.69<br>Sum of children : 1,003',
    type: 'LT_ELEC',
    itemStyle: { color: '#ffb77f' },
    tooltip_text: 'Inconsistent node<br>Sum of datapoints : 30.69<br>Sum of children : 1,003'
  },
]

Here are examples of links formatting :

links : [
  {
    color: 'Electrical flow',
    source: "Electricity meter 1 D",
    target: "Electricity meter TD",
    value: 100,
    lineStyle: { color: '#d8db14', opacity: 0.5 },
    trueValue: 0
  },
  {
    color: 'Electrical flow',
    source: "Electricity meter 1 D",
    target: 'Electricity meter -2 (D)',
    value: 190.66666666666669,
    lineStyle: { color: '#d8db14', opacity: 0.5 },
    trueValue: 90.66666666666667
  }
]

You should also know that I sorted my nodes by type.

Does anyone know what I could do to improve the rendering? Because some nodes only have one type of link so they could be placed elsewhere. Here is an example of a change that would make the diagram more readable (I made this example by dragging the nodes).

sankey_diagram_readable

Thanks in advance !

Cannot export file on .NET Core application

I’m working with a .NET Core application.
I can see the data on the view, but if I try to export this data (in Excel (using closedXML) or csv file), It doesn’t works. Checking on devTools there is not problems.

This is the controller, in devTools on the response section I can see the data:

public IActionResult ExportExcel ( DateTime fechaDesde ) 
{
    var builder = new StringBuilder();
    DataTable dt = _conteoFirmasRepository.GetListDT( fechaDesde ); // get data in datatable
    builder.AppendLine( "firmasConsumidas,Flujo,Contacto" );

    foreach ( DataRow item in dt.Rows ) { 
        builder.AppendLine( $"{item["firmasConsumidas"].ToString()},{item["Flujo"].ToString()}, {item["Contacto"].ToString()}");
    }

    return File(Encoding.UTF8.GetBytes( builder.ToString() ), "text/csv", "signatures.csv"); // Problem: NOT returns the file
}

This is the view, button for showing the table works without a problem:

@{
    ViewData["Title"] = "Home Page";
}

<div class="row justify-content-center" >
  <div class="alert alert-primary" role="alert" >
      VidSigner Counter
  </div>

  <div class="container justify-content-center" >        
        <div class="mb-3">
            <label for="txtFechaDesde" class="form-label">Start Date</label>
            <input type="date" class="form-control" id="txtFechaDesde" aria-describedby="emailHelp">
            <div id="FechaDesHelp" class="form-text">Set Start Date</div>
        </div>
        <div>
            <button type="button" class="btn btn-primary boton-consulta-totales">Consultar</button>
        </div>
        <div>
            <button type="button" class="btn btn-success boton-exporta-totales">Exportar</button>
        </div>        
    </div>

    <table class="table table-stripped table-hover" id="tablaFirmas" >
        <thead>
            <tr>
                <th>Firmas Consumidas</th>
                <th>Flujo</th>
                <th>Contacto</th>
            </tr>
        </thead>
        <tbody></tbody>
    </table>
</div>

The view has a section for js:

@section Scripts {
    <script src="~/js/conteofirmas.js"></script>
}

And this is the section for my js:

fetch(`/Home/ExportExcel?fechaDesde=${modelo.fechaDesde}`) // call the action
  .then(response => {
      return response.ok ? console.log("Ok") : Promise.reject(response)
  }) // on console I have the "Ok"       
}

Button for call the action:

$(document).on("click", ".boton-exporta-totales", function () { 
    ExportarExcel()
})

There is an other js function for call the data on a table, but this works correctly.
Any idea about the error?
please I hope you can help me.
thanks a lot.

Recording audio produced by SpeechSynthesis

to date I have tried several methods to try to record the voice generated by SpeechSynthesis without success, the only thing that has worked for me is to use Audacity to record the sound of the system, “But this is very inefficient in terms of automation”.

The question is if there is as of today 12/18/2024, a way to record the SpeechSynthesis voice without it being affected by the system sound, or if there is a way to record all the sound produced by a single Web page, thus managing to pigeonhole the SpeechSynthesis sound.

Search for information and tips

The list view does not show events before January 1st 2024

I created a calendar with multiple views, custom event rendering & contextual menu…using hooks. And all works just great except one thing : when I switch the list view the calendar shows only the events from January 1st, 2024 instead of 2018.

I tried many things:

calendar.changeView('list', {start: '2018-01-01', end: '2050-01-01'})

with and without time and timezone. And

calendar.changeView('listAllYears', {start: '2018-01-01', end: '2050-01-'}) 

with several variations of (here is the most extensive one) :

views: {
 listAllYears: {
    type: 'list',
    duration: { years: 100 },
    visibleRange: { start: '2018-01-01', end: '2050-01-01' },
    listDayFormat: { weekday: 'long',  month: 'long', day: 'numeric', year: 'numeric'},
    listDaySideFormat: { weekday: 'long',  month: 'long', day: 'numeric', year: 'numeric'},
  },
},

And

calendar.setOption('visibleRange', { start: '2018-01-01', end: '2050-01-01' }) 

before amd after the changeView command.

Depending on the combination, the best I got was the header to diplay the right date range but not the events.

I tracked the data and all events are provided to the calendar since 2018.

I expect the list to show the events since 2018, not only since January 1st, 2024.

Using a TextField as one of the options of MUI Autocomplete

In a React app, I’m using a MUI Autocomplete field as a searchable dropdown. I’m trying to make 1 of the options be a TextField in which the user can fill in some text.

Rendering this TextField is working fine, but I cannot get it to allow the user to select the TextField by clicking on it.

Is there a way to make it possible for the user to highlight this TextField and start typing text in it, without the Autocomplete options list closing down?

screenshot

Edit react

Or this static code below:

import "./styles.css";

import { Autocomplete, TextField } from "@mui/material";
import { useState } from "react";

export default function App() {
  const [selectedOption, setSelectedOption] = useState("");
  return (
    <Autocomplete
      options={["Option 1", "Option 2", "Option 3"]}
      renderInput={(params) => <TextField {...params} />}
      renderOption={(props, option) => {
        if (option == "Option 3") {
          return (
            <li {...props}>
              <TextField onClick={(e) => e.stopPropagation()} />
            </li>
          );
        } else return <li {...props}>{option}</li>;
      }}
      value={selectedOption}
      onChange={(_, newValue) => {
        if (newValue) {
          setSelectedOption(newValue);
        }
      }}
    />
  );
}

Quill.js removes tags like “DIV” and styles and classes

I am working on WYSIWIG editor, where I can update HTML in special popup and insert it back into Quill editor as a semantic HTML. The resulting HTML should be exactly like we edited. But when I try to change tags to “div”, they are turned to “p” and classes are removed.

Here is my example: https://codepen.io/Maria2/pen/PwYWVKY

class Dom {
  constructor(selector) {
    this.$el =
      typeof selector === "string"
        ? document.querySelector(selector)
        : selector;
  }

  html(html) {
    if (typeof html === "string") {
      this.$el.innerHTML = html;
      return this;
    }

    return this.$el.outerHTML.trim();
  }

  innerHtml(html) {
    if (typeof html === "string") {
      this.$el.innerHTML = html;
      return this;
    }

    return this.$el.innerHTML.trim();
  }

  text(text) {
    if (typeof text !== "undefined") {
      this.$el.textContent = text;
      return this;
    }

    if (this.$el.tagName.toLowerCase() === "input") {
      return this.$el.value.trim();
    }

    return this.$el.textContent;
  }

  clear() {
    this.html("");
    return this;
  }

  on(eventType, callback) {
    this.$el.addEventListener(eventType, callback);
  }

  off(eventType, callback) {
    this.$el.removeEventListener(eventType, callback);
  }

  append(node) {
    if (node instanceof Dom) {
      node = node.$el;
    }

    if (Element.prototype.append) {
      this.$el.append(node);
    } else {
      this.$el.appendChild(node);
    }
  }

  closest(selector) {
    return domElt(this.$el.closest(selector));
  }

  getCoords() {
    return this.$el.getBoundingClientRect();
  }

  get data() {
    return this.$el.dataset;
  }

  findAll(selector) {
    return this.$el.querySelectorAll(selector);
  }

  css(styles = {}) {
    Object.keys(styles).forEach((key) => {
      this.$el.style[key] = styles[key];
    });
    return this.$el;
  }

  getStyles(styles = []) {
    return styles.reduce((res, s) => {
      res[s] = this.$el.style[s];
      return res;
    }, {});
  }

  find(selector) {
    return domElt(this.$el.querySelector(selector));
  }

  addClass(className) {
    this.$el.classList.add(className);
    return this;
  }

  removeClass(className) {
    this.$el.classList.remove(className);
    return this;
  }

  attr(name, value) {
    if (value) {
      this.$el.setAttribute(name, value);
      return this;
    }

    return this.$el.getAttribute(name);
  }

  focus() {
    this.$el.focus();
    if (
      typeof window.getSelection !== "undefined" &&
      typeof document.createRange !== "undefined"
    ) {
      const range = document.createRange();
      range.selectNodeContents(this.$el);
      range.collapse(false);
      const sel = window.getSelection();
      sel.removeAllRanges();
      sel.addRange(range);
    } else if (typeof document.body.createTextRange !== "undefined") {
      const textRange = document.body.createTextRange();
      textRange.moveToElementText(this.$el);
      textRange.collapse(false);
      textRange.select();
    }

    return this;
  }

  destroy() {
    this.$el.remove();
    this.$el = null;
  }
}

function domElt(selector) {
  return new Dom(selector);
}

domElt.create = (tagName, classes = "") => {
  const el = document.createElement(tagName);
  if (classes) {
    el.classList.add(classes);
  }

  return domElt(el);
};

class PopupElt {
  constructor({ togglerClass, beforeOpenCallback, onOpenCallback,onCloseCallback, additionalClass }) {
    this.togglerClass = togglerClass;
    this.additionalClass = additionalClass;
    this.popup = null;
    this.overlay = null;
    this.closeBtn = null;
    this.innerBlock = null;
    this.onCloseCallback = null;
    this.beforeOpenCallback = beforeOpenCallback;
    this.onOpenCallback = onOpenCallback;
    this.onCloseCallback = onCloseCallback;
    this.clickHandler = this.clickHandler.bind(this);
    this.closeHandler = this.closeHandler.bind(this);
  }

  clickHandler(evt) {
    evt.preventDefault();
    this.start();
  }

  start() {
    if (this.beforeOpenCallback) this.beforeOpenCallback(this);
    this.createOverlay();
    this.createPopup();
    if (this.onOpenCallback) this.onOpenCallback(this);
    this.setCloseListeners();
  }

  closeHandler(evt) {
    evt.preventDefault();
    this.closePopup();
  }

  closePopup() {
    this.removeCloseListeners();
    this.closeBtn.destroy();
    this.innerBlock.destroy();
    this.popup.destroy();
    if (this.overlay) this.overlay.destroy();
    if (this.onCloseCallback) this.onCloseCallback()
  }

  setCloseListeners() {
    this.overlay.on('click', this.closeHandler);
    this.closeBtn.on('click', this.closeHandler);
  }

  removeCloseListeners() {
    this.overlay.off('click', this.closeHandler);
    this.closeBtn.off('click', this.closeHandler);
  }

  createOverlay() {
    this.overlay = domElt.create('div', 'overlay');
    domElt(document.body).append(this.overlay);
  }

  createPopup() {
    let wrapperElt = domElt.create('div', 'popup__wrapper');
    this.innerBlock = domElt.create('div', 'popup__inside');
    this.popup = domElt.create('div', 'popup');
    this.closeBtn = domElt.create('button', 'btn')
      .addClass('popup__close')
      .attr('ariaLabel', 'закрыть попап')
      .html(`<svg fill="none" width="20" height="20" id="cross" viewBox="0 0 22 22"><path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M16.303 5.697 5.697 16.303M5.697 5.697l10.606 10.606"></path></svg>`)
    wrapperElt.append(this.closeBtn);
    wrapperElt.append(this.innerBlock);
    if (this.additionalClass) this.popup.addClass(this.additionalClass);
    this.popup.append(wrapperElt);
    domElt(document.body).append(this.popup);
  }

  async setInnerHtml(innerHtml) {
    await this.innerBlock.html(innerHtml)
  }
}

const options = (id) => ({
  debug: "info",
  modules: {
    toolbar: `#toolbar${id}`,
  },
  placeholder: "Compose an epic...",
  theme: "snow",
});

class QuillEditorClass {
  constructor({ item, options }) {
    this.item = item;
    this.popup = null;
    this.id = null;
    this.toolbarBlock = null;
    this.codeBtn = null;
    this.options = options || {};
    this.currentOptions = null;
    this.codeButton = null;
    this.saveCodeBtn = null;
    this.saveBtn = null;
    this.quill = null;
    this.textareaBlock = null;
    this.openPopup = this.openPopup.bind(this);
    this.getQuillInfo = this.getQuillInfo.bind(this);
    this.saveQuill = this.saveQuill.bind(this);
    this.setQuillInfo = this.setQuillInfo.bind(this);
    this.init();
  }

  init() {
    if (!this.item) return;
    this.id = this.item.dataset.editor;
    this.currentOptions = this.options(this.id)
    this.toolbarBlock = domElt(`#toolbar${this.id}`);
    this.quill = new Quill(this.item, this.currentOptions);
    this.codeButton = this.toolbarBlock.find('.code-button');
    this.saveBtn = this.item.parentElement.querySelector('.saveQuill')
    this.setListeners();
  }

  setListeners() {
    if (this.codeButton) this.codeButton.on('click', this.openPopup);
    if (this.saveBtn) this.saveBtn.addEventListener('click', this.saveQuill);
  }

  beforeOpenCallback(quillContent) {
    const insidePopupBlock = domElt.create('div', 'popup__textarea-wrapper');
    this.textareaBlock = domElt.create('textarea', 'popup__textarea');
    this.textareaBlock.attr('name', `#textarea${this.id}`)
    this.saveCodeBtn = domElt.create('button');
    this.saveCodeBtn.attr('class', 'btn btn--blue btn--lg');
    this.saveCodeBtn.text('Сохранить');
    this.textareaBlock.text(quillContent)
    insidePopupBlock.append(this.textareaBlock)
    insidePopupBlock.append(this.saveCodeBtn)
    return insidePopupBlock;
  }

  onCloseCallback() {
    this.saveCodeBtn.off('click', this.setQuillInfo);
    this.saveCodeBtn = null;
    this.textareaBlock = null;
  }

  openPopup(evt) {
    const quillContent = this.getQuillInfo(evt);
    this.popup = new PopupElt({
      togglerClass: `#toolbar${this.id}`,
      onOpenCallback: (popup) => {
        popup.innerBlock.append(this.beforeOpenCallback(quillContent).$el) ;
      },
      onCloseCallback: null,
      additionalClass: 'popup--editor'
    });
    this.popup.start();
    this.saveCodeBtn.on('click', this.setQuillInfo)
  }

  setQuillInfo(evt) {
    evt.preventDefault();
    console.log(this.textareaBlock.$el.value,123)
    var delta = this.quill.clipboard.convert({html: this.textareaBlock.$el.value});
    this.quill.setContents(delta);
    this.popup.closePopup();
  }

  getQuillInfo(evt) {
    evt.preventDefault();
    const length = this.quill.getLength();
    const html = this.quill.getSemanticHTML(0, length);
    const delta = this.quill.getContents();
    console.log(delta);
    console.log(html, 'html');
    return html;
  }

  saveQuill(evt) {
    evt.preventDefault();
    const length = this.quill.getLength();
    const html = this.quill.getSemanticHTML(0, length);
    const delta = this.quill.getContents();
    console.log(delta);
    console.log(html, 'html');
    return html;
  }
}


const startQuill = () => {
  const allQuills = document.querySelectorAll('[data-role="editor"]');

  if (allQuills.length) {
    allQuills.forEach((item) => {
      console.log(item)
      new QuillEditorClass({ item, options });
    });
  }
};

startQuill()
:root {
  --grey_2: #b5b5b5;;
  --padding: 10px;
  --white: #fff;
  --blue: #04043c;;
}
.label {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.label__wrapper {
  display: flex;
  width: 100%;
  position: relative;
  flex-wrap: wrap;
}

.label__wrapper--quill {
  padding-top: 0;
  flex-direction: column;
  flex-wrap: nowrap;  
  border-radius: 10px;
}

 .ql-toolbar {
    border-radius: 10px 10px 0 0;
    background-color: var(--grey_2);
  }

  .ql-container {
    margin-bottom: 10px;
    border-radius: 0 0 10px 10px;
    background-color: var(--grey_2);
  }

  .ql-editor {
    min-height: 200px;

    img {
      width: auto;
    }
  }

  .saveQuill {
    margin-right: auto;
  }

.label__wrapper--row{
  gap: 20px;
  flex-wrap: nowrap;
}

.popup {
  position: fixed;
  top: 50%;
  left: 50%;
  z-index: 13;
  width: calc(100% - 2 * var(--padding));
  max-width: 520px;
  border-radius: 10px;
  overflow: hidden;
  transform: translate(-50%, -50%);
}

.popup--imgs {
  max-width: 1440px;
}

.popup__textarea {
  padding: 8px;
  width: 100%;
  border-radius: 10px;
  border: 1px solid var(--grey_2);
}

.popup__textarea-wrapper {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  gap: 14px;
}

.popup__wrapper {
  padding: 30px 20px 30px 20px;
  background-color: var(--white);
  max-height: calc(100svh - 2*var(--padding));
  overflow: hidden;
  overflow-y: auto;
  scrollbar-width: thin;
  box-sizing: border-box;
}

.popup__inside {
  position: relative;
  width: 100%;
}

.panel {
  width: 100%;
  height: 100%;
  overflow-y: auto;
}

.panel__name {
  margin-top: 0;
  margin-bottom: 34px;
  font-size: 24px;
  font-weight: 700;
  line-height: 1.2;
}

.popup__close {
  position: absolute;
  top: 10px;
  right: 10px;
  transition: opacity 0.3s;
}

.popup__close:hover {
    opacity: 0.8;
  }

.panel__form {
  width: 100%;
}

.btn {
  display: flex;
  padding: 0;
  margin: 0;
  appearance: none;
  text-decoration: none;
  background-color: transparent;
  border: none;
  cursor: pointer;
  box-sizing: border-box;  
}

.btn svg {
    flex-shrink: 0;
}

.btn--lg {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 9px 18px;
  font-weight: 700;
}

.btn--blue {
  margin-right: auto;
  color: var(--white);
  background-color: var(--blue);
  border: 2px solid var(--blue);
  border-radius: 10px;
  transition: opacity 0.3s;
}
<div class="label__wrapper label__wrapper--quill">
    <div id="toolbar1">
      <!-- Add buttons as you would before -->
      <button class="ql-bold"></button>
      <button class="ql-italic"></button>  
      <select class="ql-size">
        <option value="small"></option>
        <!-- Note a missing, thus falsy value, is used to reset to default -->
        <option selected></option>
        <option value="large"></option>
        <option value="huge"></option>
      </select>
      <!-- Add a bold button -->
      <button class="ql-bold"></button>
      <!-- Add subscript and superscript buttons -->
      <button class="ql-script" value="sub"></button>
      <button class="ql-script" value="super"></button>
      <!-- But you can also add your own -->
      <button class="code-button">code</button>
    </div>
    <div rows="5" data-role="editor" data-editor="1"></div>
    <button class="btn btn--blue btn--lg saveQuill">Сохранить текст</button>
  </div>

Button “code” initiates popup? where you can try to edit HTML

Google cloud API for creating logs

How could we write logs to google cloud logging using just a plain JavaScript API without any additional libraries?

In docs we have this route: POST https://logging.googleapis.com/v2/entries:write
and they describe what can be sent in the body.

In our case we can’t use client libraries because we need to write logs inside remix app, hosted on cloudflare pages where we don’t have a node.js environment, so we need to implement this just by using a plain js API.

There is nothing that describes what to do with authentication, in which field you need to add a token and most importantly what kind of token.
The documentation says that you can create a service account, give it appropriate permissions and generate a json file key. But what should be done with it further?
I don’t really like the approach to storing this file in the repository.
In the docs examples they used 3rd party libraries like winston and pass the path to this json:

const {LoggingWinston} = require('@google-cloud/logging-winston');

// Creates a client
const loggingWinston = new LoggingWinston({
  projectId: 'your-project-id',
  keyFilename: '/path/to/key.json',
});

Our case:

Every time any page of our site is requested it can be a bot or a real user, we need to push logs to google logs. Do we need to exchange this json file key for some access_token for every page request of our site? How should I do it?

I have no idea what what should I do with the json file key when using just a plain js API.

How to do this text animation?

everyone. I wanted to do text animation like on modrinth.com (main page) for my website, but I don’t really know how to do that.

I tried to do this with keyframes, but I think it’ll be better to do on javascript.

Vercel says font module not found but no issues on local [closed]

I was trying to deploy my NextJS project on vercel but it gave me this error.
Vercel Error

When I run npm run build on local, it runs perfectly and doesnt show any errors or even warnings related to the font files. They are all store in public, I checked my .gitignore for the capitalization setting, didn’t find anything there.

My font files are stored in the public folder of my NextJS app and I’m using the /app directory.

Can someone tell me what I’m doing wrong?