+1
-1
@@ -1,1 +0,1 @@ | ||
| @import './lib/chaestli'; | ||
| @forward './lib/chaestli'; |
@@ -1,70 +0,48 @@ | ||
| @import '../vendor/@dreipol/scss-utils/functions/str-split'; | ||
| @import '../vendor/@dreipol/scss-utils/functions/list-expand-directions'; | ||
| @use 'sass:map'; | ||
| @use 'sass:list'; | ||
| /// Return the current BEM root by separating the rightmost part and stripping it from any element or modifier | ||
| /// @return {string} - The current BEM root | ||
| /// Create a definitions object for containers | ||
| /// @param {Map} $cfg - The config map | ||
| /// @return {Map} - The merge result | ||
| /// | ||
| @function grid--bem-root() { | ||
| $root-selector: nth(nth(&, 1), 1); | ||
| $root-selector: nth(dp-str-split($root-selector, '__'), 1); | ||
| $root-selector: nth(dp-str-split($root-selector, '--'), 1); | ||
| $root-selector: '.#{nth(dp-str-split($root-selector, '.'), 1)}'; | ||
| @function create-container-config($cfg) { | ||
| $defaults: ( | ||
| width: none, | ||
| edge: 0, | ||
| ); | ||
| @if (str-index($root-selector, '.') != 1) { | ||
| @error 'Provided root selector `#{$root-selector}` is no valid class selector!'; | ||
| } | ||
| @return $root-selector; | ||
| @return map.merge($defaults, $cfg); | ||
| } | ||
| /// Convert a BEM root selector and a list of modules into a selector list. | ||
| /// @param {string} $selector - A root module name (BEM) including a dot. | ||
| /// @param {string} $item-modules - One or multiple BEM submodule names without dashes. | ||
| /// @return {string} - A stringified list of bem modules | ||
| /// @example scss - Basic usage: input | ||
| /// $module-list: _grid--col-name-list('.form', 'label', 'input'); | ||
| /// #{$module-list} { display: block; } | ||
| /// @output css - Basic usage: output | ||
| /// .form--label, .form--input { display: block; } | ||
| /// Create a definitions object for grids | ||
| /// @param {Map} $cfg - The config map | ||
| /// @return {Map} - The merge result | ||
| /// | ||
| @function grid--col-name-list($selector, $item-modules...) { | ||
| $item-modules-list: ''; | ||
| @function create-grid-config($cfg) { | ||
| $defaults: ( | ||
| gutter: 0, | ||
| display: grid, | ||
| repeat-directive: 'auto-fill', | ||
| num-cols: 1, | ||
| ); | ||
| @each $item-module in $item-modules { | ||
| $i: index($item-modules, $item-module); | ||
| $sep: if($i == 1, '', ', '); | ||
| $item-modules-list: ($item-modules-list + $sep + '#{$selector}#{$bem-element-separator}#{$item-module}'); | ||
| } | ||
| @return $item-modules-list; | ||
| @return map.merge($defaults, $cfg); | ||
| } | ||
| /// Expand a given attribute into a 4-value gutter configuration | ||
| /// @param {number|List} $type - The property type that is being written. Example: 'padding' | ||
| /// Extract the grid gutters from the user options | ||
| /// @param {list|number} $gutter - Gutter value provided by the user | ||
| /// @return {Map} - A map containing the column and row gutters | ||
| /// | ||
| @function grid--configurate-gutter($gutter) { | ||
| @function get-grid-gaps($gutter) { | ||
| @if (type-of($gutter) == 'list') { | ||
| @return dp-list-expand-directions($gutter); | ||
| } @else { | ||
| @return (null, $gutter, null, $gutter); | ||
| @return ( | ||
| column: list.nth($gutter, 1), | ||
| row: list.nth($gutter, 2), | ||
| ); | ||
| } | ||
| } | ||
| /// Write the desired grid-gutter values. | ||
| /// @param {string} $type - The property type that is being written. Example: 'padding' | ||
| /// @param {List} $gutter-list - A list of gutter sizes with a length of 4. | ||
| /// @param {number} $modifier - A modifier to the gutter size that is being written. | ||
| /// | ||
| @mixin grid--write-gutter($type, $gutter-list, $modifier: 1) { | ||
| $direction-list: ('top', 'left', 'bottom', 'right'); | ||
| @for $i from 1 through length($direction-list) { | ||
| $dir: nth($direction-list, $i); | ||
| $val: nth($gutter-list, $i); | ||
| @if ($val) { | ||
| #{$type}-#{$dir}: ($val * $modifier); | ||
| } | ||
| } | ||
| @return ( | ||
| column: $gutter, | ||
| row: 0, | ||
| ); | ||
| } |
+87
-299
@@ -1,318 +0,106 @@ | ||
| /* stylelint-disable scss/declaration-nested-properties */ | ||
| //// | ||
| /// Grid library for all your basic grid needs! | ||
| //// | ||
| /// Dependencies and components | ||
| /// | ||
| @import '../vendor/@dreipol/scss-mq/index'; | ||
| @import '../vendor/@dreipol/scss-utils/mixins/at-root'; | ||
| @import './chaestli__vars'; | ||
| @import './chaestli__helpers'; | ||
| @import './chaestli__basics'; | ||
| @use 'sass:map'; | ||
| @use './chaestli__helpers' as *; | ||
| /// Fix buggy support for percentage values of `flex-basis` in IE10 and IE11. | ||
| /// @link https://github.com/philipwalton/flexbugs#7-flex-basis-doesnt-account-for-box-sizingborder-box | ||
| /// Create a grid container defining the width of the main content and its edges | ||
| /// @param {Map} $definition - A single container structure definition | ||
| /// @example scss - Simple container: input | ||
| /// .grid { | ||
| /// .grid__container { | ||
| /// @include chaestli.container((edge: 16px, width: 800px)); | ||
| /// } | ||
| /// } | ||
| /// | ||
| @mixin grid--fix-ie-flex-basis() { | ||
| @include dp-at-root(html, '.ie11') { | ||
| flex-basis: auto; | ||
| @content; | ||
| } | ||
| @include dp-at-root(html, '.ie10') { | ||
| flex-basis: auto; | ||
| @content; | ||
| } | ||
| } | ||
| /// Add basic grid styling to elements with a certain naming convention adapted from bootstrap | ||
| /// @param {string} $cfg.root - A root module name (BEM) including a dot | ||
| /// @param {string} $cfg.mq - A media expression that is parseable by `include-media` | ||
| /// @param {string} $cfg.container - BEM element describing a container | ||
| /// @param {string} $cfg.row - BEM element describing a row | ||
| /// @param {List} $cfg.cols - One or multiple BEM element describing a col | ||
| /// @param {string|number} $cfg.container-size - The grid-container's size | ||
| /// @output | ||
| /// .grid .grid__container { | ||
| /// max-width: 800px; | ||
| /// margin-left: auto; | ||
| /// margin-left: auto; | ||
| /// padding-left: 16px; | ||
| /// margin-left: 16px; | ||
| /// } | ||
| /// | ||
| @mixin grid--root($cfg: ()) { | ||
| // Merge default values with config | ||
| $defaults: ( | ||
| root: grid--bem-root(), | ||
| mq: null, | ||
| container: 'container', | ||
| row: 'row', | ||
| cols: ('col'), | ||
| ); | ||
| $cfg: map-merge($defaults, $cfg); | ||
| @mixin container($definition: ()) { | ||
| $definition: create-container-config($definition); | ||
| // Write config values into variables for code readability | ||
| $cfg-root: map-get($cfg, 'root'); | ||
| $cfg-mq: map-get($cfg, 'mq'); | ||
| $cfg-container: map-get($cfg, 'container'); | ||
| $cfg-row: map-get($cfg, 'row'); | ||
| $cfg-cols: map-get($cfg, 'cols'); | ||
| // read config variables | ||
| $width: map.get($definition, 'width'); | ||
| $edge: map.get($definition, 'edge'); | ||
| // Calculate additionally needed values | ||
| $col-names: grid--col-name-list($cfg-root, $cfg-cols...); | ||
| // prepare variables to render | ||
| $has-width: type-of($width) == 'number'; | ||
| $content-width: if($has-width, $width, none); | ||
| @include mq($cfg-mq) { | ||
| #{$cfg-root}#{$bem-element-separator}#{$cfg-container} { | ||
| @include grid--define-container; | ||
| } | ||
| #{$cfg-root}#{$bem-element-separator}#{$cfg-row} { | ||
| @include grid--define-row; | ||
| } | ||
| #{$col-names} { | ||
| @include grid--define-col; | ||
| } | ||
| } | ||
| // render css props | ||
| max-width: $content-width; | ||
| margin: { | ||
| left: auto; | ||
| right: auto; | ||
| }; | ||
| padding: { | ||
| left: $edge; | ||
| right: $edge; | ||
| }; | ||
| } | ||
| /// Move a column in any direction using its css margin property | ||
| /// @param {string} $cfg.direction - Either one of these values: `left` `right` `top` `bottom` | ||
| /// @param {unitless} $cfg.ratio - A unitless ratio value (0 < x <= 1) | ||
| /// @param {string} $cfg.mq - A media expression that is parseable by `include-media` | ||
| /// Enable the `display: grid;` on a container element. Notice that you can define the amount of columns your grid will have | ||
| /// and the vertical and horizontal gaps of its items | ||
| /// @param {Map} $definition - A single grid structure definition | ||
| /// | ||
| @mixin grid--move-col($cfg) { | ||
| $defaults: ( | ||
| direction: null, | ||
| ratio: null, | ||
| mq: null, | ||
| ); | ||
| $cfg: map-merge($defaults, $cfg); | ||
| $cfg-direction: map-get($cfg, 'direction'); | ||
| $cfg-ratio: map-get($cfg, 'ratio'); | ||
| $cfg-mq: map-get($cfg, 'mq'); | ||
| $percentage: (100% * $cfg-ratio); | ||
| @include mq($cfg-mq) { | ||
| margin-#{$cfg-direction}: $percentage; | ||
| } | ||
| } | ||
| /// Move a column from left to right | ||
| @mixin grid--push($cfg: ()) { | ||
| @include grid--move-col(map-merge($cfg, (direction: 'left'))); | ||
| } | ||
| /// Move a column from right to left | ||
| @mixin grid--pull($cfg: ()) { | ||
| @include grid--move-col(map-merge($cfg, (direction: 'right'))); | ||
| } | ||
| /// Define the maximum size of a grid by adding a `max-width` to the container | ||
| /// @param {string} $cfg.root - A root module name (BEM) including a dot | ||
| /// @param {string} $cfg.mq - A media expression that is parseable by `include-media` | ||
| /// @param {string} $cfg.container - BEM element describing a container | ||
| /// @param {string|number} $cfg.container-size - The grid-container's size | ||
| /// @example scss - Simple 12 columns grid with rows and cols gutter: input | ||
| /// .grid { | ||
| /// .grid__row { | ||
| /// @include chaestli.grid((gutter: (16px, 24px), num-cols: 12)); | ||
| /// } | ||
| /// } | ||
| /// | ||
| @mixin grid--constrain($cfg: ()) { | ||
| // Merge default values with config | ||
| $defaults: ( | ||
| root: grid--bem-root(), | ||
| mq: null, | ||
| container: 'container', | ||
| container-size: none, | ||
| size-prop: 'max-width', | ||
| ); | ||
| $cfg: map-merge($defaults, $cfg); | ||
| // Write config values into variables for code readability | ||
| $cfg-root: map-get($cfg, 'root'); | ||
| $cfg-mq: map-get($cfg, 'mq'); | ||
| $cfg-container: map-get($cfg, 'container'); | ||
| $cfg-container-size: map-get($cfg, 'container-size'); | ||
| $cfg-size-prop: map-get($cfg, 'size-prop'); | ||
| @include mq($cfg-mq) { | ||
| #{$cfg-root}#{$bem-element-separator}#{$cfg-container} { | ||
| #{$cfg-size-prop}: $cfg-container-size; | ||
| } | ||
| } | ||
| } | ||
| /// Scaffold spacing in between cols and between cols and container | ||
| /// @param {string} $cfg.root - A root module name (BEM) including a dot | ||
| /// @param {string} $cfg.mq - A media expression that is parseable by `include-media` | ||
| /// @param {string} $cfg.container - BEM element describing a container | ||
| /// @param {string} $cfg.row - BEM element describing a row | ||
| /// @param {List} $cfg.cols - One or multiple BEM element describing a col | ||
| /// @param {number} $cfg.gutter - The desired inner gutter size in any spacial unit | ||
| /// @param {number} $cfg.edge - The desired edge clearance in any spacial unit | ||
| /// @output | ||
| /// .grid .grid__row { | ||
| /// display: grid; | ||
| /// grid-template-columns: repeat(auto-fill, minmax(calc(100% / 12 - 16px), 1fr)); | ||
| /// column-gap: 16px; | ||
| /// row-gap: 24px; | ||
| /// } | ||
| /// | ||
| @mixin grid--space($cfg: ()) { | ||
| // Merge default values with config | ||
| $defaults: ( | ||
| root: grid--bem-root(), | ||
| container: 'container', | ||
| row: 'row', | ||
| cols: ('col'), | ||
| gutter: 0, | ||
| mq: null, | ||
| debug: null, | ||
| edge: null, | ||
| ); | ||
| $cfg: map-merge($defaults, $cfg); | ||
| @mixin grid($definition: ()) { | ||
| $definition: create-grid-config($definition); | ||
| // Write config values into variables for code readability | ||
| $cfg-root: map-get($cfg, 'root'); | ||
| $cfg-mq: map-get($cfg, 'mq'); | ||
| $cfg-container: map-get($cfg, 'container'); | ||
| $cfg-row: map-get($cfg, 'row'); | ||
| $cfg-cols: map-get($cfg, 'cols'); | ||
| $cfg-gutter: map-get($cfg, 'gutter'); | ||
| $cfg-edge: map-get($cfg, 'edge'); | ||
| $cfg-debug: map-get($cfg, 'debug'); | ||
| // read config variables | ||
| $gutter: map.get($definition, 'gutter'); | ||
| $display: map.get($definition, 'display'); | ||
| $repeat-directive: map.get($definition, 'repeat-directive'); | ||
| $num-cols: map.get($definition, 'num-cols'); | ||
| // Calculate additionally needed values | ||
| $col-names: grid--col-name-list($cfg-root, $cfg-cols...); | ||
| $gutters: grid--configurate-gutter($cfg-gutter); | ||
| $edges: grid--configurate-gutter(if($cfg-edge, $cfg-edge, $cfg-gutter)); | ||
| // prepare variables to render | ||
| $gaps: get-grid-gaps($gutter); | ||
| $column-gap: map.get($gaps, 'column'); | ||
| $row-gap: map.get($gaps, 'row'); | ||
| $column-size: if($column-gap > 0, calc(100% / #{$num-cols} - #{$column-gap}), calc(100% / #{$num-cols})); | ||
| @include mq($cfg-mq) { | ||
| #{$cfg-root}#{$bem-element-separator}#{$cfg-container} { | ||
| @include grid--write-gutter('padding', $edges); | ||
| #{$cfg-root}#{$bem-element-separator}#{$cfg-container}:not(.u-iknowwhatimdoing) { | ||
| // NOTE: Nested containers are not allowed, please nest rows/cols instead! | ||
| visibility: hidden !important; // stylelint-disable-line declaration-no-important | ||
| &::before { | ||
| visibility: visible; | ||
| content: 'Nested `grid--container` detected!'; | ||
| font-size: 20px; | ||
| line-height: 1.2; | ||
| color: red; // stylelint-disable-line color-named | ||
| } | ||
| } | ||
| } | ||
| #{$cfg-root}#{$bem-element-separator}#{$cfg-row} { | ||
| @include grid--write-gutter('margin', $gutters, -0.5); | ||
| // enable the grid debugging | ||
| @if $cfg-debug { | ||
| @include grid--debugger(( | ||
| edge: $cfg-edge, | ||
| gutter: $cfg-gutter | ||
| ), $cfg-debug); | ||
| } | ||
| } | ||
| #{$col-names} { | ||
| @include grid--write-gutter('padding', $gutters, 0.5); | ||
| } | ||
| } | ||
| // render css props | ||
| display: $display; | ||
| grid-template-columns: repeat(#{$repeat-directive}, minmax($column-size, 1fr)); | ||
| column-gap: $column-gap; | ||
| row-gap: $row-gap; | ||
| } | ||
| /// Define the col's size in flex notation | ||
| /// @param {unitless} $cfg.ratio - A unitless ratio value (0 < x <= 1) | ||
| /// @param {string} $cfg.mq - A media expression that is parseable by `include-media` | ||
| /// @param {string} $cfg.fallback - A property name that serves as fallback in IE10 and IE11 | ||
| /// Just as syntax sugar aliasing the `grid-column` rule | ||
| /// @param {list|number} $start - column start | ||
| /// @param {list|number} $end - column end | ||
| /// @example scss - Simple column 3 span over 12 columns: input | ||
| /// .grid { | ||
| /// .grid__three-col { | ||
| /// @include chaestli.column(3 span, 12 span); | ||
| /// } | ||
| /// } | ||
| /// | ||
| @mixin grid--span($cfg: ()) { | ||
| // Merge default values with config | ||
| $defaults: ( | ||
| ratio: null, | ||
| mq: null, | ||
| fallback-prop: 'width', | ||
| ); | ||
| $cfg: map-merge($defaults, $cfg); | ||
| // Write config values into variables for code readability | ||
| $cfg-ratio: map-get($cfg, 'ratio'); | ||
| $cfg-mq: map-get($cfg, 'mq'); | ||
| $cfg-fallback-prop: map-get($cfg, 'fallback-prop'); | ||
| // Calculate additionally needed values | ||
| $percentage: (100% * $cfg-ratio); | ||
| @include mq($cfg-mq) { | ||
| flex: 0 0 $percentage; | ||
| @include grid--fix-ie-flex-basis { | ||
| #{$cfg-fallback-prop}: $percentage; | ||
| } | ||
| /// @output | ||
| /// .grid .grid__three-col { | ||
| /// grid-column: 3 span / 12 span; | ||
| /// } | ||
| @mixin column($start, $end: null) { | ||
| @if ($end) { | ||
| grid-column: #{$start} / #{$end}; | ||
| } @else { | ||
| grid-column: $start; | ||
| } | ||
| } | ||
| /// Standalone version of `grid--span` with the same config | ||
| /// | ||
| @mixin grid--col($cfg: ()) { | ||
| @include grid--define-col; | ||
| @include grid--span($cfg); | ||
| } | ||
| /// Allows the grid debugging coloring the rows background showing the columns and the edges | ||
| /// @param {number} $grid-cfg.gutter - The inner gutter size | ||
| /// @param {number} $grid-cfg.edge - The edge clearance in any spacial unit | ||
| /// @param {number} $cfg.columns - The amount of columns we want to debug | ||
| /// @param {number} $cfg.column-color - The color used to debug the columns | ||
| /// @param {number} $cfg.gutter-color - The color used to debug the gutter | ||
| /// @param {number} $cfg.edge-color - The color to highlight the external edges | ||
| /// | ||
| @mixin grid--debugger( | ||
| $grid-cfg, | ||
| $cfg | ||
| ) { | ||
| $defaults: ( | ||
| column-color: rgba(0, 0, 0, 0.2), | ||
| edge-color: rgba(0, 0, 0, 0), | ||
| gutter-color: rgba(0, 0, 0, 0), | ||
| columns: null | ||
| ); | ||
| @if not map-get($cfg, 'columns') { | ||
| @error 'Please define the columns amount to debug the grid'; | ||
| } | ||
| $cfg: map-merge($defaults, $cfg); | ||
| $cfg-gutter: map-get($grid-cfg, 'gutter'); | ||
| $cfg-edge: map-get($grid-cfg, 'edge'); | ||
| $cfg-columns: map-get($cfg, 'columns'); | ||
| $cfg-gutter-color: map-get($cfg, 'gutter-color'); | ||
| $cfg-edge-color: map-get($cfg, 'edge-color'); | ||
| $cfg-column-color: map-get($cfg, 'column-color'); | ||
| $half-gutter: $cfg-gutter / 2; | ||
| $gradient-start: $half-gutter; | ||
| $gradient-end: calc(100% - #{$half-gutter}); | ||
| position: relative; | ||
| background: { | ||
| image: linear-gradient( | ||
| to right, | ||
| $cfg-gutter-color $gradient-start, | ||
| $cfg-column-color $gradient-start, | ||
| $cfg-column-color $gradient-end, | ||
| $cfg-gutter-color $gradient-end | ||
| ); | ||
| size: 1 / $cfg-columns * 100%; | ||
| position: left top; | ||
| clip: content-box; | ||
| origin: content-box; | ||
| }; | ||
| &::before, | ||
| &::after { | ||
| content: ''; | ||
| position: absolute; | ||
| top: 0; | ||
| height: 100%; | ||
| width: $cfg-edge; | ||
| background: $cfg-edge-color; | ||
| } | ||
| &::before { | ||
| right: calc(100% - #{$half-gutter}); | ||
| } | ||
| &::after { | ||
| left: calc(100% - #{$half-gutter}); | ||
| } | ||
| } |
+16
-22
| { | ||
| "name": "chaestli", | ||
| "version": "2.0.1", | ||
| "version": "3.0.0", | ||
| "description": "A scss grid framework made in Switzerland and brought to you by dreipol", | ||
@@ -10,11 +10,8 @@ "main": "_index.scss", | ||
| "scripts": { | ||
| "test": "npm run build && npm run lint && npm run test-ruby && npm run test-node", | ||
| "test-ruby": "sass test.scss", | ||
| "test-node": "node-sass test.scss", | ||
| "test": "npm run lint && npx sass test.scss", | ||
| "lint": "stylelint _index.scss lib/**/*.scss", | ||
| "clean": "rm -rf vendor", | ||
| "demo": "node-sass --output-style=compressed -o=demo demo/scss/index.scss", | ||
| "prebuild": "npm run clean && mkdir vendor", | ||
| "build": "npm run prebuild && rsync -rm `pwd`/node_modules/* --include=\"*.scss\" --include=\"*/\" --exclude=\"*\" vendor", | ||
| "prepublish": "npm test" | ||
| "doc": "mv docs/demo demo && npx sassdoc lib --dest docs && mv demo docs/demo", | ||
| "demo": "npx sass --style=compressed docs/demo/scss/index.scss:docs/demo/index.css", | ||
| "watch-demo": "npx sass --watch --style=compressed docs/demo/scss/index.scss:docs/demo/index.css", | ||
| "prepublishOnly": "npm run doc && npm test" | ||
| }, | ||
@@ -26,3 +23,2 @@ "repository": { | ||
| "files": [ | ||
| "vendor", | ||
| "lib", | ||
@@ -48,17 +44,15 @@ "_index.scss" | ||
| "homepage": "https://github.com/dreipol/chaestli#readme", | ||
| "dependencies": { | ||
| "@dreipol/scss-mq": "^1.0.0", | ||
| "@dreipol/scss-utils": "^3.0.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@dreipol/stylelint-config": "^5.0.0", | ||
| "@dreipol/stylelint-config-bem-pattern": "^2.0.1", | ||
| "@dreipol/stylelint-config-order": "^3.0.0", | ||
| "@dreipol/stylelint-config-scss": "^2.0.1", | ||
| "node-sass": "4.13.1", | ||
| "stylelint": "^13.0.0", | ||
| "stylelint-order": "^4.0.0", | ||
| "stylelint-scss": "^3.14.2", | ||
| "@dreipol/scss-mq": "^2.0.0", | ||
| "@dreipol/stylelint-config": "^7.0.0", | ||
| "@dreipol/stylelint-config-bem-pattern": "^3.0.0", | ||
| "@dreipol/stylelint-config-order": "^4.0.0", | ||
| "@dreipol/stylelint-config-scss": "^3.0.0", | ||
| "node-sass": "4.14.1", | ||
| "sassdoc": "^2.7.3", | ||
| "stylelint": "^13.7.2", | ||
| "stylelint-order": "^4.1.0", | ||
| "stylelint-scss": "^3.18.0", | ||
| "stylelint-selector-bem-pattern": "^2.1.0" | ||
| } | ||
| } |
+45
-5
@@ -0,1 +1,3 @@ | ||
| <img src="https://raw.githubusercontent.com/dreipol/chaestli/main/logo.png" width="50%"/> | ||
| # chaestli | ||
@@ -9,13 +11,51 @@ Dreipol scss grid system | ||
| # Installation | ||
| # Documentation | ||
| ```bash | ||
| npm i chaestli -S | ||
| - [API](https://www.dreipol.dev/chaestli/) | ||
| - [Demo](https://www.dreipol.dev/chaestli/demo) | ||
| ## Available mixins | ||
| Chaestli exports only 3 mixins (`chaestli.container`, `chaestli.grid`, `chaestli.column`) that combined together should be enough to scaffold your grid setup. For example: | ||
| ```scss | ||
| @use 'node_modules/@dreipol/chaestli'; | ||
| .grid { | ||
| .grid__container { | ||
| // a container having a max-with of 1024px with 16px lateral padding | ||
| @include chaestli.container(( width: 1024px, edge: 16px )); | ||
| } | ||
| .grid__row { | ||
| // a grid made of 12 columns with 8px lateral gutter | ||
| @include chaestli.grid(( gutter: 8px, num-cols: 12 )); | ||
| } | ||
| .grid__small-col { | ||
| // two columns over 12 | ||
| @include chaestli.column(2 span, 12 span); | ||
| } | ||
| .grid__medium-col { | ||
| // for columns over 12 | ||
| @include chaestli.column(4 span, 12 span); | ||
| } | ||
| .grid__big-col { | ||
| // eight columns over 12 | ||
| @include chaestli.column(8 span, 12 span); | ||
| } | ||
| } | ||
| ``` | ||
| # Documentation | ||
| # Usage | ||
| While the documentation is under construction, you can find some barebone examples in the [demo page](https://dreipol.github.io/chaestli/demo) | ||
| ## Installation | ||
| ```bash | ||
| npm i chaestli -S | ||
| ``` | ||
| [travis-image]:https://img.shields.io/travis/dreipol/chaestli.svg?style=flat-square | ||
@@ -22,0 +62,0 @@ [travis-url]:https://travis-ci.org/dreipol/chaestli |
| @import '../vendor/@dreipol/scss-utils/mixins/at-root'; | ||
| @mixin grid--define-container { | ||
| display: flex; | ||
| flex-flow: column nowrap; | ||
| width: 100%; | ||
| margin: auto; | ||
| } | ||
| @mixin grid--define-row { | ||
| display: flex; | ||
| flex-flow: row wrap; | ||
| // NOTE: Fixes an image scaling issue of flex children in IE11. | ||
| // @link https://github.com/philipwalton/flexbugs#5-column-flex-items-dont-always-preserve-intrinsic-aspect-ratios | ||
| @include dp-at-root(html, '.ie11') { | ||
| min-height: 1px; | ||
| } | ||
| } | ||
| @mixin grid--define-col { | ||
| position: relative; | ||
| min-height: 1px; | ||
| max-width: 100%; | ||
| } |
| /// BEM presets | ||
| /// | ||
| $bem-element-separator: '__' !default; | ||
| $bem-modifier-separator: '--' !default; |
| @import './lib/mq'; |
| @import '../vendor/@dreipol/scss-utils/mixins/hide-visually'; | ||
| @import '../vendor/@dreipol/scss-utils/mixins/at-root'; | ||
| /// Add a hidden but readable hook to be used in html and js | ||
| /// @param {string} $root-class - Class that serves as root name for the elements containing the mq data | ||
| /// | ||
| @mixin mq--bridge($root-class) { | ||
| #{$root-class}-type::before, | ||
| #{$root-class}-conditions::before, | ||
| #{$root-class}-expressions::before { | ||
| @include dp-hide-visually; | ||
| } | ||
| #{$root-class}-conditions { | ||
| @include dp-at-root(html, '.u-mq-info') { | ||
| &::before { | ||
| pointer-events: none; | ||
| z-index: 9999; | ||
| position: fixed; | ||
| bottom: 0; | ||
| left: 0; | ||
| display: block; | ||
| width: auto; | ||
| height: auto; | ||
| max-width: 50%; | ||
| max-height: none; | ||
| padding: 15px; | ||
| margin: 0; | ||
| clip: auto; | ||
| font-family: monospace; | ||
| font-size: 12px; | ||
| line-height: 1.1; | ||
| word-wrap: break-word; | ||
| color: #ffffff; | ||
| background-color: rgba(22, 25, 40, 0.9); | ||
| border-top-right-radius: 5px; | ||
| text-align: left; | ||
| } | ||
| &::before { | ||
| transition: visibility 0ms ease 2000ms; | ||
| visibility: hidden; | ||
| } | ||
| } | ||
| } | ||
| } |
| // stylelint-disable scss/no-duplicate-dollar-variables | ||
| @import '../vendor/@dreipol/scss-utils/functions/list-purge'; | ||
| @import '../vendor/sass-to-js/sass/sass-to-js'; | ||
| @import '../vendor/include-media-or/dist/include-media-or'; | ||
| /// Check a list of conditions for it's validity based on the used operator as only `<=` and `>` make sense | ||
| /// @param {List} $conditions - List of conditions, possibly nested | ||
| /// | ||
| @mixin mq--check-condition-validity($conditions) { | ||
| @each $condition in $conditions { | ||
| @if (type-of($condition) == 'list') { | ||
| @include mq--check-condition-validity($condition); | ||
| } @else if not map-has-key($media-expressions, $condition) { | ||
| $operator: get-expression-operator($condition); | ||
| @if not index(('<=', '≤', '>'), $operator) { | ||
| @error 'Operator `#{$operator}` is not allowed in condition `#{$condition}`'; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| /// Convert a map of media expressions into a list of all possible viewport combinations | ||
| /// @param {Map} $expressions - Map of media feature expressions, grouped by type | ||
| /// @return {Map} - Conjugated map of all possible media feature expressions | ||
| /// | ||
| @function mq--conditions($expressions) { | ||
| $result: ((),); | ||
| @each $list-key, $list-value in $expressions { | ||
| $combinations: (); | ||
| $list-value: map-merge((null: null), $list-value); | ||
| @each $item-key, $item-value in $list-value { | ||
| @each $combination in $result { | ||
| $combinations: append($combinations, map-merge($combination, ($list-key: $item-key))); | ||
| } | ||
| } | ||
| $result: $combinations; | ||
| } | ||
| @return $result; | ||
| } | ||
| /// Convert a list of media expressions into a list of sass maps with name and query properties | ||
| /// @param {Map} $expressions - A flat list of media feature expressions | ||
| /// @return {List} - A list of maps containing all available separate media queries used in the project | ||
| /// | ||
| @function mq--expressions($expressions) { | ||
| $result: (); | ||
| @each $type, $conditions in $expressions { | ||
| @each $name, $value in $conditions { | ||
| $result: append($result, (name: $name, query: $value, type: $type)); | ||
| } | ||
| } | ||
| @return $result; | ||
| } | ||
| /// Writes the viewport information into a content attribute, contained by the media expression it describes | ||
| /// @param {string} $root-class - Class that serves as root name for the elements containing the mq data | ||
| /// | ||
| @mixin mq--write($root-class) { | ||
| // Type | ||
| @each $type-key, $type-value in $media-type-expressions { | ||
| #{$root-class}-type::before { | ||
| @include mq($type-value) { | ||
| content: sassToJs($type-value); | ||
| } | ||
| } | ||
| } | ||
| // Conditions | ||
| @each $conditions in mq--conditions($media-feature-expressions) { | ||
| $media-conditions: dp-list-purge(map-values($conditions)); | ||
| #{$root-class}-conditions::before { | ||
| @include mq($media-conditions...) { | ||
| content: sassToJs($conditions); | ||
| } | ||
| } | ||
| } | ||
| // Expressions | ||
| $expressions-list: mq--expressions(map-merge((type: $media-type-expressions), $media-feature-expressions)); | ||
| #{$root-class}-expressions::before { | ||
| content: sassToJs($expressions-list); | ||
| } | ||
| } |
| @import '../vendor/@dreipol/scss-utils/functions/map-assign'; | ||
| /// Computed breakpoints system, borrowed from bootstrap | ||
| /// | ||
| $screen-xs-min: 0 !default; | ||
| $screen-sm-min: 544px !default; | ||
| $screen-md-min: 768px !default; | ||
| $screen-lg-min: 992px !default; | ||
| $screen-xl-min: 1280px !default; | ||
| $screen-xxl-min: 1600px !default; | ||
| $screen-xs-max: ($screen-sm-min - 1) !default; | ||
| $screen-sm-max: ($screen-md-min - 1) !default; | ||
| $screen-md-max: ($screen-lg-min - 1) !default; | ||
| $screen-lg-max: ($screen-xl-min - 1) !default; | ||
| $screen-xl-max: ($screen-xxl-min - 1) !default; | ||
| /// Break point definitions used by `include-media` | ||
| /// | ||
| $breakpoints: ( | ||
| xs: $screen-xs-max, | ||
| sm: $screen-sm-max, | ||
| md: $screen-md-max, | ||
| lg: $screen-lg-max, | ||
| xl: $screen-xl-max, | ||
| ) !default; | ||
| /// Media type expressions | ||
| /// | ||
| $media-type-expressions: ( | ||
| screen: 'screen', | ||
| print: 'print', | ||
| ) !default; | ||
| /// Media feature expressions | ||
| /// | ||
| $media-feature-expressions: ( | ||
| width: ( | ||
| xs: '(max-width: #{$screen-xs-max})', | ||
| sm: ('(min-width: #{$screen-sm-min}) and (max-width: #{$screen-sm-max})'), | ||
| md: ('(min-width: #{$screen-md-min}) and (max-width: #{$screen-md-max})'), | ||
| lg: ('(min-width: #{$screen-lg-min}) and (max-width: #{$screen-lg-max})'), | ||
| xl: ('(min-width: #{$screen-xl-min}) and (max-width: #{$screen-xl-max})'), | ||
| xxl: '(min-width: #{$screen-xxl-min})', | ||
| ), | ||
| orientation: ( | ||
| portrait: '(orientation: portrait)', | ||
| landscape: '(orientation: landscape)', | ||
| ), | ||
| resolution: ( | ||
| res2x: ('(min-resolution: 2dppx)'), | ||
| res3x: ('(min-resolution: 3dppx)'), | ||
| ), | ||
| ) !default; | ||
| /// Media expressions, used by `include-media` | ||
| /// @example scss - Combine width expression `xs` with resolution expression `res2x`: | ||
| /// @include media('xs', 'res2x') { color: red; } | ||
| /// | ||
| $media-expressions: dp-map-assign( | ||
| $media-type-expressions, | ||
| map-get($media-feature-expressions, width), | ||
| map-get($media-feature-expressions, orientation), | ||
| map-get($media-feature-expressions, resolution) | ||
| ) !default; |
| //// | ||
| /// Helper library to make `include-media` more useful in many ways | ||
| //// | ||
| /// Dependencies and components | ||
| /// | ||
| @import '../vendor/@dreipol/scss-utils/functions/list-purge'; | ||
| @import './mq__vars'; | ||
| @import './mq__bridge'; | ||
| @import './mq__helpers'; | ||
| /// Guard mixin to enable usage of `include-media` with `null` values and protect against wrong usage. | ||
| /// @param {...string} $conditions - Media query conditions | ||
| /// @example scss - Above breakpoint `md`: | ||
| /// @include media('>md') { color: red; } | ||
| /// @example scss - Below or equal breakpoint `lg`: | ||
| /// @include media('<=lg') { color: red; } | ||
| /// | ||
| @mixin mq($conditions...) { | ||
| $purged-conditions: dp-list-purge($conditions); | ||
| @if (length($purged-conditions) == 0) { | ||
| @content; | ||
| } @else { | ||
| @include mq--check-condition-validity($purged-conditions); | ||
| @include media($purged-conditions...) { | ||
| @content; | ||
| } | ||
| } | ||
| } | ||
| /// Append mq bridge to dom object | ||
| /// @param {string} $root-class - Class that serves as root name for the elements containing the mq data | ||
| /// | ||
| @mixin mq--spy($root-class) { | ||
| @include mq--bridge($root-class); | ||
| @include mq--write($root-class); | ||
| } |
| @import './functions/color-shade'; | ||
| @import './functions/color-tint'; | ||
| @import './functions/is-truthy'; | ||
| @import './functions/list-concat'; | ||
| @import './functions/list-contains'; | ||
| @import './functions/list-expand-directions'; | ||
| @import './functions/list-first'; | ||
| @import './functions/list-insert-nth'; | ||
| @import './functions/list-last'; | ||
| @import './functions/list-last-index'; | ||
| @import './functions/list-prepend'; | ||
| @import './functions/list-purge'; | ||
| @import './functions/list-remove'; | ||
| @import './functions/list-remove-nth'; | ||
| @import './functions/list-replace'; | ||
| @import './functions/list-replace-nth'; | ||
| @import './functions/list-slice'; | ||
| @import './functions/map-assign'; | ||
| @import './functions/map-deep-get'; | ||
| @import './functions/selector-get-element-name'; | ||
| @import './functions/str-replace'; | ||
| @import './functions/str-split'; | ||
| @import './functions/strip-unit'; | ||
| @import './functions/to-string'; | ||
| @import './functions/transition-props'; |
| @import './functions'; | ||
| @import './mixins'; |
| @import './mixins/at-root'; | ||
| @import './mixins/hide-text'; | ||
| @import './mixins/has-focus'; | ||
| @import './mixins/hide-visually'; | ||
| @import './mixins/ios-native-scrolling'; | ||
| @import './mixins/is-selectable'; | ||
| @import './mixins/is-visible'; | ||
| @import './mixins/last-row'; | ||
| @import './mixins/overlay'; | ||
| @import './mixins/placeholder'; | ||
| @import './mixins/size'; | ||
| @import './mixins/text-ellipsis'; | ||
| @import './mixins/supports-hover'; | ||
| @import './mixins/supports-touch'; |
| /// Slightly darken a color by mixing it with black | ||
| /// @link http://sass-guidelin.es/#lightening-and-darkening-colors | ||
| /// @access public | ||
| /// @param {color} $color - Color to tint | ||
| /// @param {number} $percentage - Percentage of `$color` in returned color | ||
| /// @return {Color} | ||
| /// | ||
| @function dp-color-shade($color, $percentage) { | ||
| @return mix(black, $color, $percentage); | ||
| } |
| /// Slightly lighten a color by mixing it with white | ||
| /// @link http://sass-guidelin.es/#lightening-and-darkening-colors | ||
| /// @access public | ||
| /// @param {color} $color - Color to tint | ||
| /// @param {number} $percentage - Percentage of `$color` in returned color | ||
| /// @return {Color} | ||
| /// | ||
| @function dp-color-tint($color, $percentage) { | ||
| @return mix(white, $color, $percentage); | ||
| } | ||
| /// Check if a value has a truthy value | ||
| /// @param {*} $value - The input value | ||
| /// @return {boolean} - A flag indicating if the value is truthy | ||
| /// | ||
| @function dp-is-truthy($value) { | ||
| @return if($value == null, false, $value and $value != null and $value != '' and $value != ()); | ||
| } |
| @import './to-string'; | ||
| /// Concatenate a list using a given separator | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#miscellaneous | ||
| /// @param {List} $list - The list to be concatenated | ||
| /// @param {string} $glue - A separator string to be inserted between items | ||
| /// @return {string} - A string containing list items connected with a separator string | ||
| /// | ||
| @function dp-list-concat($list, $glue: '') { | ||
| $result: null; | ||
| @if length($list) == 0 { | ||
| @return ''; | ||
| } | ||
| @for $i from 1 through length($list) { | ||
| $e: nth($list, $i); | ||
| @if type-of($e) == list { | ||
| $result: unquote('#{$result}#{to-string($e, $glue, true)}'); | ||
| } @else { | ||
| $result: if($i != length($list), unquote('#{$result}#{$e}#{$glue}'), unquote('#{$result}#{$e}')); | ||
| } | ||
| } | ||
| @return $result; | ||
| } |
| /// Checks if a given list contains a certain value | ||
| /// @param {List} $list - The haystack | ||
| /// @param {*} $val - The needle | ||
| /// | ||
| @function dp-list-contains($list, $val) { | ||
| @if index($list, $val) { | ||
| @return true; | ||
| } | ||
| @return false; | ||
| } |
| // stylelint-disable scss/no-duplicate-dollar-variables | ||
| /// Expand a given list to 4 items, similar to css padding shorthand expressions. | ||
| /// Inspired by expansions used in css shortand expressions (eg. margin or padding). | ||
| /// @param {List} $list - List containing zero to four items | ||
| /// @return {List} - Updated list expanded to always four entries | ||
| /// | ||
| @function dp-list-expand-directions($list) { | ||
| $result: (); | ||
| $len-list: length($list); | ||
| @if ($len-list == 0) { | ||
| $result: (null, null, null, null); | ||
| } @else if ($len-list == 1) { | ||
| $result: (nth($list, 1), nth($list, 1), nth($list, 1), nth($list, 1)); | ||
| } @else if ($len-list == 2) { | ||
| $result: (nth($list, 1), nth($list, 2), nth($list, 1), nth($list, 2)); | ||
| } @else if ($len-list == 3) { | ||
| $result: (nth($list, 1), nth($list, 2), nth($list, 3), nth($list, 2)); | ||
| } @else { | ||
| $result: (nth($list, 1), nth($list, 2), nth($list, 3), nth($list, 4)); | ||
| } | ||
| @return $result; | ||
| } |
| /// Return the first item of a list | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#selecting-values-from-list | ||
| /// @param {List} $list - The input list | ||
| /// @return {*} - The list's first item | ||
| /// | ||
| @function dp-list-first($list) { | ||
| @return nth($list, 1); | ||
| } |
| // stylelint-disable scss/no-duplicate-dollar-variables | ||
| /// Add a value to an arbitrary position | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#adding-values-to-a-list | ||
| /// @param {List} $list - The input list | ||
| /// @param {number} $index - The value's target list position | ||
| /// @param {*} $value - The term to be added | ||
| /// @return {*} - The output list | ||
| /// | ||
| @function dp-list-insert-nth($list, $index, $value) { | ||
| $result: null; | ||
| @if type-of($index) != number { | ||
| @warn '$index: #{quote($index)} is not a number for `insert-nth`.'; | ||
| } @else if $index < 1 { | ||
| @warn 'List index 0 must be a non-zero integer for `insert-nth`'; | ||
| } @else if $index > length($list) { | ||
| @warn 'List index is #{$index} but list is only #{length($list)} item long for `insert-nth`'; | ||
| } @else { | ||
| $result: (); | ||
| @for $i from 1 through length($list) { | ||
| @if $i == $index { | ||
| $result: append($result, $value); | ||
| } | ||
| $result: append($result, nth($list, $i)); | ||
| } | ||
| } | ||
| @return $result; | ||
| } |
| /// Return the last occurence of an item within a list | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#selecting-values-from-list | ||
| /// @param {List} $list - The input list | ||
| /// @param {*} $value - The search term | ||
| /// @return {number} - The index of the search term's last occurrence within the list | ||
| /// | ||
| @function dp-list-last-index($list, $value) { | ||
| $result: null; | ||
| @for $i from 1 through length($list) { | ||
| @if nth($list, $i) == $value { | ||
| $result: $i; | ||
| } | ||
| } | ||
| @return $result; | ||
| } |
| /// Return the last item of a list | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#selecting-values-from-list | ||
| /// @param {List} $list - The input list | ||
| /// @return {*} - The list's last item | ||
| /// | ||
| @function dp-list-last($list) { | ||
| @return nth($list, length($list)); | ||
| } |
| /// Add a value to the front of a list | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#adding-values-to-a-list | ||
| /// @param {List} $list - The input list | ||
| /// @param {*} $value - The term to be prepended | ||
| /// @return {*} - The output list | ||
| /// | ||
| @function dp-list-prepend($list, $value) { | ||
| @return join($value, $list); | ||
| } |
| @import './is-truthy'; | ||
| /// Removes all non-true values from a list | ||
| /// @link http://hugogiraudel.com/2013/10/09/advanced-sass-list-functions-again/ | ||
| /// @param {List} $list - A list containing possibly falsy values | ||
| /// @return {List} - The trimmed list | ||
| /// | ||
| @function dp-list-purge($list) { | ||
| $result: (); | ||
| @each $item in $list { | ||
| @if dp-is-truthy($item) { | ||
| $result: append($result, $item); | ||
| } | ||
| } | ||
| @return $result; | ||
| } |
| @import './list-replace-nth'; | ||
| /// Remove a value at a certain index from a list | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#removing-values-from-list | ||
| /// @param {List} $list - The input list | ||
| /// @param {number} $index - The target position to be removed | ||
| /// @return {*} - The list's first item | ||
| /// | ||
| @function dp-list-remove-nth($list, $index) { | ||
| @return dp-list-replace-nth($list, $index); | ||
| } |
| @import './list-replace'; | ||
| /// Remove a value from a list | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#removing-values-from-list | ||
| /// @param {List} $list - The input list | ||
| /// @param {*} $value - The term to be removed | ||
| /// @return {*} - The list's first item | ||
| /// | ||
| @function dp-list-remove($list, $value) { | ||
| @return dp-list-replace($list, $value); | ||
| } |
| // stylelint-disable scss/no-duplicate-dollar-variables | ||
| @import './is-truthy'; | ||
| /// Replace a value within a list by index | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#replacing-values-from-list | ||
| /// @param {List} $list - The input list | ||
| /// @param {number} $index - The value's target list position | ||
| /// @param {*} $value - The term to be inserted | ||
| /// @return {*} - The output list | ||
| /// | ||
| @function dp-list-replace-nth($list, $index, $value: null) { | ||
| $result: null; | ||
| @if type-of($index) != number { | ||
| @warn '$index: #{quote($index)} is not a number for `replace-nth`'; | ||
| } @else if $index == 0 { | ||
| @warn 'ist index 0 must be a non-zero integer for `replace-nth`.'; | ||
| } @else if abs($index) > length($list) { | ||
| @warn 'ist index is #{$index} but list is only #{length($list)} item long for `replace-nth`.'; | ||
| } @else { | ||
| $result: (); | ||
| $index: if($index < 0, length($list) + $index + 1, $index); | ||
| @for $i from 1 through length($list) { | ||
| @if $i == $index { | ||
| @if dp-is-truthy($value) { | ||
| $result: append($result, $value); | ||
| } | ||
| } @else { | ||
| $result: append($result, nth($list, $i)); | ||
| } | ||
| } | ||
| } | ||
| @return $result; | ||
| } |
| @import './is-truthy'; | ||
| /// Replace a value with another one in a list | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#replacing-values-from-list | ||
| /// @param {List} $list - The input list | ||
| /// @param {*} $old-value - The term that is being replaced | ||
| /// @param {*} $value - The term to replace it with | ||
| /// @return {*} - The output list | ||
| /// | ||
| @function dp-list-replace($list, $old-value, $value: null) { | ||
| $result: (); | ||
| @for $i from 1 through length($list) { | ||
| @if nth($list, $i) == $old-value { | ||
| @if dp-is-truthy($value) { | ||
| $result: append($result, $value); | ||
| } | ||
| } @else { | ||
| $result: append($result, nth($list, $i)); | ||
| } | ||
| } | ||
| @return $result; | ||
| } |
| // stylelint-disable scss/no-duplicate-dollar-variables | ||
| /// Return a number of values from a list | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#miscellaneous | ||
| /// @param {List} $list - The input list | ||
| /// @param {number} $start - The slice range's start index | ||
| /// @param {number} $end - The slice range's end index | ||
| /// @return {List} - A list of items | ||
| /// | ||
| @function dp-list-slice($list, $start: 1, $end: length($list)) { | ||
| $result: null; | ||
| @if type-of($start) != number or type-of($end) != number { | ||
| @warn 'Either $start or $end are not a number for `slice`'; | ||
| } @else if $start > $end { | ||
| @warn 'The start index has to be lesser than or equals to the end index for `slice`'; | ||
| } @else if $start < 1 or $end < 1 { | ||
| @warn 'List indexes must be non-zero integers for `slice`'; | ||
| } @else if $start > length($list) { | ||
| @warn 'List index is #{$start} but list is only #{length($list)} item long for `slice`'; | ||
| } @else if $end > length($list) { | ||
| @warn 'List index is #{$end} but list is only #{length($list)} item long for `slice`'; | ||
| } @else { | ||
| $result: (); | ||
| @for $i from $start through $end { | ||
| $result: append($result, nth($list, $i)); | ||
| } | ||
| } | ||
| @return $result; | ||
| } |
| /// Takes multiple maps and merges them into one | ||
| /// @link http://stackoverflow.com/questions/27740063/merge-multiple-sass-maps | ||
| /// @param {...Map} $maps - Any number of maps | ||
| /// @return {Map} - The resulting map | ||
| /// | ||
| @function dp-map-assign($maps...) { | ||
| $result: (); | ||
| @each $map in $maps { | ||
| $result: map-merge($result, $map); | ||
| } | ||
| @return $result; | ||
| } |
| /// Returns a value from a nested list (without failsafe if one of the keys does not exist) | ||
| /// @link https://css-tricks.com/snippets/sass/deep-getset-maps/ | ||
| /// @param {Map} $map - Map | ||
| /// @param {...string} $keys - Key chain | ||
| /// @return {*} - Desired value | ||
| /// | ||
| @function dp-map-deep-get($map, $keys...) { | ||
| @each $key in $keys { | ||
| $map: map-get($map, $key); | ||
| } | ||
| @return $map; | ||
| } |
| @import './str-split'; | ||
| /// Remove classes, pseudo-classes and attributes from a selector part | ||
| /// @param {string} $selector - The selector part to be cleaned | ||
| /// @return {string} | ||
| /// | ||
| @function dp-selector-get-element-name($selector) { | ||
| $result: $selector; | ||
| $markers: (' ', '.', ':', '['); | ||
| @each $marker in $markers { | ||
| $result: nth(dp-str-split($result, $marker), 1); | ||
| } | ||
| @return $result; | ||
| } |
| /// Replace `$search` with `$replace` in `$string` | ||
| /// @link https://css-tricks.com/snippets/sass/str-replace-function/ | ||
| /// @param {string} $string - Initial string | ||
| /// @param {string} $search - Substring to replace | ||
| /// @param {string} $replace ('') - New value | ||
| /// @return {string} - Updated string | ||
| /// | ||
| @function dp-str-replace($string, $search, $replace: '') { | ||
| $index: str-index($string, $search); | ||
| @if $index { | ||
| @return str-slice($string, 1, $index - 1) + $replace + dp-str-replace(str-slice($string, $index + str-length($search)), $search, $replace); | ||
| } | ||
| @return $string; | ||
| } |
| /// Split `$string` into several parts using `$delimiter`. | ||
| /// @param {string} $string - String to split | ||
| /// @param {string} $delimiter ('') - String to use as a delimiter to split `$string` | ||
| /// @return {List} | ||
| /// | ||
| @function dp-str-split($string, $delimiter: '') { | ||
| $result: (); | ||
| $length: str-length($string); | ||
| @if str-length($delimiter) == 0 { | ||
| @for $i from 1 through $length { | ||
| $result: append($result, str-slice($string, $i, $i)); | ||
| } | ||
| @return $result; | ||
| } | ||
| $running: true; | ||
| $remaining: $string; | ||
| @while $running { | ||
| $index: str-index($remaining, $delimiter); | ||
| @if $index { | ||
| $slice: str-slice($remaining, 1, $index - 1); | ||
| @if (str-length($slice) > 0) { | ||
| $result: append($result, $slice); | ||
| } | ||
| $remaining: str-slice($remaining, $index + str-length($delimiter)); | ||
| } @else { | ||
| $running: false; | ||
| } | ||
| } | ||
| @return append($result, $remaining); | ||
| } |
| /// Remove the unit of a length | ||
| /// @param {number} $number - Number to remove unit from | ||
| /// @return {number} - Unitless number | ||
| /// | ||
| @function dp-strip-unit($number) { | ||
| @if type-of($number) == 'number' and not unitless($number) { | ||
| @return $number / ($number * 0 + 1); | ||
| } | ||
| @return $number; | ||
| } |
| /// Cast an arbitrary value to a string | ||
| /// @link https://hugogiraudel.com/2014/01/27/casting-types-in-sass/#string | ||
| /// @param {*} $value - The original value | ||
| /// @return {string} - The cast string | ||
| /// | ||
| @function dp-to-string($value) { | ||
| @return inspect($value); | ||
| } |
| /// Batch transition all properties with a single transition definition | ||
| /// @param {string} $trs-defintion - The transition definition to be applied | ||
| /// @param {...Map} $props - Any number of properties to be transitioned | ||
| /// @return {List} - A list to be used as a value for a transition property | ||
| /// | ||
| @function dp-transition-props($trs-definition, $props...) { | ||
| $result: (); | ||
| @each $prop in $props { | ||
| $result: append($result, ($prop $trs-definition), 'comma'); | ||
| } | ||
| @return $result; | ||
| } |
| @import '../functions/selector-get-element-name'; | ||
| /// Use `@at-root` while adding classes (or attributes) `$classes` to an element node `$append-target`. | ||
| /// If the element was not found a standard `@at-root` is used. | ||
| /// @param {string} $append-target - $append-target string to append the classes (or attributes) to | ||
| /// @param {string} $appended-selector - A single element selector containing classes and/or attributes to be appended | ||
| /// | ||
| @mixin dp-at-root($append-target, $appended-selector: '') { | ||
| $is-element-found: false; | ||
| $combined-selectors: ''; | ||
| $selectors: nth(&, 1); | ||
| @if ($appended-selector) { | ||
| @each $selector in $selectors { | ||
| $pure-element-selector: dp-selector-get-element-name($selector); | ||
| @if (($pure-element-selector == $append-target) and (not $is-element-found)) { | ||
| $is-element-found: true; | ||
| $combined-selectors: '#{$combined-selectors} #{$selector}#{$appended-selector}'; | ||
| } @else { | ||
| $combined-selectors: '#{$combined-selectors} #{$selector}'; | ||
| } | ||
| } | ||
| } | ||
| @if ($is-element-found) { | ||
| @at-root #{$combined-selectors} { | ||
| @content; | ||
| } | ||
| } @else { | ||
| @at-root #{$append-target}#{$appended-selector} & { | ||
| @content; | ||
| } | ||
| } | ||
| } |
| @import './supports-hover'; | ||
| @import './supports-touch'; | ||
| /// Styles touch/hover states according to browser capabilities | ||
| /// @param {boolean} $toggle - State toggle; Available values: `true`, `false` or `default` (for both) | ||
| /// | ||
| @mixin dp-has-focus() { | ||
| @include dp-supports-hover { | ||
| &:hover, &:focus { | ||
| @content; | ||
| } | ||
| } | ||
| @include dp-supports-touch { | ||
| &:active { | ||
| @content; | ||
| } | ||
| } | ||
| } |
| /// Visually hide text but leave it accessible for screen readers and search engines | ||
| /// | ||
| @mixin dp-hide-text { | ||
| font: 0/0 a; // stylelint-disable font-family-no-missing-generic-family-keyword | ||
| text-shadow: none; | ||
| color: transparent; | ||
| } |
| /// Visually hide content but leave it accessible for screen readers and search engines | ||
| /// | ||
| @mixin dp-hide-visually { | ||
| position: absolute; | ||
| overflow: hidden; | ||
| max-width: 1px; | ||
| max-height: 1px; | ||
| padding: 0; | ||
| border: 0; | ||
| margin: -1px; | ||
| clip: rect(0 0 0 0); | ||
| } |
| /// Add ios native overflow scrolling to an element | ||
| /// @param {string} $orientation - The scroll direction | ||
| /// | ||
| @mixin dp-ios-native-scrolling($orientation: y) { | ||
| -webkit-overflow-scrolling: touch; | ||
| @if (not index((x, y), $orientation)) { | ||
| @error '`#{$orientation}` must be either `x` or `y`'; | ||
| } | ||
| @if ($orientation == y) { | ||
| overflow-x: hidden; | ||
| overflow-y: scroll; | ||
| } | ||
| @if ($orientation == x) { | ||
| overflow-x: scroll; | ||
| overflow-y: hidden; | ||
| } | ||
| } |
| /// Controls the selection style of the current element and all its children | ||
| /// @param {boolean} $toggle - State toggle | ||
| /// | ||
| @mixin dp-is-selectable($toggle) { | ||
| $c--label-selection: #accef7 !default; // provide a fallback value | ||
| @if $toggle { | ||
| & { | ||
| -webkit-touch-callout: text; | ||
| user-select: text; | ||
| &::selection, ::selection { | ||
| background-color: $c--label-selection; | ||
| } | ||
| } | ||
| } @else { | ||
| & { | ||
| -webkit-touch-callout: none; | ||
| user-select: none; | ||
| &::selection, ::selection { | ||
| background-color: transparent; | ||
| } | ||
| } | ||
| } | ||
| } |
| // stylelint-disable scss/no-duplicate-dollar-variables | ||
| /// Animates an element in or out, including setting their visibility correctly | ||
| /// The basic opacity transition can be extended with other properties or full transition descriptors | ||
| /// @param {boolean} $toggle - State toggle | ||
| /// @param {number} $speed - The targeted speed after which the element is hidden | ||
| /// @param {...string|...List} $transition-params - One or more strings or lists containing additional transition params | ||
| /// @example scss - Simple usage: input | ||
| /// .my-elem.my-elem__is-hidden { @include dp-is-visible(false, 300ms); } | ||
| /// .my-elem.my-elem__is-shown { @include dp-is-visible(true, 300ms); } | ||
| /// @output css - Simple usage: output | ||
| /// .my-elem.my-elem__is-hidden { | ||
| /// transition: visibility 0ms linear 300ms, opacity 300ms ease 0ms; | ||
| /// visibility: hidden; | ||
| /// opacity: 0; | ||
| /// } | ||
| /// .my-elem.my-elem__is-shown { | ||
| /// transition: visibility 0ms linear 0ms, opacity 300ms ease 0ms; | ||
| /// visibility: inherit; | ||
| /// opacity: 1; | ||
| /// } | ||
| /// | ||
| @mixin dp-is-visible($toggle, $speed, $transition-params...) { | ||
| $transitions: (); | ||
| $has-params: length($transition-params) > 0; | ||
| @if type-of($speed) != 'number' { | ||
| @error 'Provided transition speed `#{$speed}` is not of type number!'; | ||
| } | ||
| @if $has-params { | ||
| @for $i from 1 through length($transition-params) { | ||
| $param: nth($transition-params, $i); | ||
| @if type-of($param) == 'list' { // If $param contains a space (a.k.a. a whole string) | ||
| $transitions: append($transitions, unquote('#{$param}'), comma); | ||
| } @else { | ||
| $transitions: append($transitions, unquote('#{$param} #{$speed} ease 0ms'), comma); | ||
| } | ||
| } | ||
| } @else { | ||
| $transitions: append($transitions, unquote('opacity #{$speed} ease 0ms'), comma); | ||
| } | ||
| @if $toggle { | ||
| transition: join(unquote('visibility 0ms linear 0ms'), $transitions, comma); | ||
| visibility: inherit; | ||
| @if not $has-params { | ||
| opacity: 1; | ||
| } | ||
| } @else { | ||
| transition: join(unquote('visibility 0ms linear #{$speed}'), $transitions, comma); | ||
| visibility: hidden; | ||
| @if not $has-params { | ||
| opacity: 0; | ||
| } | ||
| } | ||
| } |
| /// Targets the last row in a grid of constant column count | ||
| /// @link http://keithclark.co.uk/articles/targeting-first-and-last-rows-in-css-grid-layouts/ | ||
| /// @param {number} $num-cols - The number of columns within the context | ||
| /// | ||
| @mixin dp-last-row($num-cols) { | ||
| $selector: nth(nth(&, 1), length(nth(&, 1))); | ||
| &:nth-child(#{$num-cols}n + 1):nth-last-child(-n + #{$num-cols}), | ||
| &:nth-child(#{$num-cols}n + 1):nth-last-child(-n + #{$num-cols}) ~ #{$selector} { | ||
| @content; | ||
| } | ||
| } |
| /// Add styles for a child to overlay its parent | ||
| /// @param {string} $pos - A declaration value for the `position` property | ||
| /// @param {string} $root-y - A vertical spacial property keyword | ||
| /// @param {string} $root-x - A horizontal spacial property keyword | ||
| /// | ||
| @mixin dp-overlay($pos: absolute, $root-y: top, $root-x: left) { | ||
| position: $pos; | ||
| #{$root-y}: 0; | ||
| #{$root-x}: 0; | ||
| width: 100%; | ||
| height: 100%; | ||
| } |
| // stylelint-disable selector-no-vendor-prefix | ||
| /// Add styles to the placeholder element | ||
| /// | ||
| @mixin dp-placeholder() { | ||
| &::-moz-placeholder { @content; } | ||
| &:-ms-input-placeholder { @content; } | ||
| &::-webkit-input-placeholder { @content; } | ||
| &::placeholder { @content; } | ||
| } |
| @mixin dp-size($sz) { | ||
| width: $sz; | ||
| height: $sz; | ||
| } |
| // https://docs.w3cub.com/css/@media/any-hover/ | ||
| @mixin dp-supports-hover() { | ||
| /* one or more available input mechanism(s) can hover over elements with ease */ | ||
| // The `-ms-high-contrast` directives are needed to target Internet Explorer 10 and 11 | ||
| // @link https://stackoverflow.com/q/28417056/1602864 | ||
| // TODO: Remove the `-ms-high-contrast` directives when IE will no longer be supported | ||
| @media (any-hover: hover), (-ms-high-contrast: active), (-ms-high-contrast: none) { | ||
| @content; | ||
| } | ||
| } |
| @mixin dp-supports-touch() { | ||
| /* one or more available input mechanism(s) cannot hover or there are no pointing input mechanisms */ | ||
| @media (any-hover: none) { | ||
| @content; | ||
| } | ||
| } |
| @mixin dp-text-ellipsis() { | ||
| white-space: nowrap; | ||
| overflow: hidden; | ||
| max-width: 100%; | ||
| text-overflow: ellipsis; | ||
| } |
| @import './index'; | ||
| @import './unprefixed/index'; | ||
| .test { | ||
| $test-list: (foo, bar, baz); | ||
| $test-map: (foo: bar); | ||
| $test-color: #0000ff; | ||
| // Prefixed mixins | ||
| @include dp-has-focus; | ||
| @include dp-at-root('.test'); | ||
| @include dp-hide-text; | ||
| @include dp-hide-visually; | ||
| @include dp-ios-native-scrolling; | ||
| @include dp-is-selectable(true); | ||
| @include dp-is-visible(true, 200); | ||
| @include dp-last-row(2); | ||
| @include dp-overlay; | ||
| @include dp-placeholder; | ||
| @include dp-size(200); | ||
| @include dp-text-ellipsis; | ||
| // Unprefixed mixins | ||
| @include has-focus; | ||
| @include at-root('.test'); | ||
| @include hide-text; | ||
| @include hide-visually; | ||
| @include ios-native-scrolling; | ||
| @include is-selectable(true); | ||
| @include is-visible(true, 200); | ||
| @include last-row(2); | ||
| @include overlay; | ||
| @include placeholder; | ||
| @include size(200); | ||
| @include text-ellipsis; | ||
| @include supports-hover; | ||
| @include supports-touch; | ||
| // Prefixed functions | ||
| $test-prefixed: ( | ||
| 't-1': dp-color-shade($test-color, 10), | ||
| 't-2': dp-color-tint($test-color, 10), | ||
| 't-3': dp-is-truthy('foo'), | ||
| 't-4': dp-list-concat($test-list, ','), | ||
| 't-5': dp-list-contains($test-list, foo), | ||
| 't-6': dp-list-expand-directions($test-list), | ||
| 't-7': dp-list-first($test-list), | ||
| 't-8': dp-list-insert-nth($test-list, 2, qux), | ||
| 't-9': dp-list-last($test-list), | ||
| 't-10': dp-list-last-index($test-list, bar), | ||
| 't-11': dp-list-prepend($test-list, qux), | ||
| 't-12': dp-list-purge($test-list), | ||
| 't-13': dp-list-remove($test-list, foo), | ||
| 't-14': dp-list-remove-nth($test-list, 1), | ||
| 't-15': dp-list-replace($test-list, foo, qux), | ||
| 't-16': dp-list-replace-nth($test-list, 1, qux), | ||
| 't-17': dp-list-slice($test-list, 2, 3), | ||
| 't-18': dp-map-assign($test-map), | ||
| 't-19': dp-map-deep-get($test-map, 'color'), | ||
| 't-20': dp-selector-get-element-name('.test'), | ||
| 't-21': dp-str-replace('test', 'st', 'oo'), | ||
| 't-22': dp-str-split('test'), | ||
| 't-23': dp-strip-unit(3), | ||
| 't-24': dp-to-string(foo), | ||
| 't-25': dp-transition-props('all 300ms linear'), | ||
| 't-26': dp-supports-hover(), | ||
| 't-27': dp-supports-touch(), | ||
| ); | ||
| // Unprefixed functions | ||
| $test-unprefixed: ( | ||
| 't-1': color-shade($test-color, 10), | ||
| 't-2': color-tint($test-color, 10), | ||
| 't-3': is-truthy('foo'), | ||
| 't-4': list-concat($test-list, ','), | ||
| 't-5': list-contains($test-list, foo), | ||
| 't-6': list-expand-directions($test-list), | ||
| 't-7': list-first($test-list), | ||
| 't-8': list-insert-nth($test-list, 2, qux), | ||
| 't-9': list-last($test-list), | ||
| 't-10': list-last-index($test-list, bar), | ||
| 't-11': list-prepend($test-list, qux), | ||
| 't-12': list-purge($test-list), | ||
| 't-13': list-remove($test-list, foo), | ||
| 't-14': list-remove-nth($test-list, 1), | ||
| 't-15': list-replace($test-list, foo, qux), | ||
| 't-16': list-replace-nth($test-list, 1, qux), | ||
| 't-17': list-slice($test-list, 2, 3), | ||
| 't-18': map-assign($test-map), | ||
| 't-19': map-deep-get($test-map, 'color'), | ||
| 't-20': selector-get-element-name('.test'), | ||
| 't-21': str-replace('test', 'st', 'oo'), | ||
| 't-22': str-split('test'), | ||
| 't-23': strip-unit(3), | ||
| 't-24': to-string(foo), | ||
| 't-25': transition-props('all 300ms linear'), | ||
| 't-26': supports-hover(), | ||
| 't-27': supports-touch(), | ||
| ); | ||
| $test-fixtures: ( | ||
| 't-1': #0000e6, | ||
| 't-2': #1a1aff, | ||
| 't-3': true, | ||
| 't-4': 'foo,bar,baz', | ||
| 't-5': true, | ||
| 't-6': (foo, bar, baz, bar), | ||
| 't-7': foo, | ||
| 't-8': foo qux bar baz, | ||
| 't-9': baz, | ||
| 't-10': 2, | ||
| 't-11': (qux, foo, bar, baz), | ||
| 't-12': foo bar baz, | ||
| 't-13': bar baz, | ||
| 't-14': bar baz, | ||
| 't-15': qux bar baz, | ||
| 't-16': qux bar baz, | ||
| 't-17': bar baz, | ||
| 't-18': (foo: bar), | ||
| 't-19': null, | ||
| 't-20': 'test', | ||
| 't-21': 'teoo', | ||
| 't-22': 't' 'e' 's' 't', | ||
| 't-23': 3, | ||
| 't-24': 'foo', | ||
| 't-25': (), | ||
| ); | ||
| @for $i from 1 through length($test-fixtures) { | ||
| $fixture: map-get($test-fixtures, 't-#{$i}'); | ||
| $prefixed: map-get($test-prefixed, 't-#{$i}'); | ||
| $unprefixed: map-get($test-unprefixed, 't-#{$i}'); | ||
| @if ($prefixed != $unprefixed) { | ||
| @error 'Comparison test ##{$i} failing! - Prefixed: #{$prefixed} ~vs~ Unprefixed: #{$unprefixed}'; | ||
| } | ||
| @if ($prefixed != $fixture) { | ||
| @error 'Comparison test ##{$i} failing! - Fixture: #{$fixture} ~vs~ Prefixed: #{$prefixed}'; | ||
| } | ||
| } | ||
| } |
| @import '../functions'; | ||
| @function color-shade($args...) { | ||
| @return dp-color-shade($args...); | ||
| } | ||
| @function color-tint($args...) { | ||
| @return dp-color-tint($args...); | ||
| } | ||
| @function is-truthy($args...) { | ||
| @return dp-is-truthy($args...); | ||
| } | ||
| @function list-concat($args...) { | ||
| @return dp-list-concat($args...); | ||
| } | ||
| @function list-contains($args...) { | ||
| @return dp-list-contains($args...); | ||
| } | ||
| @function list-expand-directions($args...) { | ||
| @return dp-list-expand-directions($args...); | ||
| } | ||
| @function list-first($args...) { | ||
| @return dp-list-first($args...); | ||
| } | ||
| @function list-insert-nth($args...) { | ||
| @return dp-list-insert-nth($args...); | ||
| } | ||
| @function list-last($args...) { | ||
| @return dp-list-last($args...); | ||
| } | ||
| @function list-last-index($args...) { | ||
| @return dp-list-last-index($args...); | ||
| } | ||
| @function list-prepend($args...) { | ||
| @return dp-list-prepend($args...); | ||
| } | ||
| @function list-purge($args...) { | ||
| @return dp-list-purge($args...); | ||
| } | ||
| @function list-remove($args...) { | ||
| @return dp-list-remove($args...); | ||
| } | ||
| @function list-remove-nth($args...) { | ||
| @return dp-list-remove-nth($args...); | ||
| } | ||
| @function list-replace($args...) { | ||
| @return dp-list-replace($args...); | ||
| } | ||
| @function list-replace-nth($args...) { | ||
| @return dp-list-replace-nth($args...); | ||
| } | ||
| @function list-slice($args...) { | ||
| @return dp-list-slice($args...); | ||
| } | ||
| @function map-assign($args...) { | ||
| @return dp-map-assign($args...); | ||
| } | ||
| @function map-deep-get($args...) { | ||
| @return dp-map-deep-get($args...); | ||
| } | ||
| @function selector-get-element-name($args...) { | ||
| @return dp-selector-get-element-name($args...); | ||
| } | ||
| @function str-replace($args...) { | ||
| @return dp-str-replace($args...); | ||
| } | ||
| @function str-split($args...) { | ||
| @return dp-str-split($args...); | ||
| } | ||
| @function strip-unit($args...) { | ||
| @return dp-strip-unit($args...); | ||
| } | ||
| @function to-string($args...) { | ||
| @return dp-to-string($args...); | ||
| } | ||
| @function transition-props($args...) { | ||
| @return dp-transition-props($args...); | ||
| } |
| @import './functions'; | ||
| @import './mixins'; |
| @import '../mixins'; | ||
| @mixin at-root($args...) { | ||
| @include dp-at-root($args...) { | ||
| @content; | ||
| } | ||
| } | ||
| @mixin has-focus($args...) { | ||
| @include dp-has-focus($args...) { | ||
| @content; | ||
| } | ||
| } | ||
| @mixin hide-text($args...) { | ||
| @include dp-hide-text($args...); | ||
| } | ||
| @mixin hide-visually($args...) { | ||
| @include dp-hide-visually($args...); | ||
| } | ||
| @mixin ios-native-scrolling($args...) { | ||
| @include dp-ios-native-scrolling($args...); | ||
| } | ||
| @mixin is-selectable($args...) { | ||
| @include dp-is-selectable($args...); | ||
| } | ||
| @mixin is-visible($args...) { | ||
| @include dp-is-visible($args...); | ||
| } | ||
| @mixin last-row($args...) { | ||
| @include dp-last-row($args...) { | ||
| @content; | ||
| } | ||
| } | ||
| @mixin overlay($args...) { | ||
| @include dp-overlay($args...); | ||
| } | ||
| @mixin placeholder($args...) { | ||
| @include dp-placeholder($args...) { | ||
| @content; | ||
| } | ||
| } | ||
| @mixin size($args...) { | ||
| @include dp-size($args...); | ||
| } | ||
| @mixin text-ellipsis($args...) { | ||
| @include dp-text-ellipsis($args...); | ||
| } | ||
| @mixin supports-hover($args...) { | ||
| @include dp-supports-hover($args...); | ||
| } | ||
| @mixin supports-touch($args...) { | ||
| @include dp-supports-touch($args...); | ||
| } |
| @charset "UTF-8"; | ||
| //// | ||
| /// `include-media-or` - An `include-media` fork, enabling OR conjunctions and nested queries | ||
| /// Author: Rouven Bühlmann (@nirazul) | ||
| /// | ||
| /// This project is licensed under the terms of the MIT license | ||
| //// | ||
| //// | ||
| /// include-media library public configuration | ||
| /// @author Eduardo Boucas | ||
| /// @access public | ||
| //// | ||
| /// | ||
| /// Creates a list of global breakpoints | ||
| /// | ||
| /// @example scss - Creates a single breakpoint with the label `phone` | ||
| /// $breakpoints: ('phone': 320px); | ||
| /// | ||
| $breakpoints: ( | ||
| 'phone': 320px, | ||
| 'tablet': 768px, | ||
| 'desktop': 1024px | ||
| ) !default; | ||
| /// | ||
| /// Creates a list of static expressions or media types | ||
| /// | ||
| /// @example scss - Creates a single media type (screen) | ||
| /// $media-expressions: ('screen': 'screen'); | ||
| /// | ||
| /// @example scss - Creates a static expression with logical disjunction (OR operator) | ||
| /// $media-expressions: ( | ||
| /// 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)' | ||
| /// ); | ||
| /// | ||
| $media-expressions: ( | ||
| 'screen': 'screen', | ||
| 'print': 'print', | ||
| 'handheld': 'handheld', | ||
| 'landscape': '(orientation: landscape)', | ||
| 'portrait': '(orientation: portrait)', | ||
| 'retina2x': ('(-webkit-min-device-pixel-ratio: 2)', '(min-resolution: 192dpi)', '(min-resolution: 2dppx)'), | ||
| 'retina3x': ('(-webkit-min-device-pixel-ratio: 3)', '(min-resolution: 350dpi)', '(min-resolution: 3dppx)') | ||
| ) !default; | ||
| /// | ||
| /// Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals | ||
| /// | ||
| /// @example scss - Interval for pixels is defined as `1` by default | ||
| /// @include media('>128px') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 129px) {} | ||
| /// | ||
| /// @example scss - Interval for ems is defined as `0.01` by default | ||
| /// @include media('>20em') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 20.01em) {} | ||
| /// | ||
| /// @example scss - Interval for rems is defined as `0.1` by default, to be used with `font-size: 62.5%;` | ||
| /// @include media('>2.0rem') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 2.1rem) {} | ||
| /// | ||
| $unit-intervals: ( | ||
| 'px': 1, | ||
| 'em': 0.01, | ||
| 'rem': 0.1, | ||
| '': 0 | ||
| ) !default; | ||
| /// | ||
| /// Defines whether support for media queries is available, useful for creating separate stylesheets | ||
| /// for browsers that don't support media queries. | ||
| /// | ||
| /// @example scss - Disables support for media queries | ||
| /// $im-media-support: false; | ||
| /// @include media('>=tablet') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| $im-media-support: true !default; | ||
| /// | ||
| /// Selects which breakpoint to emulate when support for media queries is disabled. Media queries that start at or | ||
| /// intercept the breakpoint will be displayed, any others will be ignored. | ||
| /// | ||
| /// @example scss - This media query will show because it intercepts the static breakpoint | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// @include media('>=tablet') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| /// @example scss - This media query will NOT show because it does not intercept the desktop breakpoint | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'tablet'; | ||
| /// @include media('>=desktop') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* No output */ | ||
| /// | ||
| $im-no-media-breakpoint: 'desktop' !default; | ||
| /// | ||
| /// Selects which media expressions are allowed in an expression for it to be used when media queries | ||
| /// are not supported. | ||
| /// | ||
| /// @example scss - This media query will show because it intercepts the static breakpoint and contains only accepted media expressions | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// $im-no-media-expressions: ('screen'); | ||
| /// @include media('>=tablet', 'screen') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| /// @example scss - This media query will NOT show because it intercepts the static breakpoint but contains a media expression that is not accepted | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// $im-no-media-expressions: ('screen'); | ||
| /// @include media('>=tablet', 'retina2x') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* No output */ | ||
| /// | ||
| $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default; | ||
| //// | ||
| /// Parsing engine | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Get operator of an expression | ||
| /// | ||
| /// @param {String} $expression - Expression to extract operator from | ||
| /// | ||
| /// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤` | ||
| /// | ||
| @function get-expression-operator($expression) { | ||
| @each $operator in ('>=', '>', '<=', '<', '≥', '≤') { | ||
| @if str-index($expression, $operator) { | ||
| @return $operator; | ||
| } | ||
| } | ||
| // It is not possible to include a mixin inside a function, so we have to | ||
| // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because | ||
| // functions cannot be called anywhere in Sass, we need to hack the call in | ||
| // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with | ||
| // Sass 3.3, change this line in `@if im-log(..) {}` instead. | ||
| $_: im-log('No operator found in `#{$expression}`.'); | ||
| } | ||
| /// | ||
| /// Get dimension of an expression, based on a found operator | ||
| /// | ||
| /// @param {String} $expression - Expression to extract dimension from | ||
| /// @param {String} $operator - Operator from `$expression` | ||
| /// | ||
| /// @return {String} - `width` or `height` (or potentially anything else) | ||
| /// | ||
| @function get-expression-dimension($expression, $operator) { | ||
| $operator-index: str-index($expression, $operator); | ||
| $parsed-dimension: str-slice($expression, 0, $operator-index - 1); | ||
| $dimension: 'width'; | ||
| @if str-length($parsed-dimension) > 0 { | ||
| $dimension: $parsed-dimension; | ||
| } | ||
| @return $dimension; | ||
| } | ||
| /// | ||
| /// Get dimension prefix based on an operator | ||
| /// | ||
| /// @param {String} $operator - Operator | ||
| /// | ||
| /// @return {String} - `min` or `max` | ||
| /// | ||
| @function get-expression-prefix($operator) { | ||
| @return if(index(('<', '<=', '≤'), $operator), 'max', 'min'); | ||
| } | ||
| /// | ||
| /// Get value of an expression, based on a found operator | ||
| /// | ||
| /// @param {String} $expression - Expression to extract value from | ||
| /// @param {String} $operator - Operator from `$expression` | ||
| /// | ||
| /// @return {Number} - A numeric value | ||
| /// | ||
| @function get-expression-value($expression, $operator) { | ||
| $operator-index: str-index($expression, $operator); | ||
| $value: str-slice($expression, $operator-index + str-length($operator)); | ||
| @if map-has-key($breakpoints, $value) { | ||
| $value: map-get($breakpoints, $value); | ||
| } @else { | ||
| $value: to-number($value); | ||
| } | ||
| $interval: map-get($unit-intervals, unit($value)); | ||
| @if not $interval { | ||
| // It is not possible to include a mixin inside a function, so we have to | ||
| // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because | ||
| // functions cannot be called anywhere in Sass, we need to hack the call in | ||
| // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with | ||
| // Sass 3.3, change this line in `@if im-log(..) {}` instead. | ||
| $_: im-log('Unknown unit `#{unit($value)}`.'); | ||
| } | ||
| @if $operator == '>' { | ||
| $value: $value + $interval; | ||
| } @else if $operator == '<' { | ||
| $value: $value - $interval; | ||
| } | ||
| @return $value; | ||
| } | ||
| /// | ||
| /// Parse an expression to return a valid media-query expression | ||
| /// | ||
| /// @param {String} $expression - Expression to parse | ||
| /// | ||
| /// @return {String} - Valid media query | ||
| /// | ||
| @function parse-expression($expression) { | ||
| // If it is part of $media-expressions, it has no operator | ||
| // then there is no need to go any further, just return the value | ||
| @if map-has-key($media-expressions, $expression) { | ||
| @return map-get($media-expressions, $expression); | ||
| } | ||
| $operator: get-expression-operator($expression); | ||
| $dimension: get-expression-dimension($expression, $operator); | ||
| $prefix: get-expression-prefix($operator); | ||
| $value: get-expression-value($expression, $operator); | ||
| @return '(#{$prefix}-#{$dimension}: #{$value})'; | ||
| } | ||
| /// | ||
| /// Get a shallow list of resolved feature queries | ||
| /// | ||
| /// @param {String} $queries - A nested list structure | ||
| /// @param {Boolean} $do-parse - Try to parse expressions | ||
| /// | ||
| /// @return {List} - A one item deep nested list like `(a b, c d e)` | ||
| /// | ||
| @function resolve-feature-queries($queries, $do-parse: true) { | ||
| $separator: list-separator($queries); | ||
| $result: if($separator == 'space', ((), ), ()); | ||
| @each $query in $queries { | ||
| $chainable-queries: (); | ||
| @if (type-of($query) == 'list') { | ||
| // List item is a list itself | ||
| $chainable-queries: resolve-feature-queries($query); | ||
| } @else { | ||
| // List item is a string | ||
| $parsed-query: if($do-parse, parse-expression($query), $query); | ||
| @if (type-of($parsed-query) == 'list') { | ||
| // Parsed expression is a list | ||
| $chainable-queries: resolve-feature-queries($parsed-query, false); | ||
| } @else { | ||
| // Parsed expression is a string | ||
| $chainable-queries: ($parsed-query); | ||
| } | ||
| } | ||
| $result: append-feature-query($result, $chainable-queries, $separator); | ||
| } | ||
| @return $result; | ||
| } | ||
| /// | ||
| /// Combine two query lists as a logical AND / OR operation | ||
| /// | ||
| /// @param {List} $base-queries - The host list | ||
| /// @param {List} $append-queries - The list that is being appended | ||
| /// @param {String} $separator - Either `space` or `comma` | ||
| /// | ||
| /// @return {List} - A one item deep nested list like `(a b, c d e)` | ||
| /// | ||
| @function append-feature-query($base-queries, $append-queries, $separator) { | ||
| $result: if($separator == 'space', (), $base-queries); | ||
| @each $append-query in $append-queries { | ||
| @if ($separator == 'space') { | ||
| // Logical AND | ||
| @each $base-query in $base-queries { | ||
| $updated-query: join($base-query, $append-query, $separator); | ||
| $result: append($result, $updated-query, 'comma'); | ||
| } | ||
| } @else { | ||
| // Logical OR | ||
| $result: append($result, $append-query, 'comma'); | ||
| } | ||
| } | ||
| @return $result; | ||
| } | ||
| /// | ||
| /// Parse a list of resolved expressions to return a valid media-query | ||
| /// | ||
| /// @param {List} $queries - A one item deep nested list like `(a b, c d e)` | ||
| /// | ||
| /// @return {String} - A valid media-query string | ||
| /// | ||
| @function parse-media-query($queries) { | ||
| $result: null; | ||
| $flat-queries: (); | ||
| $separator: list-separator($queries); | ||
| $conjunction: if($separator == 'space', ' and ', ', '); | ||
| @if (type-of($queries) == 'string') { | ||
| @return $queries; | ||
| } | ||
| @each $query in $queries { | ||
| @if (type-of($query) == 'list') { | ||
| $flat-queries: append($flat-queries, parse-media-query($query)); | ||
| } @else { | ||
| $flat-queries: append($flat-queries, $query); | ||
| } | ||
| } | ||
| @for $i from 1 through length($flat-queries) { | ||
| $e: nth($flat-queries, $i); | ||
| $result: unquote('#{$result}#{$e}#{if($i != length($flat-queries), $conjunction, '')}'); | ||
| } | ||
| @return $result; | ||
| } | ||
| //// | ||
| /// Cross-engine logging engine | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Log a message either with `@error` if supported | ||
| /// else with `@warn`, using `feature-exists('at-error')` | ||
| /// to detect support. | ||
| /// | ||
| /// @param {String} $message - Message to log | ||
| /// | ||
| @function im-log($message) { | ||
| @if feature-exists('at-error') { | ||
| @error $message; | ||
| } @else { | ||
| @warn $message; | ||
| $_: noop(); | ||
| } | ||
| @return $message; | ||
| } | ||
| /// | ||
| /// Wrapper mixin for the log function so it can be used with a more friendly | ||
| /// API than `@if im-log('..') {}` or `$_: im-log('..')`. Basically, use the function | ||
| /// within functions because it is not possible to include a mixin in a function | ||
| /// and use the mixin everywhere else because it's much more elegant. | ||
| /// | ||
| /// @param {String} $message - Message to log | ||
| /// | ||
| @mixin log($message) { | ||
| @if im-log($message) {} | ||
| } | ||
| /// | ||
| /// Function with no `@return` called next to `@warn` in Sass 3.3 | ||
| /// to trigger a compiling error and stop the process. | ||
| /// | ||
| @function noop() {} | ||
| /// | ||
| /// Determines whether a list of conditions is intercepted by the static breakpoint. | ||
| /// | ||
| /// @param {Arglist} $conditions - Media query conditions | ||
| /// | ||
| /// @return {Boolean} - Returns true if the conditions are intercepted by the static breakpoint | ||
| /// | ||
| @function im-intercepts-static-breakpoint($conditions...) { | ||
| $no-media-breakpoint-value: map-get($breakpoints, $im-no-media-breakpoint); | ||
| @if not $no-media-breakpoint-value { | ||
| @if im-log('`#{$im-no-media-breakpoint}` is not a valid breakpoint.') {} | ||
| } | ||
| @each $condition in $conditions { | ||
| @if not map-has-key($media-expressions, $condition) { | ||
| $operator: get-expression-operator($condition); | ||
| $prefix: get-expression-prefix($operator); | ||
| $value: get-expression-value($condition, $operator); | ||
| @if ($prefix == 'max' and $value <= $no-media-breakpoint-value) or | ||
| ($prefix == 'min' and $value > $no-media-breakpoint-value) { | ||
| @return false; | ||
| } | ||
| } @else if not index($im-no-media-expressions, $condition) { | ||
| @return false; | ||
| } | ||
| } | ||
| @return true; | ||
| } | ||
| /// | ||
| /// Slice `$list` between `$start` and `$end` indexes | ||
| /// | ||
| /// @access private | ||
| /// | ||
| /// @param {List} $list - List to slice | ||
| /// @param {Number} $start [1] - Start index | ||
| /// @param {Number} $end [length($list)] - End index | ||
| /// | ||
| /// @return {List} Sliced list | ||
| /// | ||
| @function slice($list, $start: 1, $end: length($list)) { | ||
| @if length($list) < 1 or $start > $end { | ||
| @return (); | ||
| } | ||
| $result: (); | ||
| @for $i from $start through $end { | ||
| $result: append($result, nth($list, $i)); | ||
| } | ||
| @return $result; | ||
| } | ||
| //// | ||
| /// String to number converter | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Casts a string into a number | ||
| /// | ||
| /// @param {String | Number} $value - Value to be parsed | ||
| /// | ||
| /// @return {Number} | ||
| /// | ||
| @function to-number($value) { | ||
| @if type-of($value) == 'number' { | ||
| @return $value; | ||
| } @else if type-of($value) != 'string' { | ||
| $_: im-log('Value for `to-number` should be a number or a string.'); | ||
| } | ||
| $first-character: str-slice($value, 1, 1); | ||
| $result: 0; | ||
| $digits: 0; | ||
| $minus: ($first-character == '-'); | ||
| $numbers: ('0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9); | ||
| // Remove +/- sign if present at first character | ||
| @if ($first-character == '+' or $first-character == '-') { | ||
| $value: str-slice($value, 2); | ||
| } | ||
| @for $i from 1 through str-length($value) { | ||
| $character: str-slice($value, $i, $i); | ||
| @if not (index(map-keys($numbers), $character) or $character == '.') { | ||
| @return to-length(if($minus, -$result, $result), str-slice($value, $i)) | ||
| } | ||
| @if $character == '.' { | ||
| $digits: 1; | ||
| } @else if $digits == 0 { | ||
| $result: $result * 10 + map-get($numbers, $character); | ||
| } @else { | ||
| $digits: $digits * 10; | ||
| $result: $result + map-get($numbers, $character) / $digits; | ||
| } | ||
| } | ||
| @return if($minus, -$result, $result); | ||
| } | ||
| /// | ||
| /// Add `$unit` to `$value` | ||
| /// | ||
| /// @param {Number} $value - Value to add unit to | ||
| /// @param {String} $unit - String representation of the unit | ||
| /// | ||
| /// @return {Number} - `$value` expressed in `$unit` | ||
| /// | ||
| @function to-length($value, $unit) { | ||
| $units: ('px': 1px, 'cm': 1cm, 'mm': 1mm, '%': 1%, 'ch': 1ch, 'pc': 1pc, 'in': 1in, 'em': 1em, 'rem': 1rem, 'pt': 1pt, 'ex': 1ex, 'vw': 1vw, 'vh': 1vh, 'vmin': 1vmin, 'vmax': 1vmax); | ||
| @if not index(map-keys($units), $unit) { | ||
| $_: im-log('Invalid unit `#{$unit}`.'); | ||
| } | ||
| @return $value * map-get($units, $unit); | ||
| } | ||
| /// | ||
| /// This mixin aims at redefining the configuration just for the scope of | ||
| /// the call. It is helpful when having a component needing an extended | ||
| /// configuration such as custom breakpoints (referred to as tweakpoints) | ||
| /// for instance. | ||
| /// | ||
| /// @author Hugo Giraudel | ||
| /// | ||
| /// @param {Map} $tweakpoints [()] - Map of tweakpoints to be merged with `$breakpoints` | ||
| /// @param {Map} $tweak-media-expressions [()] - Map of tweaked media expressions to be merged with `$media-expression` | ||
| /// | ||
| /// @example scss - Extend the global breakpoints with a tweakpoint | ||
| /// @include media-context(('custom': 678px)) { | ||
| /// .foo { | ||
| /// @include media('>phone', '<=custom') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// @example scss - Extend the global media expressions with a custom one | ||
| /// @include media-context($tweak-media-expressions: ('all': 'all')) { | ||
| /// .foo { | ||
| /// @include media('all', '>phone') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// @example scss - Extend both configuration maps | ||
| /// @include media-context(('custom': 678px), ('all': 'all')) { | ||
| /// .foo { | ||
| /// @include media('all', '>phone', '<=custom') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| @mixin media-context($tweakpoints: (), $tweak-media-expressions: ()) { | ||
| // Save global configuration | ||
| $global-breakpoints: $breakpoints; | ||
| $global-media-expressions: $media-expressions; | ||
| // Update global configuration | ||
| $breakpoints: map-merge($breakpoints, $tweakpoints) !global; | ||
| $media-expressions: map-merge($media-expressions, $tweak-media-expressions) !global; | ||
| @content; | ||
| // Restore global configuration | ||
| $breakpoints: $global-breakpoints !global; | ||
| $media-expressions: $global-media-expressions !global; | ||
| } | ||
| //// | ||
| /// include-media public exposed API | ||
| /// @author Eduardo Boucas | ||
| /// @access public | ||
| //// | ||
| /// | ||
| /// Generates a media query based on a list of conditions | ||
| /// | ||
| /// @param {Arglist} $conditions - Media query conditions | ||
| /// | ||
| /// @example scss - With a single set breakpoint | ||
| /// @include media('>phone') { } | ||
| /// | ||
| /// @example scss - With two set breakpoints | ||
| /// @include media('>phone', '<=tablet') { } | ||
| /// | ||
| /// @example scss - With custom values | ||
| /// @include media('>=358px', '<850px') { } | ||
| /// | ||
| /// @example scss - With set breakpoints with custom values | ||
| /// @include media('>desktop', '<=1350px') { } | ||
| /// | ||
| /// @example scss - With a static expression | ||
| /// @include media('retina2x') { } | ||
| /// | ||
| /// @example scss - Mixing everything | ||
| /// @include media('>=350px', '<tablet', 'retina3x') { } | ||
| /// | ||
| @mixin media($conditions...) { | ||
| $is-list-mode: (length($conditions) == 1 and type-of(nth($conditions, 1)) == 'list'); | ||
| @if ($im-media-support and length($conditions) == 0) or | ||
| (not $im-media-support and im-intercepts-static-breakpoint($conditions...) and not $is-list-mode) { | ||
| @content; | ||
| } @else if ($im-media-support and $is-list-mode) { | ||
| // List mode with AND / OR conditions | ||
| @media #{unquote(parse-media-query(resolve-feature-queries(nth($conditions, 1))))} { | ||
| @content; | ||
| } | ||
| } @else if ($im-media-support and length($conditions) > 0) { | ||
| // Legacy mode | ||
| @media #{unquote(parse-media-query(parse-expression(nth($conditions, 1))))} { | ||
| // Recursive call | ||
| @include media(slice($conditions, 2)...) { | ||
| @content; | ||
| } | ||
| } | ||
| } | ||
| } |
| @charset "UTF-8"; | ||
| // _ _ _ _ _ | ||
| // (_) | | | | | (_) | ||
| // _ _ __ ___| |_ _ __| | ___ _ __ ___ ___ __| |_ __ _ | ||
| // | | '_ \ / __| | | | |/ _` |/ _ \ | '_ ` _ \ / _ \/ _` | |/ _` | | ||
| // | | | | | (__| | |_| | (_| | __/ | | | | | | __/ (_| | | (_| | | ||
| // |_|_| |_|\___|_|\__,_|\__,_|\___| |_| |_| |_|\___|\__,_|_|\__,_| | ||
| // | ||
| // Simple, elegant and maintainable media queries in Sass | ||
| // v1.4.9 | ||
| // | ||
| // http://include-media.com | ||
| // | ||
| // Authors: Eduardo Boucas (@eduardoboucas) | ||
| // Hugo Giraudel (@hugogiraudel) | ||
| // | ||
| // This project is licensed under the terms of the MIT license | ||
| //// | ||
| /// include-media library public configuration | ||
| /// @author Eduardo Boucas | ||
| /// @access public | ||
| //// | ||
| /// | ||
| /// Creates a list of global breakpoints | ||
| /// | ||
| /// @example scss - Creates a single breakpoint with the label `phone` | ||
| /// $breakpoints: ('phone': 320px); | ||
| /// | ||
| $breakpoints: ( | ||
| 'phone': 320px, | ||
| 'tablet': 768px, | ||
| 'desktop': 1024px | ||
| ) !default; | ||
| /// | ||
| /// Creates a list of static expressions or media types | ||
| /// | ||
| /// @example scss - Creates a single media type (screen) | ||
| /// $media-expressions: ('screen': 'screen'); | ||
| /// | ||
| /// @example scss - Creates a static expression with logical disjunction (OR operator) | ||
| /// $media-expressions: ( | ||
| /// 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)' | ||
| /// ); | ||
| /// | ||
| $media-expressions: ( | ||
| 'screen': 'screen', | ||
| 'print': 'print', | ||
| 'handheld': 'handheld', | ||
| 'landscape': '(orientation: landscape)', | ||
| 'portrait': '(orientation: portrait)', | ||
| 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi), (min-resolution: 2dppx)', | ||
| 'retina3x': '(-webkit-min-device-pixel-ratio: 3), (min-resolution: 350dpi), (min-resolution: 3dppx)' | ||
| ) !default; | ||
| /// | ||
| /// Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals | ||
| /// | ||
| /// @example scss - Interval for pixels is defined as `1` by default | ||
| /// @include media('>128px') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 129px) {} | ||
| /// | ||
| /// @example scss - Interval for ems is defined as `0.01` by default | ||
| /// @include media('>20em') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 20.01em) {} | ||
| /// | ||
| /// @example scss - Interval for rems is defined as `0.1` by default, to be used with `font-size: 62.5%;` | ||
| /// @include media('>2.0rem') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 2.1rem) {} | ||
| /// | ||
| $unit-intervals: ( | ||
| 'px': 1, | ||
| 'em': 0.01, | ||
| 'rem': 0.1, | ||
| '': 0 | ||
| ) !default; | ||
| /// | ||
| /// Defines whether support for media queries is available, useful for creating separate stylesheets | ||
| /// for browsers that don't support media queries. | ||
| /// | ||
| /// @example scss - Disables support for media queries | ||
| /// $im-media-support: false; | ||
| /// @include media('>=tablet') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| $im-media-support: true !default; | ||
| /// | ||
| /// Selects which breakpoint to emulate when support for media queries is disabled. Media queries that start at or | ||
| /// intercept the breakpoint will be displayed, any others will be ignored. | ||
| /// | ||
| /// @example scss - This media query will show because it intercepts the static breakpoint | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// @include media('>=tablet') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| /// @example scss - This media query will NOT show because it does not intercept the desktop breakpoint | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'tablet'; | ||
| /// @include media('>=desktop') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* No output */ | ||
| /// | ||
| $im-no-media-breakpoint: 'desktop' !default; | ||
| /// | ||
| /// Selects which media expressions are allowed in an expression for it to be used when media queries | ||
| /// are not supported. | ||
| /// | ||
| /// @example scss - This media query will show because it intercepts the static breakpoint and contains only accepted media expressions | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// $im-no-media-expressions: ('screen'); | ||
| /// @include media('>=tablet', 'screen') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| /// @example scss - This media query will NOT show because it intercepts the static breakpoint but contains a media expression that is not accepted | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// $im-no-media-expressions: ('screen'); | ||
| /// @include media('>=tablet', 'retina2x') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* No output */ | ||
| /// | ||
| $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default; | ||
| //// | ||
| /// Cross-engine logging engine | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Log a message either with `@error` if supported | ||
| /// else with `@warn`, using `feature-exists('at-error')` | ||
| /// to detect support. | ||
| /// | ||
| /// @param {String} $message - Message to log | ||
| /// | ||
| @function im-log($message) { | ||
| @if feature-exists('at-error') { | ||
| @error $message; | ||
| } @else { | ||
| @warn $message; | ||
| $_: noop(); | ||
| } | ||
| @return $message; | ||
| } | ||
| /// | ||
| /// Wrapper mixin for the log function so it can be used with a more friendly | ||
| /// API than `@if im-log('..') {}` or `$_: im-log('..')`. Basically, use the function | ||
| /// within functions because it is not possible to include a mixin in a function | ||
| /// and use the mixin everywhere else because it's much more elegant. | ||
| /// | ||
| /// @param {String} $message - Message to log | ||
| /// | ||
| @mixin log($message) { | ||
| @if im-log($message) {} | ||
| } | ||
| /// | ||
| /// Function with no `@return` called next to `@warn` in Sass 3.3 | ||
| /// to trigger a compiling error and stop the process. | ||
| /// | ||
| @function noop() {} | ||
| /// | ||
| /// Determines whether a list of conditions is intercepted by the static breakpoint. | ||
| /// | ||
| /// @param {Arglist} $conditions - Media query conditions | ||
| /// | ||
| /// @return {Boolean} - Returns true if the conditions are intercepted by the static breakpoint | ||
| /// | ||
| @function im-intercepts-static-breakpoint($conditions...) { | ||
| $no-media-breakpoint-value: map-get($breakpoints, $im-no-media-breakpoint); | ||
| @if not $no-media-breakpoint-value { | ||
| @if im-log('`#{$im-no-media-breakpoint}` is not a valid breakpoint.') {} | ||
| } | ||
| @each $condition in $conditions { | ||
| @if not map-has-key($media-expressions, $condition) { | ||
| $operator: get-expression-operator($condition); | ||
| $prefix: get-expression-prefix($operator); | ||
| $value: get-expression-value($condition, $operator); | ||
| @if ($prefix == 'max' and $value <= $no-media-breakpoint-value) or | ||
| ($prefix == 'min' and $value > $no-media-breakpoint-value) { | ||
| @return false; | ||
| } | ||
| } @else if not index($im-no-media-expressions, $condition) { | ||
| @return false; | ||
| } | ||
| } | ||
| @return true; | ||
| } | ||
| //// | ||
| /// Parsing engine | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Get operator of an expression | ||
| /// | ||
| /// @param {String} $expression - Expression to extract operator from | ||
| /// | ||
| /// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤` | ||
| /// | ||
| @function get-expression-operator($expression) { | ||
| @each $operator in ('>=', '>', '<=', '<', '≥', '≤') { | ||
| @if str-index($expression, $operator) { | ||
| @return $operator; | ||
| } | ||
| } | ||
| // It is not possible to include a mixin inside a function, so we have to | ||
| // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because | ||
| // functions cannot be called anywhere in Sass, we need to hack the call in | ||
| // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with | ||
| // Sass 3.3, change this line in `@if im-log(..) {}` instead. | ||
| $_: im-log('No operator found in `#{$expression}`.'); | ||
| } | ||
| /// | ||
| /// Get dimension of an expression, based on a found operator | ||
| /// | ||
| /// @param {String} $expression - Expression to extract dimension from | ||
| /// @param {String} $operator - Operator from `$expression` | ||
| /// | ||
| /// @return {String} - `width` or `height` (or potentially anything else) | ||
| /// | ||
| @function get-expression-dimension($expression, $operator) { | ||
| $operator-index: str-index($expression, $operator); | ||
| $parsed-dimension: str-slice($expression, 0, $operator-index - 1); | ||
| $dimension: 'width'; | ||
| @if str-length($parsed-dimension) > 0 { | ||
| $dimension: $parsed-dimension; | ||
| } | ||
| @return $dimension; | ||
| } | ||
| /// | ||
| /// Get dimension prefix based on an operator | ||
| /// | ||
| /// @param {String} $operator - Operator | ||
| /// | ||
| /// @return {String} - `min` or `max` | ||
| /// | ||
| @function get-expression-prefix($operator) { | ||
| @return if(index(('<', '<=', '≤'), $operator), 'max', 'min'); | ||
| } | ||
| /// | ||
| /// Get value of an expression, based on a found operator | ||
| /// | ||
| /// @param {String} $expression - Expression to extract value from | ||
| /// @param {String} $operator - Operator from `$expression` | ||
| /// | ||
| /// @return {Number} - A numeric value | ||
| /// | ||
| @function get-expression-value($expression, $operator) { | ||
| $operator-index: str-index($expression, $operator); | ||
| $value: str-slice($expression, $operator-index + str-length($operator)); | ||
| @if map-has-key($breakpoints, $value) { | ||
| $value: map-get($breakpoints, $value); | ||
| } @else { | ||
| $value: to-number($value); | ||
| } | ||
| $interval: map-get($unit-intervals, unit($value)); | ||
| @if not $interval { | ||
| // It is not possible to include a mixin inside a function, so we have to | ||
| // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because | ||
| // functions cannot be called anywhere in Sass, we need to hack the call in | ||
| // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with | ||
| // Sass 3.3, change this line in `@if im-log(..) {}` instead. | ||
| $_: im-log('Unknown unit `#{unit($value)}`.'); | ||
| } | ||
| @if $operator == '>' { | ||
| $value: $value + $interval; | ||
| } @else if $operator == '<' { | ||
| $value: $value - $interval; | ||
| } | ||
| @return $value; | ||
| } | ||
| /// | ||
| /// Parse an expression to return a valid media-query expression | ||
| /// | ||
| /// @param {String} $expression - Expression to parse | ||
| /// | ||
| /// @return {String} - Valid media query | ||
| /// | ||
| @function parse-expression($expression) { | ||
| // If it is part of $media-expressions, it has no operator | ||
| // then there is no need to go any further, just return the value | ||
| @if map-has-key($media-expressions, $expression) { | ||
| @return map-get($media-expressions, $expression); | ||
| } | ||
| $operator: get-expression-operator($expression); | ||
| $dimension: get-expression-dimension($expression, $operator); | ||
| $prefix: get-expression-prefix($operator); | ||
| $value: get-expression-value($expression, $operator); | ||
| @return '(#{$prefix}-#{$dimension}: #{$value})'; | ||
| } | ||
| /// | ||
| /// Slice `$list` between `$start` and `$end` indexes | ||
| /// | ||
| /// @access private | ||
| /// | ||
| /// @param {List} $list - List to slice | ||
| /// @param {Number} $start [1] - Start index | ||
| /// @param {Number} $end [length($list)] - End index | ||
| /// | ||
| /// @return {List} Sliced list | ||
| /// | ||
| @function slice($list, $start: 1, $end: length($list)) { | ||
| @if length($list) < 1 or $start > $end { | ||
| @return (); | ||
| } | ||
| $result: (); | ||
| @for $i from $start through $end { | ||
| $result: append($result, nth($list, $i)); | ||
| } | ||
| @return $result; | ||
| } | ||
| //// | ||
| /// String to number converter | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Casts a string into a number | ||
| /// | ||
| /// @param {String | Number} $value - Value to be parsed | ||
| /// | ||
| /// @return {Number} | ||
| /// | ||
| @function to-number($value) { | ||
| @if type-of($value) == 'number' { | ||
| @return $value; | ||
| } @else if type-of($value) != 'string' { | ||
| $_: im-log('Value for `to-number` should be a number or a string.'); | ||
| } | ||
| $first-character: str-slice($value, 1, 1); | ||
| $result: 0; | ||
| $digits: 0; | ||
| $minus: ($first-character == '-'); | ||
| $numbers: ('0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9); | ||
| // Remove +/- sign if present at first character | ||
| @if ($first-character == '+' or $first-character == '-') { | ||
| $value: str-slice($value, 2); | ||
| } | ||
| @for $i from 1 through str-length($value) { | ||
| $character: str-slice($value, $i, $i); | ||
| @if not (index(map-keys($numbers), $character) or $character == '.') { | ||
| @return to-length(if($minus, -$result, $result), str-slice($value, $i)) | ||
| } | ||
| @if $character == '.' { | ||
| $digits: 1; | ||
| } @else if $digits == 0 { | ||
| $result: $result * 10 + map-get($numbers, $character); | ||
| } @else { | ||
| $digits: $digits * 10; | ||
| $result: $result + map-get($numbers, $character) / $digits; | ||
| } | ||
| } | ||
| @return if($minus, -$result, $result); | ||
| } | ||
| /// | ||
| /// Add `$unit` to `$value` | ||
| /// | ||
| /// @param {Number} $value - Value to add unit to | ||
| /// @param {String} $unit - String representation of the unit | ||
| /// | ||
| /// @return {Number} - `$value` expressed in `$unit` | ||
| /// | ||
| @function to-length($value, $unit) { | ||
| $units: ('px': 1px, 'cm': 1cm, 'mm': 1mm, '%': 1%, 'ch': 1ch, 'pc': 1pc, 'in': 1in, 'em': 1em, 'rem': 1rem, 'pt': 1pt, 'ex': 1ex, 'vw': 1vw, 'vh': 1vh, 'vmin': 1vmin, 'vmax': 1vmax); | ||
| @if not index(map-keys($units), $unit) { | ||
| $_: im-log('Invalid unit `#{$unit}`.'); | ||
| } | ||
| @return $value * map-get($units, $unit); | ||
| } | ||
| /// | ||
| /// This mixin aims at redefining the configuration just for the scope of | ||
| /// the call. It is helpful when having a component needing an extended | ||
| /// configuration such as custom breakpoints (referred to as tweakpoints) | ||
| /// for instance. | ||
| /// | ||
| /// @author Hugo Giraudel | ||
| /// | ||
| /// @param {Map} $tweakpoints [()] - Map of tweakpoints to be merged with `$breakpoints` | ||
| /// @param {Map} $tweak-media-expressions [()] - Map of tweaked media expressions to be merged with `$media-expression` | ||
| /// | ||
| /// @example scss - Extend the global breakpoints with a tweakpoint | ||
| /// @include media-context(('custom': 678px)) { | ||
| /// .foo { | ||
| /// @include media('>phone', '<=custom') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// @example scss - Extend the global media expressions with a custom one | ||
| /// @include media-context($tweak-media-expressions: ('all': 'all')) { | ||
| /// .foo { | ||
| /// @include media('all', '>phone') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// @example scss - Extend both configuration maps | ||
| /// @include media-context(('custom': 678px), ('all': 'all')) { | ||
| /// .foo { | ||
| /// @include media('all', '>phone', '<=custom') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| @mixin media-context($tweakpoints: (), $tweak-media-expressions: ()) { | ||
| // Save global configuration | ||
| $global-breakpoints: $breakpoints; | ||
| $global-media-expressions: $media-expressions; | ||
| // Update global configuration | ||
| $breakpoints: map-merge($breakpoints, $tweakpoints) !global; | ||
| $media-expressions: map-merge($media-expressions, $tweak-media-expressions) !global; | ||
| @content; | ||
| // Restore global configuration | ||
| $breakpoints: $global-breakpoints !global; | ||
| $media-expressions: $global-media-expressions !global; | ||
| } | ||
| //// | ||
| /// include-media public exposed API | ||
| /// @author Eduardo Boucas | ||
| /// @access public | ||
| //// | ||
| /// | ||
| /// Generates a media query based on a list of conditions | ||
| /// | ||
| /// @param {Arglist} $conditions - Media query conditions | ||
| /// | ||
| /// @example scss - With a single set breakpoint | ||
| /// @include media('>phone') { } | ||
| /// | ||
| /// @example scss - With two set breakpoints | ||
| /// @include media('>phone', '<=tablet') { } | ||
| /// | ||
| /// @example scss - With custom values | ||
| /// @include media('>=358px', '<850px') { } | ||
| /// | ||
| /// @example scss - With set breakpoints with custom values | ||
| /// @include media('>desktop', '<=1350px') { } | ||
| /// | ||
| /// @example scss - With a static expression | ||
| /// @include media('retina2x') { } | ||
| /// | ||
| /// @example scss - Mixing everything | ||
| /// @include media('>=350px', '<tablet', 'retina3x') { } | ||
| /// | ||
| @mixin media($conditions...) { | ||
| @if ($im-media-support and length($conditions) == 0) or | ||
| (not $im-media-support and im-intercepts-static-breakpoint($conditions...)) { | ||
| @content; | ||
| } @else if ($im-media-support and length($conditions) > 0) { | ||
| @media #{unquote(parse-expression(nth($conditions, 1)))} { | ||
| // Recursive call | ||
| @include media(slice($conditions, 2)...) { | ||
| @content; | ||
| } | ||
| } | ||
| } | ||
| } |
| //// | ||
| /// include-media library public configuration | ||
| /// @author Eduardo Boucas | ||
| /// @access public | ||
| //// | ||
| /// | ||
| /// Creates a list of global breakpoints | ||
| /// | ||
| /// @example scss - Creates a single breakpoint with the label `phone` | ||
| /// $breakpoints: ('phone': 320px); | ||
| /// | ||
| $breakpoints: ( | ||
| 'phone': 320px, | ||
| 'tablet': 768px, | ||
| 'desktop': 1024px | ||
| ) !default; | ||
| /// | ||
| /// Creates a list of static expressions or media types | ||
| /// | ||
| /// @example scss - Creates a single media type (screen) | ||
| /// $media-expressions: ('screen': 'screen'); | ||
| /// | ||
| /// @example scss - Creates a static expression with logical disjunction (OR operator) | ||
| /// $media-expressions: ( | ||
| /// 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)' | ||
| /// ); | ||
| /// | ||
| $media-expressions: ( | ||
| 'screen': 'screen', | ||
| 'print': 'print', | ||
| 'handheld': 'handheld', | ||
| 'landscape': '(orientation: landscape)', | ||
| 'portrait': '(orientation: portrait)', | ||
| 'retina2x': ('(-webkit-min-device-pixel-ratio: 2)', '(min-resolution: 192dpi)', '(min-resolution: 2dppx)'), | ||
| 'retina3x': ('(-webkit-min-device-pixel-ratio: 3)', '(min-resolution: 350dpi)', '(min-resolution: 3dppx)') | ||
| ) !default; | ||
| /// | ||
| /// Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals | ||
| /// | ||
| /// @example scss - Interval for pixels is defined as `1` by default | ||
| /// @include media('>128px') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 129px) {} | ||
| /// | ||
| /// @example scss - Interval for ems is defined as `0.01` by default | ||
| /// @include media('>20em') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 20.01em) {} | ||
| /// | ||
| /// @example scss - Interval for rems is defined as `0.1` by default, to be used with `font-size: 62.5%;` | ||
| /// @include media('>2.0rem') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 2.1rem) {} | ||
| /// | ||
| $unit-intervals: ( | ||
| 'px': 1, | ||
| 'em': 0.01, | ||
| 'rem': 0.1, | ||
| '': 0 | ||
| ) !default; | ||
| /// | ||
| /// Defines whether support for media queries is available, useful for creating separate stylesheets | ||
| /// for browsers that don't support media queries. | ||
| /// | ||
| /// @example scss - Disables support for media queries | ||
| /// $im-media-support: false; | ||
| /// @include media('>=tablet') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| $im-media-support: true !default; | ||
| /// | ||
| /// Selects which breakpoint to emulate when support for media queries is disabled. Media queries that start at or | ||
| /// intercept the breakpoint will be displayed, any others will be ignored. | ||
| /// | ||
| /// @example scss - This media query will show because it intercepts the static breakpoint | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// @include media('>=tablet') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| /// @example scss - This media query will NOT show because it does not intercept the desktop breakpoint | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'tablet'; | ||
| /// @include media('>=desktop') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* No output */ | ||
| /// | ||
| $im-no-media-breakpoint: 'desktop' !default; | ||
| /// | ||
| /// Selects which media expressions are allowed in an expression for it to be used when media queries | ||
| /// are not supported. | ||
| /// | ||
| /// @example scss - This media query will show because it intercepts the static breakpoint and contains only accepted media expressions | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// $im-no-media-expressions: ('screen'); | ||
| /// @include media('>=tablet', 'screen') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| /// @example scss - This media query will NOT show because it intercepts the static breakpoint but contains a media expression that is not accepted | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// $im-no-media-expressions: ('screen'); | ||
| /// @include media('>=tablet', 'retina2x') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* No output */ | ||
| /// | ||
| $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default; |
| //// | ||
| /// include-media public exposed API | ||
| /// @author Eduardo Boucas | ||
| /// @access public | ||
| //// | ||
| /// | ||
| /// Generates a media query based on a list of conditions | ||
| /// | ||
| /// @param {Arglist} $conditions - Media query conditions | ||
| /// | ||
| /// @example scss - With a single set breakpoint | ||
| /// @include media('>phone') { } | ||
| /// | ||
| /// @example scss - With two set breakpoints | ||
| /// @include media('>phone', '<=tablet') { } | ||
| /// | ||
| /// @example scss - With custom values | ||
| /// @include media('>=358px', '<850px') { } | ||
| /// | ||
| /// @example scss - With set breakpoints with custom values | ||
| /// @include media('>desktop', '<=1350px') { } | ||
| /// | ||
| /// @example scss - With a static expression | ||
| /// @include media('retina2x') { } | ||
| /// | ||
| /// @example scss - Mixing everything | ||
| /// @include media('>=350px', '<tablet', 'retina3x') { } | ||
| /// | ||
| @mixin media($conditions...) { | ||
| $is-list-mode: (length($conditions) == 1 and type-of(nth($conditions, 1)) == 'list'); | ||
| @if ($im-media-support and length($conditions) == 0) or | ||
| (not $im-media-support and im-intercepts-static-breakpoint($conditions...) and not $is-list-mode) { | ||
| @content; | ||
| } @else if ($im-media-support and $is-list-mode) { | ||
| // List mode with AND / OR conditions | ||
| @media #{unquote(parse-media-query(resolve-feature-queries(nth($conditions, 1))))} { | ||
| @content; | ||
| } | ||
| } @else if ($im-media-support and length($conditions) > 0) { | ||
| // Legacy mode | ||
| @media #{unquote(parse-media-query(parse-expression(nth($conditions, 1))))} { | ||
| // Recursive call | ||
| @include media(slice($conditions, 2)...) { | ||
| @content; | ||
| } | ||
| } | ||
| } | ||
| } |
| //// | ||
| /// Parsing engine | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Get operator of an expression | ||
| /// | ||
| /// @param {String} $expression - Expression to extract operator from | ||
| /// | ||
| /// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤` | ||
| /// | ||
| @function get-expression-operator($expression) { | ||
| @each $operator in ('>=', '>', '<=', '<', '≥', '≤') { | ||
| @if str-index($expression, $operator) { | ||
| @return $operator; | ||
| } | ||
| } | ||
| // It is not possible to include a mixin inside a function, so we have to | ||
| // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because | ||
| // functions cannot be called anywhere in Sass, we need to hack the call in | ||
| // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with | ||
| // Sass 3.3, change this line in `@if im-log(..) {}` instead. | ||
| $_: im-log('No operator found in `#{$expression}`.'); | ||
| } | ||
| /// | ||
| /// Get dimension of an expression, based on a found operator | ||
| /// | ||
| /// @param {String} $expression - Expression to extract dimension from | ||
| /// @param {String} $operator - Operator from `$expression` | ||
| /// | ||
| /// @return {String} - `width` or `height` (or potentially anything else) | ||
| /// | ||
| @function get-expression-dimension($expression, $operator) { | ||
| $operator-index: str-index($expression, $operator); | ||
| $parsed-dimension: str-slice($expression, 0, $operator-index - 1); | ||
| $dimension: 'width'; | ||
| @if str-length($parsed-dimension) > 0 { | ||
| $dimension: $parsed-dimension; | ||
| } | ||
| @return $dimension; | ||
| } | ||
| /// | ||
| /// Get dimension prefix based on an operator | ||
| /// | ||
| /// @param {String} $operator - Operator | ||
| /// | ||
| /// @return {String} - `min` or `max` | ||
| /// | ||
| @function get-expression-prefix($operator) { | ||
| @return if(index(('<', '<=', '≤'), $operator), 'max', 'min'); | ||
| } | ||
| /// | ||
| /// Get value of an expression, based on a found operator | ||
| /// | ||
| /// @param {String} $expression - Expression to extract value from | ||
| /// @param {String} $operator - Operator from `$expression` | ||
| /// | ||
| /// @return {Number} - A numeric value | ||
| /// | ||
| @function get-expression-value($expression, $operator) { | ||
| $operator-index: str-index($expression, $operator); | ||
| $value: str-slice($expression, $operator-index + str-length($operator)); | ||
| @if map-has-key($breakpoints, $value) { | ||
| $value: map-get($breakpoints, $value); | ||
| } @else { | ||
| $value: to-number($value); | ||
| } | ||
| $interval: map-get($unit-intervals, unit($value)); | ||
| @if not $interval { | ||
| // It is not possible to include a mixin inside a function, so we have to | ||
| // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because | ||
| // functions cannot be called anywhere in Sass, we need to hack the call in | ||
| // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with | ||
| // Sass 3.3, change this line in `@if im-log(..) {}` instead. | ||
| $_: im-log('Unknown unit `#{unit($value)}`.'); | ||
| } | ||
| @if $operator == '>' { | ||
| $value: $value + $interval; | ||
| } @else if $operator == '<' { | ||
| $value: $value - $interval; | ||
| } | ||
| @return $value; | ||
| } | ||
| /// | ||
| /// Parse an expression to return a valid media-query expression | ||
| /// | ||
| /// @param {String} $expression - Expression to parse | ||
| /// | ||
| /// @return {String} - Valid media query | ||
| /// | ||
| @function parse-expression($expression) { | ||
| // If it is part of $media-expressions, it has no operator | ||
| // then there is no need to go any further, just return the value | ||
| @if map-has-key($media-expressions, $expression) { | ||
| @return map-get($media-expressions, $expression); | ||
| } | ||
| $operator: get-expression-operator($expression); | ||
| $dimension: get-expression-dimension($expression, $operator); | ||
| $prefix: get-expression-prefix($operator); | ||
| $value: get-expression-value($expression, $operator); | ||
| @return '(#{$prefix}-#{$dimension}: #{$value})'; | ||
| } |
| /// | ||
| /// Get a shallow list of resolved feature queries | ||
| /// | ||
| /// @param {String} $queries - A nested list structure | ||
| /// @param {Boolean} $do-parse - Try to parse expressions | ||
| /// | ||
| /// @return {List} - A one item deep nested list like `(a b, c d e)` | ||
| /// | ||
| @function resolve-feature-queries($queries, $do-parse: true) { | ||
| $separator: list-separator($queries); | ||
| $result: if($separator == 'space', ((), ), ()); | ||
| @each $query in $queries { | ||
| $chainable-queries: (); | ||
| @if (type-of($query) == 'list') { | ||
| // List item is a list itself | ||
| $chainable-queries: resolve-feature-queries($query); | ||
| } @else { | ||
| // List item is a string | ||
| $parsed-query: if($do-parse, parse-expression($query), $query); | ||
| @if (type-of($parsed-query) == 'list') { | ||
| // Parsed expression is a list | ||
| $chainable-queries: resolve-feature-queries($parsed-query, false); | ||
| } @else { | ||
| // Parsed expression is a string | ||
| $chainable-queries: ($parsed-query); | ||
| } | ||
| } | ||
| $result: append-feature-query($result, $chainable-queries, $separator); | ||
| } | ||
| @return $result; | ||
| } | ||
| /// | ||
| /// Combine two query lists as a logical AND / OR operation | ||
| /// | ||
| /// @param {List} $base-queries - The host list | ||
| /// @param {List} $append-queries - The list that is being appended | ||
| /// @param {String} $separator - Either `space` or `comma` | ||
| /// | ||
| /// @return {List} - A one item deep nested list like `(a b, c d e)` | ||
| /// | ||
| @function append-feature-query($base-queries, $append-queries, $separator) { | ||
| $result: if($separator == 'space', (), $base-queries); | ||
| @each $append-query in $append-queries { | ||
| @if ($separator == 'space') { | ||
| // Logical AND | ||
| @each $base-query in $base-queries { | ||
| $updated-query: join($base-query, $append-query, $separator); | ||
| $result: append($result, $updated-query, 'comma'); | ||
| } | ||
| } @else { | ||
| // Logical OR | ||
| $result: append($result, $append-query, 'comma'); | ||
| } | ||
| } | ||
| @return $result; | ||
| } | ||
| /// | ||
| /// Parse a list of resolved expressions to return a valid media-query | ||
| /// | ||
| /// @param {List} $queries - A one item deep nested list like `(a b, c d e)` | ||
| /// | ||
| /// @return {String} - A valid media-query string | ||
| /// | ||
| @function parse-media-query($queries) { | ||
| $result: null; | ||
| $flat-queries: (); | ||
| $separator: list-separator($queries); | ||
| $conjunction: if($separator == 'space', ' and ', ', '); | ||
| @if (type-of($queries) == 'string') { | ||
| @return $queries; | ||
| } | ||
| @each $query in $queries { | ||
| @if (type-of($query) == 'list') { | ||
| $flat-queries: append($flat-queries, parse-media-query($query)); | ||
| } @else { | ||
| $flat-queries: append($flat-queries, $query); | ||
| } | ||
| } | ||
| @for $i from 1 through length($flat-queries) { | ||
| $e: nth($flat-queries, $i); | ||
| $result: unquote('#{$result}#{$e}#{if($i != length($flat-queries), $conjunction, '')}'); | ||
| } | ||
| @return $result; | ||
| } |
| //// | ||
| /// Cross-engine logging engine | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Log a message either with `@error` if supported | ||
| /// else with `@warn`, using `feature-exists('at-error')` | ||
| /// to detect support. | ||
| /// | ||
| /// @param {String} $message - Message to log | ||
| /// | ||
| @function im-log($message) { | ||
| @if feature-exists('at-error') { | ||
| @error $message; | ||
| } @else { | ||
| @warn $message; | ||
| $_: noop(); | ||
| } | ||
| @return $message; | ||
| } | ||
| /// | ||
| /// Wrapper mixin for the log function so it can be used with a more friendly | ||
| /// API than `@if im-log('..') {}` or `$_: im-log('..')`. Basically, use the function | ||
| /// within functions because it is not possible to include a mixin in a function | ||
| /// and use the mixin everywhere else because it's much more elegant. | ||
| /// | ||
| /// @param {String} $message - Message to log | ||
| /// | ||
| @mixin log($message) { | ||
| @if im-log($message) {} | ||
| } | ||
| /// | ||
| /// Function with no `@return` called next to `@warn` in Sass 3.3 | ||
| /// to trigger a compiling error and stop the process. | ||
| /// | ||
| @function noop() {} |
| /// | ||
| /// Determines whether a list of conditions is intercepted by the static breakpoint. | ||
| /// | ||
| /// @param {Arglist} $conditions - Media query conditions | ||
| /// | ||
| /// @return {Boolean} - Returns true if the conditions are intercepted by the static breakpoint | ||
| /// | ||
| @function im-intercepts-static-breakpoint($conditions...) { | ||
| $no-media-breakpoint-value: map-get($breakpoints, $im-no-media-breakpoint); | ||
| @if not $no-media-breakpoint-value { | ||
| @if im-log('`#{$im-no-media-breakpoint}` is not a valid breakpoint.') {} | ||
| } | ||
| @each $condition in $conditions { | ||
| @if not map-has-key($media-expressions, $condition) { | ||
| $operator: get-expression-operator($condition); | ||
| $prefix: get-expression-prefix($operator); | ||
| $value: get-expression-value($condition, $operator); | ||
| @if ($prefix == 'max' and $value <= $no-media-breakpoint-value) or | ||
| ($prefix == 'min' and $value > $no-media-breakpoint-value) { | ||
| @return false; | ||
| } | ||
| } @else if not index($im-no-media-expressions, $condition) { | ||
| @return false; | ||
| } | ||
| } | ||
| @return true; | ||
| } |
| /// | ||
| /// Slice `$list` between `$start` and `$end` indexes | ||
| /// | ||
| /// @access private | ||
| /// | ||
| /// @param {List} $list - List to slice | ||
| /// @param {Number} $start [1] - Start index | ||
| /// @param {Number} $end [length($list)] - End index | ||
| /// | ||
| /// @return {List} Sliced list | ||
| /// | ||
| @function slice($list, $start: 1, $end: length($list)) { | ||
| @if length($list) < 1 or $start > $end { | ||
| @return (); | ||
| } | ||
| $result: (); | ||
| @for $i from $start through $end { | ||
| $result: append($result, nth($list, $i)); | ||
| } | ||
| @return $result; | ||
| } |
| //// | ||
| /// String to number converter | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Casts a string into a number | ||
| /// | ||
| /// @param {String | Number} $value - Value to be parsed | ||
| /// | ||
| /// @return {Number} | ||
| /// | ||
| @function to-number($value) { | ||
| @if type-of($value) == 'number' { | ||
| @return $value; | ||
| } @else if type-of($value) != 'string' { | ||
| $_: im-log('Value for `to-number` should be a number or a string.'); | ||
| } | ||
| $first-character: str-slice($value, 1, 1); | ||
| $result: 0; | ||
| $digits: 0; | ||
| $minus: ($first-character == '-'); | ||
| $numbers: ('0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9); | ||
| // Remove +/- sign if present at first character | ||
| @if ($first-character == '+' or $first-character == '-') { | ||
| $value: str-slice($value, 2); | ||
| } | ||
| @for $i from 1 through str-length($value) { | ||
| $character: str-slice($value, $i, $i); | ||
| @if not (index(map-keys($numbers), $character) or $character == '.') { | ||
| @return to-length(if($minus, -$result, $result), str-slice($value, $i)) | ||
| } | ||
| @if $character == '.' { | ||
| $digits: 1; | ||
| } @else if $digits == 0 { | ||
| $result: $result * 10 + map-get($numbers, $character); | ||
| } @else { | ||
| $digits: $digits * 10; | ||
| $result: $result + map-get($numbers, $character) / $digits; | ||
| } | ||
| } | ||
| @return if($minus, -$result, $result); | ||
| } | ||
| /// | ||
| /// Add `$unit` to `$value` | ||
| /// | ||
| /// @param {Number} $value - Value to add unit to | ||
| /// @param {String} $unit - String representation of the unit | ||
| /// | ||
| /// @return {Number} - `$value` expressed in `$unit` | ||
| /// | ||
| @function to-length($value, $unit) { | ||
| $units: ('px': 1px, 'cm': 1cm, 'mm': 1mm, '%': 1%, 'ch': 1ch, 'pc': 1pc, 'in': 1in, 'em': 1em, 'rem': 1rem, 'pt': 1pt, 'ex': 1ex, 'vw': 1vw, 'vh': 1vh, 'vmin': 1vmin, 'vmax': 1vmax); | ||
| @if not index(map-keys($units), $unit) { | ||
| $_: im-log('Invalid unit `#{$unit}`.'); | ||
| } | ||
| @return $value * map-get($units, $unit); | ||
| } |
| /// | ||
| /// This mixin aims at redefining the configuration just for the scope of | ||
| /// the call. It is helpful when having a component needing an extended | ||
| /// configuration such as custom breakpoints (referred to as tweakpoints) | ||
| /// for instance. | ||
| /// | ||
| /// @author Hugo Giraudel | ||
| /// | ||
| /// @param {Map} $tweakpoints [()] - Map of tweakpoints to be merged with `$breakpoints` | ||
| /// @param {Map} $tweak-media-expressions [()] - Map of tweaked media expressions to be merged with `$media-expression` | ||
| /// | ||
| /// @example scss - Extend the global breakpoints with a tweakpoint | ||
| /// @include media-context(('custom': 678px)) { | ||
| /// .foo { | ||
| /// @include media('>phone', '<=custom') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// @example scss - Extend the global media expressions with a custom one | ||
| /// @include media-context($tweak-media-expressions: ('all': 'all')) { | ||
| /// .foo { | ||
| /// @include media('all', '>phone') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// @example scss - Extend both configuration maps | ||
| /// @include media-context(('custom': 678px), ('all': 'all')) { | ||
| /// .foo { | ||
| /// @include media('all', '>phone', '<=custom') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| @mixin media-context($tweakpoints: (), $tweak-media-expressions: ()) { | ||
| // Save global configuration | ||
| $global-breakpoints: $breakpoints; | ||
| $global-media-expressions: $media-expressions; | ||
| // Update global configuration | ||
| $breakpoints: map-merge($breakpoints, $tweakpoints) !global; | ||
| $media-expressions: map-merge($media-expressions, $tweak-media-expressions) !global; | ||
| @content; | ||
| // Restore global configuration | ||
| $breakpoints: $global-breakpoints !global; | ||
| $media-expressions: $global-media-expressions !global; | ||
| } |
| /// Test `$function` with `$spec` test suite | ||
| /// | ||
| /// @author Hugo Giraudel | ||
| /// | ||
| /// @param {String} $function - Name of function to test | ||
| /// @param {Map} $spec - Test suite to run `$function` against | ||
| /// | ||
| /// @return {Map} | ||
| /// * `function`: `$function` | ||
| /// * `length`: the length of `$tests` | ||
| /// * `pass`: number of passing tests out of `length` | ||
| /// * `fail`: number of failing tests out of `length` | ||
| /// * `tests`: list of maps containing: | ||
| /// * `input`: test input (key from `$tests`) | ||
| /// * `expected`: expected result from `input` | ||
| /// * `actual`: actual result from `input` | ||
| /// * `pass`: whether the test passed or not | ||
| /// * `fail`: whether the test failed or not | ||
| /// | ||
| /// @example scss - Testing of a `double` function | ||
| /// @function double($value) { @return $value * 2 } | ||
| /// | ||
| /// $test: test('double', ( | ||
| /// 1: 2, | ||
| /// 2: 4, | ||
| /// 3: 6, | ||
| /// 4: 8, | ||
| /// )); | ||
| /// | ||
| /// @example scss - Result of `double` tests | ||
| /// ( | ||
| /// 'function': 'double', | ||
| /// 'length': 4, | ||
| /// 'pass': 4, | ||
| /// 'fail': 0, | ||
| /// 'tests': ( ... ), | ||
| /// ) | ||
| /// | ||
| /// @example scss - `tests` value from result of `double` tests | ||
| /// ( | ||
| /// ( | ||
| /// 'input': 1, | ||
| /// 'expected': 2, | ||
| /// 'actual': 2, | ||
| /// 'pass': true, | ||
| /// 'fail': false, | ||
| /// ), | ||
| /// // ... | ||
| /// ) | ||
| @function test($function, $spec) { | ||
| $passing-tests: 0; | ||
| $tests: (); | ||
| @each $arguments, $expected-result in $spec { | ||
| $actual-result: call($function, $arguments...); | ||
| $passed: $actual-result == $expected-result; | ||
| $tests: append($tests, ( | ||
| 'input': $arguments, | ||
| 'expected': $expected-result, | ||
| 'actual': $actual-result, | ||
| 'pass': $passed, | ||
| 'fail': not $passed, | ||
| ), 'comma'); | ||
| @if $passed { | ||
| $passing-tests: $passing-tests + 1; | ||
| } | ||
| } | ||
| @return ( | ||
| 'function': $function, | ||
| 'length': length($tests), | ||
| 'tests': $tests, | ||
| 'pass': $passing-tests, | ||
| 'fail': length($tests) - $passing-tests, | ||
| ); | ||
| } | ||
| /// Mixin decorating the result of `test(..)` function to throw it as an error | ||
| /// | ||
| /// @author Hugo Giraudel | ||
| /// | ||
| /// @param {Map} $data - Return of `test(..)` function | ||
| /// | ||
| /// @example scss - Printing the result of `double` function test | ||
| /// @include run(test('double', $tests-double)); | ||
| /// | ||
| /// @example scss - Result of `double` function test | ||
| /// Started tests for function `double` | ||
| /// ---------- | ||
| /// Test 1 out of 4... ✔ | ||
| /// Test 2 out of 4... ✔ | ||
| /// Test 3 out of 4... ✔ | ||
| /// Test 4 out of 4... ✔ | ||
| /// ---------- | ||
| /// Finished: 0 test(s) failing out of 4 | ||
| @mixin run($data) { | ||
| $output: ''; | ||
| $length: map-get($data, 'length'); | ||
| $tests: map-get($data, 'tests'); | ||
| @each $test in $tests { | ||
| $output: $output | ||
| + 'Test #{index($tests, $test)} out of #{$length}... ' | ||
| + if(map-get($test, 'pass'), '✔', '✘\a Expected : `#{map-get($test, "expected")}`\a Actual : `#{map-get($test, "actual")}`') + '\a '; | ||
| } | ||
| $message: 'Started tests for function `#{map-get($data, "function")}`\a ' | ||
| + '----------\a ' | ||
| + $output + '\a ' | ||
| + '----------\a ' | ||
| + 'Finished: #{map-get($data, "fail")} test(s) failing out of #{$length}'; | ||
| @if map-get($data, "fail") > 0 { | ||
| @error $message; | ||
| } @else { | ||
| @warn $message; | ||
| } | ||
| } |
| @import 'parse-expression'; | ||
| @import 'to-number'; |
| @import '../dist/include-media'; | ||
| @import 'helpers/SassyTester'; | ||
| $tests-parse-expression: map-merge($media-expressions, ( | ||
| '>phone': '(min-width: 321px)', | ||
| '<phone': '(max-width: 319px)', | ||
| '>=phone': '(min-width: 320px)', | ||
| '≥phone': '(min-width: 320px)', | ||
| '<=phone': '(max-width: 320px)', | ||
| '≤phone': '(max-width: 320px)', | ||
| '>tablet': '(min-width: 769px)', | ||
| '<tablet': '(max-width: 767px)', | ||
| '>=tablet': '(min-width: 768px)', | ||
| '≥tablet': '(min-width: 768px)', | ||
| '<=tablet': '(max-width: 768px)', | ||
| '≤tablet': '(max-width: 768px)', | ||
| '>desktop': '(min-width: 1025px)', | ||
| '<desktop': '(max-width: 1023px)', | ||
| '>=desktop': '(min-width: 1024px)', | ||
| '≥desktop': '(min-width: 1024px)', | ||
| '<=desktop': '(max-width: 1024px)', | ||
| '≤desktop': '(max-width: 1024px)' | ||
| )); | ||
| @include run(test('parse-expression', $tests-parse-expression)); |
| @import '../dist/include-media'; | ||
| @import 'helpers/SassyTester'; | ||
| $tests-parse-media-query: ( | ||
| (('a' 'b'),): 'a and b', | ||
| (('a', 'b'),): 'a, b', | ||
| (('a' 'b', 'c' 'd'),): 'a and b, c and d', | ||
| (('a', 'b', 'c' 'd'),): 'a, b, c and d', | ||
| ); | ||
| @include run(test('parse-media-query', $tests-parse-media-query)); |
| @import '../dist/include-media'; | ||
| @import 'helpers/SassyTester'; | ||
| $tests-resolve-feature-queries: ( | ||
| (('screen' '>desktop'), ): ( | ||
| ('screen' '(min-width: 1025px)'), | ||
| ), | ||
| (('screen', '>desktop'), ): ( | ||
| ('screen'), ('(min-width: 1025px)'), | ||
| ), | ||
| (('screen' ('>phone', '<desktop')), ): ( | ||
| ('screen' '(min-width: 321px)'), ('screen' '(max-width: 1023px)'), | ||
| ), | ||
| (('retina2x' '>phone'), ): ( | ||
| ('(-webkit-min-device-pixel-ratio: 2)' '(min-width: 321px)'), | ||
| ('(min-resolution: 192dpi)' '(min-width: 321px)'), | ||
| ('(min-resolution: 2dppx)' '(min-width: 321px)'), | ||
| ), | ||
| (('screen' ('height>300px' 'landscape')), ): ( | ||
| ('screen' '(min-height: 301px)' '(orientation: landscape)'), | ||
| ), | ||
| ((('screen', 'portrait') ('height>300px', 'landscape')), ): ( | ||
| ('screen' '(min-height: 301px)'), | ||
| ('(orientation: portrait)' '(min-height: 301px)'), | ||
| ('screen' '(orientation: landscape)'), | ||
| ('(orientation: portrait)' '(orientation: landscape)'), | ||
| ), | ||
| ); | ||
| @include run(test('resolve-feature-queries', $tests-resolve-feature-queries)); |
| @import '../dist/include-media'; | ||
| @import 'helpers/SassyTester'; | ||
| $tests-to-number: (); | ||
| @for $i from 1 through 10 { | ||
| $tests-to-number: map-merge($tests-to-number, ( | ||
| '#{$i}': $i, | ||
| '#{-$i}': -$i, | ||
| '#{$i / 10}': ($i / 10), | ||
| '#{$i / 100}': ($i / 100), | ||
| '#{$i}px': $i * 1px, | ||
| '#{$i}em': $i * 1em, | ||
| '#{$i}rem': $i * 1rem, | ||
| '#{-$i}px': -$i * 1px, | ||
| )); | ||
| } | ||
| @include run(test('to-number', $tests-to-number)); |
| @import "./utils/formatters"; | ||
| @import "./utils/list-to-js"; | ||
| @import "./utils/map-to-js"; | ||
| //--- | ||
| // [$propName] - optional param (otherwise $value is taken as first) | ||
| // if provided- function returns {'propName': sassToJs($value)} | ||
| // | ||
| // $value [map, list] - Converts provided value to JSON string | ||
| // $value [bool, nulls, number, string, color] - Converts provided value to a proper format for JSON | ||
| // | ||
| // Data types in Sass: http://sass-lang.com/documentation/file.SASS_REFERENCE.html#data_types | ||
| // | ||
| // Examples of usage: | ||
| // | ||
| // .breakpoints-data{ | ||
| // font-family: sassToJs($mediaBreakPointsMap); | ||
| // } | ||
| // | ||
| // .zoom-control:before{ | ||
| // content: sassToJs("maxZoomStep", $zoomStep); | ||
| // } | ||
| // | ||
| //--- | ||
| @function sassToJs($propName: null, $value: null) { | ||
| @if ($value == null) { | ||
| $value: $propName; // emulates sassToJs($value) | ||
| } @else { | ||
| @return '{#{sassToJs($propName)}:#{sassToJs($value)}}'; // returns {"propName": sassToJs($value)} | ||
| } | ||
| //--- VARS --- | ||
| $valueType: type-of($value); | ||
| $jsonPropValue: ''; | ||
| //--- CHECKING TYPE --- | ||
| // BOOL | ||
| @if ($valueType == 'bool') { | ||
| $jsonPropValue: $value; | ||
| // NULLS | ||
| } @else if ($valueType == 'null') { | ||
| $jsonPropValue: 'null'; | ||
| // NUMBER | ||
| } @else if ($valueType == 'number') { | ||
| // '100px' has also type "number" but after multiplying to "0" gives "0px" | ||
| @if (unitless($value)) { | ||
| // e.g. 100 | ||
| $jsonPropValue: $value; | ||
| } @else { | ||
| // e.g. 100px | ||
| $jsonPropValue: sassToJs_formatAsString($value); | ||
| } | ||
| // MAP | ||
| } @else if ($valueType == 'map') { | ||
| $jsonPropValue: sassToJs_Map($value); | ||
| // LIST | ||
| } @else if ($valueType == 'list') { | ||
| $jsonPropValue: sassToJs_List($value); | ||
| // STRING, COLOR | ||
| } @else { | ||
| $jsonPropValue: sassToJs_formatAsString($value); | ||
| } | ||
| //--- RETURN --- | ||
| @return $jsonPropValue; | ||
| } |
| //--- | ||
| // Formats provided argument as a string | ||
| // Might be used for strings, colors and Sass numbers like "100px" | ||
| //--- | ||
| @function sassToJs_formatAsString($value) { | ||
| @return '"#{$value}"'; | ||
| } |
| //---------- | ||
| // Converts Sass list to JSON string | ||
| //---------- | ||
| @function sassToJs_List($list) { | ||
| //--- VARS --- | ||
| $list-map-length: length($list); | ||
| $iteration: 0; | ||
| $json: '['; // open square bracket | ||
| // add each property from Sass list | ||
| @each $item in $list { | ||
| // ITEM VALUE | ||
| $json: $json + sassToJs($item); | ||
| // Add comma if not the last item | ||
| $iteration: $iteration + 1; | ||
| @if ($iteration < $list-map-length) { | ||
| $json: $json + ','; | ||
| } | ||
| } | ||
| $json: $json + ']'; // close square bracket | ||
| //--- RETURN --- | ||
| @return $json; | ||
| } |
| //---------- | ||
| // Converts Sass map to JSON string | ||
| //---------- | ||
| @function sassToJs_Map($list-map) { | ||
| //--- VARS --- | ||
| $list-map-length: length($list-map); | ||
| $iteration: 0; | ||
| $json: '{'; // open curly brackets | ||
| // add each property from Sass map | ||
| @each $propName, $propValue in $list-map { | ||
| // PROP NAME | ||
| $json: $json + sassToJs_formatAsString($propName) + ':'; | ||
| // PROP VALUE | ||
| $json: $json + sassToJs($propValue); | ||
| // Add comma if not the last item | ||
| $iteration: $iteration + 1; | ||
| @if ($iteration < $list-map-length) { | ||
| $json: $json + ','; | ||
| } | ||
| } | ||
| $json: $json + '}'; // close curly brackets | ||
| //--- RETURN --- | ||
| @return $json; | ||
| } |
| @import './functions/color-shade'; | ||
| @import './functions/color-tint'; | ||
| @import './functions/is-truthy'; | ||
| @import './functions/list-concat'; | ||
| @import './functions/list-contains'; | ||
| @import './functions/list-expand-directions'; | ||
| @import './functions/list-first'; | ||
| @import './functions/list-insert-nth'; | ||
| @import './functions/list-last'; | ||
| @import './functions/list-last-index'; | ||
| @import './functions/list-prepend'; | ||
| @import './functions/list-purge'; | ||
| @import './functions/list-remove'; | ||
| @import './functions/list-remove-nth'; | ||
| @import './functions/list-replace'; | ||
| @import './functions/list-replace-nth'; | ||
| @import './functions/list-slice'; | ||
| @import './functions/map-assign'; | ||
| @import './functions/map-deep-get'; | ||
| @import './functions/selector-get-element-name'; | ||
| @import './functions/str-replace'; | ||
| @import './functions/str-split'; | ||
| @import './functions/strip-unit'; | ||
| @import './functions/to-string'; | ||
| @import './functions/transition-props'; |
| @import './functions'; | ||
| @import './mixins'; |
| @import './mixins/at-root'; | ||
| @import './mixins/hide-text'; | ||
| @import './mixins/has-focus'; | ||
| @import './mixins/hide-visually'; | ||
| @import './mixins/ios-native-scrolling'; | ||
| @import './mixins/is-selectable'; | ||
| @import './mixins/is-visible'; | ||
| @import './mixins/last-row'; | ||
| @import './mixins/overlay'; | ||
| @import './mixins/placeholder'; | ||
| @import './mixins/size'; | ||
| @import './mixins/text-ellipsis'; | ||
| @import './mixins/supports-hover'; | ||
| @import './mixins/supports-touch'; |
| /// Slightly darken a color by mixing it with black | ||
| /// @link http://sass-guidelin.es/#lightening-and-darkening-colors | ||
| /// @access public | ||
| /// @param {color} $color - Color to tint | ||
| /// @param {number} $percentage - Percentage of `$color` in returned color | ||
| /// @return {Color} | ||
| /// | ||
| @function dp-color-shade($color, $percentage) { | ||
| @return mix(black, $color, $percentage); | ||
| } |
| /// Slightly lighten a color by mixing it with white | ||
| /// @link http://sass-guidelin.es/#lightening-and-darkening-colors | ||
| /// @access public | ||
| /// @param {color} $color - Color to tint | ||
| /// @param {number} $percentage - Percentage of `$color` in returned color | ||
| /// @return {Color} | ||
| /// | ||
| @function dp-color-tint($color, $percentage) { | ||
| @return mix(white, $color, $percentage); | ||
| } | ||
| /// Check if a value has a truthy value | ||
| /// @param {*} $value - The input value | ||
| /// @return {boolean} - A flag indicating if the value is truthy | ||
| /// | ||
| @function dp-is-truthy($value) { | ||
| @return if($value == null, false, $value and $value != null and $value != '' and $value != ()); | ||
| } |
| @import './to-string'; | ||
| /// Concatenate a list using a given separator | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#miscellaneous | ||
| /// @param {List} $list - The list to be concatenated | ||
| /// @param {string} $glue - A separator string to be inserted between items | ||
| /// @return {string} - A string containing list items connected with a separator string | ||
| /// | ||
| @function dp-list-concat($list, $glue: '') { | ||
| $result: null; | ||
| @if length($list) == 0 { | ||
| @return ''; | ||
| } | ||
| @for $i from 1 through length($list) { | ||
| $e: nth($list, $i); | ||
| @if type-of($e) == list { | ||
| $result: unquote('#{$result}#{to-string($e, $glue, true)}'); | ||
| } @else { | ||
| $result: if($i != length($list), unquote('#{$result}#{$e}#{$glue}'), unquote('#{$result}#{$e}')); | ||
| } | ||
| } | ||
| @return $result; | ||
| } |
| /// Checks if a given list contains a certain value | ||
| /// @param {List} $list - The haystack | ||
| /// @param {*} $val - The needle | ||
| /// | ||
| @function dp-list-contains($list, $val) { | ||
| @if index($list, $val) { | ||
| @return true; | ||
| } | ||
| @return false; | ||
| } |
| // stylelint-disable scss/no-duplicate-dollar-variables | ||
| /// Expand a given list to 4 items, similar to css padding shorthand expressions. | ||
| /// Inspired by expansions used in css shortand expressions (eg. margin or padding). | ||
| /// @param {List} $list - List containing zero to four items | ||
| /// @return {List} - Updated list expanded to always four entries | ||
| /// | ||
| @function dp-list-expand-directions($list) { | ||
| $result: (); | ||
| $len-list: length($list); | ||
| @if ($len-list == 0) { | ||
| $result: (null, null, null, null); | ||
| } @else if ($len-list == 1) { | ||
| $result: (nth($list, 1), nth($list, 1), nth($list, 1), nth($list, 1)); | ||
| } @else if ($len-list == 2) { | ||
| $result: (nth($list, 1), nth($list, 2), nth($list, 1), nth($list, 2)); | ||
| } @else if ($len-list == 3) { | ||
| $result: (nth($list, 1), nth($list, 2), nth($list, 3), nth($list, 2)); | ||
| } @else { | ||
| $result: (nth($list, 1), nth($list, 2), nth($list, 3), nth($list, 4)); | ||
| } | ||
| @return $result; | ||
| } |
| /// Return the first item of a list | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#selecting-values-from-list | ||
| /// @param {List} $list - The input list | ||
| /// @return {*} - The list's first item | ||
| /// | ||
| @function dp-list-first($list) { | ||
| @return nth($list, 1); | ||
| } |
| // stylelint-disable scss/no-duplicate-dollar-variables | ||
| /// Add a value to an arbitrary position | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#adding-values-to-a-list | ||
| /// @param {List} $list - The input list | ||
| /// @param {number} $index - The value's target list position | ||
| /// @param {*} $value - The term to be added | ||
| /// @return {*} - The output list | ||
| /// | ||
| @function dp-list-insert-nth($list, $index, $value) { | ||
| $result: null; | ||
| @if type-of($index) != number { | ||
| @warn '$index: #{quote($index)} is not a number for `insert-nth`.'; | ||
| } @else if $index < 1 { | ||
| @warn 'List index 0 must be a non-zero integer for `insert-nth`'; | ||
| } @else if $index > length($list) { | ||
| @warn 'List index is #{$index} but list is only #{length($list)} item long for `insert-nth`'; | ||
| } @else { | ||
| $result: (); | ||
| @for $i from 1 through length($list) { | ||
| @if $i == $index { | ||
| $result: append($result, $value); | ||
| } | ||
| $result: append($result, nth($list, $i)); | ||
| } | ||
| } | ||
| @return $result; | ||
| } |
| /// Return the last occurence of an item within a list | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#selecting-values-from-list | ||
| /// @param {List} $list - The input list | ||
| /// @param {*} $value - The search term | ||
| /// @return {number} - The index of the search term's last occurrence within the list | ||
| /// | ||
| @function dp-list-last-index($list, $value) { | ||
| $result: null; | ||
| @for $i from 1 through length($list) { | ||
| @if nth($list, $i) == $value { | ||
| $result: $i; | ||
| } | ||
| } | ||
| @return $result; | ||
| } |
| /// Return the last item of a list | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#selecting-values-from-list | ||
| /// @param {List} $list - The input list | ||
| /// @return {*} - The list's last item | ||
| /// | ||
| @function dp-list-last($list) { | ||
| @return nth($list, length($list)); | ||
| } |
| /// Add a value to the front of a list | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#adding-values-to-a-list | ||
| /// @param {List} $list - The input list | ||
| /// @param {*} $value - The term to be prepended | ||
| /// @return {*} - The output list | ||
| /// | ||
| @function dp-list-prepend($list, $value) { | ||
| @return join($value, $list); | ||
| } |
| @import './is-truthy'; | ||
| /// Removes all non-true values from a list | ||
| /// @link http://hugogiraudel.com/2013/10/09/advanced-sass-list-functions-again/ | ||
| /// @param {List} $list - A list containing possibly falsy values | ||
| /// @return {List} - The trimmed list | ||
| /// | ||
| @function dp-list-purge($list) { | ||
| $result: (); | ||
| @each $item in $list { | ||
| @if dp-is-truthy($item) { | ||
| $result: append($result, $item); | ||
| } | ||
| } | ||
| @return $result; | ||
| } |
| @import './list-replace-nth'; | ||
| /// Remove a value at a certain index from a list | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#removing-values-from-list | ||
| /// @param {List} $list - The input list | ||
| /// @param {number} $index - The target position to be removed | ||
| /// @return {*} - The list's first item | ||
| /// | ||
| @function dp-list-remove-nth($list, $index) { | ||
| @return dp-list-replace-nth($list, $index); | ||
| } |
| @import './list-replace'; | ||
| /// Remove a value from a list | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#removing-values-from-list | ||
| /// @param {List} $list - The input list | ||
| /// @param {*} $value - The term to be removed | ||
| /// @return {*} - The list's first item | ||
| /// | ||
| @function dp-list-remove($list, $value) { | ||
| @return dp-list-replace($list, $value); | ||
| } |
| // stylelint-disable scss/no-duplicate-dollar-variables | ||
| @import './is-truthy'; | ||
| /// Replace a value within a list by index | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#replacing-values-from-list | ||
| /// @param {List} $list - The input list | ||
| /// @param {number} $index - The value's target list position | ||
| /// @param {*} $value - The term to be inserted | ||
| /// @return {*} - The output list | ||
| /// | ||
| @function dp-list-replace-nth($list, $index, $value: null) { | ||
| $result: null; | ||
| @if type-of($index) != number { | ||
| @warn '$index: #{quote($index)} is not a number for `replace-nth`'; | ||
| } @else if $index == 0 { | ||
| @warn 'ist index 0 must be a non-zero integer for `replace-nth`.'; | ||
| } @else if abs($index) > length($list) { | ||
| @warn 'ist index is #{$index} but list is only #{length($list)} item long for `replace-nth`.'; | ||
| } @else { | ||
| $result: (); | ||
| $index: if($index < 0, length($list) + $index + 1, $index); | ||
| @for $i from 1 through length($list) { | ||
| @if $i == $index { | ||
| @if dp-is-truthy($value) { | ||
| $result: append($result, $value); | ||
| } | ||
| } @else { | ||
| $result: append($result, nth($list, $i)); | ||
| } | ||
| } | ||
| } | ||
| @return $result; | ||
| } |
| @import './is-truthy'; | ||
| /// Replace a value with another one in a list | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#replacing-values-from-list | ||
| /// @param {List} $list - The input list | ||
| /// @param {*} $old-value - The term that is being replaced | ||
| /// @param {*} $value - The term to replace it with | ||
| /// @return {*} - The output list | ||
| /// | ||
| @function dp-list-replace($list, $old-value, $value: null) { | ||
| $result: (); | ||
| @for $i from 1 through length($list) { | ||
| @if nth($list, $i) == $old-value { | ||
| @if dp-is-truthy($value) { | ||
| $result: append($result, $value); | ||
| } | ||
| } @else { | ||
| $result: append($result, nth($list, $i)); | ||
| } | ||
| } | ||
| @return $result; | ||
| } |
| // stylelint-disable scss/no-duplicate-dollar-variables | ||
| /// Return a number of values from a list | ||
| /// @link https://hugogiraudel.com/2013/08/08/advanced-sass-list-functions/#miscellaneous | ||
| /// @param {List} $list - The input list | ||
| /// @param {number} $start - The slice range's start index | ||
| /// @param {number} $end - The slice range's end index | ||
| /// @return {List} - A list of items | ||
| /// | ||
| @function dp-list-slice($list, $start: 1, $end: length($list)) { | ||
| $result: null; | ||
| @if type-of($start) != number or type-of($end) != number { | ||
| @warn 'Either $start or $end are not a number for `slice`'; | ||
| } @else if $start > $end { | ||
| @warn 'The start index has to be lesser than or equals to the end index for `slice`'; | ||
| } @else if $start < 1 or $end < 1 { | ||
| @warn 'List indexes must be non-zero integers for `slice`'; | ||
| } @else if $start > length($list) { | ||
| @warn 'List index is #{$start} but list is only #{length($list)} item long for `slice`'; | ||
| } @else if $end > length($list) { | ||
| @warn 'List index is #{$end} but list is only #{length($list)} item long for `slice`'; | ||
| } @else { | ||
| $result: (); | ||
| @for $i from $start through $end { | ||
| $result: append($result, nth($list, $i)); | ||
| } | ||
| } | ||
| @return $result; | ||
| } |
| /// Takes multiple maps and merges them into one | ||
| /// @link http://stackoverflow.com/questions/27740063/merge-multiple-sass-maps | ||
| /// @param {...Map} $maps - Any number of maps | ||
| /// @return {Map} - The resulting map | ||
| /// | ||
| @function dp-map-assign($maps...) { | ||
| $result: (); | ||
| @each $map in $maps { | ||
| $result: map-merge($result, $map); | ||
| } | ||
| @return $result; | ||
| } |
| /// Returns a value from a nested list (without failsafe if one of the keys does not exist) | ||
| /// @link https://css-tricks.com/snippets/sass/deep-getset-maps/ | ||
| /// @param {Map} $map - Map | ||
| /// @param {...string} $keys - Key chain | ||
| /// @return {*} - Desired value | ||
| /// | ||
| @function dp-map-deep-get($map, $keys...) { | ||
| @each $key in $keys { | ||
| $map: map-get($map, $key); | ||
| } | ||
| @return $map; | ||
| } |
| @import './str-split'; | ||
| /// Remove classes, pseudo-classes and attributes from a selector part | ||
| /// @param {string} $selector - The selector part to be cleaned | ||
| /// @return {string} | ||
| /// | ||
| @function dp-selector-get-element-name($selector) { | ||
| $result: $selector; | ||
| $markers: (' ', '.', ':', '['); | ||
| @each $marker in $markers { | ||
| $result: nth(dp-str-split($result, $marker), 1); | ||
| } | ||
| @return $result; | ||
| } |
| /// Replace `$search` with `$replace` in `$string` | ||
| /// @link https://css-tricks.com/snippets/sass/str-replace-function/ | ||
| /// @param {string} $string - Initial string | ||
| /// @param {string} $search - Substring to replace | ||
| /// @param {string} $replace ('') - New value | ||
| /// @return {string} - Updated string | ||
| /// | ||
| @function dp-str-replace($string, $search, $replace: '') { | ||
| $index: str-index($string, $search); | ||
| @if $index { | ||
| @return str-slice($string, 1, $index - 1) + $replace + dp-str-replace(str-slice($string, $index + str-length($search)), $search, $replace); | ||
| } | ||
| @return $string; | ||
| } |
| /// Split `$string` into several parts using `$delimiter`. | ||
| /// @param {string} $string - String to split | ||
| /// @param {string} $delimiter ('') - String to use as a delimiter to split `$string` | ||
| /// @return {List} | ||
| /// | ||
| @function dp-str-split($string, $delimiter: '') { | ||
| $result: (); | ||
| $length: str-length($string); | ||
| @if str-length($delimiter) == 0 { | ||
| @for $i from 1 through $length { | ||
| $result: append($result, str-slice($string, $i, $i)); | ||
| } | ||
| @return $result; | ||
| } | ||
| $running: true; | ||
| $remaining: $string; | ||
| @while $running { | ||
| $index: str-index($remaining, $delimiter); | ||
| @if $index { | ||
| $slice: str-slice($remaining, 1, $index - 1); | ||
| @if (str-length($slice) > 0) { | ||
| $result: append($result, $slice); | ||
| } | ||
| $remaining: str-slice($remaining, $index + str-length($delimiter)); | ||
| } @else { | ||
| $running: false; | ||
| } | ||
| } | ||
| @return append($result, $remaining); | ||
| } |
| /// Remove the unit of a length | ||
| /// @param {number} $number - Number to remove unit from | ||
| /// @return {number} - Unitless number | ||
| /// | ||
| @function dp-strip-unit($number) { | ||
| @if type-of($number) == 'number' and not unitless($number) { | ||
| @return $number / ($number * 0 + 1); | ||
| } | ||
| @return $number; | ||
| } |
| /// Cast an arbitrary value to a string | ||
| /// @link https://hugogiraudel.com/2014/01/27/casting-types-in-sass/#string | ||
| /// @param {*} $value - The original value | ||
| /// @return {string} - The cast string | ||
| /// | ||
| @function dp-to-string($value) { | ||
| @return inspect($value); | ||
| } |
| /// Batch transition all properties with a single transition definition | ||
| /// @param {string} $trs-defintion - The transition definition to be applied | ||
| /// @param {...Map} $props - Any number of properties to be transitioned | ||
| /// @return {List} - A list to be used as a value for a transition property | ||
| /// | ||
| @function dp-transition-props($trs-definition, $props...) { | ||
| $result: (); | ||
| @each $prop in $props { | ||
| $result: append($result, ($prop $trs-definition), 'comma'); | ||
| } | ||
| @return $result; | ||
| } |
| @import '../functions/selector-get-element-name'; | ||
| /// Use `@at-root` while adding classes (or attributes) `$classes` to an element node `$append-target`. | ||
| /// If the element was not found a standard `@at-root` is used. | ||
| /// @param {string} $append-target - $append-target string to append the classes (or attributes) to | ||
| /// @param {string} $appended-selector - A single element selector containing classes and/or attributes to be appended | ||
| /// | ||
| @mixin dp-at-root($append-target, $appended-selector: '') { | ||
| $is-element-found: false; | ||
| $combined-selectors: ''; | ||
| $selectors: nth(&, 1); | ||
| @if ($appended-selector) { | ||
| @each $selector in $selectors { | ||
| $pure-element-selector: dp-selector-get-element-name($selector); | ||
| @if (($pure-element-selector == $append-target) and (not $is-element-found)) { | ||
| $is-element-found: true; | ||
| $combined-selectors: '#{$combined-selectors} #{$selector}#{$appended-selector}'; | ||
| } @else { | ||
| $combined-selectors: '#{$combined-selectors} #{$selector}'; | ||
| } | ||
| } | ||
| } | ||
| @if ($is-element-found) { | ||
| @at-root #{$combined-selectors} { | ||
| @content; | ||
| } | ||
| } @else { | ||
| @at-root #{$append-target}#{$appended-selector} & { | ||
| @content; | ||
| } | ||
| } | ||
| } |
| @import './supports-hover'; | ||
| @import './supports-touch'; | ||
| /// Styles touch/hover states according to browser capabilities | ||
| /// @param {boolean} $toggle - State toggle; Available values: `true`, `false` or `default` (for both) | ||
| /// | ||
| @mixin dp-has-focus() { | ||
| @include dp-supports-hover { | ||
| &:hover, &:focus { | ||
| @content; | ||
| } | ||
| } | ||
| @include dp-supports-touch { | ||
| &:active { | ||
| @content; | ||
| } | ||
| } | ||
| } |
| /// Visually hide text but leave it accessible for screen readers and search engines | ||
| /// | ||
| @mixin dp-hide-text { | ||
| font: 0/0 a; // stylelint-disable font-family-no-missing-generic-family-keyword | ||
| text-shadow: none; | ||
| color: transparent; | ||
| } |
| /// Visually hide content but leave it accessible for screen readers and search engines | ||
| /// | ||
| @mixin dp-hide-visually { | ||
| position: absolute; | ||
| overflow: hidden; | ||
| max-width: 1px; | ||
| max-height: 1px; | ||
| padding: 0; | ||
| border: 0; | ||
| margin: -1px; | ||
| clip: rect(0 0 0 0); | ||
| } |
| /// Add ios native overflow scrolling to an element | ||
| /// @param {string} $orientation - The scroll direction | ||
| /// | ||
| @mixin dp-ios-native-scrolling($orientation: y) { | ||
| -webkit-overflow-scrolling: touch; | ||
| @if (not index((x, y), $orientation)) { | ||
| @error '`#{$orientation}` must be either `x` or `y`'; | ||
| } | ||
| @if ($orientation == y) { | ||
| overflow-x: hidden; | ||
| overflow-y: scroll; | ||
| } | ||
| @if ($orientation == x) { | ||
| overflow-x: scroll; | ||
| overflow-y: hidden; | ||
| } | ||
| } |
| /// Controls the selection style of the current element and all its children | ||
| /// @param {boolean} $toggle - State toggle | ||
| /// | ||
| @mixin dp-is-selectable($toggle) { | ||
| $c--label-selection: #accef7 !default; // provide a fallback value | ||
| @if $toggle { | ||
| & { | ||
| -webkit-touch-callout: text; | ||
| user-select: text; | ||
| &::selection, ::selection { | ||
| background-color: $c--label-selection; | ||
| } | ||
| } | ||
| } @else { | ||
| & { | ||
| -webkit-touch-callout: none; | ||
| user-select: none; | ||
| &::selection, ::selection { | ||
| background-color: transparent; | ||
| } | ||
| } | ||
| } | ||
| } |
| // stylelint-disable scss/no-duplicate-dollar-variables | ||
| /// Animates an element in or out, including setting their visibility correctly | ||
| /// The basic opacity transition can be extended with other properties or full transition descriptors | ||
| /// @param {boolean} $toggle - State toggle | ||
| /// @param {number} $speed - The targeted speed after which the element is hidden | ||
| /// @param {...string|...List} $transition-params - One or more strings or lists containing additional transition params | ||
| /// @example scss - Simple usage: input | ||
| /// .my-elem.my-elem__is-hidden { @include dp-is-visible(false, 300ms); } | ||
| /// .my-elem.my-elem__is-shown { @include dp-is-visible(true, 300ms); } | ||
| /// @output css - Simple usage: output | ||
| /// .my-elem.my-elem__is-hidden { | ||
| /// transition: visibility 0ms linear 300ms, opacity 300ms ease 0ms; | ||
| /// visibility: hidden; | ||
| /// opacity: 0; | ||
| /// } | ||
| /// .my-elem.my-elem__is-shown { | ||
| /// transition: visibility 0ms linear 0ms, opacity 300ms ease 0ms; | ||
| /// visibility: inherit; | ||
| /// opacity: 1; | ||
| /// } | ||
| /// | ||
| @mixin dp-is-visible($toggle, $speed, $transition-params...) { | ||
| $transitions: (); | ||
| $has-params: length($transition-params) > 0; | ||
| @if type-of($speed) != 'number' { | ||
| @error 'Provided transition speed `#{$speed}` is not of type number!'; | ||
| } | ||
| @if $has-params { | ||
| @for $i from 1 through length($transition-params) { | ||
| $param: nth($transition-params, $i); | ||
| @if type-of($param) == 'list' { // If $param contains a space (a.k.a. a whole string) | ||
| $transitions: append($transitions, unquote('#{$param}'), comma); | ||
| } @else { | ||
| $transitions: append($transitions, unquote('#{$param} #{$speed} ease 0ms'), comma); | ||
| } | ||
| } | ||
| } @else { | ||
| $transitions: append($transitions, unquote('opacity #{$speed} ease 0ms'), comma); | ||
| } | ||
| @if $toggle { | ||
| transition: join(unquote('visibility 0ms linear 0ms'), $transitions, comma); | ||
| visibility: inherit; | ||
| @if not $has-params { | ||
| opacity: 1; | ||
| } | ||
| } @else { | ||
| transition: join(unquote('visibility 0ms linear #{$speed}'), $transitions, comma); | ||
| visibility: hidden; | ||
| @if not $has-params { | ||
| opacity: 0; | ||
| } | ||
| } | ||
| } |
| /// Targets the last row in a grid of constant column count | ||
| /// @link http://keithclark.co.uk/articles/targeting-first-and-last-rows-in-css-grid-layouts/ | ||
| /// @param {number} $num-cols - The number of columns within the context | ||
| /// | ||
| @mixin dp-last-row($num-cols) { | ||
| $selector: nth(nth(&, 1), length(nth(&, 1))); | ||
| &:nth-child(#{$num-cols}n + 1):nth-last-child(-n + #{$num-cols}), | ||
| &:nth-child(#{$num-cols}n + 1):nth-last-child(-n + #{$num-cols}) ~ #{$selector} { | ||
| @content; | ||
| } | ||
| } |
| /// Add styles for a child to overlay its parent | ||
| /// @param {string} $pos - A declaration value for the `position` property | ||
| /// @param {string} $root-y - A vertical spacial property keyword | ||
| /// @param {string} $root-x - A horizontal spacial property keyword | ||
| /// | ||
| @mixin dp-overlay($pos: absolute, $root-y: top, $root-x: left) { | ||
| position: $pos; | ||
| #{$root-y}: 0; | ||
| #{$root-x}: 0; | ||
| width: 100%; | ||
| height: 100%; | ||
| } |
| // stylelint-disable selector-no-vendor-prefix | ||
| /// Add styles to the placeholder element | ||
| /// | ||
| @mixin dp-placeholder() { | ||
| &::-moz-placeholder { @content; } | ||
| &:-ms-input-placeholder { @content; } | ||
| &::-webkit-input-placeholder { @content; } | ||
| &::placeholder { @content; } | ||
| } |
| @mixin dp-size($sz) { | ||
| width: $sz; | ||
| height: $sz; | ||
| } |
| // https://docs.w3cub.com/css/@media/any-hover/ | ||
| @mixin dp-supports-hover() { | ||
| /* one or more available input mechanism(s) can hover over elements with ease */ | ||
| // The `-ms-high-contrast` directives are needed to target Internet Explorer 10 and 11 | ||
| // @link https://stackoverflow.com/q/28417056/1602864 | ||
| // TODO: Remove the `-ms-high-contrast` directives when IE will no longer be supported | ||
| @media (any-hover: hover), (-ms-high-contrast: active), (-ms-high-contrast: none) { | ||
| @content; | ||
| } | ||
| } |
| @mixin dp-supports-touch() { | ||
| /* one or more available input mechanism(s) cannot hover or there are no pointing input mechanisms */ | ||
| @media (any-hover: none) { | ||
| @content; | ||
| } | ||
| } |
| @mixin dp-text-ellipsis() { | ||
| white-space: nowrap; | ||
| overflow: hidden; | ||
| max-width: 100%; | ||
| text-overflow: ellipsis; | ||
| } |
| @import './index'; | ||
| @import './unprefixed/index'; | ||
| .test { | ||
| $test-list: (foo, bar, baz); | ||
| $test-map: (foo: bar); | ||
| $test-color: #0000ff; | ||
| // Prefixed mixins | ||
| @include dp-has-focus; | ||
| @include dp-at-root('.test'); | ||
| @include dp-hide-text; | ||
| @include dp-hide-visually; | ||
| @include dp-ios-native-scrolling; | ||
| @include dp-is-selectable(true); | ||
| @include dp-is-visible(true, 200); | ||
| @include dp-last-row(2); | ||
| @include dp-overlay; | ||
| @include dp-placeholder; | ||
| @include dp-size(200); | ||
| @include dp-text-ellipsis; | ||
| // Unprefixed mixins | ||
| @include has-focus; | ||
| @include at-root('.test'); | ||
| @include hide-text; | ||
| @include hide-visually; | ||
| @include ios-native-scrolling; | ||
| @include is-selectable(true); | ||
| @include is-visible(true, 200); | ||
| @include last-row(2); | ||
| @include overlay; | ||
| @include placeholder; | ||
| @include size(200); | ||
| @include text-ellipsis; | ||
| @include supports-hover; | ||
| @include supports-touch; | ||
| // Prefixed functions | ||
| $test-prefixed: ( | ||
| 't-1': dp-color-shade($test-color, 10), | ||
| 't-2': dp-color-tint($test-color, 10), | ||
| 't-3': dp-is-truthy('foo'), | ||
| 't-4': dp-list-concat($test-list, ','), | ||
| 't-5': dp-list-contains($test-list, foo), | ||
| 't-6': dp-list-expand-directions($test-list), | ||
| 't-7': dp-list-first($test-list), | ||
| 't-8': dp-list-insert-nth($test-list, 2, qux), | ||
| 't-9': dp-list-last($test-list), | ||
| 't-10': dp-list-last-index($test-list, bar), | ||
| 't-11': dp-list-prepend($test-list, qux), | ||
| 't-12': dp-list-purge($test-list), | ||
| 't-13': dp-list-remove($test-list, foo), | ||
| 't-14': dp-list-remove-nth($test-list, 1), | ||
| 't-15': dp-list-replace($test-list, foo, qux), | ||
| 't-16': dp-list-replace-nth($test-list, 1, qux), | ||
| 't-17': dp-list-slice($test-list, 2, 3), | ||
| 't-18': dp-map-assign($test-map), | ||
| 't-19': dp-map-deep-get($test-map, 'color'), | ||
| 't-20': dp-selector-get-element-name('.test'), | ||
| 't-21': dp-str-replace('test', 'st', 'oo'), | ||
| 't-22': dp-str-split('test'), | ||
| 't-23': dp-strip-unit(3), | ||
| 't-24': dp-to-string(foo), | ||
| 't-25': dp-transition-props('all 300ms linear'), | ||
| 't-26': dp-supports-hover(), | ||
| 't-27': dp-supports-touch(), | ||
| ); | ||
| // Unprefixed functions | ||
| $test-unprefixed: ( | ||
| 't-1': color-shade($test-color, 10), | ||
| 't-2': color-tint($test-color, 10), | ||
| 't-3': is-truthy('foo'), | ||
| 't-4': list-concat($test-list, ','), | ||
| 't-5': list-contains($test-list, foo), | ||
| 't-6': list-expand-directions($test-list), | ||
| 't-7': list-first($test-list), | ||
| 't-8': list-insert-nth($test-list, 2, qux), | ||
| 't-9': list-last($test-list), | ||
| 't-10': list-last-index($test-list, bar), | ||
| 't-11': list-prepend($test-list, qux), | ||
| 't-12': list-purge($test-list), | ||
| 't-13': list-remove($test-list, foo), | ||
| 't-14': list-remove-nth($test-list, 1), | ||
| 't-15': list-replace($test-list, foo, qux), | ||
| 't-16': list-replace-nth($test-list, 1, qux), | ||
| 't-17': list-slice($test-list, 2, 3), | ||
| 't-18': map-assign($test-map), | ||
| 't-19': map-deep-get($test-map, 'color'), | ||
| 't-20': selector-get-element-name('.test'), | ||
| 't-21': str-replace('test', 'st', 'oo'), | ||
| 't-22': str-split('test'), | ||
| 't-23': strip-unit(3), | ||
| 't-24': to-string(foo), | ||
| 't-25': transition-props('all 300ms linear'), | ||
| 't-26': supports-hover(), | ||
| 't-27': supports-touch(), | ||
| ); | ||
| $test-fixtures: ( | ||
| 't-1': #0000e6, | ||
| 't-2': #1a1aff, | ||
| 't-3': true, | ||
| 't-4': 'foo,bar,baz', | ||
| 't-5': true, | ||
| 't-6': (foo, bar, baz, bar), | ||
| 't-7': foo, | ||
| 't-8': foo qux bar baz, | ||
| 't-9': baz, | ||
| 't-10': 2, | ||
| 't-11': (qux, foo, bar, baz), | ||
| 't-12': foo bar baz, | ||
| 't-13': bar baz, | ||
| 't-14': bar baz, | ||
| 't-15': qux bar baz, | ||
| 't-16': qux bar baz, | ||
| 't-17': bar baz, | ||
| 't-18': (foo: bar), | ||
| 't-19': null, | ||
| 't-20': 'test', | ||
| 't-21': 'teoo', | ||
| 't-22': 't' 'e' 's' 't', | ||
| 't-23': 3, | ||
| 't-24': 'foo', | ||
| 't-25': (), | ||
| ); | ||
| @for $i from 1 through length($test-fixtures) { | ||
| $fixture: map-get($test-fixtures, 't-#{$i}'); | ||
| $prefixed: map-get($test-prefixed, 't-#{$i}'); | ||
| $unprefixed: map-get($test-unprefixed, 't-#{$i}'); | ||
| @if ($prefixed != $unprefixed) { | ||
| @error 'Comparison test ##{$i} failing! - Prefixed: #{$prefixed} ~vs~ Unprefixed: #{$unprefixed}'; | ||
| } | ||
| @if ($prefixed != $fixture) { | ||
| @error 'Comparison test ##{$i} failing! - Fixture: #{$fixture} ~vs~ Prefixed: #{$prefixed}'; | ||
| } | ||
| } | ||
| } |
| @import '../functions'; | ||
| @function color-shade($args...) { | ||
| @return dp-color-shade($args...); | ||
| } | ||
| @function color-tint($args...) { | ||
| @return dp-color-tint($args...); | ||
| } | ||
| @function is-truthy($args...) { | ||
| @return dp-is-truthy($args...); | ||
| } | ||
| @function list-concat($args...) { | ||
| @return dp-list-concat($args...); | ||
| } | ||
| @function list-contains($args...) { | ||
| @return dp-list-contains($args...); | ||
| } | ||
| @function list-expand-directions($args...) { | ||
| @return dp-list-expand-directions($args...); | ||
| } | ||
| @function list-first($args...) { | ||
| @return dp-list-first($args...); | ||
| } | ||
| @function list-insert-nth($args...) { | ||
| @return dp-list-insert-nth($args...); | ||
| } | ||
| @function list-last($args...) { | ||
| @return dp-list-last($args...); | ||
| } | ||
| @function list-last-index($args...) { | ||
| @return dp-list-last-index($args...); | ||
| } | ||
| @function list-prepend($args...) { | ||
| @return dp-list-prepend($args...); | ||
| } | ||
| @function list-purge($args...) { | ||
| @return dp-list-purge($args...); | ||
| } | ||
| @function list-remove($args...) { | ||
| @return dp-list-remove($args...); | ||
| } | ||
| @function list-remove-nth($args...) { | ||
| @return dp-list-remove-nth($args...); | ||
| } | ||
| @function list-replace($args...) { | ||
| @return dp-list-replace($args...); | ||
| } | ||
| @function list-replace-nth($args...) { | ||
| @return dp-list-replace-nth($args...); | ||
| } | ||
| @function list-slice($args...) { | ||
| @return dp-list-slice($args...); | ||
| } | ||
| @function map-assign($args...) { | ||
| @return dp-map-assign($args...); | ||
| } | ||
| @function map-deep-get($args...) { | ||
| @return dp-map-deep-get($args...); | ||
| } | ||
| @function selector-get-element-name($args...) { | ||
| @return dp-selector-get-element-name($args...); | ||
| } | ||
| @function str-replace($args...) { | ||
| @return dp-str-replace($args...); | ||
| } | ||
| @function str-split($args...) { | ||
| @return dp-str-split($args...); | ||
| } | ||
| @function strip-unit($args...) { | ||
| @return dp-strip-unit($args...); | ||
| } | ||
| @function to-string($args...) { | ||
| @return dp-to-string($args...); | ||
| } | ||
| @function transition-props($args...) { | ||
| @return dp-transition-props($args...); | ||
| } |
| @import './functions'; | ||
| @import './mixins'; |
| @import '../mixins'; | ||
| @mixin at-root($args...) { | ||
| @include dp-at-root($args...) { | ||
| @content; | ||
| } | ||
| } | ||
| @mixin has-focus($args...) { | ||
| @include dp-has-focus($args...) { | ||
| @content; | ||
| } | ||
| } | ||
| @mixin hide-text($args...) { | ||
| @include dp-hide-text($args...); | ||
| } | ||
| @mixin hide-visually($args...) { | ||
| @include dp-hide-visually($args...); | ||
| } | ||
| @mixin ios-native-scrolling($args...) { | ||
| @include dp-ios-native-scrolling($args...); | ||
| } | ||
| @mixin is-selectable($args...) { | ||
| @include dp-is-selectable($args...); | ||
| } | ||
| @mixin is-visible($args...) { | ||
| @include dp-is-visible($args...); | ||
| } | ||
| @mixin last-row($args...) { | ||
| @include dp-last-row($args...) { | ||
| @content; | ||
| } | ||
| } | ||
| @mixin overlay($args...) { | ||
| @include dp-overlay($args...); | ||
| } | ||
| @mixin placeholder($args...) { | ||
| @include dp-placeholder($args...) { | ||
| @content; | ||
| } | ||
| } | ||
| @mixin size($args...) { | ||
| @include dp-size($args...); | ||
| } | ||
| @mixin text-ellipsis($args...) { | ||
| @include dp-text-ellipsis($args...); | ||
| } | ||
| @mixin supports-hover($args...) { | ||
| @include dp-supports-hover($args...); | ||
| } | ||
| @mixin supports-touch($args...) { | ||
| @include dp-supports-touch($args...); | ||
| } |
| @charset "UTF-8"; | ||
| //// | ||
| /// `include-media-or` - An `include-media` fork, enabling OR conjunctions and nested queries | ||
| /// Author: Rouven Bühlmann (@nirazul) | ||
| /// | ||
| /// This project is licensed under the terms of the MIT license | ||
| //// | ||
| //// | ||
| /// include-media library public configuration | ||
| /// @author Eduardo Boucas | ||
| /// @access public | ||
| //// | ||
| /// | ||
| /// Creates a list of global breakpoints | ||
| /// | ||
| /// @example scss - Creates a single breakpoint with the label `phone` | ||
| /// $breakpoints: ('phone': 320px); | ||
| /// | ||
| $breakpoints: ( | ||
| 'phone': 320px, | ||
| 'tablet': 768px, | ||
| 'desktop': 1024px | ||
| ) !default; | ||
| /// | ||
| /// Creates a list of static expressions or media types | ||
| /// | ||
| /// @example scss - Creates a single media type (screen) | ||
| /// $media-expressions: ('screen': 'screen'); | ||
| /// | ||
| /// @example scss - Creates a static expression with logical disjunction (OR operator) | ||
| /// $media-expressions: ( | ||
| /// 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)' | ||
| /// ); | ||
| /// | ||
| $media-expressions: ( | ||
| 'screen': 'screen', | ||
| 'print': 'print', | ||
| 'handheld': 'handheld', | ||
| 'landscape': '(orientation: landscape)', | ||
| 'portrait': '(orientation: portrait)', | ||
| 'retina2x': ('(-webkit-min-device-pixel-ratio: 2)', '(min-resolution: 192dpi)', '(min-resolution: 2dppx)'), | ||
| 'retina3x': ('(-webkit-min-device-pixel-ratio: 3)', '(min-resolution: 350dpi)', '(min-resolution: 3dppx)') | ||
| ) !default; | ||
| /// | ||
| /// Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals | ||
| /// | ||
| /// @example scss - Interval for pixels is defined as `1` by default | ||
| /// @include media('>128px') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 129px) {} | ||
| /// | ||
| /// @example scss - Interval for ems is defined as `0.01` by default | ||
| /// @include media('>20em') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 20.01em) {} | ||
| /// | ||
| /// @example scss - Interval for rems is defined as `0.1` by default, to be used with `font-size: 62.5%;` | ||
| /// @include media('>2.0rem') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 2.1rem) {} | ||
| /// | ||
| $unit-intervals: ( | ||
| 'px': 1, | ||
| 'em': 0.01, | ||
| 'rem': 0.1, | ||
| '': 0 | ||
| ) !default; | ||
| /// | ||
| /// Defines whether support for media queries is available, useful for creating separate stylesheets | ||
| /// for browsers that don't support media queries. | ||
| /// | ||
| /// @example scss - Disables support for media queries | ||
| /// $im-media-support: false; | ||
| /// @include media('>=tablet') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| $im-media-support: true !default; | ||
| /// | ||
| /// Selects which breakpoint to emulate when support for media queries is disabled. Media queries that start at or | ||
| /// intercept the breakpoint will be displayed, any others will be ignored. | ||
| /// | ||
| /// @example scss - This media query will show because it intercepts the static breakpoint | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// @include media('>=tablet') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| /// @example scss - This media query will NOT show because it does not intercept the desktop breakpoint | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'tablet'; | ||
| /// @include media('>=desktop') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* No output */ | ||
| /// | ||
| $im-no-media-breakpoint: 'desktop' !default; | ||
| /// | ||
| /// Selects which media expressions are allowed in an expression for it to be used when media queries | ||
| /// are not supported. | ||
| /// | ||
| /// @example scss - This media query will show because it intercepts the static breakpoint and contains only accepted media expressions | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// $im-no-media-expressions: ('screen'); | ||
| /// @include media('>=tablet', 'screen') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| /// @example scss - This media query will NOT show because it intercepts the static breakpoint but contains a media expression that is not accepted | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// $im-no-media-expressions: ('screen'); | ||
| /// @include media('>=tablet', 'retina2x') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* No output */ | ||
| /// | ||
| $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default; | ||
| //// | ||
| /// Parsing engine | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Get operator of an expression | ||
| /// | ||
| /// @param {String} $expression - Expression to extract operator from | ||
| /// | ||
| /// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤` | ||
| /// | ||
| @function get-expression-operator($expression) { | ||
| @each $operator in ('>=', '>', '<=', '<', '≥', '≤') { | ||
| @if str-index($expression, $operator) { | ||
| @return $operator; | ||
| } | ||
| } | ||
| // It is not possible to include a mixin inside a function, so we have to | ||
| // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because | ||
| // functions cannot be called anywhere in Sass, we need to hack the call in | ||
| // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with | ||
| // Sass 3.3, change this line in `@if im-log(..) {}` instead. | ||
| $_: im-log('No operator found in `#{$expression}`.'); | ||
| } | ||
| /// | ||
| /// Get dimension of an expression, based on a found operator | ||
| /// | ||
| /// @param {String} $expression - Expression to extract dimension from | ||
| /// @param {String} $operator - Operator from `$expression` | ||
| /// | ||
| /// @return {String} - `width` or `height` (or potentially anything else) | ||
| /// | ||
| @function get-expression-dimension($expression, $operator) { | ||
| $operator-index: str-index($expression, $operator); | ||
| $parsed-dimension: str-slice($expression, 0, $operator-index - 1); | ||
| $dimension: 'width'; | ||
| @if str-length($parsed-dimension) > 0 { | ||
| $dimension: $parsed-dimension; | ||
| } | ||
| @return $dimension; | ||
| } | ||
| /// | ||
| /// Get dimension prefix based on an operator | ||
| /// | ||
| /// @param {String} $operator - Operator | ||
| /// | ||
| /// @return {String} - `min` or `max` | ||
| /// | ||
| @function get-expression-prefix($operator) { | ||
| @return if(index(('<', '<=', '≤'), $operator), 'max', 'min'); | ||
| } | ||
| /// | ||
| /// Get value of an expression, based on a found operator | ||
| /// | ||
| /// @param {String} $expression - Expression to extract value from | ||
| /// @param {String} $operator - Operator from `$expression` | ||
| /// | ||
| /// @return {Number} - A numeric value | ||
| /// | ||
| @function get-expression-value($expression, $operator) { | ||
| $operator-index: str-index($expression, $operator); | ||
| $value: str-slice($expression, $operator-index + str-length($operator)); | ||
| @if map-has-key($breakpoints, $value) { | ||
| $value: map-get($breakpoints, $value); | ||
| } @else { | ||
| $value: to-number($value); | ||
| } | ||
| $interval: map-get($unit-intervals, unit($value)); | ||
| @if not $interval { | ||
| // It is not possible to include a mixin inside a function, so we have to | ||
| // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because | ||
| // functions cannot be called anywhere in Sass, we need to hack the call in | ||
| // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with | ||
| // Sass 3.3, change this line in `@if im-log(..) {}` instead. | ||
| $_: im-log('Unknown unit `#{unit($value)}`.'); | ||
| } | ||
| @if $operator == '>' { | ||
| $value: $value + $interval; | ||
| } @else if $operator == '<' { | ||
| $value: $value - $interval; | ||
| } | ||
| @return $value; | ||
| } | ||
| /// | ||
| /// Parse an expression to return a valid media-query expression | ||
| /// | ||
| /// @param {String} $expression - Expression to parse | ||
| /// | ||
| /// @return {String} - Valid media query | ||
| /// | ||
| @function parse-expression($expression) { | ||
| // If it is part of $media-expressions, it has no operator | ||
| // then there is no need to go any further, just return the value | ||
| @if map-has-key($media-expressions, $expression) { | ||
| @return map-get($media-expressions, $expression); | ||
| } | ||
| $operator: get-expression-operator($expression); | ||
| $dimension: get-expression-dimension($expression, $operator); | ||
| $prefix: get-expression-prefix($operator); | ||
| $value: get-expression-value($expression, $operator); | ||
| @return '(#{$prefix}-#{$dimension}: #{$value})'; | ||
| } | ||
| /// | ||
| /// Get a shallow list of resolved feature queries | ||
| /// | ||
| /// @param {String} $queries - A nested list structure | ||
| /// @param {Boolean} $do-parse - Try to parse expressions | ||
| /// | ||
| /// @return {List} - A one item deep nested list like `(a b, c d e)` | ||
| /// | ||
| @function resolve-feature-queries($queries, $do-parse: true) { | ||
| $separator: list-separator($queries); | ||
| $result: if($separator == 'space', ((), ), ()); | ||
| @each $query in $queries { | ||
| $chainable-queries: (); | ||
| @if (type-of($query) == 'list') { | ||
| // List item is a list itself | ||
| $chainable-queries: resolve-feature-queries($query); | ||
| } @else { | ||
| // List item is a string | ||
| $parsed-query: if($do-parse, parse-expression($query), $query); | ||
| @if (type-of($parsed-query) == 'list') { | ||
| // Parsed expression is a list | ||
| $chainable-queries: resolve-feature-queries($parsed-query, false); | ||
| } @else { | ||
| // Parsed expression is a string | ||
| $chainable-queries: ($parsed-query); | ||
| } | ||
| } | ||
| $result: append-feature-query($result, $chainable-queries, $separator); | ||
| } | ||
| @return $result; | ||
| } | ||
| /// | ||
| /// Combine two query lists as a logical AND / OR operation | ||
| /// | ||
| /// @param {List} $base-queries - The host list | ||
| /// @param {List} $append-queries - The list that is being appended | ||
| /// @param {String} $separator - Either `space` or `comma` | ||
| /// | ||
| /// @return {List} - A one item deep nested list like `(a b, c d e)` | ||
| /// | ||
| @function append-feature-query($base-queries, $append-queries, $separator) { | ||
| $result: if($separator == 'space', (), $base-queries); | ||
| @each $append-query in $append-queries { | ||
| @if ($separator == 'space') { | ||
| // Logical AND | ||
| @each $base-query in $base-queries { | ||
| $updated-query: join($base-query, $append-query, $separator); | ||
| $result: append($result, $updated-query, 'comma'); | ||
| } | ||
| } @else { | ||
| // Logical OR | ||
| $result: append($result, $append-query, 'comma'); | ||
| } | ||
| } | ||
| @return $result; | ||
| } | ||
| /// | ||
| /// Parse a list of resolved expressions to return a valid media-query | ||
| /// | ||
| /// @param {List} $queries - A one item deep nested list like `(a b, c d e)` | ||
| /// | ||
| /// @return {String} - A valid media-query string | ||
| /// | ||
| @function parse-media-query($queries) { | ||
| $result: null; | ||
| $flat-queries: (); | ||
| $separator: list-separator($queries); | ||
| $conjunction: if($separator == 'space', ' and ', ', '); | ||
| @if (type-of($queries) == 'string') { | ||
| @return $queries; | ||
| } | ||
| @each $query in $queries { | ||
| @if (type-of($query) == 'list') { | ||
| $flat-queries: append($flat-queries, parse-media-query($query)); | ||
| } @else { | ||
| $flat-queries: append($flat-queries, $query); | ||
| } | ||
| } | ||
| @for $i from 1 through length($flat-queries) { | ||
| $e: nth($flat-queries, $i); | ||
| $result: unquote('#{$result}#{$e}#{if($i != length($flat-queries), $conjunction, '')}'); | ||
| } | ||
| @return $result; | ||
| } | ||
| //// | ||
| /// Cross-engine logging engine | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Log a message either with `@error` if supported | ||
| /// else with `@warn`, using `feature-exists('at-error')` | ||
| /// to detect support. | ||
| /// | ||
| /// @param {String} $message - Message to log | ||
| /// | ||
| @function im-log($message) { | ||
| @if feature-exists('at-error') { | ||
| @error $message; | ||
| } @else { | ||
| @warn $message; | ||
| $_: noop(); | ||
| } | ||
| @return $message; | ||
| } | ||
| /// | ||
| /// Wrapper mixin for the log function so it can be used with a more friendly | ||
| /// API than `@if im-log('..') {}` or `$_: im-log('..')`. Basically, use the function | ||
| /// within functions because it is not possible to include a mixin in a function | ||
| /// and use the mixin everywhere else because it's much more elegant. | ||
| /// | ||
| /// @param {String} $message - Message to log | ||
| /// | ||
| @mixin log($message) { | ||
| @if im-log($message) {} | ||
| } | ||
| /// | ||
| /// Function with no `@return` called next to `@warn` in Sass 3.3 | ||
| /// to trigger a compiling error and stop the process. | ||
| /// | ||
| @function noop() {} | ||
| /// | ||
| /// Determines whether a list of conditions is intercepted by the static breakpoint. | ||
| /// | ||
| /// @param {Arglist} $conditions - Media query conditions | ||
| /// | ||
| /// @return {Boolean} - Returns true if the conditions are intercepted by the static breakpoint | ||
| /// | ||
| @function im-intercepts-static-breakpoint($conditions...) { | ||
| $no-media-breakpoint-value: map-get($breakpoints, $im-no-media-breakpoint); | ||
| @if not $no-media-breakpoint-value { | ||
| @if im-log('`#{$im-no-media-breakpoint}` is not a valid breakpoint.') {} | ||
| } | ||
| @each $condition in $conditions { | ||
| @if not map-has-key($media-expressions, $condition) { | ||
| $operator: get-expression-operator($condition); | ||
| $prefix: get-expression-prefix($operator); | ||
| $value: get-expression-value($condition, $operator); | ||
| @if ($prefix == 'max' and $value <= $no-media-breakpoint-value) or | ||
| ($prefix == 'min' and $value > $no-media-breakpoint-value) { | ||
| @return false; | ||
| } | ||
| } @else if not index($im-no-media-expressions, $condition) { | ||
| @return false; | ||
| } | ||
| } | ||
| @return true; | ||
| } | ||
| /// | ||
| /// Slice `$list` between `$start` and `$end` indexes | ||
| /// | ||
| /// @access private | ||
| /// | ||
| /// @param {List} $list - List to slice | ||
| /// @param {Number} $start [1] - Start index | ||
| /// @param {Number} $end [length($list)] - End index | ||
| /// | ||
| /// @return {List} Sliced list | ||
| /// | ||
| @function slice($list, $start: 1, $end: length($list)) { | ||
| @if length($list) < 1 or $start > $end { | ||
| @return (); | ||
| } | ||
| $result: (); | ||
| @for $i from $start through $end { | ||
| $result: append($result, nth($list, $i)); | ||
| } | ||
| @return $result; | ||
| } | ||
| //// | ||
| /// String to number converter | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Casts a string into a number | ||
| /// | ||
| /// @param {String | Number} $value - Value to be parsed | ||
| /// | ||
| /// @return {Number} | ||
| /// | ||
| @function to-number($value) { | ||
| @if type-of($value) == 'number' { | ||
| @return $value; | ||
| } @else if type-of($value) != 'string' { | ||
| $_: im-log('Value for `to-number` should be a number or a string.'); | ||
| } | ||
| $first-character: str-slice($value, 1, 1); | ||
| $result: 0; | ||
| $digits: 0; | ||
| $minus: ($first-character == '-'); | ||
| $numbers: ('0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9); | ||
| // Remove +/- sign if present at first character | ||
| @if ($first-character == '+' or $first-character == '-') { | ||
| $value: str-slice($value, 2); | ||
| } | ||
| @for $i from 1 through str-length($value) { | ||
| $character: str-slice($value, $i, $i); | ||
| @if not (index(map-keys($numbers), $character) or $character == '.') { | ||
| @return to-length(if($minus, -$result, $result), str-slice($value, $i)) | ||
| } | ||
| @if $character == '.' { | ||
| $digits: 1; | ||
| } @else if $digits == 0 { | ||
| $result: $result * 10 + map-get($numbers, $character); | ||
| } @else { | ||
| $digits: $digits * 10; | ||
| $result: $result + map-get($numbers, $character) / $digits; | ||
| } | ||
| } | ||
| @return if($minus, -$result, $result); | ||
| } | ||
| /// | ||
| /// Add `$unit` to `$value` | ||
| /// | ||
| /// @param {Number} $value - Value to add unit to | ||
| /// @param {String} $unit - String representation of the unit | ||
| /// | ||
| /// @return {Number} - `$value` expressed in `$unit` | ||
| /// | ||
| @function to-length($value, $unit) { | ||
| $units: ('px': 1px, 'cm': 1cm, 'mm': 1mm, '%': 1%, 'ch': 1ch, 'pc': 1pc, 'in': 1in, 'em': 1em, 'rem': 1rem, 'pt': 1pt, 'ex': 1ex, 'vw': 1vw, 'vh': 1vh, 'vmin': 1vmin, 'vmax': 1vmax); | ||
| @if not index(map-keys($units), $unit) { | ||
| $_: im-log('Invalid unit `#{$unit}`.'); | ||
| } | ||
| @return $value * map-get($units, $unit); | ||
| } | ||
| /// | ||
| /// This mixin aims at redefining the configuration just for the scope of | ||
| /// the call. It is helpful when having a component needing an extended | ||
| /// configuration such as custom breakpoints (referred to as tweakpoints) | ||
| /// for instance. | ||
| /// | ||
| /// @author Hugo Giraudel | ||
| /// | ||
| /// @param {Map} $tweakpoints [()] - Map of tweakpoints to be merged with `$breakpoints` | ||
| /// @param {Map} $tweak-media-expressions [()] - Map of tweaked media expressions to be merged with `$media-expression` | ||
| /// | ||
| /// @example scss - Extend the global breakpoints with a tweakpoint | ||
| /// @include media-context(('custom': 678px)) { | ||
| /// .foo { | ||
| /// @include media('>phone', '<=custom') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// @example scss - Extend the global media expressions with a custom one | ||
| /// @include media-context($tweak-media-expressions: ('all': 'all')) { | ||
| /// .foo { | ||
| /// @include media('all', '>phone') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// @example scss - Extend both configuration maps | ||
| /// @include media-context(('custom': 678px), ('all': 'all')) { | ||
| /// .foo { | ||
| /// @include media('all', '>phone', '<=custom') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| @mixin media-context($tweakpoints: (), $tweak-media-expressions: ()) { | ||
| // Save global configuration | ||
| $global-breakpoints: $breakpoints; | ||
| $global-media-expressions: $media-expressions; | ||
| // Update global configuration | ||
| $breakpoints: map-merge($breakpoints, $tweakpoints) !global; | ||
| $media-expressions: map-merge($media-expressions, $tweak-media-expressions) !global; | ||
| @content; | ||
| // Restore global configuration | ||
| $breakpoints: $global-breakpoints !global; | ||
| $media-expressions: $global-media-expressions !global; | ||
| } | ||
| //// | ||
| /// include-media public exposed API | ||
| /// @author Eduardo Boucas | ||
| /// @access public | ||
| //// | ||
| /// | ||
| /// Generates a media query based on a list of conditions | ||
| /// | ||
| /// @param {Arglist} $conditions - Media query conditions | ||
| /// | ||
| /// @example scss - With a single set breakpoint | ||
| /// @include media('>phone') { } | ||
| /// | ||
| /// @example scss - With two set breakpoints | ||
| /// @include media('>phone', '<=tablet') { } | ||
| /// | ||
| /// @example scss - With custom values | ||
| /// @include media('>=358px', '<850px') { } | ||
| /// | ||
| /// @example scss - With set breakpoints with custom values | ||
| /// @include media('>desktop', '<=1350px') { } | ||
| /// | ||
| /// @example scss - With a static expression | ||
| /// @include media('retina2x') { } | ||
| /// | ||
| /// @example scss - Mixing everything | ||
| /// @include media('>=350px', '<tablet', 'retina3x') { } | ||
| /// | ||
| @mixin media($conditions...) { | ||
| $is-list-mode: (length($conditions) == 1 and type-of(nth($conditions, 1)) == 'list'); | ||
| @if ($im-media-support and length($conditions) == 0) or | ||
| (not $im-media-support and im-intercepts-static-breakpoint($conditions...) and not $is-list-mode) { | ||
| @content; | ||
| } @else if ($im-media-support and $is-list-mode) { | ||
| // List mode with AND / OR conditions | ||
| @media #{unquote(parse-media-query(resolve-feature-queries(nth($conditions, 1))))} { | ||
| @content; | ||
| } | ||
| } @else if ($im-media-support and length($conditions) > 0) { | ||
| // Legacy mode | ||
| @media #{unquote(parse-media-query(parse-expression(nth($conditions, 1))))} { | ||
| // Recursive call | ||
| @include media(slice($conditions, 2)...) { | ||
| @content; | ||
| } | ||
| } | ||
| } | ||
| } |
| @charset "UTF-8"; | ||
| // _ _ _ _ _ | ||
| // (_) | | | | | (_) | ||
| // _ _ __ ___| |_ _ __| | ___ _ __ ___ ___ __| |_ __ _ | ||
| // | | '_ \ / __| | | | |/ _` |/ _ \ | '_ ` _ \ / _ \/ _` | |/ _` | | ||
| // | | | | | (__| | |_| | (_| | __/ | | | | | | __/ (_| | | (_| | | ||
| // |_|_| |_|\___|_|\__,_|\__,_|\___| |_| |_| |_|\___|\__,_|_|\__,_| | ||
| // | ||
| // Simple, elegant and maintainable media queries in Sass | ||
| // v1.4.9 | ||
| // | ||
| // http://include-media.com | ||
| // | ||
| // Authors: Eduardo Boucas (@eduardoboucas) | ||
| // Hugo Giraudel (@hugogiraudel) | ||
| // | ||
| // This project is licensed under the terms of the MIT license | ||
| //// | ||
| /// include-media library public configuration | ||
| /// @author Eduardo Boucas | ||
| /// @access public | ||
| //// | ||
| /// | ||
| /// Creates a list of global breakpoints | ||
| /// | ||
| /// @example scss - Creates a single breakpoint with the label `phone` | ||
| /// $breakpoints: ('phone': 320px); | ||
| /// | ||
| $breakpoints: ( | ||
| 'phone': 320px, | ||
| 'tablet': 768px, | ||
| 'desktop': 1024px | ||
| ) !default; | ||
| /// | ||
| /// Creates a list of static expressions or media types | ||
| /// | ||
| /// @example scss - Creates a single media type (screen) | ||
| /// $media-expressions: ('screen': 'screen'); | ||
| /// | ||
| /// @example scss - Creates a static expression with logical disjunction (OR operator) | ||
| /// $media-expressions: ( | ||
| /// 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)' | ||
| /// ); | ||
| /// | ||
| $media-expressions: ( | ||
| 'screen': 'screen', | ||
| 'print': 'print', | ||
| 'handheld': 'handheld', | ||
| 'landscape': '(orientation: landscape)', | ||
| 'portrait': '(orientation: portrait)', | ||
| 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi), (min-resolution: 2dppx)', | ||
| 'retina3x': '(-webkit-min-device-pixel-ratio: 3), (min-resolution: 350dpi), (min-resolution: 3dppx)' | ||
| ) !default; | ||
| /// | ||
| /// Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals | ||
| /// | ||
| /// @example scss - Interval for pixels is defined as `1` by default | ||
| /// @include media('>128px') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 129px) {} | ||
| /// | ||
| /// @example scss - Interval for ems is defined as `0.01` by default | ||
| /// @include media('>20em') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 20.01em) {} | ||
| /// | ||
| /// @example scss - Interval for rems is defined as `0.1` by default, to be used with `font-size: 62.5%;` | ||
| /// @include media('>2.0rem') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 2.1rem) {} | ||
| /// | ||
| $unit-intervals: ( | ||
| 'px': 1, | ||
| 'em': 0.01, | ||
| 'rem': 0.1, | ||
| '': 0 | ||
| ) !default; | ||
| /// | ||
| /// Defines whether support for media queries is available, useful for creating separate stylesheets | ||
| /// for browsers that don't support media queries. | ||
| /// | ||
| /// @example scss - Disables support for media queries | ||
| /// $im-media-support: false; | ||
| /// @include media('>=tablet') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| $im-media-support: true !default; | ||
| /// | ||
| /// Selects which breakpoint to emulate when support for media queries is disabled. Media queries that start at or | ||
| /// intercept the breakpoint will be displayed, any others will be ignored. | ||
| /// | ||
| /// @example scss - This media query will show because it intercepts the static breakpoint | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// @include media('>=tablet') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| /// @example scss - This media query will NOT show because it does not intercept the desktop breakpoint | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'tablet'; | ||
| /// @include media('>=desktop') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* No output */ | ||
| /// | ||
| $im-no-media-breakpoint: 'desktop' !default; | ||
| /// | ||
| /// Selects which media expressions are allowed in an expression for it to be used when media queries | ||
| /// are not supported. | ||
| /// | ||
| /// @example scss - This media query will show because it intercepts the static breakpoint and contains only accepted media expressions | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// $im-no-media-expressions: ('screen'); | ||
| /// @include media('>=tablet', 'screen') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| /// @example scss - This media query will NOT show because it intercepts the static breakpoint but contains a media expression that is not accepted | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// $im-no-media-expressions: ('screen'); | ||
| /// @include media('>=tablet', 'retina2x') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* No output */ | ||
| /// | ||
| $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default; | ||
| //// | ||
| /// Cross-engine logging engine | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Log a message either with `@error` if supported | ||
| /// else with `@warn`, using `feature-exists('at-error')` | ||
| /// to detect support. | ||
| /// | ||
| /// @param {String} $message - Message to log | ||
| /// | ||
| @function im-log($message) { | ||
| @if feature-exists('at-error') { | ||
| @error $message; | ||
| } @else { | ||
| @warn $message; | ||
| $_: noop(); | ||
| } | ||
| @return $message; | ||
| } | ||
| /// | ||
| /// Wrapper mixin for the log function so it can be used with a more friendly | ||
| /// API than `@if im-log('..') {}` or `$_: im-log('..')`. Basically, use the function | ||
| /// within functions because it is not possible to include a mixin in a function | ||
| /// and use the mixin everywhere else because it's much more elegant. | ||
| /// | ||
| /// @param {String} $message - Message to log | ||
| /// | ||
| @mixin log($message) { | ||
| @if im-log($message) {} | ||
| } | ||
| /// | ||
| /// Function with no `@return` called next to `@warn` in Sass 3.3 | ||
| /// to trigger a compiling error and stop the process. | ||
| /// | ||
| @function noop() {} | ||
| /// | ||
| /// Determines whether a list of conditions is intercepted by the static breakpoint. | ||
| /// | ||
| /// @param {Arglist} $conditions - Media query conditions | ||
| /// | ||
| /// @return {Boolean} - Returns true if the conditions are intercepted by the static breakpoint | ||
| /// | ||
| @function im-intercepts-static-breakpoint($conditions...) { | ||
| $no-media-breakpoint-value: map-get($breakpoints, $im-no-media-breakpoint); | ||
| @if not $no-media-breakpoint-value { | ||
| @if im-log('`#{$im-no-media-breakpoint}` is not a valid breakpoint.') {} | ||
| } | ||
| @each $condition in $conditions { | ||
| @if not map-has-key($media-expressions, $condition) { | ||
| $operator: get-expression-operator($condition); | ||
| $prefix: get-expression-prefix($operator); | ||
| $value: get-expression-value($condition, $operator); | ||
| @if ($prefix == 'max' and $value <= $no-media-breakpoint-value) or | ||
| ($prefix == 'min' and $value > $no-media-breakpoint-value) { | ||
| @return false; | ||
| } | ||
| } @else if not index($im-no-media-expressions, $condition) { | ||
| @return false; | ||
| } | ||
| } | ||
| @return true; | ||
| } | ||
| //// | ||
| /// Parsing engine | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Get operator of an expression | ||
| /// | ||
| /// @param {String} $expression - Expression to extract operator from | ||
| /// | ||
| /// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤` | ||
| /// | ||
| @function get-expression-operator($expression) { | ||
| @each $operator in ('>=', '>', '<=', '<', '≥', '≤') { | ||
| @if str-index($expression, $operator) { | ||
| @return $operator; | ||
| } | ||
| } | ||
| // It is not possible to include a mixin inside a function, so we have to | ||
| // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because | ||
| // functions cannot be called anywhere in Sass, we need to hack the call in | ||
| // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with | ||
| // Sass 3.3, change this line in `@if im-log(..) {}` instead. | ||
| $_: im-log('No operator found in `#{$expression}`.'); | ||
| } | ||
| /// | ||
| /// Get dimension of an expression, based on a found operator | ||
| /// | ||
| /// @param {String} $expression - Expression to extract dimension from | ||
| /// @param {String} $operator - Operator from `$expression` | ||
| /// | ||
| /// @return {String} - `width` or `height` (or potentially anything else) | ||
| /// | ||
| @function get-expression-dimension($expression, $operator) { | ||
| $operator-index: str-index($expression, $operator); | ||
| $parsed-dimension: str-slice($expression, 0, $operator-index - 1); | ||
| $dimension: 'width'; | ||
| @if str-length($parsed-dimension) > 0 { | ||
| $dimension: $parsed-dimension; | ||
| } | ||
| @return $dimension; | ||
| } | ||
| /// | ||
| /// Get dimension prefix based on an operator | ||
| /// | ||
| /// @param {String} $operator - Operator | ||
| /// | ||
| /// @return {String} - `min` or `max` | ||
| /// | ||
| @function get-expression-prefix($operator) { | ||
| @return if(index(('<', '<=', '≤'), $operator), 'max', 'min'); | ||
| } | ||
| /// | ||
| /// Get value of an expression, based on a found operator | ||
| /// | ||
| /// @param {String} $expression - Expression to extract value from | ||
| /// @param {String} $operator - Operator from `$expression` | ||
| /// | ||
| /// @return {Number} - A numeric value | ||
| /// | ||
| @function get-expression-value($expression, $operator) { | ||
| $operator-index: str-index($expression, $operator); | ||
| $value: str-slice($expression, $operator-index + str-length($operator)); | ||
| @if map-has-key($breakpoints, $value) { | ||
| $value: map-get($breakpoints, $value); | ||
| } @else { | ||
| $value: to-number($value); | ||
| } | ||
| $interval: map-get($unit-intervals, unit($value)); | ||
| @if not $interval { | ||
| // It is not possible to include a mixin inside a function, so we have to | ||
| // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because | ||
| // functions cannot be called anywhere in Sass, we need to hack the call in | ||
| // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with | ||
| // Sass 3.3, change this line in `@if im-log(..) {}` instead. | ||
| $_: im-log('Unknown unit `#{unit($value)}`.'); | ||
| } | ||
| @if $operator == '>' { | ||
| $value: $value + $interval; | ||
| } @else if $operator == '<' { | ||
| $value: $value - $interval; | ||
| } | ||
| @return $value; | ||
| } | ||
| /// | ||
| /// Parse an expression to return a valid media-query expression | ||
| /// | ||
| /// @param {String} $expression - Expression to parse | ||
| /// | ||
| /// @return {String} - Valid media query | ||
| /// | ||
| @function parse-expression($expression) { | ||
| // If it is part of $media-expressions, it has no operator | ||
| // then there is no need to go any further, just return the value | ||
| @if map-has-key($media-expressions, $expression) { | ||
| @return map-get($media-expressions, $expression); | ||
| } | ||
| $operator: get-expression-operator($expression); | ||
| $dimension: get-expression-dimension($expression, $operator); | ||
| $prefix: get-expression-prefix($operator); | ||
| $value: get-expression-value($expression, $operator); | ||
| @return '(#{$prefix}-#{$dimension}: #{$value})'; | ||
| } | ||
| /// | ||
| /// Slice `$list` between `$start` and `$end` indexes | ||
| /// | ||
| /// @access private | ||
| /// | ||
| /// @param {List} $list - List to slice | ||
| /// @param {Number} $start [1] - Start index | ||
| /// @param {Number} $end [length($list)] - End index | ||
| /// | ||
| /// @return {List} Sliced list | ||
| /// | ||
| @function slice($list, $start: 1, $end: length($list)) { | ||
| @if length($list) < 1 or $start > $end { | ||
| @return (); | ||
| } | ||
| $result: (); | ||
| @for $i from $start through $end { | ||
| $result: append($result, nth($list, $i)); | ||
| } | ||
| @return $result; | ||
| } | ||
| //// | ||
| /// String to number converter | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Casts a string into a number | ||
| /// | ||
| /// @param {String | Number} $value - Value to be parsed | ||
| /// | ||
| /// @return {Number} | ||
| /// | ||
| @function to-number($value) { | ||
| @if type-of($value) == 'number' { | ||
| @return $value; | ||
| } @else if type-of($value) != 'string' { | ||
| $_: im-log('Value for `to-number` should be a number or a string.'); | ||
| } | ||
| $first-character: str-slice($value, 1, 1); | ||
| $result: 0; | ||
| $digits: 0; | ||
| $minus: ($first-character == '-'); | ||
| $numbers: ('0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9); | ||
| // Remove +/- sign if present at first character | ||
| @if ($first-character == '+' or $first-character == '-') { | ||
| $value: str-slice($value, 2); | ||
| } | ||
| @for $i from 1 through str-length($value) { | ||
| $character: str-slice($value, $i, $i); | ||
| @if not (index(map-keys($numbers), $character) or $character == '.') { | ||
| @return to-length(if($minus, -$result, $result), str-slice($value, $i)) | ||
| } | ||
| @if $character == '.' { | ||
| $digits: 1; | ||
| } @else if $digits == 0 { | ||
| $result: $result * 10 + map-get($numbers, $character); | ||
| } @else { | ||
| $digits: $digits * 10; | ||
| $result: $result + map-get($numbers, $character) / $digits; | ||
| } | ||
| } | ||
| @return if($minus, -$result, $result); | ||
| } | ||
| /// | ||
| /// Add `$unit` to `$value` | ||
| /// | ||
| /// @param {Number} $value - Value to add unit to | ||
| /// @param {String} $unit - String representation of the unit | ||
| /// | ||
| /// @return {Number} - `$value` expressed in `$unit` | ||
| /// | ||
| @function to-length($value, $unit) { | ||
| $units: ('px': 1px, 'cm': 1cm, 'mm': 1mm, '%': 1%, 'ch': 1ch, 'pc': 1pc, 'in': 1in, 'em': 1em, 'rem': 1rem, 'pt': 1pt, 'ex': 1ex, 'vw': 1vw, 'vh': 1vh, 'vmin': 1vmin, 'vmax': 1vmax); | ||
| @if not index(map-keys($units), $unit) { | ||
| $_: im-log('Invalid unit `#{$unit}`.'); | ||
| } | ||
| @return $value * map-get($units, $unit); | ||
| } | ||
| /// | ||
| /// This mixin aims at redefining the configuration just for the scope of | ||
| /// the call. It is helpful when having a component needing an extended | ||
| /// configuration such as custom breakpoints (referred to as tweakpoints) | ||
| /// for instance. | ||
| /// | ||
| /// @author Hugo Giraudel | ||
| /// | ||
| /// @param {Map} $tweakpoints [()] - Map of tweakpoints to be merged with `$breakpoints` | ||
| /// @param {Map} $tweak-media-expressions [()] - Map of tweaked media expressions to be merged with `$media-expression` | ||
| /// | ||
| /// @example scss - Extend the global breakpoints with a tweakpoint | ||
| /// @include media-context(('custom': 678px)) { | ||
| /// .foo { | ||
| /// @include media('>phone', '<=custom') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// @example scss - Extend the global media expressions with a custom one | ||
| /// @include media-context($tweak-media-expressions: ('all': 'all')) { | ||
| /// .foo { | ||
| /// @include media('all', '>phone') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// @example scss - Extend both configuration maps | ||
| /// @include media-context(('custom': 678px), ('all': 'all')) { | ||
| /// .foo { | ||
| /// @include media('all', '>phone', '<=custom') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| @mixin media-context($tweakpoints: (), $tweak-media-expressions: ()) { | ||
| // Save global configuration | ||
| $global-breakpoints: $breakpoints; | ||
| $global-media-expressions: $media-expressions; | ||
| // Update global configuration | ||
| $breakpoints: map-merge($breakpoints, $tweakpoints) !global; | ||
| $media-expressions: map-merge($media-expressions, $tweak-media-expressions) !global; | ||
| @content; | ||
| // Restore global configuration | ||
| $breakpoints: $global-breakpoints !global; | ||
| $media-expressions: $global-media-expressions !global; | ||
| } | ||
| //// | ||
| /// include-media public exposed API | ||
| /// @author Eduardo Boucas | ||
| /// @access public | ||
| //// | ||
| /// | ||
| /// Generates a media query based on a list of conditions | ||
| /// | ||
| /// @param {Arglist} $conditions - Media query conditions | ||
| /// | ||
| /// @example scss - With a single set breakpoint | ||
| /// @include media('>phone') { } | ||
| /// | ||
| /// @example scss - With two set breakpoints | ||
| /// @include media('>phone', '<=tablet') { } | ||
| /// | ||
| /// @example scss - With custom values | ||
| /// @include media('>=358px', '<850px') { } | ||
| /// | ||
| /// @example scss - With set breakpoints with custom values | ||
| /// @include media('>desktop', '<=1350px') { } | ||
| /// | ||
| /// @example scss - With a static expression | ||
| /// @include media('retina2x') { } | ||
| /// | ||
| /// @example scss - Mixing everything | ||
| /// @include media('>=350px', '<tablet', 'retina3x') { } | ||
| /// | ||
| @mixin media($conditions...) { | ||
| @if ($im-media-support and length($conditions) == 0) or | ||
| (not $im-media-support and im-intercepts-static-breakpoint($conditions...)) { | ||
| @content; | ||
| } @else if ($im-media-support and length($conditions) > 0) { | ||
| @media #{unquote(parse-expression(nth($conditions, 1)))} { | ||
| // Recursive call | ||
| @include media(slice($conditions, 2)...) { | ||
| @content; | ||
| } | ||
| } | ||
| } | ||
| } |
| //// | ||
| /// include-media library public configuration | ||
| /// @author Eduardo Boucas | ||
| /// @access public | ||
| //// | ||
| /// | ||
| /// Creates a list of global breakpoints | ||
| /// | ||
| /// @example scss - Creates a single breakpoint with the label `phone` | ||
| /// $breakpoints: ('phone': 320px); | ||
| /// | ||
| $breakpoints: ( | ||
| 'phone': 320px, | ||
| 'tablet': 768px, | ||
| 'desktop': 1024px | ||
| ) !default; | ||
| /// | ||
| /// Creates a list of static expressions or media types | ||
| /// | ||
| /// @example scss - Creates a single media type (screen) | ||
| /// $media-expressions: ('screen': 'screen'); | ||
| /// | ||
| /// @example scss - Creates a static expression with logical disjunction (OR operator) | ||
| /// $media-expressions: ( | ||
| /// 'retina2x': '(-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi)' | ||
| /// ); | ||
| /// | ||
| $media-expressions: ( | ||
| 'screen': 'screen', | ||
| 'print': 'print', | ||
| 'handheld': 'handheld', | ||
| 'landscape': '(orientation: landscape)', | ||
| 'portrait': '(orientation: portrait)', | ||
| 'retina2x': ('(-webkit-min-device-pixel-ratio: 2)', '(min-resolution: 192dpi)', '(min-resolution: 2dppx)'), | ||
| 'retina3x': ('(-webkit-min-device-pixel-ratio: 3)', '(min-resolution: 350dpi)', '(min-resolution: 3dppx)') | ||
| ) !default; | ||
| /// | ||
| /// Defines a number to be added or subtracted from each unit when declaring breakpoints with exclusive intervals | ||
| /// | ||
| /// @example scss - Interval for pixels is defined as `1` by default | ||
| /// @include media('>128px') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 129px) {} | ||
| /// | ||
| /// @example scss - Interval for ems is defined as `0.01` by default | ||
| /// @include media('>20em') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 20.01em) {} | ||
| /// | ||
| /// @example scss - Interval for rems is defined as `0.1` by default, to be used with `font-size: 62.5%;` | ||
| /// @include media('>2.0rem') {} | ||
| /// | ||
| /// /* Generates: */ | ||
| /// @media (min-width: 2.1rem) {} | ||
| /// | ||
| $unit-intervals: ( | ||
| 'px': 1, | ||
| 'em': 0.01, | ||
| 'rem': 0.1, | ||
| '': 0 | ||
| ) !default; | ||
| /// | ||
| /// Defines whether support for media queries is available, useful for creating separate stylesheets | ||
| /// for browsers that don't support media queries. | ||
| /// | ||
| /// @example scss - Disables support for media queries | ||
| /// $im-media-support: false; | ||
| /// @include media('>=tablet') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| $im-media-support: true !default; | ||
| /// | ||
| /// Selects which breakpoint to emulate when support for media queries is disabled. Media queries that start at or | ||
| /// intercept the breakpoint will be displayed, any others will be ignored. | ||
| /// | ||
| /// @example scss - This media query will show because it intercepts the static breakpoint | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// @include media('>=tablet') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| /// @example scss - This media query will NOT show because it does not intercept the desktop breakpoint | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'tablet'; | ||
| /// @include media('>=desktop') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* No output */ | ||
| /// | ||
| $im-no-media-breakpoint: 'desktop' !default; | ||
| /// | ||
| /// Selects which media expressions are allowed in an expression for it to be used when media queries | ||
| /// are not supported. | ||
| /// | ||
| /// @example scss - This media query will show because it intercepts the static breakpoint and contains only accepted media expressions | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// $im-no-media-expressions: ('screen'); | ||
| /// @include media('>=tablet', 'screen') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* Generates: */ | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// | ||
| /// @example scss - This media query will NOT show because it intercepts the static breakpoint but contains a media expression that is not accepted | ||
| /// $im-media-support: false; | ||
| /// $im-no-media-breakpoint: 'desktop'; | ||
| /// $im-no-media-expressions: ('screen'); | ||
| /// @include media('>=tablet', 'retina2x') { | ||
| /// .foo { | ||
| /// color: tomato; | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// /* No output */ | ||
| /// | ||
| $im-no-media-expressions: ('screen', 'portrait', 'landscape') !default; |
| //// | ||
| /// include-media public exposed API | ||
| /// @author Eduardo Boucas | ||
| /// @access public | ||
| //// | ||
| /// | ||
| /// Generates a media query based on a list of conditions | ||
| /// | ||
| /// @param {Arglist} $conditions - Media query conditions | ||
| /// | ||
| /// @example scss - With a single set breakpoint | ||
| /// @include media('>phone') { } | ||
| /// | ||
| /// @example scss - With two set breakpoints | ||
| /// @include media('>phone', '<=tablet') { } | ||
| /// | ||
| /// @example scss - With custom values | ||
| /// @include media('>=358px', '<850px') { } | ||
| /// | ||
| /// @example scss - With set breakpoints with custom values | ||
| /// @include media('>desktop', '<=1350px') { } | ||
| /// | ||
| /// @example scss - With a static expression | ||
| /// @include media('retina2x') { } | ||
| /// | ||
| /// @example scss - Mixing everything | ||
| /// @include media('>=350px', '<tablet', 'retina3x') { } | ||
| /// | ||
| @mixin media($conditions...) { | ||
| $is-list-mode: (length($conditions) == 1 and type-of(nth($conditions, 1)) == 'list'); | ||
| @if ($im-media-support and length($conditions) == 0) or | ||
| (not $im-media-support and im-intercepts-static-breakpoint($conditions...) and not $is-list-mode) { | ||
| @content; | ||
| } @else if ($im-media-support and $is-list-mode) { | ||
| // List mode with AND / OR conditions | ||
| @media #{unquote(parse-media-query(resolve-feature-queries(nth($conditions, 1))))} { | ||
| @content; | ||
| } | ||
| } @else if ($im-media-support and length($conditions) > 0) { | ||
| // Legacy mode | ||
| @media #{unquote(parse-media-query(parse-expression(nth($conditions, 1))))} { | ||
| // Recursive call | ||
| @include media(slice($conditions, 2)...) { | ||
| @content; | ||
| } | ||
| } | ||
| } | ||
| } |
| //// | ||
| /// Parsing engine | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Get operator of an expression | ||
| /// | ||
| /// @param {String} $expression - Expression to extract operator from | ||
| /// | ||
| /// @return {String} - Any of `>=`, `>`, `<=`, `<`, `≥`, `≤` | ||
| /// | ||
| @function get-expression-operator($expression) { | ||
| @each $operator in ('>=', '>', '<=', '<', '≥', '≤') { | ||
| @if str-index($expression, $operator) { | ||
| @return $operator; | ||
| } | ||
| } | ||
| // It is not possible to include a mixin inside a function, so we have to | ||
| // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because | ||
| // functions cannot be called anywhere in Sass, we need to hack the call in | ||
| // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with | ||
| // Sass 3.3, change this line in `@if im-log(..) {}` instead. | ||
| $_: im-log('No operator found in `#{$expression}`.'); | ||
| } | ||
| /// | ||
| /// Get dimension of an expression, based on a found operator | ||
| /// | ||
| /// @param {String} $expression - Expression to extract dimension from | ||
| /// @param {String} $operator - Operator from `$expression` | ||
| /// | ||
| /// @return {String} - `width` or `height` (or potentially anything else) | ||
| /// | ||
| @function get-expression-dimension($expression, $operator) { | ||
| $operator-index: str-index($expression, $operator); | ||
| $parsed-dimension: str-slice($expression, 0, $operator-index - 1); | ||
| $dimension: 'width'; | ||
| @if str-length($parsed-dimension) > 0 { | ||
| $dimension: $parsed-dimension; | ||
| } | ||
| @return $dimension; | ||
| } | ||
| /// | ||
| /// Get dimension prefix based on an operator | ||
| /// | ||
| /// @param {String} $operator - Operator | ||
| /// | ||
| /// @return {String} - `min` or `max` | ||
| /// | ||
| @function get-expression-prefix($operator) { | ||
| @return if(index(('<', '<=', '≤'), $operator), 'max', 'min'); | ||
| } | ||
| /// | ||
| /// Get value of an expression, based on a found operator | ||
| /// | ||
| /// @param {String} $expression - Expression to extract value from | ||
| /// @param {String} $operator - Operator from `$expression` | ||
| /// | ||
| /// @return {Number} - A numeric value | ||
| /// | ||
| @function get-expression-value($expression, $operator) { | ||
| $operator-index: str-index($expression, $operator); | ||
| $value: str-slice($expression, $operator-index + str-length($operator)); | ||
| @if map-has-key($breakpoints, $value) { | ||
| $value: map-get($breakpoints, $value); | ||
| } @else { | ||
| $value: to-number($value); | ||
| } | ||
| $interval: map-get($unit-intervals, unit($value)); | ||
| @if not $interval { | ||
| // It is not possible to include a mixin inside a function, so we have to | ||
| // rely on the `im-log(..)` function rather than the `log(..)` mixin. Because | ||
| // functions cannot be called anywhere in Sass, we need to hack the call in | ||
| // a dummy variable, such as `$_`. If anybody ever raise a scoping issue with | ||
| // Sass 3.3, change this line in `@if im-log(..) {}` instead. | ||
| $_: im-log('Unknown unit `#{unit($value)}`.'); | ||
| } | ||
| @if $operator == '>' { | ||
| $value: $value + $interval; | ||
| } @else if $operator == '<' { | ||
| $value: $value - $interval; | ||
| } | ||
| @return $value; | ||
| } | ||
| /// | ||
| /// Parse an expression to return a valid media-query expression | ||
| /// | ||
| /// @param {String} $expression - Expression to parse | ||
| /// | ||
| /// @return {String} - Valid media query | ||
| /// | ||
| @function parse-expression($expression) { | ||
| // If it is part of $media-expressions, it has no operator | ||
| // then there is no need to go any further, just return the value | ||
| @if map-has-key($media-expressions, $expression) { | ||
| @return map-get($media-expressions, $expression); | ||
| } | ||
| $operator: get-expression-operator($expression); | ||
| $dimension: get-expression-dimension($expression, $operator); | ||
| $prefix: get-expression-prefix($operator); | ||
| $value: get-expression-value($expression, $operator); | ||
| @return '(#{$prefix}-#{$dimension}: #{$value})'; | ||
| } |
| /// | ||
| /// Get a shallow list of resolved feature queries | ||
| /// | ||
| /// @param {String} $queries - A nested list structure | ||
| /// @param {Boolean} $do-parse - Try to parse expressions | ||
| /// | ||
| /// @return {List} - A one item deep nested list like `(a b, c d e)` | ||
| /// | ||
| @function resolve-feature-queries($queries, $do-parse: true) { | ||
| $separator: list-separator($queries); | ||
| $result: if($separator == 'space', ((), ), ()); | ||
| @each $query in $queries { | ||
| $chainable-queries: (); | ||
| @if (type-of($query) == 'list') { | ||
| // List item is a list itself | ||
| $chainable-queries: resolve-feature-queries($query); | ||
| } @else { | ||
| // List item is a string | ||
| $parsed-query: if($do-parse, parse-expression($query), $query); | ||
| @if (type-of($parsed-query) == 'list') { | ||
| // Parsed expression is a list | ||
| $chainable-queries: resolve-feature-queries($parsed-query, false); | ||
| } @else { | ||
| // Parsed expression is a string | ||
| $chainable-queries: ($parsed-query); | ||
| } | ||
| } | ||
| $result: append-feature-query($result, $chainable-queries, $separator); | ||
| } | ||
| @return $result; | ||
| } | ||
| /// | ||
| /// Combine two query lists as a logical AND / OR operation | ||
| /// | ||
| /// @param {List} $base-queries - The host list | ||
| /// @param {List} $append-queries - The list that is being appended | ||
| /// @param {String} $separator - Either `space` or `comma` | ||
| /// | ||
| /// @return {List} - A one item deep nested list like `(a b, c d e)` | ||
| /// | ||
| @function append-feature-query($base-queries, $append-queries, $separator) { | ||
| $result: if($separator == 'space', (), $base-queries); | ||
| @each $append-query in $append-queries { | ||
| @if ($separator == 'space') { | ||
| // Logical AND | ||
| @each $base-query in $base-queries { | ||
| $updated-query: join($base-query, $append-query, $separator); | ||
| $result: append($result, $updated-query, 'comma'); | ||
| } | ||
| } @else { | ||
| // Logical OR | ||
| $result: append($result, $append-query, 'comma'); | ||
| } | ||
| } | ||
| @return $result; | ||
| } | ||
| /// | ||
| /// Parse a list of resolved expressions to return a valid media-query | ||
| /// | ||
| /// @param {List} $queries - A one item deep nested list like `(a b, c d e)` | ||
| /// | ||
| /// @return {String} - A valid media-query string | ||
| /// | ||
| @function parse-media-query($queries) { | ||
| $result: null; | ||
| $flat-queries: (); | ||
| $separator: list-separator($queries); | ||
| $conjunction: if($separator == 'space', ' and ', ', '); | ||
| @if (type-of($queries) == 'string') { | ||
| @return $queries; | ||
| } | ||
| @each $query in $queries { | ||
| @if (type-of($query) == 'list') { | ||
| $flat-queries: append($flat-queries, parse-media-query($query)); | ||
| } @else { | ||
| $flat-queries: append($flat-queries, $query); | ||
| } | ||
| } | ||
| @for $i from 1 through length($flat-queries) { | ||
| $e: nth($flat-queries, $i); | ||
| $result: unquote('#{$result}#{$e}#{if($i != length($flat-queries), $conjunction, '')}'); | ||
| } | ||
| @return $result; | ||
| } |
| //// | ||
| /// Cross-engine logging engine | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Log a message either with `@error` if supported | ||
| /// else with `@warn`, using `feature-exists('at-error')` | ||
| /// to detect support. | ||
| /// | ||
| /// @param {String} $message - Message to log | ||
| /// | ||
| @function im-log($message) { | ||
| @if feature-exists('at-error') { | ||
| @error $message; | ||
| } @else { | ||
| @warn $message; | ||
| $_: noop(); | ||
| } | ||
| @return $message; | ||
| } | ||
| /// | ||
| /// Wrapper mixin for the log function so it can be used with a more friendly | ||
| /// API than `@if im-log('..') {}` or `$_: im-log('..')`. Basically, use the function | ||
| /// within functions because it is not possible to include a mixin in a function | ||
| /// and use the mixin everywhere else because it's much more elegant. | ||
| /// | ||
| /// @param {String} $message - Message to log | ||
| /// | ||
| @mixin log($message) { | ||
| @if im-log($message) {} | ||
| } | ||
| /// | ||
| /// Function with no `@return` called next to `@warn` in Sass 3.3 | ||
| /// to trigger a compiling error and stop the process. | ||
| /// | ||
| @function noop() {} |
| /// | ||
| /// Determines whether a list of conditions is intercepted by the static breakpoint. | ||
| /// | ||
| /// @param {Arglist} $conditions - Media query conditions | ||
| /// | ||
| /// @return {Boolean} - Returns true if the conditions are intercepted by the static breakpoint | ||
| /// | ||
| @function im-intercepts-static-breakpoint($conditions...) { | ||
| $no-media-breakpoint-value: map-get($breakpoints, $im-no-media-breakpoint); | ||
| @if not $no-media-breakpoint-value { | ||
| @if im-log('`#{$im-no-media-breakpoint}` is not a valid breakpoint.') {} | ||
| } | ||
| @each $condition in $conditions { | ||
| @if not map-has-key($media-expressions, $condition) { | ||
| $operator: get-expression-operator($condition); | ||
| $prefix: get-expression-prefix($operator); | ||
| $value: get-expression-value($condition, $operator); | ||
| @if ($prefix == 'max' and $value <= $no-media-breakpoint-value) or | ||
| ($prefix == 'min' and $value > $no-media-breakpoint-value) { | ||
| @return false; | ||
| } | ||
| } @else if not index($im-no-media-expressions, $condition) { | ||
| @return false; | ||
| } | ||
| } | ||
| @return true; | ||
| } |
| /// | ||
| /// Slice `$list` between `$start` and `$end` indexes | ||
| /// | ||
| /// @access private | ||
| /// | ||
| /// @param {List} $list - List to slice | ||
| /// @param {Number} $start [1] - Start index | ||
| /// @param {Number} $end [length($list)] - End index | ||
| /// | ||
| /// @return {List} Sliced list | ||
| /// | ||
| @function slice($list, $start: 1, $end: length($list)) { | ||
| @if length($list) < 1 or $start > $end { | ||
| @return (); | ||
| } | ||
| $result: (); | ||
| @for $i from $start through $end { | ||
| $result: append($result, nth($list, $i)); | ||
| } | ||
| @return $result; | ||
| } |
| //// | ||
| /// String to number converter | ||
| /// @author Hugo Giraudel | ||
| /// @access private | ||
| //// | ||
| /// | ||
| /// Casts a string into a number | ||
| /// | ||
| /// @param {String | Number} $value - Value to be parsed | ||
| /// | ||
| /// @return {Number} | ||
| /// | ||
| @function to-number($value) { | ||
| @if type-of($value) == 'number' { | ||
| @return $value; | ||
| } @else if type-of($value) != 'string' { | ||
| $_: im-log('Value for `to-number` should be a number or a string.'); | ||
| } | ||
| $first-character: str-slice($value, 1, 1); | ||
| $result: 0; | ||
| $digits: 0; | ||
| $minus: ($first-character == '-'); | ||
| $numbers: ('0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9); | ||
| // Remove +/- sign if present at first character | ||
| @if ($first-character == '+' or $first-character == '-') { | ||
| $value: str-slice($value, 2); | ||
| } | ||
| @for $i from 1 through str-length($value) { | ||
| $character: str-slice($value, $i, $i); | ||
| @if not (index(map-keys($numbers), $character) or $character == '.') { | ||
| @return to-length(if($minus, -$result, $result), str-slice($value, $i)) | ||
| } | ||
| @if $character == '.' { | ||
| $digits: 1; | ||
| } @else if $digits == 0 { | ||
| $result: $result * 10 + map-get($numbers, $character); | ||
| } @else { | ||
| $digits: $digits * 10; | ||
| $result: $result + map-get($numbers, $character) / $digits; | ||
| } | ||
| } | ||
| @return if($minus, -$result, $result); | ||
| } | ||
| /// | ||
| /// Add `$unit` to `$value` | ||
| /// | ||
| /// @param {Number} $value - Value to add unit to | ||
| /// @param {String} $unit - String representation of the unit | ||
| /// | ||
| /// @return {Number} - `$value` expressed in `$unit` | ||
| /// | ||
| @function to-length($value, $unit) { | ||
| $units: ('px': 1px, 'cm': 1cm, 'mm': 1mm, '%': 1%, 'ch': 1ch, 'pc': 1pc, 'in': 1in, 'em': 1em, 'rem': 1rem, 'pt': 1pt, 'ex': 1ex, 'vw': 1vw, 'vh': 1vh, 'vmin': 1vmin, 'vmax': 1vmax); | ||
| @if not index(map-keys($units), $unit) { | ||
| $_: im-log('Invalid unit `#{$unit}`.'); | ||
| } | ||
| @return $value * map-get($units, $unit); | ||
| } |
| /// | ||
| /// This mixin aims at redefining the configuration just for the scope of | ||
| /// the call. It is helpful when having a component needing an extended | ||
| /// configuration such as custom breakpoints (referred to as tweakpoints) | ||
| /// for instance. | ||
| /// | ||
| /// @author Hugo Giraudel | ||
| /// | ||
| /// @param {Map} $tweakpoints [()] - Map of tweakpoints to be merged with `$breakpoints` | ||
| /// @param {Map} $tweak-media-expressions [()] - Map of tweaked media expressions to be merged with `$media-expression` | ||
| /// | ||
| /// @example scss - Extend the global breakpoints with a tweakpoint | ||
| /// @include media-context(('custom': 678px)) { | ||
| /// .foo { | ||
| /// @include media('>phone', '<=custom') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// @example scss - Extend the global media expressions with a custom one | ||
| /// @include media-context($tweak-media-expressions: ('all': 'all')) { | ||
| /// .foo { | ||
| /// @include media('all', '>phone') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| /// @example scss - Extend both configuration maps | ||
| /// @include media-context(('custom': 678px), ('all': 'all')) { | ||
| /// .foo { | ||
| /// @include media('all', '>phone', '<=custom') { | ||
| /// // ... | ||
| /// } | ||
| /// } | ||
| /// } | ||
| /// | ||
| @mixin media-context($tweakpoints: (), $tweak-media-expressions: ()) { | ||
| // Save global configuration | ||
| $global-breakpoints: $breakpoints; | ||
| $global-media-expressions: $media-expressions; | ||
| // Update global configuration | ||
| $breakpoints: map-merge($breakpoints, $tweakpoints) !global; | ||
| $media-expressions: map-merge($media-expressions, $tweak-media-expressions) !global; | ||
| @content; | ||
| // Restore global configuration | ||
| $breakpoints: $global-breakpoints !global; | ||
| $media-expressions: $global-media-expressions !global; | ||
| } |
| /// Test `$function` with `$spec` test suite | ||
| /// | ||
| /// @author Hugo Giraudel | ||
| /// | ||
| /// @param {String} $function - Name of function to test | ||
| /// @param {Map} $spec - Test suite to run `$function` against | ||
| /// | ||
| /// @return {Map} | ||
| /// * `function`: `$function` | ||
| /// * `length`: the length of `$tests` | ||
| /// * `pass`: number of passing tests out of `length` | ||
| /// * `fail`: number of failing tests out of `length` | ||
| /// * `tests`: list of maps containing: | ||
| /// * `input`: test input (key from `$tests`) | ||
| /// * `expected`: expected result from `input` | ||
| /// * `actual`: actual result from `input` | ||
| /// * `pass`: whether the test passed or not | ||
| /// * `fail`: whether the test failed or not | ||
| /// | ||
| /// @example scss - Testing of a `double` function | ||
| /// @function double($value) { @return $value * 2 } | ||
| /// | ||
| /// $test: test('double', ( | ||
| /// 1: 2, | ||
| /// 2: 4, | ||
| /// 3: 6, | ||
| /// 4: 8, | ||
| /// )); | ||
| /// | ||
| /// @example scss - Result of `double` tests | ||
| /// ( | ||
| /// 'function': 'double', | ||
| /// 'length': 4, | ||
| /// 'pass': 4, | ||
| /// 'fail': 0, | ||
| /// 'tests': ( ... ), | ||
| /// ) | ||
| /// | ||
| /// @example scss - `tests` value from result of `double` tests | ||
| /// ( | ||
| /// ( | ||
| /// 'input': 1, | ||
| /// 'expected': 2, | ||
| /// 'actual': 2, | ||
| /// 'pass': true, | ||
| /// 'fail': false, | ||
| /// ), | ||
| /// // ... | ||
| /// ) | ||
| @function test($function, $spec) { | ||
| $passing-tests: 0; | ||
| $tests: (); | ||
| @each $arguments, $expected-result in $spec { | ||
| $actual-result: call($function, $arguments...); | ||
| $passed: $actual-result == $expected-result; | ||
| $tests: append($tests, ( | ||
| 'input': $arguments, | ||
| 'expected': $expected-result, | ||
| 'actual': $actual-result, | ||
| 'pass': $passed, | ||
| 'fail': not $passed, | ||
| ), 'comma'); | ||
| @if $passed { | ||
| $passing-tests: $passing-tests + 1; | ||
| } | ||
| } | ||
| @return ( | ||
| 'function': $function, | ||
| 'length': length($tests), | ||
| 'tests': $tests, | ||
| 'pass': $passing-tests, | ||
| 'fail': length($tests) - $passing-tests, | ||
| ); | ||
| } | ||
| /// Mixin decorating the result of `test(..)` function to throw it as an error | ||
| /// | ||
| /// @author Hugo Giraudel | ||
| /// | ||
| /// @param {Map} $data - Return of `test(..)` function | ||
| /// | ||
| /// @example scss - Printing the result of `double` function test | ||
| /// @include run(test('double', $tests-double)); | ||
| /// | ||
| /// @example scss - Result of `double` function test | ||
| /// Started tests for function `double` | ||
| /// ---------- | ||
| /// Test 1 out of 4... ✔ | ||
| /// Test 2 out of 4... ✔ | ||
| /// Test 3 out of 4... ✔ | ||
| /// Test 4 out of 4... ✔ | ||
| /// ---------- | ||
| /// Finished: 0 test(s) failing out of 4 | ||
| @mixin run($data) { | ||
| $output: ''; | ||
| $length: map-get($data, 'length'); | ||
| $tests: map-get($data, 'tests'); | ||
| @each $test in $tests { | ||
| $output: $output | ||
| + 'Test #{index($tests, $test)} out of #{$length}... ' | ||
| + if(map-get($test, 'pass'), '✔', '✘\a Expected : `#{map-get($test, "expected")}`\a Actual : `#{map-get($test, "actual")}`') + '\a '; | ||
| } | ||
| $message: 'Started tests for function `#{map-get($data, "function")}`\a ' | ||
| + '----------\a ' | ||
| + $output + '\a ' | ||
| + '----------\a ' | ||
| + 'Finished: #{map-get($data, "fail")} test(s) failing out of #{$length}'; | ||
| @if map-get($data, "fail") > 0 { | ||
| @error $message; | ||
| } @else { | ||
| @warn $message; | ||
| } | ||
| } |
| @import 'parse-expression'; | ||
| @import 'to-number'; |
| @import '../dist/include-media'; | ||
| @import 'helpers/SassyTester'; | ||
| $tests-parse-expression: map-merge($media-expressions, ( | ||
| '>phone': '(min-width: 321px)', | ||
| '<phone': '(max-width: 319px)', | ||
| '>=phone': '(min-width: 320px)', | ||
| '≥phone': '(min-width: 320px)', | ||
| '<=phone': '(max-width: 320px)', | ||
| '≤phone': '(max-width: 320px)', | ||
| '>tablet': '(min-width: 769px)', | ||
| '<tablet': '(max-width: 767px)', | ||
| '>=tablet': '(min-width: 768px)', | ||
| '≥tablet': '(min-width: 768px)', | ||
| '<=tablet': '(max-width: 768px)', | ||
| '≤tablet': '(max-width: 768px)', | ||
| '>desktop': '(min-width: 1025px)', | ||
| '<desktop': '(max-width: 1023px)', | ||
| '>=desktop': '(min-width: 1024px)', | ||
| '≥desktop': '(min-width: 1024px)', | ||
| '<=desktop': '(max-width: 1024px)', | ||
| '≤desktop': '(max-width: 1024px)' | ||
| )); | ||
| @include run(test('parse-expression', $tests-parse-expression)); |
| @import '../dist/include-media'; | ||
| @import 'helpers/SassyTester'; | ||
| $tests-parse-media-query: ( | ||
| (('a' 'b'),): 'a and b', | ||
| (('a', 'b'),): 'a, b', | ||
| (('a' 'b', 'c' 'd'),): 'a and b, c and d', | ||
| (('a', 'b', 'c' 'd'),): 'a, b, c and d', | ||
| ); | ||
| @include run(test('parse-media-query', $tests-parse-media-query)); |
| @import '../dist/include-media'; | ||
| @import 'helpers/SassyTester'; | ||
| $tests-resolve-feature-queries: ( | ||
| (('screen' '>desktop'), ): ( | ||
| ('screen' '(min-width: 1025px)'), | ||
| ), | ||
| (('screen', '>desktop'), ): ( | ||
| ('screen'), ('(min-width: 1025px)'), | ||
| ), | ||
| (('screen' ('>phone', '<desktop')), ): ( | ||
| ('screen' '(min-width: 321px)'), ('screen' '(max-width: 1023px)'), | ||
| ), | ||
| (('retina2x' '>phone'), ): ( | ||
| ('(-webkit-min-device-pixel-ratio: 2)' '(min-width: 321px)'), | ||
| ('(min-resolution: 192dpi)' '(min-width: 321px)'), | ||
| ('(min-resolution: 2dppx)' '(min-width: 321px)'), | ||
| ), | ||
| (('screen' ('height>300px' 'landscape')), ): ( | ||
| ('screen' '(min-height: 301px)' '(orientation: landscape)'), | ||
| ), | ||
| ((('screen', 'portrait') ('height>300px', 'landscape')), ): ( | ||
| ('screen' '(min-height: 301px)'), | ||
| ('(orientation: portrait)' '(min-height: 301px)'), | ||
| ('screen' '(orientation: landscape)'), | ||
| ('(orientation: portrait)' '(orientation: landscape)'), | ||
| ), | ||
| ); | ||
| @include run(test('resolve-feature-queries', $tests-resolve-feature-queries)); |
| @import '../dist/include-media'; | ||
| @import 'helpers/SassyTester'; | ||
| $tests-to-number: (); | ||
| @for $i from 1 through 10 { | ||
| $tests-to-number: map-merge($tests-to-number, ( | ||
| '#{$i}': $i, | ||
| '#{-$i}': -$i, | ||
| '#{$i / 10}': ($i / 10), | ||
| '#{$i / 100}': ($i / 100), | ||
| '#{$i}px': $i * 1px, | ||
| '#{$i}em': $i * 1em, | ||
| '#{$i}rem': $i * 1rem, | ||
| '#{-$i}px': -$i * 1px, | ||
| )); | ||
| } | ||
| @include run(test('to-number', $tests-to-number)); |
| #navbar { | ||
| width: 80%; | ||
| height: 23px; | ||
| } | ||
| #navbar ul { | ||
| list-style-type: none; | ||
| } | ||
| #navbar li { | ||
| float: left; | ||
| a { | ||
| font-weight: bold; | ||
| } | ||
| } |
| div { width: foo(42px); height: bar(42px); } |
| div { color: foo("bar"); } |
| .outside { | ||
| color: red; | ||
| } |
| @import 'outside'; |
| @import "vars"; | ||
| @import "struct"; | ||
| .myvars { | ||
| content: quote($import_counter); | ||
| } |
| .common-struct { | ||
| content: "common-struct"; | ||
| } |
| $import_counter: $import_counter + 1; | ||
| .common-vars { | ||
| content: "common-vars"; | ||
| } |
| @import "_common"; | ||
| @import "a1"; | ||
| .a2 { | ||
| content: "a2"; | ||
| } | ||
| .a1 { | ||
| content: "a1"; | ||
| } |
| @import "b1"; | ||
| .b2 { | ||
| content: "b2"; | ||
| } |
| .b1 { | ||
| content: "b1"; | ||
| } |
| $import_counter: 0; | ||
| @import "a"; | ||
| @import "common"; | ||
| @import "b"; | ||
| #the-last { | ||
| content: "LAST"; | ||
| } |
| #navbar { | ||
| width: 80%; | ||
| height: 23px; | ||
| } | ||
| #navbar ul { | ||
| list-style-type: none; | ||
| } | ||
| #navbar li { | ||
| float: left; | ||
| a { | ||
| font-weight: bold; | ||
| } | ||
| } |
| /* bar.scss */ |
| @import "file-not-processed-by-loader", "file-processed-by-loader"; |
| $variable-defined-by-file-not-processed-by-loader: 'red'; |
| body { | ||
| color: $variable-defined-by-file-not-processed-by-loader; | ||
| } |
| /* foo.scss */ |
| @import 'foo'; | ||
| @import 'bar'; |
| @function colorBlue() { | ||
| @return #0000fe; | ||
| } |
| @import 'vars'; | ||
| @import 'colorBlue'; | ||
| body { | ||
| background: $color; | ||
| color: colorBlue(); | ||
| } |
| #navbar { | ||
| width: 80%; | ||
| height: 23px; | ||
| } | ||
| #navbar ul { | ||
| list-style-type: none; | ||
| } | ||
| #navbar li { | ||
| float: left; | ||
| a { | ||
| font-weight: bold; | ||
| } | ||
| } |
| #navbar { | ||
| width: 80%; | ||
| height: 23px; | ||
| } | ||
| #navbar ul { | ||
| list-style-type: none; | ||
| } | ||
| #navbar li { | ||
| float: left; | ||
| a { | ||
| font-weight: bold; | ||
| } | ||
| } |
| #navbar { | ||
| width: 80%; | ||
| height: 23px; | ||
| } | ||
| #navbar ul { | ||
| list-style-type: none; | ||
| } | ||
| #navbar li { | ||
| float: left; | ||
| a { | ||
| font-weight: bold; | ||
| } | ||
| } |
| #navbar { | ||
| width: 80%; | ||
| height: 23px; | ||
| } | ||
| #navbar ul { | ||
| list-style-type: none; | ||
| } | ||
| #navbar li { | ||
| float: left; | ||
| a { | ||
| font-weight: bold; | ||
| } | ||
| } |
| body { | ||
| background-color: $green; | ||
| } |
| #navbar { | ||
| width: 80%; | ||
| height: 23px; | ||
| } | ||
| #navbar ul { | ||
| list-style-type: none; | ||
| } | ||
| #navbar li { | ||
| float: left; | ||
| a { | ||
| font-weight: bold; | ||
| } | ||
| } |
| .foo { | ||
| margin: 1.23456789 px; | ||
| } |
| @import 'colors'; | ||
| body { | ||
| background: $color; | ||
| } | ||
| $color: orange; |
| $color: red; |
| #navbar { | ||
| width: 80%; | ||
| height: 23px; | ||
| } | ||
| #navbar ul { | ||
| list-style-type: none; | ||
| } | ||
| #navbar li { | ||
| float: left; | ||
| a { | ||
| font-weight: bold; | ||
| } | ||
| } |
| #navbar { | ||
| width: 80%; | ||
| height: 23px; | ||
| } | ||
| #navbar ul { | ||
| list-style-type: none; | ||
| } | ||
| #navbar li { | ||
| float: left; | ||
| a { | ||
| font-weight: bold; | ||
| } | ||
| } |
| #navbar { | ||
| width: 80%; | ||
| height: 23px; | ||
| } | ||
| #navbar ul { | ||
| list-style-type: none; | ||
| } | ||
| #navbar li { | ||
| float: left; | ||
| a { | ||
| font-weight: bold; | ||
| } | ||
| } |
| #navbar { | ||
| width: 80%; | ||
| height: 23px; | ||
| } | ||
| #navbar ul { | ||
| list-style-type: none; | ||
| } | ||
| #navbar li { | ||
| float: left; | ||
| a { | ||
| font-weight: bold; | ||
| } | ||
| } |
| @import "partials/one"; | ||
| .one { | ||
| color: red; | ||
| } |
| @import "partials/three"; | ||
| .one { | ||
| color: darkred; | ||
| } |
| .three { | ||
| color: darkgreen; | ||
| } |
| @import "partials/three"; | ||
| .two { | ||
| color: darkblue; | ||
| } |
| .three { | ||
| color: green; | ||
| } |
| .two { | ||
| color: blue; | ||
| } |
| .three { | ||
| color: darkgreen; | ||
| } |
| @import "partials/three"; | ||
| .three { | ||
| color: green; | ||
| } |
| a {color:green;} |
| body{background:white} |
| @import './foo'; |
| @import './white'; |
| body{background:white} |
| @import "./utils/formatters"; | ||
| @import "./utils/list-to-js"; | ||
| @import "./utils/map-to-js"; | ||
| //--- | ||
| // [$propName] - optional param (otherwise $value is taken as first) | ||
| // if provided- function returns {'propName': sassToJs($value)} | ||
| // | ||
| // $value [map, list] - Converts provided value to JSON string | ||
| // $value [bool, nulls, number, string, color] - Converts provided value to a proper format for JSON | ||
| // | ||
| // Data types in Sass: http://sass-lang.com/documentation/file.SASS_REFERENCE.html#data_types | ||
| // | ||
| // Examples of usage: | ||
| // | ||
| // .breakpoints-data{ | ||
| // font-family: sassToJs($mediaBreakPointsMap); | ||
| // } | ||
| // | ||
| // .zoom-control:before{ | ||
| // content: sassToJs("maxZoomStep", $zoomStep); | ||
| // } | ||
| // | ||
| //--- | ||
| @function sassToJs($propName: null, $value: null) { | ||
| @if ($value == null) { | ||
| $value: $propName; // emulates sassToJs($value) | ||
| } @else { | ||
| @return '{#{sassToJs($propName)}:#{sassToJs($value)}}'; // returns {"propName": sassToJs($value)} | ||
| } | ||
| //--- VARS --- | ||
| $valueType: type-of($value); | ||
| $jsonPropValue: ''; | ||
| //--- CHECKING TYPE --- | ||
| // BOOL | ||
| @if ($valueType == 'bool') { | ||
| $jsonPropValue: $value; | ||
| // NULLS | ||
| } @else if ($valueType == 'null') { | ||
| $jsonPropValue: 'null'; | ||
| // NUMBER | ||
| } @else if ($valueType == 'number') { | ||
| // '100px' has also type "number" but after multiplying to "0" gives "0px" | ||
| @if (unitless($value)) { | ||
| // e.g. 100 | ||
| $jsonPropValue: $value; | ||
| } @else { | ||
| // e.g. 100px | ||
| $jsonPropValue: sassToJs_formatAsString($value); | ||
| } | ||
| // MAP | ||
| } @else if ($valueType == 'map') { | ||
| $jsonPropValue: sassToJs_Map($value); | ||
| // LIST | ||
| } @else if ($valueType == 'list') { | ||
| $jsonPropValue: sassToJs_List($value); | ||
| // STRING, COLOR | ||
| } @else { | ||
| $jsonPropValue: sassToJs_formatAsString($value); | ||
| } | ||
| //--- RETURN --- | ||
| @return $jsonPropValue; | ||
| } |
| //--- | ||
| // Formats provided argument as a string | ||
| // Might be used for strings, colors and Sass numbers like "100px" | ||
| //--- | ||
| @function sassToJs_formatAsString($value) { | ||
| @return '"#{$value}"'; | ||
| } |
| //---------- | ||
| // Converts Sass list to JSON string | ||
| //---------- | ||
| @function sassToJs_List($list) { | ||
| //--- VARS --- | ||
| $list-map-length: length($list); | ||
| $iteration: 0; | ||
| $json: '['; // open square bracket | ||
| // add each property from Sass list | ||
| @each $item in $list { | ||
| // ITEM VALUE | ||
| $json: $json + sassToJs($item); | ||
| // Add comma if not the last item | ||
| $iteration: $iteration + 1; | ||
| @if ($iteration < $list-map-length) { | ||
| $json: $json + ','; | ||
| } | ||
| } | ||
| $json: $json + ']'; // close square bracket | ||
| //--- RETURN --- | ||
| @return $json; | ||
| } |
| //---------- | ||
| // Converts Sass map to JSON string | ||
| //---------- | ||
| @function sassToJs_Map($list-map) { | ||
| //--- VARS --- | ||
| $list-map-length: length($list-map); | ||
| $iteration: 0; | ||
| $json: '{'; // open curly brackets | ||
| // add each property from Sass map | ||
| @each $propName, $propValue in $list-map { | ||
| // PROP NAME | ||
| $json: $json + sassToJs_formatAsString($propName) + ':'; | ||
| // PROP VALUE | ||
| $json: $json + sassToJs($propValue); | ||
| // Add comma if not the last item | ||
| $iteration: $iteration + 1; | ||
| @if ($iteration < $list-map-length) { | ||
| $json: $json + ','; | ||
| } | ||
| } | ||
| $json: $json + '}'; // close curly brackets | ||
| //--- RETURN --- | ||
| @return $json; | ||
| } |
0
-100%69
137.93%8716
-95.97%11
22.22%6
-96.95%- Removed
- Removed
- Removed
- Removed
- Removed
- Removed