diff --git a/Gemfile b/Gemfile index c88697a51471..d39964ce833b 100644 --- a/Gemfile +++ b/Gemfile @@ -5,4 +5,5 @@ group :development, :test do gem 'jekyll-redirect-from', '~> 0.14.0' gem 'jekyll-sitemap', '~> 1.2.0' gem 'jekyll-toc', '~> 0.9.0' + gem 'wdm', '~> 0.1.1', :install_if => Gem.win_platform? end diff --git a/Gemfile.lock b/Gemfile.lock index 1194eaec4a63..bd2a8a412f6c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -65,6 +65,7 @@ GEM sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) + wdm (0.1.1) PLATFORMS ruby @@ -75,6 +76,7 @@ DEPENDENCIES jekyll-redirect-from (~> 0.14.0) jekyll-sitemap (~> 1.2.0) jekyll-toc (~> 0.9.0) + wdm (~> 0.1.1) BUNDLED WITH 1.17.1 diff --git a/package.json b/package.json index 29665aa4ae19..a91e04f9fdeb 100644 --- a/package.json +++ b/package.json @@ -178,7 +178,7 @@ }, { "path": "./dist/css/bootstrap.min.css", - "maxSize": "22 kB" + "maxSize": "22.5 kB" }, { "path": "./dist/js/bootstrap.bundle.js", diff --git a/scss/_spinners.scss b/scss/_spinners.scss new file mode 100644 index 000000000000..edf70d9fbe2e --- /dev/null +++ b/scss/_spinners.scss @@ -0,0 +1,63 @@ +// +// Rotating border +// + +@keyframes spinner-border { + to { transform: rotate(360deg); } +} + +.spinner-border { + position: relative; + display: inline-block; + width: $spinner-width; + height: $spinner-height; + overflow: hidden; + text-indent: -999em; + vertical-align: text-bottom; + border: $spinner-border-width solid; + border-color: currentColor transparent currentColor currentColor; + border-radius: 50%; + animation: spinner-border .75s linear infinite; +} + +.spinner-border-sm { + width: $spinner-width-sm; + height: $spinner-height-sm; + border-width: $spinner-border-width-sm; +} + +// +// Growing circle +// + +@keyframes spinner-grow { + 0% { + opacity: 0; + transform: scale(0); + } + 50% { + opacity: 1; + } + 100% { + opacity: 0; + transform: scale(1); + } +} + +.spinner-grow { + position: relative; + display: inline-block; + width: $spinner-width; + height: $spinner-height; + overflow: hidden; + text-indent: -999em; + vertical-align: text-bottom; + background-color: currentColor; + border-radius: 50%; + animation: spinner-grow .75s linear infinite; +} + +.spinner-grow-sm { + width: $spinner-width-sm; + height: $spinner-height-sm; +} diff --git a/scss/_variables.scss b/scss/_variables.scss index dac260c19708..c636860bdadc 100644 --- a/scss/_variables.scss +++ b/scss/_variables.scss @@ -1024,6 +1024,17 @@ $carousel-transition-duration: .6s !default; $carousel-transition: transform $carousel-transition-duration ease-in-out !default; // Define transform transition first if using multiple transitions (e.g., `transform 2s ease, opacity .5s ease-out`) +// Spinners + +$spinner-width: 2rem !default; +$spinner-height: $spinner-width !default; +$spinner-border-width: .25em !default; + +$spinner-width-sm: 1rem !default; +$spinner-height-sm: $spinner-width-sm !default; +$spinner-border-width-sm: .2em !default; + + // Close $close-font-size: $font-size-base * 1.5 !default; diff --git a/scss/bootstrap.scss b/scss/bootstrap.scss index e3f76546b24a..6f7e4eef15bf 100644 --- a/scss/bootstrap.scss +++ b/scss/bootstrap.scss @@ -38,5 +38,6 @@ @import "tooltip"; @import "popover"; @import "carousel"; +@import "spinners"; @import "utilities"; @import "print"; diff --git a/site/_data/nav.yml b/site/_data/nav.yml index 14002d395155..cb0defd890f4 100644 --- a/site/_data/nav.yml +++ b/site/_data/nav.yml @@ -49,6 +49,7 @@ - title: Popovers - title: Progress - title: Scrollspy + - title: Spinners - title: Tooltips - title: Utilities diff --git a/site/docs/4.1/assets/js/vendor/clipboard.min.js b/site/docs/4.1/assets/js/vendor/clipboard.min.js index b00ee5153525..d3fe2aee574c 100644 --- a/site/docs/4.1/assets/js/vendor/clipboard.min.js +++ b/site/docs/4.1/assets/js/vendor/clipboard.min.js @@ -1,7 +1,7 @@ /*! - * clipboard.js v2.0.0 + * clipboard.js v2.0.3 * https://zenorocha.github.io/clipboard.js * * Licensed MIT © Zeno Rocha */ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return function(t){function e(o){if(n[o])return n[o].exports;var r=n[o]={i:o,l:!1,exports:{}};return t[o].call(r.exports,r,r.exports,e),r.l=!0,r.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,o){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:o})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=3)}([function(t,e,n){var o,r,i;!function(a,c){r=[t,n(7)],o=c,void 0!==(i="function"==typeof o?o.apply(e,r):o)&&(t.exports=i)}(0,function(t,e){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var o=function(t){return t&&t.__esModule?t:{default:t}}(e),r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=function(){function t(t,e){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};this.action=t.action,this.container=t.container,this.emitter=t.emitter,this.target=t.target,this.text=t.text,this.trigger=t.trigger,this.selectedText=""}},{key:"initSelection",value:function(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function(){var t=this,e="rtl"==document.documentElement.getAttribute("dir");this.removeFake(),this.fakeHandlerCallback=function(){return t.removeFake()},this.fakeHandler=this.container.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[e?"right":"left"]="-9999px";var n=window.pageYOffset||document.documentElement.scrollTop;this.fakeElem.style.top=n+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,this.container.appendChild(this.fakeElem),this.selectedText=(0,o.default)(this.fakeElem),this.copyText()}},{key:"removeFake",value:function(){this.fakeHandler&&(this.container.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(this.container.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function(){this.selectedText=(0,o.default)(this.target),this.copyText()}},{key:"copyText",value:function(){var t=void 0;try{t=document.execCommand(this.action)}catch(e){t=!1}this.handleResult(t)}},{key:"handleResult",value:function(t){this.emitter.emit(t?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function(){this.trigger&&this.trigger.focus(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function(){this.removeFake()}},{key:"action",set:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"copy";if(this._action=t,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function(){return this._action}},{key:"target",set:function(t){if(void 0!==t){if(!t||"object"!==(void 0===t?"undefined":r(t))||1!==t.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&t.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(t.hasAttribute("readonly")||t.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=t}},get:function(){return this._target}}]),t}();t.exports=a})},function(t,e,n){function o(t,e,n){if(!t&&!e&&!n)throw new Error("Missing required arguments");if(!c.string(e))throw new TypeError("Second argument must be a String");if(!c.fn(n))throw new TypeError("Third argument must be a Function");if(c.node(t))return r(t,e,n);if(c.nodeList(t))return i(t,e,n);if(c.string(t))return a(t,e,n);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function r(t,e,n){return t.addEventListener(e,n),{destroy:function(){t.removeEventListener(e,n)}}}function i(t,e,n){return Array.prototype.forEach.call(t,function(t){t.addEventListener(e,n)}),{destroy:function(){Array.prototype.forEach.call(t,function(t){t.removeEventListener(e,n)})}}}function a(t,e,n){return u(document.body,t,e,n)}var c=n(6),u=n(5);t.exports=o},function(t,e){function n(){}n.prototype={on:function(t,e,n){var o=this.e||(this.e={});return(o[t]||(o[t]=[])).push({fn:e,ctx:n}),this},once:function(t,e,n){function o(){r.off(t,o),e.apply(n,arguments)}var r=this;return o._=e,this.on(t,o,n)},emit:function(t){var e=[].slice.call(arguments,1),n=((this.e||(this.e={}))[t]||[]).slice(),o=0,r=n.length;for(o;o0&&void 0!==arguments[0]?arguments[0]:{};this.action="function"==typeof t.action?t.action:this.defaultAction,this.target="function"==typeof t.target?t.target:this.defaultTarget,this.text="function"==typeof t.text?t.text:this.defaultText,this.container="object"===d(t.container)?t.container:document.body}},{key:"listenClick",value:function(t){var e=this;this.listener=(0,f.default)(t,"click",function(t){return e.onClick(t)})}},{key:"onClick",value:function(t){var e=t.delegateTarget||t.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new l.default({action:this.action(e),target:this.target(e),text:this.text(e),container:this.container,trigger:e,emitter:this})}},{key:"defaultAction",value:function(t){return u("action",t)}},{key:"defaultTarget",value:function(t){var e=u("target",t);if(e)return document.querySelector(e)}},{key:"defaultText",value:function(t){return u("text",t)}},{key:"destroy",value:function(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:["copy","cut"],e="string"==typeof t?[t]:t,n=!!document.queryCommandSupported;return e.forEach(function(t){n=n&&!!document.queryCommandSupported(t)}),n}}]),e}(s.default);t.exports=p})},function(t,e){function n(t,e){for(;t&&t.nodeType!==o;){if("function"==typeof t.matches&&t.matches(e))return t;t=t.parentNode}}var o=9;if("undefined"!=typeof Element&&!Element.prototype.matches){var r=Element.prototype;r.matches=r.matchesSelector||r.mozMatchesSelector||r.msMatchesSelector||r.oMatchesSelector||r.webkitMatchesSelector}t.exports=n},function(t,e,n){function o(t,e,n,o,r){var a=i.apply(this,arguments);return t.addEventListener(n,a,r),{destroy:function(){t.removeEventListener(n,a,r)}}}function r(t,e,n,r,i){return"function"==typeof t.addEventListener?o.apply(null,arguments):"function"==typeof n?o.bind(null,document).apply(null,arguments):("string"==typeof t&&(t=document.querySelectorAll(t)),Array.prototype.map.call(t,function(t){return o(t,e,n,r,i)}))}function i(t,e,n,o){return function(n){n.delegateTarget=a(n.target,e),n.delegateTarget&&o.call(t,n)}}var a=n(4);t.exports=r},function(t,e){e.node=function(t){return void 0!==t&&t instanceof HTMLElement&&1===t.nodeType},e.nodeList=function(t){var n=Object.prototype.toString.call(t);return void 0!==t&&("[object NodeList]"===n||"[object HTMLCollection]"===n)&&"length"in t&&(0===t.length||e.node(t[0]))},e.string=function(t){return"string"==typeof t||t instanceof String},e.fn=function(t){return"[object Function]"===Object.prototype.toString.call(t)}},function(t,e){function n(t){var e;if("SELECT"===t.nodeName)t.focus(),e=t.value;else if("INPUT"===t.nodeName||"TEXTAREA"===t.nodeName){var n=t.hasAttribute("readonly");n||t.setAttribute("readonly",""),t.select(),t.setSelectionRange(0,t.value.length),n||t.removeAttribute("readonly"),e=t.value}else{t.hasAttribute("contenteditable")&&t.focus();var o=window.getSelection(),r=document.createRange();r.selectNodeContents(t),o.removeAllRanges(),o.addRange(r),e=o.toString()}return e}t.exports=n}])}); \ No newline at end of file +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(window,function(){return function(n){var o={};function r(t){if(o[t])return o[t].exports;var e=o[t]={i:t,l:!1,exports:{}};return n[t].call(e.exports,e,e.exports,r),e.l=!0,e.exports}return r.m=n,r.c=o,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=0)}([function(t,e,n){"use strict";var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=function(){function o(t,e){for(var n=0;nLoading... +{% endcapture %} +{% include example.html content=example %} + +### Colors + +The border spinner uses `currentColor` for its `border-color`, meaning you can customize the color with [text color utilities][color]. You can use any of our text color utilities on the standard spinner. + +{% capture example %} +{% for color in site.data.theme-colors %} +
Loading...
{% endfor %} +{% endcapture %} +{% include example.html content=example %} + +{% capture callout %} +**Why not use `border-color` utilities?** Each border spinner specifies a `transparent` border for at least one side, so `.border-{color}` utilities would override that. +{% endcapture %} +{% include callout.html content=callout type="info" %} + +## Growing spinner + +If you don't fancy a border spinner, switch to the grow spinner. While it doesn't technically spin, it does repeatedly grow! + +{% capture example %} +
Loading...
+{% endcapture %} +{% include example.html content=example %} + +Once again, this spinner is built with `currentColor`, so you can easily change its appearance with [text color utilities][color]. Here it is in blue, along with the supported variants. + +{% capture example %} +{% for color in site.data.theme-colors %} +
Loading...
{% endfor %} +{% endcapture %} +{% include example.html content=example %} + +## Alignment + +Spinners in Bootstrap are built with `rem`s, `currentColor`, and `display: inline-flex`. This means they can easily be resized, recolored, and quickly aligned. + +### Margin + +Use [margin utilities][margin] like `.m-5` for easy spacing. + +{% capture example %} +
Loading...
+{% endcapture %} +{% include example.html content=example %} + +### Placement + +Use [flexbox utilities][flex], [float utilities][float], or [text alignment][text] utilities to place spinners exactly where you need them in any situation. + +#### Flex + +{% capture example %} +
+
Loading...
+
+{% endcapture %} +{% include example.html content=example %} + +{% capture example %} +
+ Loading... +
+
+{% endcapture %} +{% include example.html content=example %} + +#### Floats + +{% capture example %} +
+
Loading...
+
+{% endcapture %} +{% include example.html content=example %} + +#### Text align + +{% capture example %} +
+
Loading...
+
+{% endcapture %} +{% include example.html content=example %} + +## Size + +Add `.spinner-border-sm` and `.spinner-grow-sm` to make a smaller spinner that can quickly be used within other components. + +{% capture example %} +
Loading...
+
Loading...
+{% endcapture %} +{% include example.html content=example %} + +Or, use custom CSS or inline styles to change the dimensions as needed. + +{% capture example %} +
Loading...
+
Loading...
+{% endcapture %} +{% include example.html content=example %} + +## Buttons + +Use spinners within buttons to indicate an action is currently processing or taking place. You may also swap the text out of the spinner element and utilize button text as needed. + +{% capture example %} + + +{% endcapture %} +{% include example.html content=example %} + +{% capture example %} + + +{% endcapture %} +{% include example.html content=example %} + + +[color]: {{ site.baseurl }}/docs/{{ site.docs_version }}/utilities/colors/ +[display]: {{ site.baseurl }}/docs/{{ site.docs_version }}/utilities/display/ +[flex]: {{ site.baseurl }}/docs/{{ site.docs_version }}/utilities/flex/ +[float]: {{ site.baseurl }}/docs/{{ site.docs_version }}/utilities/float/ +[margin]: {{ site.baseurl }}/docs/{{ site.docs_version }}/utilities/spacing/ +[sizing]: {{ site.baseurl }}/docs/{{ site.docs_version }}/utilities/sizing/ +[text]: {{ site.baseurl }}/docs/{{ site.docs_version }}/content/typography/