Sunday, 30 May 2021

Instantiate ES6 class

I am trying to implement the following tags with my design.

I am using the class Tags to simply create tags within my input field, however when I initialize the library I get an error.

const ACTIVE_CLASS = "bg-light";
const VALUE_ATTRIBUTE = "data-value";

class Tags {
  /**
   * @param {HTMLSelectElement} selectElement
   */
  constructor(selectElement) {
    this.selectElement = selectElement;
    this.selectElement.style.display = "none";
    this.placeholder = this.getPlaceholder();
    this.allowNew = selectElement.dataset.allowNew ? true : false;

    // Create elements
    this.holderElement = document.createElement("div");
    this.containerElement = document.createElement("div");
    this.dropElement = document.createElement("ul");
    this.searchInput = document.createElement("input");

    this.holderElement.appendChild(this.containerElement);
    this.containerElement.appendChild(this.searchInput);
    this.holderElement.appendChild(this.dropElement);
    // insert after
    this.selectElement.parentNode.insertBefore(this.holderElement, this.selectElement.nextSibling);

    // Configure them
    this.configureSearchInput();
    this.configureHolderElement();
    this.configureDropElement();
    this.configureContainerElement();
    this.buildSuggestions();
  }

  /**
   * Attach to all elements matched by the selector
   * @param {string} selector
   */
  static init(selector = "select[multiple]") {
    let list = document.querySelectorAll(selector);
    for (let i = 0; i < list.length; i++) {
      let el = list[i];
      let inst = new Tags(el);
    }
  }

  /**
   * @returns {string}
   */
  getPlaceholder() {
    let firstOption = this.selectElement.querySelector("option");
    if (!firstOption) {
      return;
    }
    if (!firstOption.value) {
      let placeholder = firstOption.innerText;
      firstOption.remove();
      return placeholder;
    }
    if (this.selectElement.getAttribute("placeholder")) {
      return this.selectElement.getAttribute("placeholder");
    }
    if (this.selectElement.getAttribute("data-placeholder")) {
      return this.selectElement.getAttribute("data-placeholder");
    }
    return "";
  }

  configureDropElement() {
    this.dropElement.classList.add("dropdown-menu");
  }

  configureHolderElement() {
    this.holderElement.classList.add("form-control");
    this.holderElement.classList.add("dropdown");
  }

  configureContainerElement() {
    this.containerElement.addEventListener("click", (event) => {
      this.searchInput.focus();
    });

    // add initial values
    let initialValues = this.selectElement.querySelectorAll("option[selected]");
    for (let j = 0; j < initialValues.length; j++) {
      let initialValue = initialValues[j];
      if (!initialValue.value) {
        continue;
      }
      this.addItem(initialValue.innerText, initialValue.value);
    }
  }

  configureSearchInput() {
    this.searchInput.type = "text";
    this.searchInput.autocomplete = false;
    this.searchInput.style.border = 0;
    this.searchInput.style.outline = 0;
    this.searchInput.style.maxWidth = "100%";

    this.adjustWidth();

    this.searchInput.addEventListener("input", (event) => {
      this.adjustWidth();
      if (this.searchInput.value.length >= 1) {
        this.showSuggestions();
      } else {
        this.hideSuggestions();
      }
    });
    // keypress doesn't send arrow keys
    this.searchInput.addEventListener("keydown", (event) => {
      if (event.code == "Enter") {
        let selection = this.getActiveSelection();
        if (selection) {
          this.addItem(selection.innerText, selection.getAttribute(VALUE_ATTRIBUTE));
          this.resetSearchInput();
          this.hideSuggestions();
        } else {
          // We use what is typed
          if (this.allowNew) {
            this.addItem(this.searchInput.value);
            this.resetSearchInput();
            this.hideSuggestions();
          }
        }
        event.preventDefault();
        return;
      }
      if (event.code == "ArrowUp") {
        this.moveSelectionUp();
      }
      if (event.code == "ArrowDown") {
        this.moveSelectionDown();
      }
      if (event.code == "Backspace") {
        if (this.searchInput.value.length == 0) {
          this.removeLastItem();
          this.adjustWidth();
        }
      }
    });
  }

  moveSelectionUp() {
    let active = this.getActiveSelection();
    if (active) {
      let prev = active.parentNode;
      do {
        prev = prev.previousSibling;
      } while (prev && prev.style.display == "none");
      if (!prev) {
        return;
      }
      active.classList.remove(ACTIVE_CLASS);
      prev.querySelector("a").classList.add(ACTIVE_CLASS);
    }
  }

  moveSelectionDown() {
    let active = this.getActiveSelection();
    if (active) {
      let next = active.parentNode;
      do {
        next = next.nextSibling;
      } while (next && next.style.display == "none");
      if (!next) {
        return;
      }
      active.classList.remove(ACTIVE_CLASS);
      next.querySelector("a").classList.add(ACTIVE_CLASS);
    }
  }

