_feature-targeting.scss 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. //
  2. // Copyright 2019 Google Inc.
  3. //
  4. // Permission is hereby granted, free of charge, to any person obtaining a copy
  5. // of this software and associated documentation files (the "Software"), to deal
  6. // in the Software without restriction, including without limitation the rights
  7. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. // copies of the Software, and to permit persons to whom the Software is
  9. // furnished to do so, subject to the following conditions:
  10. //
  11. // The above copyright notice and this permission notice shall be included in
  12. // all copies or substantial portions of the Software.
  13. //
  14. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20. // THE SOFTWARE.
  21. //
  22. @use 'sass:list';
  23. @use 'sass:map';
  24. @use 'sass:meta';
  25. // ==Terminology==
  26. // Feature:
  27. // A simple string (e.g. `color`) representing a cross-cutting feature in
  28. // Material.
  29. // Feature query:
  30. // A structure that represents a query for a feature or combination of features. This may be
  31. // either a feature or a map containing `op` and `queries` fields. A single feature represents a
  32. // simple query for just that feature. A map represents a complex query made up of an operator,
  33. // `op`, applied to a list of sub-queries, `queries`.
  34. // (e.g. `color`, `(op: any, queries: (color, typography))`).
  35. // Feature target:
  36. // A map that contains the feature being targeted as well as the current feature query. This is
  37. // the structure that is intended to be passed to the `@mdc-feature-targets` mixin.
  38. // (e.g. `(target: color, query: (op: any, queries: (color, typography))`).
  39. //
  40. // Public
  41. //
  42. $all-features: (structure, color, typography, animation);
  43. $all-query-operators: (any, all, without);
  44. // Creates a feature target from the given feature query and targeted feature.
  45. @function create-target($feature-query, $targeted-feature) {
  46. $feature-target: (
  47. query: $feature-query,
  48. target: $targeted-feature,
  49. );
  50. $valid: verify-target_($feature-target);
  51. @return $feature-target;
  52. }
  53. // Parses a list of feature targets to produce a map containing the feature query and list of
  54. // available features.
  55. @function parse-targets($feature-targets) {
  56. $valid: verify-target_($feature-targets...);
  57. $available-features: ();
  58. @each $target in $feature-targets {
  59. $available-features: list.append(
  60. $available-features,
  61. map.get($target, target)
  62. );
  63. }
  64. @return (
  65. available: $available-features,
  66. query: map.get(list.nth($feature-targets, 1), query)
  67. );
  68. }
  69. // Creates a feature query that is satisfied iff all of its sub-queries are satisfied.
  70. @function all($feature-queries...) {
  71. $valid: verify-query_($feature-queries...);
  72. @return (op: all, queries: $feature-queries);
  73. }
  74. // Creates a feature query that is satisfied iff any of its sub-queries are satisfied.
  75. @function any($feature-queries...) {
  76. $valid: verify-query_($feature-queries...);
  77. @return (op: any, queries: $feature-queries);
  78. }
  79. // Creates a feature query that is satisfied iff its sub-query is not satisfied.
  80. @function without($feature-query) {
  81. $valid: verify-query_($feature-query);
  82. // NOTE: we need to use `append`, just putting parens around a single value doesn't make it a list in Sass.
  83. @return (op: without, queries: list.append((), $feature-query));
  84. }
  85. //
  86. // Package-internal
  87. //
  88. // Verifies that the given feature targets are valid, throws an error otherwise.
  89. @function verify-target_($feature-targets...) {
  90. @each $target in $feature-targets {
  91. @if meta.type-of($target) != map {
  92. @error "Invalid feature target: '#{$target}'. Must be a map.";
  93. }
  94. $targeted-feature: map.get($target, target);
  95. $feature-query: map.get($target, query);
  96. $valid: verify-feature_($targeted-feature) and
  97. verify-query_($feature-query);
  98. }
  99. @return true;
  100. }
  101. // Checks whether the given feature query is satisfied by the given list of available features.
  102. @function is-query-satisfied_($feature-query, $available-features) {
  103. $valid: verify-query_($feature-query);
  104. $valid: verify-feature_($available-features...);
  105. @if meta.type-of($feature-query) == map {
  106. $op: map.get($feature-query, op);
  107. $sub-queries: map.get($feature-query, queries);
  108. @if $op == without {
  109. @return not
  110. is-query-satisfied_(list.nth($sub-queries, 1), $available-features);
  111. }
  112. @if $op == any {
  113. @each $sub-query in $sub-queries {
  114. @if is-query-satisfied_($sub-query, $available-features) {
  115. @return true;
  116. }
  117. }
  118. @return false;
  119. }
  120. @if $op == all {
  121. @each $sub-query in $sub-queries {
  122. @if not is-query-satisfied_($sub-query, $available-features) {
  123. @return false;
  124. }
  125. }
  126. @return true;
  127. }
  128. }
  129. @return list-contains_($available-features, $feature-query);
  130. }
  131. //
  132. // Private
  133. //
  134. // Verifies that the given feature(s) are valid, throws an error otherwise.
  135. @function verify-feature_($features...) {
  136. @each $feature in $features {
  137. @if not list-contains_($all-features, $feature) {
  138. @error "Invalid feature: '#{$feature}'. Valid features are: #{$all-features}.";
  139. }
  140. }
  141. @return true;
  142. }
  143. // Verifies that the given feature queries are valid, throws an error otherwise.
  144. @function verify-query_($feature-queries...) {
  145. @each $query in $feature-queries {
  146. @if meta.type-of($query) == map {
  147. $op: map.get($query, op);
  148. $sub-queries: map.get($query, queries);
  149. $valid: verify-query_($sub-queries...);
  150. @if not list-contains_($all-query-operators, $op) {
  151. @error "Invalid feature query operator: '#{$op}'. " +
  152. "Valid operators are: #{$all-query-operators}";
  153. }
  154. } @else {
  155. $valid: verify-feature_($query);
  156. }
  157. }
  158. @return true;
  159. }
  160. // Checks whether the given list contains the given item.
  161. @function list-contains_($list, $item) {
  162. @return list.index($list, $item) != null;
  163. }
  164. // Tracks whether the current context is inside a `mdc-feature-targets` mixin.
  165. $targets-context_: false;
  166. // Mixin that annotates the contained styles as applying to specific cross-cutting features
  167. // indicated by the given list of feature targets.
  168. @mixin targets($feature-targets...) {
  169. // Prevent accidental nesting of this mixin, which could lead to unexpected results.
  170. @if $targets-context_ {
  171. @error "mdc-feature-targets must not be used inside of another mdc-feature-targets block";
  172. }
  173. $targets-context_: true !global;
  174. $parsed-targets: parse-targets($feature-targets);
  175. @if is-query-satisfied_(
  176. map.get($parsed-targets, query),
  177. map.get($parsed-targets, available)
  178. )
  179. {
  180. @content;
  181. }
  182. $targets-context_: false !global;
  183. }