b30wulffz
b30wulffz

Reputation: 161

How to make a menu in React WebChat by Microsoft Bot Framework?

React webchat provides a button to upload attachments.

enter image description here

Instead of using the default attachment button, I trying to replace it by a button which will toggle the menu. This menu will provide a variety of filetype which a user can send, similar to the one on the image below.

enter image description here

But I am unable to figure out a way to modify the functioning of attachment button or even change it.

Please help.

Upvotes: 0

Views: 703

Answers (1)

Steven Kanberg
Steven Kanberg

Reputation: 6383

Technically, this is doable, but it comes with several caveats. First and foremost, you are playing with the DOM which is strongly discouraged in a React environment. Second, there is a lot happening here to make this work which really opens you up to errors in the future should some component, feature, or element change under the hood. A number of elements are utilized here however they don't have proper id's or they rely on generated class names. In order to isolate certain elements, you will need to locate them by assigned name or role...which is tenuous. (I can tell you right now that there are already plans to redesign the sendBox to accommodate upcoming functionality.)

Before I get into the details, if you really want to do this right, I would recommend you clone the BotFramework-WebChat repo and make your own changes under the hood. Then you won't be playing with the DOM directly and, if done right, will produce a more stable environment. It would be incumbent on you to keep your version synced with the official version. Either route you go, you will have upkeep to perform on your code to keep things current and working.


This should get you going in the direction you want. Be aware that this is focused largely on the Web Chat code. I did not build real functionality beyond that. Even the first "functional" button (see below), while allowing you to select a file, only sends the file name in an activity. Getting and sending a file and how your bot responds to that activity is beyond the scope of your ask and will not be touched on.

Helper function - Used to simplify creating the different elements to be used.

const createElement = ( elType, id, className, option ) => {
  const el = document.createElement( elType );
  switch ( el.localName ) {
  case 'button':
    if ( id ) {
      el.setAttribute( 'id', id );
      el.setAttribute( 'type', 'button' );
    }
    if ( className ) {
      el.classList.add( className );
    }
    return el;
  case 'div':
    if ( id ) {
      el.setAttribute( 'id', id );
    }
    if (className) {
      el.classList.add( className );
    }
    if (option) {
      const style = option.style;

      el.setAttribute( 'style', style );
    }
    return el;
  case 'img':
    if ( className ) {
      el.classList.add( className );
    }
    if ( option ) {
      const src = option;
      el.src = src;
    }
    return el;
  case 'input':
    if ( id ) {
      el.setAttribute( 'id', id );
    }
    if ( className ) {
      el.className = className;
    }
    if ( option ) {
      const inputName = option.name;
      const inputType = option.type;
      const style = option.style;

      el.setAttribute( 'name', inputName );
      el.setAttribute( 'type', inputType );
      el.setAttribute( 'style', style );
    }
    return el;
  case 'p':
    if ( option ) {
      const innerText = option;
      el.innerText = innerText;
    }
    return el;
  }
};

Get sendBox - Gets the sendBox and locates the file attachment button.

const parent = document.querySelector( '.main' );
const attachmentButton = document.querySelector( '[title="Upload file"]' );
if ( attachmentButton ) {
  var attachmentSVG = attachmentButton.firstElementChild;
  var attachmentParent = attachmentButton.parentElement;
}
const child = parent.querySelectorAll( 'svg' );

Create menu and "buttons" (inputs) - Note: The first button, documentButton, is functional in that it allows you to select a file. The other five buttons are not functional which is why they are designed slightly different than the first. As noted above, selecting the file only sends the file name in this example.

// Creates containers to hold menu and buttons
const menuContainer = createElement( 'div', 'menuContainer', undefined );
menuContainer.hidden = true;
const firstRow = createElement( 'div', 'firstRow' );
const secondRow = createElement( 'div', 'secondRow' );

const documentButton = createElement( 'div', 'documentBtn', 'menuItemSize', { style: 'display: none' } );
const inputFile = createElement( 'input', 'docFile', 'menuItemSize', { name: 'docFile', type: 'file', style: 'display: none' } );
const docImage = createElement( 'img', undefined, 'menuItemSize', './images/menuDoc.png' );
documentButton.appendChild( inputFile );
documentButton.appendChild( docImage );
const docLabel = createElement( 'p', undefined, undefined, 'Document' );
const docWrapper = createElement( 'div', undefined, 'menuItemWrapper' );
docWrapper.appendChild( documentButton );
docWrapper.appendChild( docImage );
docWrapper.appendChild( docLabel );
firstRow.appendChild( docWrapper );
menuContainer.appendChild( firstRow );

// Enables button, allows file selection, and "sends" file via postBack.
docImage.onclick = () => { inputFile.click() };
inputFile.onchange = ( e ) => { handleChange( e ) };

