]> git.ipfire.org Git - thirdparty/bootstrap.git/commitdiff
Create toast JS plugin, add unit tests.
authorJohann-S <johann.servoire@gmail.com>
Thu, 23 Aug 2018 16:31:25 +0000 (19:31 +0300)
committerXhmikosR <xhmikosr@gmail.com>
Tue, 13 Nov 2018 06:47:32 +0000 (08:47 +0200)
build/build-plugins.js
js/src/index.js
js/src/toast.js [new file with mode: 0644]
js/tests/index.html
js/tests/unit/.eslintrc.json
js/tests/unit/toast.js [new file with mode: 0644]
js/tests/visual/toast.html [new file with mode: 0644]
package.json
scss/_toasts.scss
scss/_variables.scss

index 1de65b426dc00a3ae56fd23d86cad56a08699dbe..ec337f03ee12e38070f5e895a6b278f2cde9c9b0 100644 (file)
@@ -33,6 +33,7 @@ const bsPlugins = {
   Popover: path.resolve(__dirname, '../js/src/popover.js'),
   ScrollSpy: path.resolve(__dirname, '../js/src/scrollspy.js'),
   Tab: path.resolve(__dirname, '../js/src/tab.js'),
+  Toast: path.resolve(__dirname, '../js/src/toast.js'),
   Tooltip: path.resolve(__dirname, '../js/src/tooltip.js'),
   Util: path.resolve(__dirname, '../js/src/util.js')
 }
index 580562907f1aa889fe73471839d883cfb00cd743..6d99ff3918f63c6a541a091fae43d8cba443972f 100644 (file)
@@ -8,6 +8,7 @@ import Modal from './modal'
 import Popover from './popover'
 import Scrollspy from './scrollspy'
 import Tab from './tab'
+import Toast from './toast'
 import Tooltip from './tooltip'
 import Util from './util'
 
@@ -46,5 +47,6 @@ export {
   Popover,
   Scrollspy,
   Tab,
+  Toast,
   Tooltip
 }
