# Javascript Code Examples

# Chrome Extension

I built a simple Chrome Extension (local only) as a weekend development project.

I then wrote a blog post (opens new window) outlining my coding experience.

# Tech Stack

# Code

# Snackbar Component

import $ from 'jquery';
import Snackbar from 'node-snackbar';
import tailwindConfig from '@tailwindConfig';

let globalConfig = () => ({
  text: null,
  textColor: tailwindConfig.theme.colors.white,
  pos: 'bottom-right',
  customClass: 'c-toast',
  width: 'auto',
  showAction: false,
  actionText: 'Dismiss',
  actionTextColor: tailwindConfig.theme.colors.red['500'],
  backgroundColor: tailwindConfig.theme.colors.gray['800'],
  duration: 5000,
});

export let mixin = ({ node = null, config = {} } = {}) => {
  let state = {
    node,
    $node: $(node),
    config: Object.assign(globalConfig(), config),
  };

  let module = {
    onClick(event) {
      let $target = $(event.currentTarget);
      let data = $target.data();
      let settings = Object.assign({}, state.config, data);

      Snackbar.show(settings);
    },
    init() {
      state.$node.on('click', this.onClick.bind(this));
    },
  };

  return module;
};

export let snackbarStatic = (config = {}) => {
  let settings = Object.assign({}, globalConfig(), config);

  return Snackbar.show(settings);
};