  /**
   * Adjust the field to fit its content
   */
  adjustWidth() {
    if (this.searchInput.value) {
      this.searchInput.size = this.searchInput.value.length + 1;
    } else {
      // Show the placeholder only if empty
      if (this.getSelectedValues().length) {
        this.searchInput.placeholder = "";
        this.searchInput.size = 1;
      } else {
        this.searchInput.size = this.placeholder.length;
        this.searchInput.placeholder = this.placeholder;
      }
    }
  }

  /**
   * Add suggestions from element
   */
  buildSuggestions() {
    let options = this.selectElement.querySelectorAll("option");
    for (let i = 0; i < options.length; i++) {
      let opt = options[i];
      if (!opt.getAttribute("value")) {
        continue;
      }
      let newChild = document.createElement("li");
      let newChildLink = document.createElement("a");
      newChild.append(newChildLink);
      newChildLink.classList.add("dropdown-item");
      newChildLink.setAttribute(VALUE_ATTRIBUTE, opt.getAttribute("value"));
      newChildLink.setAttribute("href", "#");
      newChildLink.innerText = opt.innerText;
      this.dropElement.appendChild(newChild);

      // Hover sets active item
      newChildLink.addEventListener("mouseenter", (event) => {
        this.removeActiveSelection();
        newChild.querySelector("a").classList.add(ACTIVE_CLASS);
      });

      newChildLink.addEventListener("click", (event) => {
        event.preventDefault();
        this.addItem(newChildLink.innerText, newChildLink.getAttribute(VALUE_ATTRIBUTE));
        this.resetSearchInput();
        this.hideSuggestions();
      });
    }
  }

  resetSearchInput() {
    this.searchInput.value = "";
    this.adjustWidth();
  }

  /**
   * @returns {array}
   */
  getSelectedValues() {
    let selected = this.selectElement.querySelectorAll("option:checked");
    return Array.from(selected).map((el) => el.value);
  }

  /**
   * The element create with buildSuggestions
   */
  showSuggestions() {
    if (!this.dropElement.classList.contains("show")) {
      this.dropElement.classList.add("show");
    }

    // Position next to search input
    this.dropElement.style.left = this.searchInput.offsetLeft + "px";

    // Get search value
    let search = this.searchInput.value.toLocaleLowerCase();

    // Get current values
    let values = this.getSelectedValues();

    // Filter the list according to search string
    let list = this.dropElement.querySelectorAll("li");
    let found = false;
    let firstItem = null;
    for (let i = 0; i < list.length; i++) {
      let item = list[i];
      let text = item.innerText.toLocaleLowerCase();
      let link = item.querySelector("a");

      // Remove previous selection
      link.classList.remove(ACTIVE_CLASS);

      // Hide selected values
      if (values.indexOf(link.getAttribute(VALUE_ATTRIBUTE)) != -1) {
        item.style.display = "none";
        continue;
      }

      if (text.indexOf(search) !== -1) {
        item.style.display = "list-item";
        found = true;
        if (!firstItem) {
          firstItem = item;
        }
      } else {
        item.style.display = "none";
      }
    }

    // Special case if nothing matches
    if (!found) {
      this.dropElement.classList.remove("show");
    }

    // Always select first item
    if (firstItem) {
      if (this.holderElement.classList.contains("is-invalid")) {
        this.holderElement.classList.remove("is-invalid");
      }
      firstItem.querySelector("a").classList.add(ACTIVE_CLASS);
    } else {
      // No item and we don't allow new items => error
      if (!this.allowNew) {
        this.holderElement.classList.add("is-invalid");
      }
    }
  }

  /**
   * The element create with buildSuggestions
   */
  hideSuggestions(dropEl) {
    if (this.dropElement.classList.contains("show")) {
      this.dropElement.classList.remove("show");
    }
    if (this.holderElement.classList.contains("is-invalid")) {
      this.holderElement.classList.remove("is-invalid");
    }
  }

  /**
   * @returns {HTMLElement}
   */
  getActiveSelection() {
    return this.dropElement.querySelector("a." + ACTIVE_CLASS);
  }

  removeActiveSelection() {
    let selection = this.getActiveSelection();
    if (selection) {
      selection.classList.remove(ACTIVE_CLASS);
    }
  }

  removeLastItem() {
    let items = this.containerElement.querySelectorAll("span");
    if (!items.length) {
      return;
    }
    let lastItem = items[items.length - 1];
    this.removeItem(lastItem.getAttribute(VALUE_ATTRIBUTE));
  }

  /**
   * @param {string} text
   * @param {string} value
   */
  addItem(text, value) {
    if (!value) {
      value = text;
    }
    let span = document.createElement("span");
    span.classList.add("badge");
    span.classList.add("bg-primary");
    span.classList.add("me-2");
    span.setAttribute(VALUE_ATTRIBUTE, value);
    span.innerText = text;
    this.containerElement.insertBefore(span, this.searchInput);

    // update select
    let opt = this.selectElement.querySelector('option[value="' + value + '"]');
    if (opt) {
      opt.setAttribute("selected", "selected");
    } else {
      // we need to create a new option
      opt = document.createElement("option");
      opt.value = value;
      opt.innerText = text;
      opt.setAttribute("selected", "selected");
      this.selectElement.appendChild(opt);
    }
  }