diff --git a/js/src/toast.js b/js/src/toast.js
new file mode 100644 (file)
index 0000000..cb6de97
--- /dev/null
@@ -0,0 +1,211 @@
+import $ from 'jquery'
+import Util from './util'
+
+/**
+ * --------------------------------------------------------------------------
+ * Bootstrap (v4.1.3): toast.js
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * --------------------------------------------------------------------------
+ */
+
+const Toast = (($) => {
+  /**
+   * ------------------------------------------------------------------------
+   * Constants
+   * ------------------------------------------------------------------------
+   */
+
+  const NAME               = 'toast'
+  const VERSION            = '4.1.3'
+  const DATA_KEY           = 'bs.toast'
+  const EVENT_KEY          = `.${DATA_KEY}`
+  const JQUERY_NO_CONFLICT = $.fn[NAME]
+
+  const Event = {
+    HIDE   : `hide${EVENT_KEY}`,
+    HIDDEN : `hidden${EVENT_KEY}`,
+    SHOW   : `show${EVENT_KEY}`,
+    SHOWN  : `shown${EVENT_KEY}`
+  }
+
+  const ClassName = {
+    FADE : 'fade',
+    HIDE : 'hide',
+    SHOW : 'show'
+  }
+
+  const DefaultType = {
+    animation : 'boolean',
+    autohide  : 'boolean',
+    delay     : '(number|object)'
+  }
+
+  const Default = {
+    animation : true,
+    autohide  : true,
+    delay     : {
+      show: 0,
+      hide: 500
+    }
+  }
+
+  /**
+   * ------------------------------------------------------------------------
+   * Class Definition
+   * ------------------------------------------------------------------------
+   */
+
+  class Toast {
+    constructor(element, config) {
+      this._element = element
+      this._config  = this._getConfig(config)
+      this._timeout = null
+    }
+
+    // Getters
+
+    static get VERSION() {
+      return VERSION
+    }
+
+    static get DefaultType() {
+      return DefaultType
+    }
+
+    // Public
+
+    show() {
+      $(this._element).trigger(Event.SHOW)
+
+      if (this._config.animation) {
+        this._element.classList.add(ClassName.FADE)
+      }
+
+      const complete = () => {
+        $(this._element).trigger(Event.SHOWN)
+
+        if (this._config.autohide) {
+          this.hide()
+        }
+      }
+
+      this._timeout = setTimeout(() => {
+        this._element.classList.add(ClassName.SHOW)
+
+        if (this._config.animation) {
+          const transitionDuration = Util.getTransitionDurationFromElement(this._element)
+
+          $(this._element)
+            .one(Util.TRANSITION_END, complete)
+            .emulateTransitionEnd(transitionDuration)
+        } else {
+          complete()
+        }
+      }, this._config.delay.show)
+    }
+
+    hide() {
+      if (!this._element.classList.contains(ClassName.SHOW)) {
+        return
+      }
+
+      $(this._element).trigger(Event.HIDE)
+
+      const complete = () => {
+        $(this._element).trigger(Event.HIDDEN)
+      }
+
+      this._timeout = setTimeout(() => {
+        this._element.classList.remove(ClassName.SHOW)
+
+        if (this._config.animation) {
+          const transitionDuration = Util.getTransitionDurationFromElement(this._element)
+
+          $(this._element)
+            .one(Util.TRANSITION_END, complete)
+            .emulateTransitionEnd(transitionDuration)
+        } else {
+          complete()
+        }
+      }, this._config.delay.hide)
+    }
+
+    dispose() {
+      clearTimeout(this._timeout)
+      this._timeout = null
+
+      if (this._element.classList.contains(ClassName.SHOW)) {
+        this._element.classList.remove(ClassName.SHOW)
+      }
+
+      $.removeData(this._element, DATA_KEY)
+      this._element = null
+      this._config  = null
+    }
+
+    // Private
+
+    _getConfig(config) {
+      config = {
+        ...Default,
+        ...$(this._element).data(),
+        ...typeof config === 'object' && config ? config : {}
+      }
+
+      if (typeof config.delay === 'number') {
+        config.delay = {
+          show: config.delay,
+          hide: config.delay
+        }
+      }
+
+      Util.typeCheckConfig(
+        NAME,
+        config,
+        this.constructor.DefaultType
+      )
+
+      return config
+    }
+
+    // Static
+
+    static _jQueryInterface(config) {
+      return this.each(function () {
+        const $element = $(this)
+        let data       = $element.data(DATA_KEY)
+        const _config  = typeof config === 'object' && config
+
+        if (!data) {
+          data = new Toast(this, _config)
+          $element.data(DATA_KEY, data)
+        }
+
+        if (typeof config === 'string') {
+          if (typeof data[config] === 'undefined') {
+            throw new TypeError(`No method named "${config}"`)
+          }
+
+          data[config](this)
+        }
+      })
+    }
+  }
+
+  /**
+   * ------------------------------------------------------------------------
+   * jQuery
+   * ------------------------------------------------------------------------
+   */
+
+  $.fn[NAME]             = Toast._jQueryInterface
+  $.fn[NAME].Constructor = Toast
+  $.fn[NAME].noConflict  = () => {
+    $.fn[NAME] = JQUERY_NO_CONFLICT
+    return Toast._jQueryInterface
+  }
+
+  return Toast
+})($)
+
+export default Toast
index 1bcdc5380e2ee5e83cb663625d30ff8207808b43..06bfa2c43439b01131f54e3543faae896a83bb29 100644 (file)
     <script src="../dist/tab.js"></script>
     <script src="../dist/tooltip.js"></script>
     <script src="../dist/popover.js"></script>
+    <script src="../dist/toast.js"></script>
 
     <!-- Unit Tests -->
     <script src="unit/alert.js"></script>
     <script src="unit/tooltip.js"></script>
     <script src="unit/popover.js"></script>
     <script src="unit/util.js"></script>