export default {
  init({ selector, context, config = {} } = {}) {
    return window
      .jQuery(selector, context)
      .once('Snackbar')
      .map((index, node) => mixin({ node, config }).init());
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

# File Input Component

import { jsClasses } from '@/helpers/settings';

const FileInput = (($) => {
  class FileInput {
    constructor(node) {
      this.$node = $(node);
      this.$input = this.$node.find('[data-js="input"]').first();
      this.$text = this.$node.find('[data-js="text"]').first();
      this.maxTextLength = 25;
      this.activeClass = jsClasses.active;
    }

    getSanitizedFilenameFromPath(path) {
      const sanitizedPath = path.replace(/^.*\\/, '');

      return sanitizedPath.length > this.maxTextLength
        ? sanitizedPath.substring(0, this.maxTextLength - 3) + '...'
        : sanitizedPath;
    }

    onChange(event) {
      const value = $(event.currentTarget).val();
      const text = this.getSanitizedFilenameFromPath(value);

      this.$text.text(text);
      this.$node.toggleClass(this.activeClass, true);
    }

    setEventBindings() {
      this.$input.on('change', this.onChange.bind(this));
    }

    init() {
      this.setEventBindings();
    }
  }

  return {
    init({ selector, context }) {
      const $selector = $(selector);

      return $(selector, context)
        .once('FileInput')
        .map((index, node) => {
          const module = new FileInput(node);

          module.init();

          return module;
        });
    },
  };
})(window.jQuery);

export default Object.create(FileInput);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# Search Results Component

import { jsClasses } from '@/helpers/settings';
import debounce from 'lodash.debounce';

let SearchResults = (({ jQuery: $ }) => {
  class SearchResults {
    constructor(node) {
      this.$node = $(node);
      this.$input = this.$node.find('[data-js="input"]');
      this.$results = this.$node.find('[data-js="results"]');
      this.$resultGroup = this.$node.find('[data-js="result-group"]');
      this.$result = this.$node.find('[data-js="result"]');
      this.isBusy = false;
      this.xhr = null;
    }

    setSearchInputValue(value) {
      this.$input.val(value);

      return this;
    }

    toggleResultsVisibility() {
      this.$results.toggleClass(jsClasses.notActive, false);
    }

    getResults() {
      let query = this.$input.val();

      this.xhr = $.getJSON('/search_results/get_results', {
        query,
      })
        .done((markup) => {
          if (!markup) {
            markup = '<p>No Results Found</p>';
          }

          this.$results.html(markup);
        })
        .always(() => {
          this.$node.toggleClass(jsClasses.loading, false);
        });
    }

    onInput() {
      if (!this.$input.val()) return;

      if (this.xhr) {
        this.xhr.abort();
      }

      this.$node.toggleClass(jsClasses.loading, true);
      this.$results.html('');

      this.getResults();
    }

    setSubscriptions() {
      PubSub.subscribe('search-callout-input', (msg, data) => {
        this.setSearchInputValue(data.value);

        this.$input.trigger('input');

        this.getResults();
      });

      PubSub.subscribe('search-overlay-transition-end', (msg, data) => {
        if (!data.value) return;

        this.$input.focus();
      });
    }

    setEventBindings() {
      this.$input.one('input', this.toggleResultsVisibility.bind(this));
      this.$input.on('input', debounce(this.onInput.bind(this), 500));
    }

    init() {
      this.setEventBindings();
      this.setSubscriptions();
    }
  }

  return {
    init({ selector, context }) {
      const $selector = $(selector);

      return $(selector, context)
        .once('SearchResults')
        .map((index, node) => {
          const module = new SearchResults(node);

          module.init();

          return module;
        });
    },
  };
})(window);

export default Object.create(SearchResults);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

# Search Overlay Component

import PubSub from 'pubsub-js';
import '@/libraries/jquery/jquery.scrollLock';
import bodyLockState from '@/modules/body-lock-state';
import { jsClasses } from '@/helpers/settings';
import { white } from 'ansi-colors';

const SearchOverlay = (({ jQuery: $ }, document) => {
  class SearchOverlay {
    constructor(node) {
      this.$node = $(node);
      this.$toggleBtn = $('[data-js="search-overlay-toggle"]');
      this.$navMobile = $('[data-js="nav-mobile"]');
      this.$container = $('[data-js="search-overlay-content"]');
      this.keycodeLookup = {
        s: {
          keycode: 83,
          fn: this.onSKeyPressed.bind(this),
        },
        esc: {
          keycode: 27,
          fn: this.onEscKeyPressed.bind(this),
        },
      };
    }

    toggleWindowLock(shouldToggle) {
      bodyLockState.set('search', shouldToggle);

      return this;
    }

    toggle(bool) {
      let isActive = this.$node.hasClass(jsClasses.active);
      let shouldToggle = typeof bool === 'boolean' ? bool : !isActive;

      this.$node.one('transitionend', () => {
        PubSub.publish('search-overlay-transition-end', {
          value: shouldToggle,
        });
      });

      this.$node.toggleClass(jsClasses.active, shouldToggle);

      this.toggleWindowLock(shouldToggle);
    }

    onNodeClick(event) {
      if (
        event.target != this.$container.get(0) &&
        this.$container.find(event.target).length == 0
      ) {
        this.toggle(false);
      }
    }

    onSKeyPressed(event) {
      this.toggle(true);
    }

    onEscKeyPressed(event) {
      let isActive = this.$node.hasClass(jsClasses.active);

      if (!isActive) {
        return;
      }

      this.toggle(false);
    }

    onDocumentKeyPress(event) {
      let keycode = event.keyCode ? event.keyCode : event.which;

      let whitelistedKey = Object.keys(this.keycodeLookup).filter(
        (key) => this.keycodeLookup[key].keycode === keycode
      );

      if (!whitelistedKey || !whitelistedKey.length) {
        return;
      }

      let $target = $(event.target);
      let blacklist = ['input', 'textarea'];

      if (blacklist.some((nodeType) => $target.is(nodeType))) {
        return;
      }

      this.keycodeLookup[whitelistedKey].fn(event);
    }

    setEventBindings() {
      this.$toggleBtn.on('click', this.toggle.bind(this));
      this.$node.on('click', this.onNodeClick.bind(this));
      $(document).on('keydown', this.onDocumentKeyPress.bind(this));
    }

    setSubscriptions() {
      PubSub.subscribe('toggle-overlay', (msg, data) => {
        this.toggle(data.value);
      });
    }

    init() {
      this.setEventBindings();
      this.setSubscriptions();
    }
  }

  return {
    init({ selector, context }) {
      return $(selector, context)
        .once('SearchOverlay')
        .map((index, node) => {
          const module = new SearchOverlay(node);

          module.init();

          return module;
        });
    },
  };
})(window, document);

export default Object.create(SearchOverlay);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124