1

I'm trying to simulate a search on web version of Whatsapp (https://web.whatsapp.com). My goal is to do this task using pure JS, for study purposes. By looking the source code, i can see the search field is actually an editable DIV element :

enter image description here

With this source :

<div role="textbox" class="_13NKt copyable-text selectable-text" contenteditable="true" data-tab="3" dir="ltr"></div>

Here is what i tried :

1 - I first locate the element on page :

var node = document.getElementsByClassName('_13NKt copyable-text selectable-text')[0];

2 - I then set innertext :

node.innerText = 'test';

3 - The div is filled (although the placeholder is still there) , but the event that makes the search is not triggered :

enter image description here

4 - So i try to dispatch events that could trigger the 'search' event of the div :

node.dispatchEvent(new Event('input', { bubbles: true }));
node.dispatchEvent(new Event('change', { bubbles: true }));
node.dispatchEvent(new Event('keydown', { bubbles: true }));

Nothing really helped. At this point, the only way to make the page really search for 'test' string, is to manually click on the div and hit space bar.

What am i missing ?

Andy
  • 61,948
  • 13
  • 68
  • 95
delphirules
  • 6,443
  • 17
  • 59
  • 108
  • How is this related to React/React-Native? – Andy Sep 02 '21 at 18:06
  • @Andy As far as i know, the web version of Whatsapp is on React – delphirules Sep 02 '21 at 18:09
  • `getElementsByClassName` returns not a single node but `HTMLCollection` – capchuck Sep 02 '21 at 18:09
  • 2
    @capchuck, OP is selecting the first element of that collection. – Andy Sep 02 '21 at 18:10
  • @delphirules I'm not sure, but maybe you need to pass value not by inserting innertext. – capchuck Sep 02 '21 at 18:29
  • @capchuck pass value to a div ? – delphirules Sep 02 '21 at 18:29
  • @delphirules for app to switch between placeholder state and showing input state it would be needed to check if there is new value in event or not. So I think you need to pass value inside event, not the element. – capchuck Sep 02 '21 at 18:31
  • Because simplest logic inside app would look like this: if there is value inside event we show the value, if there is no value, we show placeholder. – capchuck Sep 02 '21 at 18:32
  • I think you can try [this](https://stackoverflow.com/a/62111884/16752963) way – capchuck Sep 02 '21 at 18:36
  • @capchuck I agree about the value, but as far as i know, a DIV does not have a 'value' property. I think the solution of the issue is somehow 'notify' the DIV that a change was made, but i don't know how to do it – delphirules Sep 02 '21 at 18:42
  • 1
    I looked arount this topic. Because of React using it's own events system it will not simply work with native `dispatchEvent` method. Maybe [this](https://stackoverflow.com/questions/39065010/why-react-event-handler-is-not-called-on-dispatchevent), [this](https://lifesaver.codes/answer/trigger-simulated-input-value-change-for-react-16-(after-react-dom-15-6-0-updated)) and [this](https://github.com/vitalyq/react-trigger-change/blob/master/lib/change.js) will be helpful :) – capchuck Sep 02 '21 at 20:02
  • Hi. Please post a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example), preferably in a [Stacksnippet](https://stackoverflow.blog/2014/09/16/introducing-runnable-javascript-css-and-html-code-snippets/) – Alexandre Elshobokshy Sep 06 '21 at 12:14
  • @AlexandreElshobokshy Actually you can just navigate to web.whatsapp.com and use Devtools to run the code on the question text – delphirules Sep 06 '21 at 14:28
  • @delphirules "Actually you can just navigate to web.whatsapp.com and use Devtools to run the code on the question text". That shows us the code of WhatsApp but not your code. Your question leaves many questions open. Like where does the placeholder text come from? What does your event listener look like? By providing a code example with your own code it would be much easier to figure out how we can help you. – igorshmigor Sep 08 '21 at 09:29
  • What happens if you click the search field? Maybe the DOM changes? – htho Sep 10 '21 at 04:34
  • @igorshmigor atually my code is exactly as described on the question ; i don't have an app, i'm using Devtools to simulate what i'm trying to do – delphirules Sep 10 '21 at 13:53
  • @htho Yes, if a manually click on the search field and hit space bar, the DOM changes and the search is made as expected. But it's a manual procedure, i'm trying to do it only using pure JS – delphirules Sep 10 '21 at 13:54

5 Answers5

3

I've tried to replicated web.whatsapp.com. As mentioned above by myf for the solution need those attributes <div role="textbox" contenteditable=true> and the change event not fire in that case.

The input event works and for intercept the string we can to use event.target.textContent instead event.target.value.

document.addEventListener('DOMContentLoaded', function () {
  const searchBar = document.querySelector('.search-bar');
  const input = document.querySelector('.search-input');
  const clearButton = document.querySelector('.clear-button');

  const focusedField = () => {
    searchBar.classList.add('focused');
    input.focus();
  };

  const outSideClick = ev => {
    ev.stopPropagation();
    const isSearchBar = ev.target.closest('.search-bar');
    const isEmptyField = input.textContent.length;

    if (!isSearchBar && isEmptyField === 0) {
      searchBar.classList.remove('focused');
      document.removeEventListener('click', outSideClick);
    }
  };

  const showClearBtn = ev => {
    const isEmptyFiled = ev.target.textContent.length;
    console.log(ev.target.textContent);
    if (isEmptyFiled === 0) {
      clearButton.classList.remove('active');
      return;
    }
    clearButton.classList.add('active');
  };

  const clearText = () => {
    input.textContent = '';
    clearButton.classList.remove('active');
  };

  clearButton.addEventListener('click', clearText);
  input.addEventListener('input', showClearBtn);
  searchBar.addEventListener('mouseup', focusedField);
  document.addEventListener('mouseup', outSideClick);
});
*,
::after,
::before {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
    'Open Sans', 'Helvetica Neue', sans-serif;
}

:root {
  --bg: hsl(201, 27%, 10%);
  --input-field: hsl(197, 7%, 21%);
  --font-color: hsl(206, 3%, 52%);
  --search-bar-height: 48px;
}

html {
  height: 100%;
}

body {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: var(--bg);
}

.search-bar {
  height: var(--search-bar-height);
  width: 350px;
  display: flex;
  align-items: center;
  position: relative;
  padding-inline: 1em 1.5em;
  overflow: hidden;
  background-color: var(--input-field);
  border-radius: 50px;
  z-index: 10;
}

.search-bar.focused .icon[data-icon='search'] {
  opacity: 0;
  transform: rotate(45deg);
}
.search-bar.focused .icon[data-icon='back'] {
  opacity: 1;
  transform: rotate(0deg);
}

.search-bar.focused .search-placeholder {
  display: none;
}

.search-button,
.clear-button {
  width: calc(var(--search-bar-height) / 2);
  height: calc(var(--search-bar-height) / 2);
  display: flex;
  position: relative;
  background-color: transparent;
  border: none;
  outline: none;
  transition: opacity 0.3s ease-in-out;
}

.icon {
  position: absolute;
  inset: 0;
  transition: all 0.3s ease-in-out;
  cursor: pointer;
}
.icon path {
  fill: var(--font-color);
}
.icon[data-icon='back'] {
  transform: rotate(-45deg);
  opacity: 0;
}
.clear-button {
  opacity: 0;
  pointer-events: none;
}
.clear-button.active {
  opacity: 1;
  pointer-events: initial;
}

.search-field {
  height: 2em;
  display: flex;
  margin-inline-start: 1em;
  flex-grow: 1;
  position: relative;
  overflow: hidden;
}

.search-placeholder,
.search-input {
  height: inherit;
  position: absolute;
  top: 3px;
  font-size: 1rem;
  color: var(--font-color);
  transition: all 0.3s ease-in-out;
  white-space: nowrap;
}

.search-placeholder {
  text-overflow: ellipsis;
  pointer-events: none;
  user-select: none;
}

.search-input {
  width: 100%;
  outline: none;
  border: 1px solid transparent;
  /* border transparent for caret visibility  */
}
<div class="search-bar" tabindex="1">
  <button class="search-button">
        <span class="icon" data-icon="search">
          <svg viewBox="0 0 24 24" width="24" height="24">
            <path
              fill="currentColor"
              d="M15.009 13.805h-.636l-.22-.219a5.184 5.184 0 0 0 1.256-3.386 5.207 5.207 0 1 0-5.207 5.208 5.183 5.183 0 0 0 3.385-1.255l.221.22v.635l4.004 3.999 1.194-1.195-3.997-4.007zm-4.808 0a3.605 3.605 0 1 1 0-7.21 3.605 3.605 0 0 1 0 7.21z"
            ></path>
          </svg>
        </span>
        <span class="icon" data-icon="back">
          <svg viewBox="0 0 24 24" width="24" height="24">
            <path
              fill="currentColor"
              d="M12 4l1.4 1.4L7.8 11H20v2H7.8l5.6 5.6L12 20l-8-8 8-8z"
            ></path>
          </svg>
        </span>
      </button>
  <div class="search-field">
    <div class="search-input" role="textbox" dir="ltr" tabindex="1" contenteditable="true"></div>
    <div class="search-placeholder">Search or start new chat</div>
  </div>
  <button class="clear-button">
        <span class="icon" data-icon="clear">
          <svg viewBox="0 0 24 24" width="24" height="24">
            <path
              fill="currentColor"
              d="M17.25 7.8L16.2 6.75l-4.2 4.2-4.2-4.2L6.75 7.8l4.2 4.2-4.2 4.2 1.05 1.05 4.2-4.2 4.2 4.2 1.05-1.05-4.2-4.2 4.2-4.2z"
            ></path>
          </svg>
        </span>
      </button>
</div>
Anton
  • 8,058
  • 1
  • 9
  • 27
  • Thank you very much for this example, but what i'd really need is a code that i could run directly on Whatsapp site. A local code works fine, but i can't modify the source of Whatsapp to run this code – delphirules Sep 14 '21 at 11:49
2

i'm also working on extension for Whatsapp and this works for me.

const input = document.querySelector('footer ._13NKt.copyable-text.selectable-text[contenteditable="true"]');
            input.innerText = `${value}`;
            const event = new Event('input', {bubbles: true});
            input.dispatchEvent(event);
Sandeep
  • 21
  • 1
1

onChange event triggers only for these supported elements:

<input type="checkbox">, <input type="color">, <input type="date">,
<input type="datetime">, <input type="email">, <input type="file">,
<input type="month">, <input type="number">, <input type="password">,
 <input type="radio">, <input type="range">, <input type="search">,
 <input type="tel">, <input type="text">, <input type="time">,
 <input type="url">, <input type="week">, <select> and <textarea>

source: https://www.w3schools.com/jsref/event_onchange.asp

WhatsApp is built by ReactJS for web and ReactNative for mobile apps.

React has a event binder that reacts to certain changes and feeds back to the DOM.

If you're trying to recreate it using vanilla javascript, use the input tag

Phillip Zoghbi
  • 512
  • 3
  • 15
0
  1. If you don't need any rich text formatting in the "value", then <div role="textbox" contenteditable> is probably overkill which will bring more complications than usage of simple native <input> with similar semantic and native convenient properties like dispatching of events when it's value is changed by user (the input event).
  2. Sadly, even native input elements do no fire change nor input events when it's value is changed by JS "from the outside". Complex workarounds for this would involve similar techniques as for the "fake input / conteneditable div": for example using mutationobserver for watching the input's value / div's innerHTML and/or listening to all keyboard, mouse and clipboard events in relevant parts of the document), but…
  3. …there's stupidly simple workaround for this: just fire the "changed" handler yourself when you know it is necessary.

Using these pieces of information can give us such simple POC with all event handlers attached as (and invoked from) inline attributes ("DOM0", what sometimes even makes quite sense being nice "vanilla" declarative markup):

<input placeholder="type, paste or change text here" id="i" size="40"
oninput="
  // native inline 'input event handler'
  // of native 'text input element' ('this' refers to it)
  console.log(this.value);
">
<button
onclick="
  // 'do something' with the content in a 'programmatic' way:
  i.value += '!!'; 
  // then invoke the native inline 'input' event handler directly:
  i.oninput();
">append '!!'</button>

Same could be used in higher DOM event handlers, but it would not be as self-explanatory as this POC.

myf
  • 9,874
  • 2
  • 37
  • 49
  • Could you please provide a working code for the issue, without having to change the page source (which i don't have access) ? – delphirules Sep 06 '21 at 17:57
0

Following your example:

var node = document.getElementsByClassName('_13NKt copyable-text selectable-text')[0];

node.value = 'test'

const clickEvent = new KeyboardEvent('keydown', {
    bubbles: true, cancelable: true, keyCode: 13
});

node.dispatchEvent(clickEvent);

I don't know about whatsapp being based on React but assuming it is, you could hook this after mounting the app

Adam
  • 161
  • 3