+    <script src="unit/toast.js"></script>
   </head>
   <body>
     <div id="qunit-container">
index a7fa64af0edfb8207cc0a22757c01bf78a4ab58b..7a3b99ead0fa6d9852814fe9b64757a47824c45c 100644 (file)
@@ -11,7 +11,8 @@
     "Alert": false,
     "Button": false,
     "Carousel": false,
-    "Simulator": false
+    "Simulator": false,
+    "Toast": false
   },
   "parserOptions": {
     "ecmaVersion": 5,
diff --git a/js/tests/unit/toast.js b/js/tests/unit/toast.js
new file mode 100644 (file)
index 0000000..873661c
--- /dev/null
@@ -0,0 +1,235 @@
+$(function () {
+  'use strict'
+
+  if (typeof bootstrap !== 'undefined') {
+    window.Toast = bootstrap.Toast
+  }
+
+  QUnit.module('toast plugin')
+
+  QUnit.test('should be defined on jquery object', function (assert) {
+    assert.expect(1)
+    assert.ok($(document.body).toast, 'toast method is defined')
+  })
+
+  QUnit.module('toast', {
+    beforeEach: function () {
+      // Run all tests in noConflict mode -- it's the only way to ensure that the plugin works in noConflict mode
+      $.fn.bootstrapToast = $.fn.toast.noConflict()
+    },
+    afterEach: function () {
+      $.fn.toast = $.fn.bootstrapToast
+      delete $.fn.bootstrapToast
+      $('#qunit-fixture').html('')
+    }
+  })
+
+  QUnit.test('should provide no conflict', function (assert) {
+    assert.expect(1)
+    assert.strictEqual(typeof $.fn.toast, 'undefined', 'toast was set back to undefined (org value)')
+  })
+
+  QUnit.test('should return the current version', function (assert) {
+    assert.expect(1)
+    assert.strictEqual(typeof Toast.VERSION, 'string')
+  })
+
+  QUnit.test('should throw explicit error on undefined method', function (assert) {
+    assert.expect(1)
+    var $el = $('<div/>')
+    $el.bootstrapToast()
+
+    try {
+      $el.bootstrapToast('noMethod')
+    } catch (err) {
+      assert.strictEqual(err.message, 'No method named "noMethod"')
+    }
+  })
+
+  QUnit.test('should return jquery collection containing the element', function (assert) {
+    assert.expect(2)
+
+    var $el = $('<div/>')
+    var $toast = $el.bootstrapToast()
+    assert.ok($toast instanceof $, 'returns jquery collection')
+    assert.strictEqual($toast[0], $el[0], 'collection contains element')
+  })
+
+  QUnit.test('should auto hide', function (assert) {
+    assert.expect(1)
+    var done = assert.async()
+
+    var toastHtml =
+      '<div class="toast" data-delay="1">' +
+        '<div class="toast-body">' +
+          'a simple toast' +
+        '</div>' +
+      '</div>'
+
+    var $toast = $(toastHtml)
+      .bootstrapToast()
+      .appendTo($('#qunit-fixture'))
+
+    $toast.on('hidden.bs.toast', function () {
+      assert.strictEqual($toast.hasClass('show'), false)
+      done()
+    })
+      .bootstrapToast('show')
+  })
+
+  QUnit.test('should not add fade class', function (assert) {
+    assert.expect(1)
+    var done = assert.async()
+
+    var toastHtml =
+      '<div class="toast" data-delay="1" data-animation="false">' +
+        '<div class="toast-body">' +
+          'a simple toast' +
+        '</div>' +
+      '</div>'
+
+    var $toast = $(toastHtml)
+      .bootstrapToast()
+      .appendTo($('#qunit-fixture'))
+
+    $toast.on('shown.bs.toast', function () {
+      assert.strictEqual($toast.hasClass('fade'), false)
+      done()
+    })
+      .bootstrapToast('show')
+  })
+
+  QUnit.test('should allow to hide toast manually', function (assert) {
+    assert.expect(1)
+    var done = assert.async()
+
+    var toastHtml =
+      '<div class="toast" data-delay="1" data-autohide="false">' +
+        '<div class="toast-body">' +
+          'a simple toast' +
+        '</div>' +
+      '</div>'
+
+    var $toast = $(toastHtml)
+      .bootstrapToast()
+      .appendTo($('#qunit-fixture'))
+
+    $toast
+      .on('shown.bs.toast', function () {
+        $toast.bootstrapToast('hide')
+      })
+      .on('hidden.bs.toast', function () {
+        assert.strictEqual($toast.hasClass('show'), false)
+        done()
+      })
+      .bootstrapToast('show')
+  })
+
+  QUnit.test('should do nothing when we call hide on a non shown toast', function (assert) {
+    assert.expect(1)
+
+    var $toast = $('<div />')
+      .bootstrapToast()
+      .appendTo($('#qunit-fixture'))
+
+    var spy = sinon.spy($toast[0].classList, 'contains')
+
+    $toast.bootstrapToast('hide')
+
+    assert.strictEqual(spy.called, true)
+  })
+
+  QUnit.test('should allow to destroy toast', function (assert) {
+    assert.expect(2)
+
+    var $toast = $('<div />')
+      .bootstrapToast()
+      .appendTo($('#qunit-fixture'))
+
+    assert.ok(typeof $toast.data('bs.toast') !== 'undefined')
+
+    $toast.bootstrapToast('dispose')
+
+    assert.ok(typeof $toast.data('bs.toast') === 'undefined')
+  })
+
+  QUnit.test('should allow to destroy toast and hide it before that', function (assert) {
+    assert.expect(4)
+    var done = assert.async()
+
+    var toastHtml =
+      '<div class="toast" data-delay="0" data-autohide="false">' +
+        '<div class="toast-body">' +
+          'a simple toast' +
+        '</div>' +
+      '</div>'
+
+    var $toast = $(toastHtml)
+      .bootstrapToast()
+      .appendTo($('#qunit-fixture'))
+
+    $toast.one('shown.bs.toast', function () {
+      setTimeout(function () {
+        assert.ok($toast.hasClass('show'))
+        assert.ok(typeof $toast.data('bs.toast') !== 'undefined')
+
+        $toast.bootstrapToast('dispose')
+
+        assert.ok(typeof $toast.data('bs.toast') === 'undefined')
+        assert.ok($toast.hasClass('show') === false)
+
+        done()
+      }, 1)
+    })
+      .bootstrapToast('show')
+  })
+
+  QUnit.test('should allow to pass delay object in html', function (assert) {
+    assert.expect(1)
+    var done = assert.async()
+
+    var toastHtml =
+      '<div class="toast" data-delay=\'{"show": 0, "hide": 1}\'>' +
+        '<div class="toast-body">' +
+          'a simple toast' +
+        '</div>' +
+      '</div>'
+
+    var $toast = $(toastHtml)
+      .bootstrapToast()
+      .appendTo($('#qunit-fixture'))
+
+    $toast.on('shown.bs.toast', function () {
+      assert.strictEqual($toast.hasClass('show'), true)
+      done()
+    })
+      .bootstrapToast('show')
+  })
+
+  QUnit.test('should allow to config in js', function (assert) {
+    assert.expect(1)
+    var done = assert.async()
+
+    var toastHtml =
+      '<div class="toast">' +
+        '<div class="toast-body">' +
+          'a simple toast' +
+        '</div>' +
+      '</div>'
+
+    var $toast = $(toastHtml)
+      .bootstrapToast({
+        delay: {
+          show: 0,
+          hide: 1
+        }
+      })
+      .appendTo($('#qunit-fixture'))
+
+    $toast.on('shown.bs.toast', function () {
+      assert.strictEqual($toast.hasClass('show'), true)
+      done()
+    })
+      .bootstrapToast('show')
+  })
+})
diff --git a/js/tests/visual/toast.html b/js/tests/visual/toast.html
new file mode 100644 (file)
index 0000000..0daf8b5
--- /dev/null
@@ -0,0 +1,69 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <link rel="stylesheet" href="../../../dist/css/bootstrap.min.css">
+    <title>Toast</title>
+    <style>
+      .notifications {
+        position: absolute;
+        top: 10px;
+        right: 10px;
+      }
+    </style>
+  </head>
+  <body>
+    <div class="container">
+      <h1>Toast <small>Bootstrap Visual Test</small></h1>
+
+      <div class="row mt-3">
+        <div class="col-md-12">
+          <button id="btnShowToast" class="btn btn-primary">Show toast</button>
+          <button id="btnHideToast" class="btn btn-primary">Hide toast</button>
+        </div>
+      </div>
+    </div>
+
+    <div class="notifications">
+      <div class="toast" data-delay='{"show": 0, "hide": 2000}'>
+        <div class="toast-header">
+          <img class="rounded mr-2" data-src="holder.js/20x20?size=1&text=.&bg=#007aff" alt="">
+          <strong class="mr-auto">Bootstrap</strong>
+          <small>11 mins ago</small>
+        </div>
+        <div class="toast-body">
+          Hello, world! This is a toast message with <strong>autohide</strong> in 2 seconds
+        </div>
+      </div>
+
+      <div class="toast" data-autohide="false">
+        <div class="toast-header">
+          <img class="rounded mr-2" data-src="holder.js/20x20?size=1&text=.&bg=#007aff" alt="">
+          <strong class="mr-auto">Bootstrap</strong>
+          <small class="text-muted">2 seconds ago</small>
+        </div>
+        <div class="toast-body">
+          Heads up, toasts will stack automatically
+        </div>
+      </div>
+    </div>
+
+    <script src="../../../site/docs/4.1/assets/js/vendor/jquery-slim.min.js"></script>
+    <script src="../../dist/util.js"></script>
+    <script src="../../dist/toast.js"></script>
+    <script>
+      $(function () {
+        $('.toast').toast()
+
+        $('#btnShowToast').on('click', function () {
+          $('.toast').toast('show')
+        })
+
+        $('#btnHideToast').on('click', function () {
+          $('.toast').toast('hide')
+        })
+      })
+    </script>
+  </body>
+</html>
index a91e04f9fdebb5e8473ebbf9a44730a155352714..9e3a7bbf0f07a8059927a78e33aca9809f18bd06 100644 (file)
     },
     {
       "path": "./dist/js/bootstrap.js",
-      "maxSize": "22 kB"
+      "maxSize": "23 kB"
     },
     {
       "path": "./dist/js/bootstrap.min.js",
index 248a212989469d1feb41da2ec2e7b5920f9f1edd..5ec9cab43ae6dca97350f0aaca872b3b5e04c6a8 100644 (file)
@@ -1,4 +1,5 @@
 .toast {
+  display: none;
   max-width: $toast-max-width;
   overflow: hidden; // cheap rounded corners on nested items
   font-size: $toast-font-size; // knock it down to 14px
   }
 }
 
+.toast.show {
+  display: inherit;
+}
+
 .toast-header {
   display: flex;
   align-items: center;
index ce958b3c42bc3ad7cf8991b30483baf5a0d12116..86d55c8f3b83db3a758ebf6f3ad12471207aad08 100644 (file)
@@ -866,7 +866,7 @@ $toast-padding-y: .25rem !default;
 $toast-font-size: .875rem !default;
 $toast-background-color: rgba($white, .85) !default;
 $toast-border-width: 1px !default;
-$toast-border-color: rgba(0,0,0,.1) !default;
+$toast-border-color: rgba(0, 0, 0, .1) !default;
 $toast-border-radius: .25rem !default;
 $toast-box-shadow: 0 .25rem .75rem rgba($black, .1) !default;