// ************************************* // // # jigsass-tools-mq // -> Easy peasy media queries. // Heavily based on Kaelig's Sass-Mq, // with a few minor adjustments // for JigSass // //// /// @group media queries //// // // ************************************* @charset 'UTF-8'; // ------------------------------------- // Dependencies // ------------------------------------- @if (not function-exists(jigsass-set)) { @error '`jigsass-tools-mq` has a dependancy on `jigsass-tools-maps`. Please `@import` it.'; } // ------------------------------------- // Config // ------------------------------------- /// Named breakpoints /// /// Breakpoint names and values can and should be redefined to fit a /// project design and the language used be the team working on it. /// However, please keep in mind that **the JigSass framework depends /// on a 0-sized length breakpoint called `default` being defined**, /// as it is internally used to set up default values in several places. /// /// The `jigsass-mq` mixin will try and resolve values from the `lengths` sub-map /// when evaluating the `$from` and `$until` arguments. When evaluating the `$misc` /// argument, it will try and resolve values from the `features` sub-map. /// --- /// @type Map /// @prop {map} lengths /// A map of `name: width` key-value pairs, defining widths for /// commonly used media queries. Used when evaluating the `$from` and `$until` arguments. /// @prop {map} features /// A map of `name: rule` key-value pairs, defining commonly /// used miscellaneous media-query features. /// Used when evaluating the `$from` and `$until` arguments. $jigsass-breakpoints: ( lengths: ( default: 0, tiny: 320px, small: 480px, medium: 600px, large: 1024px, x-large: 1280px, ), features: ( landscape: '(orientation: landscape)', partrait: '(orientation: portrait)', hidpi: '(-webkit-min-device-pixel-ratio: 1.3), (min-resolution: 120dpi), (min-resolution: 1.3dppx)', ), ) !default; /// Get the name of the zero-sized breakpoint /// --- /// @param {Map} $breakpoints [$jigsass-breakpoints] /// The map of breakpoints to iterate over. /// --- /// @return {String} - The name of the default breakpoint /// --- @function jigsass-get-default-breakpoint($breakpoints: $jigsass-breakpoints) { $default-bp: null; @each $bp-name, $bp in map-get($breakpoints, lengths) { @if (($bp / ($bp * 0 + 1)) == 0 and not $default-bp) { $default-bp: $bp-name; } } @return $default-bp; } // For deprecation @function _get-default-breakpoint($breakpoints: $jigsass-breakpoints) { @warn 'DEPRECATION WARNING: The `_get-default-breakpoint` function will be depracated in the ' + 'next major release of `jigsass-tools-mq`. Use `jigsass-get-default-breakpoint` instead.'; @return jigsass-get-default-breakpoint($breakpoints); } /// The name of the currently active min-width breakpoint /// --- /// @type String $jigsass-mq-active-breakpoint: jigsass-get-default-breakpoint(); /// Responsive mode /// /// Set to `false` to enable support for browsers that do not support @media queries, /// (IE <= 8, Firefox <= 3, Opera <= 9) /// /// You could create a stylesheet served exclusively to older browsers, /// where @media queries are rasterized /// --- /// @example scss /// // old-ie.scss /// $jigsass-mq-responsive: false; /// @import 'main'; // @media queries in this file will be /// // rasterized up to $jigsass-mq-static-breakpoint /// // larger breakpoints will be ignored /// --- /// @type Boolean /// @link https://github.com/TxHawks/jigsass-tools-mq#responsive-mode-off /// - Disabled responsive mode documentation $jigsass-mq-responsive: true !default; /// Static breakpoint (for fixed-width layouts) /// /// Define the breakpoint from $jigsass-breakpoints that should /// be used as the target width for the fixed-width layout /// (i.e. when $jigsass-mq-responsive is set to 'false') in an old-ie.scss /// --- /// @example scss /// // tablet-only.scss /// // /// // Ignore all styles above `tablet` breakpoint, /// // and fix the styles (e.g. layout) at `tablet` width /// $jigsass-mq-responsive: false; /// $jigsass-mq-static-breakpoint: tablet; /// @import 'main'; // @media queries in this file will be rasterized up to tablet /// // larger breakpoints will be ignored /// --- /// @type String $jigsass-mq-static-breakpoint: large !default; /// Show breakpoints in the top right corner /// /// If you want to display the currently active breakpoint in the top /// right corner of your site during development, add the breakpoints /// to this list, ordered by width, e.g. (mobile, tablet, desktop). /// /// @type map $jigsass-mq-show-breakpoints: () !default; /// Customize the media type (e.g. `@media screen` or `@media print`) /// By default jigsass-mq does not restrict the media type (`@media …`) /// --- /// @type String /// @link https://github.com/TxHawks/tools-jigsass-mq#changing-media-type /// Full documentation and examples $jigsass-mq-media-type: all !default; // ------------------------------------- // Functions // ------------------------------------- /// Get a breakpoint's width /// --- /// @access private /// --- /// @param {string} $name /// Breakpoint to get the width of /// @param {map} $breakpoints [$jigsass-breakpoints] /// The map to search for the breakpoint in. /// --- /// @example scss /// $tablet-width: jigsass-mq-get-breakpoint-width(tablet); /// @media (min-width: jigsass-mq-get-breakpoint-width(desktop)) {} /// --- /// @returns {Number} Value in pixels @function jigsass-mq-get-breakpoint-width($name, $breakpoints: $jigsass-breakpoints) { @if jigsass-deep-has-key($breakpoints, lengths, $name) { @return jigsass-get($breakpoints, lengths, $name); } @else { @warn 'jigsass-mq-get-breakpoint-width: Breakpoint #{$name} wasn\'t found in $breakpoints.'; @return $name; } } /// Sort length breakpoints by size from small to large. /// --- /// @param {Map} $bps [$jigsass-breakpoints.lengths] /// The breakpoints to sort /// --- /// @return {Map} /// The sorted breakpoints /// --- @function jigsass-mq-sort-length-breakpoints($bps: map-get($jigsass-breakpoints, lengths)) { $bp-names: map-keys($bps); $bp-values: map-values($bps); $temp-bp-values: $bp-values; // Used for sorting $sorted-bp-values: (); @each $bp-name, $bp-value in $bps { @if (type-of($bp-value) != number) { @error 'jigsass-mq-sort-length-breakpoints: Length breakpoints must resolve to numbers, ' + 'but `#{$bp-name}` is `#{inspect($bp-value)}`, which is a `#{type-of($bp-value)}`'; } } // Sort breakpoint widths from small to large @while length($temp-bp-values) > 0 { $smallest: nth($temp-bp-values, 1); @each $bp-value in $temp-bp-values { $smallest: if($bp-value < $smallest, $bp-value, $smallest); } // Append smallest value to sorted list $sorted-bp-values: append($sorted-bp-values, $smallest); // Remove smallest value from list and continue iterating. $values: (); @each $i in $temp-bp-values { @if ($i != $smallest) { $values: append($values, $i); } } $temp-bp-values: $values; } $breakpoints: (); @each $bp-value in $sorted-bp-values { $original-position: index($bp-values, $bp-value); $breakpoints: map-merge( $breakpoints, (nth($bp-names, $original-position) : $bp-value) ); } @return $breakpoints; } /// Check if a breakpoint is defined as a named breakpoint /// --- /// @param {String} $breakpoint /// The breakpoint name to check for. /// @param {Map} $breakpoints [$jigsass-breakpoints] /// The map to check for `$breakpoint` in. /// --- /// @return {Boolean} Is the breakpoint defined. /// --- @function jigsass-mq-breakpoint-defined($breakpoint, $breakpoints: $jigsass-breakpoints) { @return jigsass-deep-has-key($breakpoints, lengths, $breakpoint) or jigsass-deep-has-key($breakpoints, features, $breakpoint); } // ------------------------------------- // Mixins // ------------------------------------- /// Generate media queries /// --- /// @param {String | Number | Boolean} $from [false] /// Min-width (inclusive) in pixels, or one of `$jigsass-breakpoints.lengths` /// @param {String | number | Boolean} $until [false] /// Max-width (exclusive) in pixels, or one of `$jigsass-breakpoints.lengths` /// @param {String | List | Boolean} $misc [false] /// Miscellaneous media query parameters. One of `$jigsass-breakpoints.features` /// or a free-form string. Can also be a list of multiple media-query features. /// @param {String} $media-type [$jigsass-mq-media-type] /// Media type: screen, print… /// /// @ignore Undocumented API, for advanced use only: /// @ignore @param {String} $static-breakpoint [$jigsass-mq-static-breakpoint] /// A custom static breakpoint, scoped for the current run of the mixin. /// --- /// @content styling rules, wrapped into a @media query when responsive mode in turned on. /// --- /// @example scss /// .element { /// @include mq($from: mobile) { /// color: red; /// } /// @include mq($until: tablet) { /// color: blue; /// } /// @include mq(mobile, tablet) { /// color: green; /// } /// @include mq($from: tablet, $misc: landscape hidpi) { /// color: teal; /// } /// @include mq(950px) { /// color: hotpink; /// } /// @include mq(tablet, $media-type: screen) { /// color: hotpink; /// } /// // Advanced use: /// @include mq(x-large:, $static-breakpoint: x-large) { /// color: hotpink; /// } /// } /// --- @mixin jigsass-mq( $from: false, $until: false, $misc: false, $media-type: $jigsass-mq-media-type, $static-breakpoint: $jigsass-mq-static-breakpoint ) { $min-width: 0; $max-width: 0; $misc-features: false; $media-query: ''; $breakpoints: $jigsass-breakpoints; $responsive: $jigsass-mq-responsive; // Save the default breakpoint $_default-bp: $jigsass-mq-active-breakpoint; $_active-bp: $_default-bp; // From: this breakpoint (inclusive) @if $from { @if type-of($from) == number { $min-width: _jigsass-mq-px2em($from); } @else { @if type-of(jigsass-mq-get-breakpoint-width($from, $breakpoints)) == 'string' { @error 'jigsass-mq: #{$from} is not a length breakpoint in `$jigsass-breakpoints`.'; } // Set the active min-width breakpoint $_active-bp: $from; $min-width: _jigsass-mq-px2em(jigsass-mq-get-breakpoint-width($from, $breakpoints)); } } // Until: that breakpoint (exclusive) @if $until { @if type-of($until) == number { $max-width: _jigsass-mq-px2em($until); } @else { @if type-of(jigsass-mq-get-breakpoint-width($until, $breakpoints)) == 'string' { @error 'jigsass-mq: #{$until} is not a length breakpoint in `$jigsass-breakpoints`.'; } $_max-value: jigsass-mq-get-breakpoint-width($until, $breakpoints); $max-width: _jigsass-mq-px2em($_max-value) - if($_max-value == 0, 0, .01em); } } // Misc: non-widths features @if $misc { $misc-features: ''; @each $feature in $misc { @if (jigsass-deep-has-key($jigsass-breakpoints, features, $feature)) { $feature: jigsass-get($jigsass-breakpoints, features, $feature); } @if (type-of($feature) == string) { $feature: if(str-slice($feature, 1, 1) == '(', $feature, '(' + $feature); $feature: if(str-slice($feature, -1) == ')', $feature, $feature + ')'); $misc-features: '#{$misc-features} and #{$feature}'; } @else { @warn 'jigsass-mq: `#{inspect($misc)}` will be ignored, since it is a ' + '#{type-of($misc)}, not a string.'; } } // Remove unnecessary ' and ' at beginning of `$misc-features` $misc-features: str-slice(unquote($misc-features), 6); } // Responsive support is disabled, rasterize the output outside @media blocks // The browser will rely on the cascade itself. @if $responsive == false { $static-breakpoint-width: jigsass-mq-get-breakpoint-width($static-breakpoint, $breakpoints); $target-width: _jigsass-mq-px2em($static-breakpoint-width); // Output only rules that start at or span our target width @if ( $misc == false and $min-width <= $target-width and ($until == false or $max-width >= $target-width) ) { @content; } } // Responsive support is enabled, output rules inside @media queries @else { @if $min-width != 0 { $media-query: '#{$media-query} and (min-width: #{$min-width})'; } @if $max-width != 0 { $media-query: '#{$media-query} and (max-width: #{$max-width})'; } @if $misc-features { $media-query: '#{$media-query} and #{$misc-features}'; } // Remove unnecessary media query prefix 'all and ' @if ($media-type == 'all' and $media-query != '') { $media-type: ''; $media-query: str-slice(unquote($media-query), 6); } // Don't create meaningless media-queries @if ($media-query != '' or ($media-type != '' and $media-type != all)) { // Set the active min-width breakpoint $jigsass-mq-active-breakpoint: $_active-bp !global; @media #{$media-type + $media-query} { @content; } // Restore the default breakpoint as the default one. $jigsass-mq-active-breakpoint: $_default-bp !global; } @else { @content; } } } /// Tweak the list of defined breakpoints within the scope of the mixin's `@content` /// --- /// @param {Map} $tweakpoints [()] /// A map of named breakpoints to add to (or be used to overwrite) /// the existing breakpoints in $jigsass-breakpoints. /// @param {Map} $length-tweakpoints [()] /// A map of named lengths to add to (or be used to overwrite) /// the existing length-breakpoints. /// @param {Map} $length-tweakpoints [()] /// A map of named miscellaneous media-query features to add to /// (or be used to overwrite) the existing feature-breakpoints. /// @param {Boolean} $overwrite [false] /// Determines whether the tweakpoints should overwrite the /// existing breakpoints, or be merged into them. /// --- /// @example scss - Add a custom length breakpoint /// @include jigsass-tweakpoints(('masthead-collapse': 550px)) { /// @include jigsass-mq(masthead-collapse) { /// .masthead { /// // ... /// } /// } /// } /// /// @example scss - Add a custom feature breakpoint /// @include jigsass-tweakpoints($feature-tweakpoints: ('8bit': ('min-color': 8))) { /// @include jigsass-mq($misc: 8bit) { /// .page--has-color { /// // ... /// } /// } /// } /// /// @example scss - Temporarily overwrite existing length breakpoints /// $masthead-bps: (small: 350px, medium: 500px, large: 800px); /// /// @include jigsass-tweakpoints($masthead-bps, $overwrite: false) { /// $lengths: map-get($jigsass-breakpoints, lengths); /// @each $length in $lengths { /// @include jigsass-mq($length) { /// // ... /// } /// } /// } /// --- @mixin jigsass-mq-tweakpoints( $tweakpoints: (), $length-tweakpoints: (), $feature-tweakpoints: (), $overwrite: false ) { // Save original state of breakpoints configuration. $_original-breakpoints: $jigsass-breakpoints; @if ($overwrite) { @if (length($tweakpoints) != 0) { $jigsass-breakpoints: $tweakpoints !global; } // Overwrite breakpoints configuration @if (length($length-tweakpoints) != 0) { $jigsass-breakpoints: jigsass-set( $jigsass-breakpoints, lengths, $length-tweakpoints ) !global; } @if (length($feature-tweakpoints) != 0) { $jigsass-breakpoints: jigsass-set( $jigsass-breakpoints, features, $feature-tweakpoints ) !global; } } @else { // Extend breakpoints configuration $new-breakpoints: if(length($tweakpoints) != 0, $tweakpoints, ()); @if (length($length-tweakpoints) != 0) { $new-breakpoints: jigsass-deep-merge($new-breakpoints, (lengths: $length-tweakpoints)); } @if (length($feature-tweakpoints) != 0) { $new-breakpoints: jigsass-deep-merge($new-breakpoints, (features: $feature-tweakpoints)); } $jigsass-breakpoints: jigsass-deep-merge($jigsass-breakpoints, $new-breakpoints) !global; } @content; // Restore original state of breakpoints configuration. $jigsass-breakpoints: $_original-breakpoints !global; } /// Export a JSON-like representation of defined breakpoints, /// and indicate if given breakpoint is active at given viewport /// conditions. /// /// The exported JSON will have to following keys: /// - **values:** an object with `: ` pairs for /// each defined length breakpoint. /// - **from:** an object with a key for each defined breakpoint, containing /// a nested object with `from: ` and `active: ` keys. /// - **until:** an object with a key for each defined breakpoint, containing /// a nested object with `until: ` and `active: ` keys. /// - **from-until:** an object with a key for each defined breakpoint, containing /// a nested object with `from: `, `until: ` and `active: ` keys. /// /// The mixin produces pretty heavy CSS (8.4k in default settings), but one /// that gets gzipped very well (8.4k -> 452b). /// --- /// @param {String} $element [body] /// The element to attach JSON data to. /// /// The JSON string will be attached to the `content` property of the /// `::after` psudo-element of the specified element. /// --- @mixin jigsass-mq-export-length-bps($element: body) { $_bps: jigsass-mq-sort-length-breakpoints(); @each $name, $value in $_bps { @include jigsass-mq($name) { #{$element}:after { content: _jigsass-mq-bps-to-json($name); display: none; } } } } // ------------------------------------- // Private helpers // ------------------------------------- /// Convert Pixel values to ems /// based on the browser's default /// font-size. /// --- /// @access private /// --- /// @param {Number} $px /// value to convert /// @param {boolean} $supress-warnings [false] /// Suppress warnings about converting unitless numbers into pixels. /// --- /// @returns {Number} - The converted number in ems @function _jigsass-mq-px2em($px, $supress-warnings: false) { $_browser-default-font-size: 16px; // Ensure `$px` is a number @if (type-of($px) != 'number') { @error '_jigsass-mq-px2em: Only numbers can be converted in into ems. ' + '#{$px} is a #{type-of($px)}.'; } // Return a unitless number if `$px` is 0 @if ($px / ($px * 0 + 1) == 0) { @return 0; } // Convert unitless number to pixels @if (unitless($px)) { @if (not $supress-warnings) { @warn '_jigsass-mq-px2em: Assuming #{$px} is in pixels'; } @return _jigsass-mq-px2em($px * 1px); } // Return em values as is. @if (unit($px) == em) { @return $px; } // Error out on all other non-pixel units. @if (unit($px) != px) { @error '_jigsass-mq-px2em: Only px values can be safely converted into ems.'; } $_result: ($px / $_browser-default-font-size) * 1em; // stylelint-disable @return if($_result == 0em, 0, $_result); // stylelint-enable } /// A helper function which generates a JSON-like /// string with information about length breakpoint /// values and which breakpoints are currently active. /// /// --- /// @param {String} $active-bp /// The name of the length breakpoint to evaluate active breakpoints for. /// --- /// @return {String} - A JSON-like string /// --- @function _jigsass-mq-bps-to-json($active-bp) { $_values: (); $_from: (); $_until: (); $_from-until: (); $_bps: jigsass-mq-sort-length-breakpoints(); $_active-value: map-get($_bps, $active-bp); @each $name, $value in $_bps { $_values: append($_values, '"' + $name + '": "' + $value + '"', comma); $_from: append( $_from, '"#{$name}": {"from": "#{$value}", "active": #{$_active-value >= $value}}', comma ); $_until: append( $_until, '"#{$name}": {"until": "#{$value}", "active": #{$_active-value < $value}}', comma ); @each $max-name, $max-value in $_bps { @if ($max-value != 0) { $min-value: $value; $min-name: $name; @if ($max-value > $min-value) { // stylelint-disable max-line-length $_from-until: append( $_from-until, '"#{$min-name}-until-#{$max-name}": {"from": "#{$min-value}", "until": "#{$max-value}", "active": #{$_active-value >= $min-value and $_active-value < $max-value}}', comma ); // stylelint-enable max-line-length } } } } $_values: '{"values": {' + $_values + '}'; $_from: '"from": {#{$_from}}'; $_until: '"until": {#{$_until}}'; $_from-until: '"from-until": {#{$_from-until}}}'; @return '#{$_values}, #{$_from}, #{$_until}, #{$_from-until}'; }