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 ?

17
  • How is this related to React/React-Native? Commented Sep 2, 2021 at 18:06
  • @Andy As far as i know, the web version of Whatsapp is on React Commented Sep 2, 2021 at 18:09
  • getElementsByClassName returns not a single node but HTMLCollection Commented Sep 2, 2021 at 18:09
  • 2
    @capchuck, OP is selecting the first element of that collection. Commented Sep 2, 2021 at 18:10
  • 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, this and this will be helpful :) Commented Sep 2, 2021 at 20:02

5 Answers 5

3
+25

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>

Sign up to request clarification or add additional context in comments.

1 Comment

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
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);

Comments

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

Comments

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.

1 Comment

Could you please provide a working code for the issue, without having to change the page source (which i don't have access) ?
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

1 Comment

Thanks for trying but because the element is a DIV, it does not have a 'value' property

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.