const handleChange = ( e ) => { 
  const file = e.target.files[0];
  store.dispatch({
    type: 'WEB_CHAT/SEND_POST_BACK',
    payload: { 
      value: { file: file.name }
    }
  })
};

const cameraButton = createElement( 'div', 'cameraBtn', 'menuItemSize' );
const cameraImage = createElement( 'img', undefined, 'menuItemSize', './images/menuCamera.png' );
cameraButton.appendChild( cameraImage );
const cameraLabel = createElement( 'p', undefined, undefined, 'Camera' );
const cameraWrapper = createElement( 'div', undefined, 'menuItemWrapper' );
cameraWrapper.appendChild( cameraButton );
cameraWrapper.appendChild( cameraLabel );
firstRow.appendChild( cameraWrapper );
menuContainer.appendChild( firstRow );

const galleryButton = createElement( 'div', 'galleryBtn', 'menuItemSize' );
const galleryImage = createElement( 'img', undefined, 'menuItemSize', './images/menuGallery.png' );
galleryButton.appendChild( galleryImage);
const galleryLabel = createElement( 'p', undefined, undefined, 'Gallery' );
const galleryWrapper = createElement( 'div', undefined, 'menuItemWrapper' );
galleryWrapper.appendChild( galleryButton );
galleryWrapper.appendChild( galleryLabel );
firstRow.appendChild( galleryWrapper );
menuContainer.appendChild( firstRow );

const audioButton = createElement( 'div', 'audioBtn', 'menuItemSize' );
const audioImage = createElement( 'img', undefined, 'menuItemSize', './images/menuAudio.png' );
audioButton.appendChild( audioImage );
const audioLabel = createElement( 'p', undefined, undefined, 'Audio' );
const audioWrapper = createElement( 'div', undefined, 'menuItemWrapper' );
audioWrapper.appendChild( audioButton );
audioWrapper.appendChild( audioLabel );
secondRow.appendChild( audioWrapper );
menuContainer.appendChild( secondRow );

const locationButton = createElement( 'div', 'locationBtn', 'menuItemSize' );
const locationImage = createElement( 'img', undefined, 'menuItemSize', './images/menuLocation.png' );
locationButton.appendChild( locationImage );
const locationLabel = createElement( 'p', undefined, undefined, 'Location' );
const locationWrapper = createElement( 'div', undefined, 'menuItemWrapper' );
locationWrapper.appendChild( locationButton );
locationWrapper.appendChild( locationLabel );
secondRow.appendChild( locationWrapper );
menuContainer.appendChild( secondRow );

const contactButton = createElement( 'div', 'contactBtn', 'menuItemSize' );
const contactImage = createElement( 'img', undefined, 'menuItemSize', './images/menuContact.png' );
contactButton.appendChild( contactImage );
const contactLabel = createElement( 'p', undefined, undefined, 'Contact' );
const contactWrapper = createElement( 'div', undefined, 'menuItemWrapper' );
contactWrapper.appendChild( contactButton );
contactWrapper.appendChild( contactLabel );
secondRow.appendChild( contactWrapper );
menuContainer.appendChild( secondRow );

let transcriptWindow = document.querySelector( '[dir="ltr"]' );
let fosterParent = createElement( 'div' );
let menuButtonContainer = createElement( 'div', 'menuButtonContainer' );
let menuButton = createElement( 'div', 'menuButton' );
transcriptWindow.appendChild( menuContainer );

Button functionality

// Recreates file attachment button that, when clicked, opens the menu
menuButton.appendChild( attachmentSVG );
menuButtonContainer.appendChild( menuButton );
fosterParent.appendChild( menuButtonContainer );
const buttonDiv = createElement( 'div' );
buttonDiv.classList = attachmentButton.classList
buttonDiv.setAttribute( 'title', 'Upload file' );
buttonDiv.classList.remove( 'webchat__icon-button' );
attachmentButton.remove();
attachmentParent.appendChild( buttonDiv );

buttonDiv.innerHTML = fosterParent.innerHTML;

// Gets elements for use with event listeners
const menuBtnContainer = document.querySelector( '#menuButtonContainer' );
const menuItems = document.querySelectorAll( '.menuItemSize' );
const menuItemButtons = [];

menuItems.forEach(item => {
  if ( item.localName === 'div' ) {
    menuItemButtons.push( item )
  }
});

// Shows/hides menu on file attachment button click
const menu = document.querySelector( '#menuContainer' );
menuBtnContainer.addEventListener('click', ( e ) => {
  e.preventDefault();
  switch ( menu.hidden ) {
  case false:
    menu.hidden = true;
    break;
  case true:
    menu.hidden = false;
    break;
  }
  return false;
});

// Hides menu when menu button is clicked
menuItemButtons.map(value => {
  value.addEventListener('click', () => {
    switch ( value.id ) {
    case 'documentBtn':
      menu.hidden = true;
      break;
    }
    return false;
  })
});

Hope of help!

enter image description here

Upvotes: 2

Related Questions