  /**
   * @param {string} value
   */
  removeItem(value) {
    let item = this.containerElement.querySelector("span[" + VALUE_ATTRIBUTE + '="' + value + '"]');
    if (!item) {
      return;
    }
    item.remove();

    // update select
    let opt = this.selectElement.querySelector('option[value="' + value + '"]');
    if (opt) {
      opt.removeAttribute("selected");
    }
  }
}

export default Tags;

import Tags
Tags.init();
<!DOCTYPE html>
<html lang="en">

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta name="description" content="">
  <meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
  <meta name="generator" content="Hugo 0.80.0">
  <title>Insider</title>

  <link rel="canonical" href="">

  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.0/font/bootstrap-icons.css">

  <!-- Favicons -->
  <link rel="apple-touch-icon" href="https://getbootstrap.com/docs/5.0/assets/img/favicons/apple-touch-icon.png" sizes="180x180">
  <link rel="icon" href="https://getbootstrap.com/docs/5.0/assets/img/favicons/favicon-32x32.png" sizes="32x32" type="image/png">
  <link rel="icon" href="https://getbootstrap.com/docs/5.0/assets/img/favicons/favicon-16x16.png" sizes="16x16" type="image/png">
  <link rel="manifest" href="https://getbootstrap.com/docs/5.0/assets/img/favicons/manifest.json">
  <link rel="mask-icon" href="https://getbootstrap.com/docs/5.0/assets/img/favicons/safari-pinned-tab.svg" color="#7952b3">
  <link rel="icon" href="https://getbootstrap.com/docs/5.0/assets/img/favicons/favicon.ico">
  <meta name="theme-color" content="#7952b3">

  <!-- Custom styles for this template -->
  <link href="https://unpkg.com/bootstrap-table@1.18.3/dist/bootstrap-table.min.css" rel="stylesheet">
</head>

<body>
  <header class="p-3 bg-dark text-white">
    <div class="container">
      <div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
        <ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
          <li>
            <button class="btn btn-outline-light" type="button" data-bs-toggle="modal" data-bs-target="#exampleModal">Check
                    </button>
          </li>
        </ul>
      </div>
    </div>
  </header>

  <main class="container pt-3">
    <div class="row mt-3">
      <h2>Content</h2>
    </div>
  </main>

  <!-- Modal -->
  <div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-lg">
      <div class="modal-content">
        <div class="modal-header">
          <h5 class="modal-title" id="exampleModalLabel">Your Check</h5>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        </div>
        <div class="modal-body">
          <br>
          <p class="h2">Check</p>
          <p>Input your symbols and we will send you all relevant information.</p>
          <br>
          <form>
            <div class="col">
              <select class="form-select" id="validationTags" multiple="" data-allow-new="true" style="display: none;">
                <option value="1" selected="selected">JavaScript</option>
                <option value="2">HTML5</option>
                <option value="3">CSS3</option>
                <option value="4">jQuery</option>
                <option value="5">React</option>
                <option value="6">Angular</option>
                <option value="7">Vue</option>
                <option value="8">Python</option>
              </select>
              <div class="form-control dropdown">
                <div><span class="badge bg-primary me-2" data-value="1">JavaScript</span><input type="text" autocomplete="false" placeholder="" size="1" style="border: 0px; outline: 0px; max-width: 100%;">
                </div>
                <ul class="dropdown-menu">
                  <li><a class="dropdown-item" data-value="1" href="#">JavaScript</a></li>
                  <li><a class="dropdown-item" data-value="2" href="#">HTML5</a></li>
                  <li><a class="dropdown-item" data-value="3" href="#">CSS3</a></li>
                  <li><a class="dropdown-item" data-value="4" href="#">jQuery</a></li>
                  <li><a class="dropdown-item" data-value="5" href="#">React</a></li>
                  <li><a class="dropdown-item" data-value="6" href="#">Angular</a></li>
                  <li><a class="dropdown-item" data-value="7" href="#">Vue</a></li>
                  <li><a class="dropdown-item" data-value="8" href="#">Python</a></li>
                </ul>
              </div>
              <div class="invalid-feedback">Please select a valid tag.</div>
            </div>
            <button type="submit" class="btn btn-primary">Submit</button>
          </form>
        </div>
      </div>
    </div>
  </div>
  <!-- Modal END -->

  <footer class="text-muted py-5">
    <div class="container">
      <p class="mb-1">Footer</p>
    </div>
  </footer>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/js/bootstrap.bundle.min.js" integrity="sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0" crossorigin="anonymous"></script>

</body>

</html>

As you can see the script does not work.

To be honest I am not quite sure why. I am guessing there is a problem when initializing the Tags-class. I currently do it the following:

    import Tags
    Tags.init();

Any suggestions what is wrong or how to correctly call the init()-function from the Tags-class?

I appreciate your replies!



from Instantiate ES6 class

No comments:

Post a Comment